### Core Engine - Updated `GameRules.ApplyMove` to return `(changed, gameOver, winnerIdx, keepTurn)` - Added keepTurn handling in `MatchLoop` to support Battleship mode B (classic rules) - Removed old single-board handling from MatchState and MatchInit - Cleaned go.mod by marking protobuf dependency as indirect ### Battleship - Implemented board-based state tracking using MatchState.Boards: - `p0_ships`, `p0_shots`, `p1_ships`, `p1_shots` - Removed legacy metadata-based ship/shot board encoding - Rewrote ValidateMove to use structured boards - Rewrote ApplyMove for classic Battleship rules (mode B): - Hits allow the attacker to keep their turn - Miss switches turn - Destroyed ship sections marked `X` - Improved CheckGameOver using structured boards ### TicTacToe - Updated ApplyMove signature to match new interface - Ensured TicTacToe always returns `keepTurn = false` - Updated code paths to use MatchState.Boards instead of Board ### Summary This commit completes the migration from a single-board architecture to a multi-board architecture across the engine, TicTacToe, and Battleship, enabling support for more complex games and multiple modes such as Battleship Mode B.
199 lines
4.8 KiB
Go
199 lines
4.8 KiB
Go
package games
|
|
|
|
import (
|
|
"fmt"
|
|
"encoding/json"
|
|
"localrepo/plugins/structs"
|
|
)
|
|
|
|
//
|
|
// BATTLESHIP RULES IMPLEMENTATION
|
|
//
|
|
// NOTES:
|
|
// - 2 players
|
|
// - Each player has 2 boards:
|
|
// 1. Their own ship board (state.Board is not reused here)
|
|
// 2. Their "shots" board (hits/misses on opponent)
|
|
// - We store boards in Player.Metadata as JSON strings
|
|
// (simplest method without changing your structs).
|
|
//
|
|
|
|
// ShipBoard and ShotBoard are encoded inside Metadata:
|
|
//
|
|
// Metadata["ship_board"] = JSON string of [][]string
|
|
// Metadata["shot_board"] = JSON string of [][]string
|
|
//
|
|
|
|
// ------------------------------
|
|
// Helpers: encode/decode
|
|
// ------------------------------
|
|
|
|
func encodeBoard(b [][]string) string {
|
|
out := "["
|
|
for i, row := range b {
|
|
out += "["
|
|
for j, col := range row {
|
|
out += fmt.Sprintf("%q", col)
|
|
if j < len(row)-1 {
|
|
out += ","
|
|
}
|
|
}
|
|
out += "]"
|
|
if i < len(b)-1 {
|
|
out += ","
|
|
}
|
|
}
|
|
out += "]"
|
|
return out
|
|
}
|
|
|
|
func decodeBoard(s string) [][]string {
|
|
var out [][]string
|
|
// should never fail; safe fallback
|
|
_ = json.Unmarshal([]byte(s), &out)
|
|
return out
|
|
}
|
|
|
|
// ------------------------------
|
|
// BattleshipRules
|
|
// ------------------------------
|
|
|
|
type BattleshipRules struct{}
|
|
|
|
func (b *BattleshipRules) MaxPlayers() int { return 2 }
|
|
|
|
func (b *BattleshipRules) InitBoards(players []*structs.Player, cfg GameConfiguration) map[string]*structs.Board {
|
|
boards := make(map[string]*structs.Board)
|
|
// One ships board and one shots board per player
|
|
for _, p := range players {
|
|
pid := fmt.Sprintf("p%d", p.Index)
|
|
|
|
// Player's fleet board (ships placement)
|
|
boards[pid+"_ships"] = structs.NewBoard(cfg.Board.Rows, cfg.Board.Cols)
|
|
|
|
// Player's attack tracking board (shots fired at opponent)
|
|
boards[pid+"_shots"] = structs.NewBoard(cfg.Board.Rows, cfg.Board.Cols)
|
|
}
|
|
return boards
|
|
}
|
|
|
|
// ------------------------------
|
|
// Assign player boards
|
|
// ------------------------------
|
|
func (b *BattleshipRules) AssignPlayerSymbols(players []*structs.Player) {
|
|
// nothing needed for classic mode
|
|
}
|
|
|
|
// ------------------------------
|
|
// ValidateMove
|
|
// payload.data = { "row": int, "col": int }
|
|
// ------------------------------
|
|
|
|
func (b *BattleshipRules) ValidateMove(state *structs.MatchState, playerIdx int, payload MovePayload) bool {
|
|
rf, ok1 := payload.Data["row"].(float64)
|
|
cf, ok2 := payload.Data["col"].(float64)
|
|
if !ok1 || !ok2 {
|
|
return false
|
|
}
|
|
r := int(rf)
|
|
c := int(cf)
|
|
|
|
shotKey := fmt.Sprintf("p%d_shots", playerIdx)
|
|
shotBoard := state.Boards[shotKey]
|
|
if shotBoard == nil {
|
|
return false
|
|
}
|
|
|
|
if !shotBoard.InBounds(r, c) {
|
|
return false
|
|
}
|
|
|
|
// can't shoot same cell twice
|
|
if !shotBoard.IsEmpty(r, c) {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// -----------------------------
|
|
// APPLY MOVE (MODE B — CLASSIC)
|
|
// -----------------------------
|
|
func (b *BattleshipRules) ApplyMove(
|
|
state *structs.MatchState,
|
|
playerIdx int,
|
|
payload MovePayload,
|
|
) (bool, bool, int, bool) {
|
|
|
|
r := int(payload.Data["row"].(float64))
|
|
c := int(payload.Data["col"].(float64))
|
|
|
|
shotKey := fmt.Sprintf("p%d_shots", playerIdx)
|
|
shipKey := fmt.Sprintf("p%d_ships", 1-playerIdx)
|
|
|
|
shots := state.Boards[shotKey]
|
|
ships := state.Boards[shipKey]
|
|
|
|
hit := false
|
|
|
|
if ships.Get(r, c) == "S" {
|
|
// hit
|
|
hit = true
|
|
shots.Set(r, c, "H")
|
|
ships.Set(r, c, "X") // mark destroyed section
|
|
} else {
|
|
shots.Set(r, c, "M")
|
|
}
|
|
|
|
// check game over
|
|
over, winner := b.CheckGameOver(state)
|
|
|
|
// keepTurn = hit (classic rule)
|
|
return true, over, winner, hit
|
|
}
|
|
|
|
// ------------------------------
|
|
// CheckGameOver
|
|
// ------------------------------
|
|
func (b *BattleshipRules) CheckGameOver(state *structs.MatchState) (bool, int) {
|
|
for i := range state.Players {
|
|
shipKey := fmt.Sprintf("p%d_ships", i)
|
|
ships := state.Boards[shipKey]
|
|
|
|
alive := false
|
|
for r := 0; r < ships.Rows; r++ {
|
|
for c := 0; c < ships.Cols; c++ {
|
|
if ships.Get(r, c) == "S" {
|
|
alive = true
|
|
break
|
|
}
|
|
}
|
|
if alive {
|
|
break
|
|
}
|
|
}
|
|
|
|
if !alive {
|
|
// this player has no ships left → opponent wins
|
|
return true, 1 - i
|
|
}
|
|
}
|
|
|
|
return false, -1
|
|
}
|
|
|
|
// ------------------------------
|
|
// Forfeit Winner
|
|
// ------------------------------
|
|
func (b *BattleshipRules) ForfeitWinner(state *structs.MatchState, leaverIndex int) int {
|
|
|
|
// If player leaves, opponent automatically wins.
|
|
if leaverIndex == 0 {
|
|
return 1
|
|
}
|
|
if leaverIndex == 1 {
|
|
return 0
|
|
}
|
|
return -1
|
|
}
|