From bcdc5faea541d2d8ccbc265923288fbce7371bd9 Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Wed, 3 Dec 2025 17:52:27 +0530 Subject: [PATCH] feat(core): migrate to multi-board architecture and implement per-game InitBoards ### Major changes - Replace single-board MatchState (`Board`) with multi-board map (`Boards`) - Update GenericMatch to initialize empty Boards and populate them via GameRules.InitBoards - Remove legacy `newEmptyBoard` helper from match.go - Update .gitignore to include *BKP* patterns ### GameRules interface - Add InitBoards(players, cfg) to allow games to construct their own board sets - Add detailed documentation explaining method responsibilities and usage - Improve MovePayload comment clarity ### TicTacToe updates - Implement InitBoards to produce a single `"tictactoe"` board - Replace all old references to `state.Board` with `state.Boards["tictactoe"]` - Make CheckGameOver, ValidateMove, and ApplyMove multi-board compatible ### Battleship updates - Implement InitBoards generating per-player ships + shots boards: - p0_ships, p0_shots - p1_ships, p1_shots ### Match flow updates - Boards are now created only when all players have joined - Initial state broadcast now includes `boards` instead of `board` This completes the backend migration for multi-board games and prepares the architecture for Battleship and other complex board-based games. --- plugins/games/battleship.go | 15 +++++++++++++++ plugins/games/rules.go | 21 ++++++++++++++++++++- plugins/games/tic_tac_toe.go | 30 +++++++++++++++++++++++++----- plugins/modules/match.go | 17 ++++------------- plugins/structs/match_state.go | 10 +++++----- 5 files changed, 69 insertions(+), 24 deletions(-) diff --git a/plugins/games/battleship.go b/plugins/games/battleship.go index f09f908..8af2289 100644 --- a/plugins/games/battleship.go +++ b/plugins/games/battleship.go @@ -62,6 +62,21 @@ 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 // ------------------------------ diff --git a/plugins/games/rules.go b/plugins/games/rules.go index dda93cd..7224eea 100644 --- a/plugins/games/rules.go +++ b/plugins/games/rules.go @@ -2,11 +2,17 @@ package games import "localrepo/plugins/structs" -// MovePayload is used for incoming move data from clients. +// MovePayload is the decoded payload sent from clients. +// It is intentionally untyped (map[string]interface{}) so each game +// can define its own move structure (e.g., row/col, coordinate, action type, etc.) type MovePayload struct { Data map[string]interface{} `json:"data"` } +// GameRules defines a generic interface for match logic. +// +// Each game (TicTacToe, Battleship, Chess, etc.) must implement this interface. +// The Nakama match handler delegates all game-specific behavior to these methods. type GameRules interface { // Number of players needed to start. MaxPlayers() int @@ -24,4 +30,17 @@ type GameRules interface { // -1 → draw // -2 → invalid ForfeitWinner(state *structs.MatchState, leaverIndex int) int + + // InitBoards initializes all the boards required for the game. + // + // This is called AFTER all players have joined the match. + // + // Examples: + // - TicTacToe → 1 board shared by both players: {"tictactoe": 3x3} + // - Battleship → 2 boards per player: + // {"p0_ships":10x10, "p0_shots":10x10, "p1_ships":..., "p1_shots":...} + // + // The returned map is stored in MatchState.Boards. + InitBoards(players []*structs.Player, cfg GameConfiguration) map[string]*structs.Board + } diff --git a/plugins/games/tic_tac_toe.go b/plugins/games/tic_tac_toe.go index 8a4ac23..ca151df 100644 --- a/plugins/games/tic_tac_toe.go +++ b/plugins/games/tic_tac_toe.go @@ -15,6 +15,12 @@ func (t *TicTacToeRules) MaxPlayers() int { return 2 } +func (t *TicTacToeRules) InitBoards(players []*structs.Player, cfg GameConfiguration) map[string]*structs.Board { + return map[string]*structs.Board{ + "tictactoe": structs.NewBoard(cfg.Board.Rows, cfg.Board.Cols), + } +} + // Assign player symbols: X and O func (t *TicTacToeRules) AssignPlayerSymbols(players []*structs.Player) { if len(players) < 2 { @@ -45,13 +51,18 @@ func (t *TicTacToeRules) ValidateMove(state *structs.MatchState, playerIdx int, r := int(row) c := int(col) + b := state.Boards["tictactoe"] + if b == nil { + return false + } + // bounds - if !state.Board.InBounds(r, c) { + if !b.InBounds(r, c) { return false } // empty? - return state.Board.IsEmpty(r, c) + return b.IsEmpty(r, c) } // ApplyMove writes X or O to the board. @@ -60,12 +71,17 @@ func (t *TicTacToeRules) ApplyMove( playerIdx int, payload MovePayload, ) (bool, bool, int) { + b := state.Boards["tictactoe"] + if b == nil { + return false, false, -1 + } + symbol := state.Players[playerIdx].Metadata["symbol"] r := int(payload.Data["row"].(float64)) c := int(payload.Data["col"].(float64)) - state.Board.Set(r, c, symbol) + b.Set(r, c, symbol) over, winner := t.CheckGameOver(state) @@ -75,8 +91,12 @@ func (t *TicTacToeRules) ApplyMove( // CheckGameOver determines win/draw state. func (t *TicTacToeRules) CheckGameOver(state *structs.MatchState) (bool, int) { + b := state.Boards["tictactoe"] + if b == nil { + return true, -1 // fallback safety + } - winnerSymbol := t.findWinner(state.Board) + winnerSymbol := t.findWinner(b) if winnerSymbol != "" { // find the player with this symbol @@ -88,7 +108,7 @@ func (t *TicTacToeRules) CheckGameOver(state *structs.MatchState) (bool, int) { return true, -1 } - if state.Board.Full() { + if b.Full() { return true, -1 // draw } diff --git a/plugins/modules/match.go b/plugins/modules/match.go index 3a9d4ea..eb859ff 100644 --- a/plugins/modules/match.go +++ b/plugins/modules/match.go @@ -63,18 +63,6 @@ func indexOfPlayerByID(players []*structs.Player, userID string) int { return -1 } -func newEmptyBoard(rows, cols int) *structs.Board { - b := &structs.Board{ - Rows: rows, - Cols: cols, - Grid: make([][]string, rows), - } - for r := 0; r < rows; r++ { - b.Grid[r] = make([]string, cols) - } - return b -} - // ------------------------- // Match interface methods // ------------------------- @@ -136,7 +124,7 @@ func (m *GenericMatch) MatchInit( // ---- 6. create initial state (board from config) ---- state := &structs.MatchState{ Players: []*structs.Player{}, - Board: newEmptyBoard(cfg.Board.Rows, cfg.Board.Cols), + Boards: map[string]*structs.Board{}, // empty, will be filled later Turn: 0, Winner: -1, GameOver: false, @@ -217,6 +205,9 @@ func (m *GenericMatch) MatchJoin( // Assign player symbols/colors/etc. Pass structs.Player directly. m.Rules.AssignPlayerSymbols(s.Players) + // Initialize boards using game rules + s.Boards = m.Rules.InitBoards(s.Players, m.Config) + // Broadcast initial state if data, err := json.Marshal(s); err == nil { if err := dispatcher.BroadcastMessage(OpState, data, nil, nil, true); err != nil { diff --git a/plugins/structs/match_state.go b/plugins/structs/match_state.go index 424787b..1ebd00e 100644 --- a/plugins/structs/match_state.go +++ b/plugins/structs/match_state.go @@ -2,9 +2,9 @@ package structs // MatchState holds the full game session state. type MatchState struct { - Players []*Player `json:"players"` - Board *Board `json:"board"` - Turn int `json:"turn"` // index in Players[] - Winner int `json:"winner"` // -1 = none, >=0 = winner index - GameOver bool `json:"game_over"` // true when the match ends + Players []*Player `json:"players"` + Boards map[string]*Board `json:"boards"` // Multiple named boards: + Turn int `json:"turn"` // index in Players[] + Winner int `json:"winner"` // -1 = none, >=0 = winner index + GameOver bool `json:"game_over"` // true when the match ends }