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.
This commit is contained in:
@@ -62,6 +62,21 @@ type BattleshipRules struct{}
|
|||||||
|
|
||||||
func (b *BattleshipRules) MaxPlayers() int { return 2 }
|
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
|
// Assign player boards
|
||||||
// ------------------------------
|
// ------------------------------
|
||||||
|
|||||||
@@ -2,11 +2,17 @@ package games
|
|||||||
|
|
||||||
import "localrepo/plugins/structs"
|
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 {
|
type MovePayload struct {
|
||||||
Data map[string]interface{} `json:"data"`
|
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 {
|
type GameRules interface {
|
||||||
// Number of players needed to start.
|
// Number of players needed to start.
|
||||||
MaxPlayers() int
|
MaxPlayers() int
|
||||||
@@ -24,4 +30,17 @@ type GameRules interface {
|
|||||||
// -1 → draw
|
// -1 → draw
|
||||||
// -2 → invalid
|
// -2 → invalid
|
||||||
ForfeitWinner(state *structs.MatchState, leaverIndex int) int
|
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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ func (t *TicTacToeRules) MaxPlayers() int {
|
|||||||
return 2
|
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
|
// Assign player symbols: X and O
|
||||||
func (t *TicTacToeRules) AssignPlayerSymbols(players []*structs.Player) {
|
func (t *TicTacToeRules) AssignPlayerSymbols(players []*structs.Player) {
|
||||||
if len(players) < 2 {
|
if len(players) < 2 {
|
||||||
@@ -45,13 +51,18 @@ func (t *TicTacToeRules) ValidateMove(state *structs.MatchState, playerIdx int,
|
|||||||
r := int(row)
|
r := int(row)
|
||||||
c := int(col)
|
c := int(col)
|
||||||
|
|
||||||
|
b := state.Boards["tictactoe"]
|
||||||
|
if b == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// bounds
|
// bounds
|
||||||
if !state.Board.InBounds(r, c) {
|
if !b.InBounds(r, c) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// empty?
|
// empty?
|
||||||
return state.Board.IsEmpty(r, c)
|
return b.IsEmpty(r, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApplyMove writes X or O to the board.
|
// ApplyMove writes X or O to the board.
|
||||||
@@ -60,12 +71,17 @@ func (t *TicTacToeRules) ApplyMove(
|
|||||||
playerIdx int,
|
playerIdx int,
|
||||||
payload MovePayload,
|
payload MovePayload,
|
||||||
) (bool, bool, int) {
|
) (bool, bool, int) {
|
||||||
|
b := state.Boards["tictactoe"]
|
||||||
|
if b == nil {
|
||||||
|
return false, false, -1
|
||||||
|
}
|
||||||
|
|
||||||
symbol := state.Players[playerIdx].Metadata["symbol"]
|
symbol := state.Players[playerIdx].Metadata["symbol"]
|
||||||
|
|
||||||
r := int(payload.Data["row"].(float64))
|
r := int(payload.Data["row"].(float64))
|
||||||
c := int(payload.Data["col"].(float64))
|
c := int(payload.Data["col"].(float64))
|
||||||
|
|
||||||
state.Board.Set(r, c, symbol)
|
b.Set(r, c, symbol)
|
||||||
|
|
||||||
over, winner := t.CheckGameOver(state)
|
over, winner := t.CheckGameOver(state)
|
||||||
|
|
||||||
@@ -75,8 +91,12 @@ func (t *TicTacToeRules) ApplyMove(
|
|||||||
|
|
||||||
// CheckGameOver determines win/draw state.
|
// CheckGameOver determines win/draw state.
|
||||||
func (t *TicTacToeRules) CheckGameOver(state *structs.MatchState) (bool, int) {
|
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 != "" {
|
if winnerSymbol != "" {
|
||||||
// find the player with this symbol
|
// find the player with this symbol
|
||||||
@@ -88,7 +108,7 @@ func (t *TicTacToeRules) CheckGameOver(state *structs.MatchState) (bool, int) {
|
|||||||
return true, -1
|
return true, -1
|
||||||
}
|
}
|
||||||
|
|
||||||
if state.Board.Full() {
|
if b.Full() {
|
||||||
return true, -1 // draw
|
return true, -1 // draw
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,18 +63,6 @@ func indexOfPlayerByID(players []*structs.Player, userID string) int {
|
|||||||
return -1
|
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
|
// Match interface methods
|
||||||
// -------------------------
|
// -------------------------
|
||||||
@@ -136,7 +124,7 @@ func (m *GenericMatch) MatchInit(
|
|||||||
// ---- 6. create initial state (board from config) ----
|
// ---- 6. create initial state (board from config) ----
|
||||||
state := &structs.MatchState{
|
state := &structs.MatchState{
|
||||||
Players: []*structs.Player{},
|
Players: []*structs.Player{},
|
||||||
Board: newEmptyBoard(cfg.Board.Rows, cfg.Board.Cols),
|
Boards: map[string]*structs.Board{}, // empty, will be filled later
|
||||||
Turn: 0,
|
Turn: 0,
|
||||||
Winner: -1,
|
Winner: -1,
|
||||||
GameOver: false,
|
GameOver: false,
|
||||||
@@ -217,6 +205,9 @@ func (m *GenericMatch) MatchJoin(
|
|||||||
// Assign player symbols/colors/etc. Pass structs.Player directly.
|
// Assign player symbols/colors/etc. Pass structs.Player directly.
|
||||||
m.Rules.AssignPlayerSymbols(s.Players)
|
m.Rules.AssignPlayerSymbols(s.Players)
|
||||||
|
|
||||||
|
// Initialize boards using game rules
|
||||||
|
s.Boards = m.Rules.InitBoards(s.Players, m.Config)
|
||||||
|
|
||||||
// Broadcast initial state
|
// Broadcast initial state
|
||||||
if data, err := json.Marshal(s); err == nil {
|
if data, err := json.Marshal(s); err == nil {
|
||||||
if err := dispatcher.BroadcastMessage(OpState, data, nil, nil, true); err != nil {
|
if err := dispatcher.BroadcastMessage(OpState, data, nil, nil, true); err != nil {
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ package structs
|
|||||||
|
|
||||||
// MatchState holds the full game session state.
|
// MatchState holds the full game session state.
|
||||||
type MatchState struct {
|
type MatchState struct {
|
||||||
Players []*Player `json:"players"`
|
Players []*Player `json:"players"`
|
||||||
Board *Board `json:"board"`
|
Boards map[string]*Board `json:"boards"` // Multiple named boards:
|
||||||
Turn int `json:"turn"` // index in Players[]
|
Turn int `json:"turn"` // index in Players[]
|
||||||
Winner int `json:"winner"` // -1 = none, >=0 = winner index
|
Winner int `json:"winner"` // -1 = none, >=0 = winner index
|
||||||
GameOver bool `json:"game_over"` // true when the match ends
|
GameOver bool `json:"game_over"` // true when the match ends
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user