feat(battleship,tictactoe,engine): add multi-board support and keepTurn logic
### 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.
This commit is contained in:
@@ -81,22 +81,7 @@ func (b *BattleshipRules) InitBoards(players []*structs.Player, cfg GameConfigur
|
||||
// Assign player boards
|
||||
// ------------------------------
|
||||
func (b *BattleshipRules) AssignPlayerSymbols(players []*structs.Player) {
|
||||
// Battleship has no symbols like X/O,
|
||||
// but we use this hook to initialize per-player boards.
|
||||
|
||||
for _, p := range players {
|
||||
// 10x10 boards
|
||||
empty := make([][]string, 10)
|
||||
for r := range empty {
|
||||
empty[r] = make([]string, 10)
|
||||
}
|
||||
|
||||
// ship board → players place ships manually via a "setup" phase
|
||||
p.Metadata["ship_board"] = encodeBoard(empty)
|
||||
|
||||
// shot board → empty grid that tracks hits/misses
|
||||
p.Metadata["shot_board"] = encodeBoard(empty)
|
||||
}
|
||||
// nothing needed for classic mode
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
@@ -105,72 +90,87 @@ func (b *BattleshipRules) AssignPlayerSymbols(players []*structs.Player) {
|
||||
// ------------------------------
|
||||
|
||||
func (b *BattleshipRules) ValidateMove(state *structs.MatchState, playerIdx int, payload MovePayload) bool {
|
||||
rF, ok1 := payload.Data["row"].(float64)
|
||||
cF, ok2 := payload.Data["col"].(float64)
|
||||
rf, ok1 := payload.Data["row"].(float64)
|
||||
cf, ok2 := payload.Data["col"].(float64)
|
||||
if !ok1 || !ok2 {
|
||||
return false
|
||||
}
|
||||
r := int(rf)
|
||||
c := int(cf)
|
||||
|
||||
r := int(rF)
|
||||
c := int(cF)
|
||||
|
||||
if r < 0 || r > 9 || c < 0 || c > 9 {
|
||||
shotKey := fmt.Sprintf("p%d_shots", playerIdx)
|
||||
shotBoard := state.Boards[shotKey]
|
||||
if shotBoard == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if this spot was already shot before
|
||||
shotBoard := decodeBoard(state.Players[playerIdx].Metadata["shot_board"])
|
||||
return shotBoard[r][c] == ""
|
||||
if !shotBoard.InBounds(r, c) {
|
||||
return false
|
||||
}
|
||||
|
||||
// can't shoot same cell twice
|
||||
if !shotBoard.IsEmpty(r, c) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// ApplyMove
|
||||
// ------------------------------
|
||||
// -----------------------------
|
||||
// APPLY MOVE (MODE B — CLASSIC)
|
||||
// -----------------------------
|
||||
func (b *BattleshipRules) ApplyMove(
|
||||
state *structs.MatchState,
|
||||
playerIdx int,
|
||||
payload MovePayload,
|
||||
) (bool, bool, int) {
|
||||
attacker := state.Players[playerIdx]
|
||||
defenderIdx := 1 - playerIdx
|
||||
defender := state.Players[defenderIdx]
|
||||
) (bool, bool, int, bool) {
|
||||
|
||||
r := int(payload.Data["row"].(float64))
|
||||
c := int(payload.Data["col"].(float64))
|
||||
|
||||
shotBoard := decodeBoard(attacker.Metadata["shot_board"])
|
||||
shipBoard := decodeBoard(defender.Metadata["ship_board"])
|
||||
shotKey := fmt.Sprintf("p%d_shots", playerIdx)
|
||||
shipKey := fmt.Sprintf("p%d_ships", 1-playerIdx)
|
||||
|
||||
if shipBoard[r][c] == "S" {
|
||||
shotBoard[r][c] = "H"
|
||||
shipBoard[r][c] = "X"
|
||||
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 {
|
||||
shotBoard[r][c] = "M"
|
||||
shots.Set(r, c, "M")
|
||||
}
|
||||
|
||||
attacker.Metadata["shot_board"] = encodeBoard(shotBoard)
|
||||
defender.Metadata["ship_board"] = encodeBoard(shipBoard)
|
||||
|
||||
// check game over
|
||||
over, winner := b.CheckGameOver(state)
|
||||
|
||||
return true, over, winner
|
||||
// keepTurn = hit (classic rule)
|
||||
return true, over, winner, hit
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// CheckGameOver
|
||||
// ------------------------------
|
||||
func (b *BattleshipRules) CheckGameOver(state *structs.MatchState) (bool, int) {
|
||||
|
||||
for i, p := range state.Players {
|
||||
ships := decodeBoard(p.Metadata["ship_board"])
|
||||
for i := range state.Players {
|
||||
shipKey := fmt.Sprintf("p%d_ships", i)
|
||||
ships := state.Boards[shipKey]
|
||||
|
||||
alive := false
|
||||
for r := range ships {
|
||||
for c := range ships[r] {
|
||||
if ships[r][c] == "S" {
|
||||
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 {
|
||||
|
||||
@@ -22,7 +22,11 @@ type GameRules interface {
|
||||
|
||||
// Apply a move.
|
||||
// Returns: (changed, gameOver, winnerIndex)
|
||||
ApplyMove(state *structs.MatchState, playerIdx int, payload MovePayload) (bool, bool, int)
|
||||
ApplyMove(
|
||||
state *structs.MatchState,
|
||||
playerIdx int,
|
||||
payload MovePayload,
|
||||
) (changed bool, gameOver bool, winnerIdx int, keepTurn bool)
|
||||
|
||||
// If a player leaves, who wins?
|
||||
// Return:
|
||||
|
||||
@@ -70,10 +70,10 @@ func (t *TicTacToeRules) ApplyMove(
|
||||
state *structs.MatchState,
|
||||
playerIdx int,
|
||||
payload MovePayload,
|
||||
) (bool, bool, int) {
|
||||
) (bool, bool, int, bool) {
|
||||
b := state.Boards["tictactoe"]
|
||||
if b == nil {
|
||||
return false, false, -1
|
||||
return false, false, -1, false
|
||||
}
|
||||
|
||||
symbol := state.Players[playerIdx].Metadata["symbol"]
|
||||
@@ -85,7 +85,7 @@ func (t *TicTacToeRules) ApplyMove(
|
||||
|
||||
over, winner := t.CheckGameOver(state)
|
||||
|
||||
return true, over, winner
|
||||
return true, over, winner, false
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -318,7 +318,7 @@ func (m *GenericMatch) MatchLoop(
|
||||
}
|
||||
|
||||
// Delegate to rules.ApplyMove which returns (changed, gameOver, winnerIndex)
|
||||
stateChanged, gameOver, winnerIdx := m.Rules.ApplyMove(s, playerIdx, payload)
|
||||
stateChanged, gameOver, winnerIdx, keepTurn := m.Rules.ApplyMove(s, playerIdx, payload)
|
||||
|
||||
if stateChanged {
|
||||
changed = true
|
||||
@@ -328,7 +328,7 @@ func (m *GenericMatch) MatchLoop(
|
||||
s.GameOver = true
|
||||
s.Winner = winnerIdx
|
||||
} else {
|
||||
if len(s.Players) > 0 {
|
||||
if !keepTurn && len(s.Players) > 0 {
|
||||
s.Turn = (s.Turn + 1) % len(s.Players)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user