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 }