Compare commits
5 Commits
v1.1.0
...
multi-game
| Author | SHA1 | Date | |
|---|---|---|---|
| f1e85a72dd | |||
| 0562d1e0c9 | |||
| 10c7933aca | |||
| d75dcd3c74 | |||
| bcdc5faea5 |
@@ -6,6 +6,14 @@ import (
|
|||||||
"localrepo/plugins/structs"
|
"localrepo/plugins/structs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var Fleet = map[string]int{
|
||||||
|
"carrier": 5,
|
||||||
|
"battleship": 4,
|
||||||
|
"cruiser": 3,
|
||||||
|
"submarine": 3,
|
||||||
|
"destroyer": 2,
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// BATTLESHIP RULES IMPLEMENTATION
|
// BATTLESHIP RULES IMPLEMENTATION
|
||||||
//
|
//
|
||||||
@@ -62,26 +70,35 @@ 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
|
||||||
// ------------------------------
|
// ------------------------------
|
||||||
func (b *BattleshipRules) AssignPlayerSymbols(players []*structs.Player) {
|
func (b *BattleshipRules) AssignPlayerSymbols(players []*structs.Player) {
|
||||||
// Battleship has no symbols like X/O,
|
// nothing needed for battleship
|
||||||
// but we use this hook to initialize per-player boards.
|
}
|
||||||
|
|
||||||
for _, p := range players {
|
// ------------------------------
|
||||||
// 10x10 boards
|
// Attach Game Metadata
|
||||||
empty := make([][]string, 10)
|
// ------------------------------
|
||||||
for r := range empty {
|
func (b *BattleshipRules) AttachGameMetadata(state *structs.MatchState) {
|
||||||
empty[r] = make([]string, 10)
|
state.Metadata["phase"] = "placement"
|
||||||
}
|
state.Metadata["p0_ready"] = false
|
||||||
|
state.Metadata["p1_ready"] = false
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------
|
// ------------------------------
|
||||||
@@ -90,72 +107,226 @@ func (b *BattleshipRules) AssignPlayerSymbols(players []*structs.Player) {
|
|||||||
// ------------------------------
|
// ------------------------------
|
||||||
|
|
||||||
func (b *BattleshipRules) ValidateMove(state *structs.MatchState, playerIdx int, payload MovePayload) bool {
|
func (b *BattleshipRules) ValidateMove(state *structs.MatchState, playerIdx int, payload MovePayload) bool {
|
||||||
rF, ok1 := payload.Data["row"].(float64)
|
switch payload.Action {
|
||||||
cF, ok2 := payload.Data["col"].(float64)
|
case "place":
|
||||||
|
return b.ValidatePlacementMove(state, playerIdx, payload)
|
||||||
|
case "shoot":
|
||||||
|
return b.ValidateShotMove(state, playerIdx, payload)
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BattleshipRules) ValidatePlacementMove(state *structs.MatchState, playerIdx int, payload MovePayload) bool {
|
||||||
|
// Allow placement until player placed all ships
|
||||||
|
if state.Metadata["phase"] != "placement" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
key := fmt.Sprintf("p%d_placed", playerIdx)
|
||||||
|
placed := 0
|
||||||
|
if state.Metadata[key] != nil {
|
||||||
|
placed = state.Metadata[key].(int)
|
||||||
|
}
|
||||||
|
return placed < len(Fleet)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BattleshipRules) ValidateShotMove(state *structs.MatchState, playerIdx int, payload MovePayload) bool {
|
||||||
|
if state.Metadata["phase"] != "battle" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
rf, ok1 := payload.Data["row"].(float64)
|
||||||
|
cf, ok2 := payload.Data["col"].(float64)
|
||||||
if !ok1 || !ok2 {
|
if !ok1 || !ok2 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
r := int(rF)
|
r := int(rf)
|
||||||
c := int(cF)
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this spot was already shot before
|
if !shotBoard.InBounds(r, c) {
|
||||||
shotBoard := decodeBoard(state.Players[playerIdx].Metadata["shot_board"])
|
return false
|
||||||
return shotBoard[r][c] == ""
|
}
|
||||||
|
|
||||||
|
if !shotBoard.IsEmpty(r, c) { // already shot
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------
|
// -----------------------------
|
||||||
// ApplyMove
|
// APPLY MOVE (MODE B — CLASSIC)
|
||||||
// ------------------------------
|
// -----------------------------
|
||||||
func (b *BattleshipRules) ApplyMove(
|
func (b *BattleshipRules) ApplyShot(
|
||||||
state *structs.MatchState,
|
state *structs.MatchState,
|
||||||
playerIdx int,
|
playerIdx int,
|
||||||
payload MovePayload,
|
payload MovePayload,
|
||||||
) (bool, bool, int) {
|
) (bool, bool, int, bool) {
|
||||||
attacker := state.Players[playerIdx]
|
if !b.bothPlayersReady(state) {
|
||||||
defenderIdx := 1 - playerIdx
|
return false, false, -1, false
|
||||||
defender := state.Players[defenderIdx]
|
}
|
||||||
|
|
||||||
r := int(payload.Data["row"].(float64))
|
r := int(payload.Data["row"].(float64))
|
||||||
c := int(payload.Data["col"].(float64))
|
c := int(payload.Data["col"].(float64))
|
||||||
|
|
||||||
shotBoard := decodeBoard(attacker.Metadata["shot_board"])
|
shotKey := fmt.Sprintf("p%d_shots", playerIdx)
|
||||||
shipBoard := decodeBoard(defender.Metadata["ship_board"])
|
shipKey := fmt.Sprintf("p%d_ships", 1-playerIdx)
|
||||||
|
|
||||||
if shipBoard[r][c] == "S" {
|
shots := state.Boards[shotKey]
|
||||||
shotBoard[r][c] = "H"
|
ships := state.Boards[shipKey]
|
||||||
shipBoard[r][c] = "X"
|
|
||||||
|
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 {
|
} else {
|
||||||
shotBoard[r][c] = "M"
|
shots.Set(r, c, "M")
|
||||||
}
|
}
|
||||||
|
|
||||||
attacker.Metadata["shot_board"] = encodeBoard(shotBoard)
|
// check game over
|
||||||
defender.Metadata["ship_board"] = encodeBoard(shipBoard)
|
|
||||||
|
|
||||||
over, winner := b.CheckGameOver(state)
|
over, winner := b.CheckGameOver(state)
|
||||||
|
|
||||||
return true, over, winner
|
// keepTurn = hit (classic rule)
|
||||||
|
return true, over, winner, hit
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BattleshipRules) ApplyMove(
|
||||||
|
state *structs.MatchState,
|
||||||
|
playerIdx int,
|
||||||
|
payload MovePayload,
|
||||||
|
) (bool, bool, int, bool) {
|
||||||
|
|
||||||
|
switch payload.Action {
|
||||||
|
case "place":
|
||||||
|
return b.ApplyPlacement(state, playerIdx, payload)
|
||||||
|
case "shoot":
|
||||||
|
return b.ApplyShot(state, playerIdx, payload)
|
||||||
|
default:
|
||||||
|
return false, false, -1, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BattleshipRules) ApplyPlacement(
|
||||||
|
state *structs.MatchState,
|
||||||
|
playerIdx int,
|
||||||
|
payload MovePayload,
|
||||||
|
) (bool, bool, int, bool) {
|
||||||
|
|
||||||
|
shipName, _ := payload.Data["ship"].(string)
|
||||||
|
rf, _ := payload.Data["row"].(float64)
|
||||||
|
cf, _ := payload.Data["col"].(float64)
|
||||||
|
dir, _ := payload.Data["dir"].(string)
|
||||||
|
|
||||||
|
r := int(rf)
|
||||||
|
c := int(cf)
|
||||||
|
|
||||||
|
size, ok := Fleet[shipName]
|
||||||
|
if !ok {
|
||||||
|
return false, false, -1, false // invalid ship name
|
||||||
|
}
|
||||||
|
|
||||||
|
shipKey := fmt.Sprintf("p%d_ships", playerIdx)
|
||||||
|
shipBoard := state.Boards[shipKey]
|
||||||
|
|
||||||
|
// Validate placement
|
||||||
|
if !b.validatePlacement(shipBoard, r, c, size, dir) {
|
||||||
|
return false, false, -1, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Place the ship
|
||||||
|
if dir == "h" {
|
||||||
|
for i := 0; i < size; i++ {
|
||||||
|
shipBoard.Set(r, c+i, "S")
|
||||||
|
}
|
||||||
|
} else { // vertical
|
||||||
|
for i := 0; i < size; i++ {
|
||||||
|
shipBoard.Set(r+i, c, "S")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track ships placed by player
|
||||||
|
placedCountKey := fmt.Sprintf("p%d_placed", playerIdx)
|
||||||
|
count := state.Metadata[placedCountKey]
|
||||||
|
if count == nil {
|
||||||
|
state.Metadata[placedCountKey] = 1
|
||||||
|
} else {
|
||||||
|
state.Metadata[placedCountKey] = count.(int) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all 5 ships placed → ready
|
||||||
|
if state.Metadata[placedCountKey].(int) == len(Fleet) {
|
||||||
|
readyKey := fmt.Sprintf("p%d_ready", playerIdx)
|
||||||
|
state.Metadata[readyKey] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if both players are ready
|
||||||
|
if b.bothPlayersReady(state) {
|
||||||
|
state.Metadata["phase"] = "battle"
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, false, -1, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BattleshipRules) validatePlacement(board *structs.Board, r, c, size int, dir string) bool {
|
||||||
|
rows, cols := board.Rows, board.Cols
|
||||||
|
|
||||||
|
if dir == "h" {
|
||||||
|
if c+size > cols {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := 0; i < size; i++ {
|
||||||
|
if board.Get(r, c+i) != "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if r+size > rows {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := 0; i < size; i++ {
|
||||||
|
if board.Get(r+i, c) != "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BattleshipRules) bothPlayersReady(state *structs.MatchState) bool {
|
||||||
|
r0 := state.Metadata["p0_ready"]
|
||||||
|
r1 := state.Metadata["p1_ready"]
|
||||||
|
|
||||||
|
return r0 == true && r1 == true
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------
|
// ------------------------------
|
||||||
// CheckGameOver
|
// CheckGameOver
|
||||||
// ------------------------------
|
// ------------------------------
|
||||||
func (b *BattleshipRules) CheckGameOver(state *structs.MatchState) (bool, int) {
|
func (b *BattleshipRules) CheckGameOver(state *structs.MatchState) (bool, int) {
|
||||||
|
for i := range state.Players {
|
||||||
for i, p := range state.Players {
|
shipKey := fmt.Sprintf("p%d_ships", i)
|
||||||
ships := decodeBoard(p.Metadata["ship_board"])
|
ships := state.Boards[shipKey]
|
||||||
|
|
||||||
alive := false
|
alive := false
|
||||||
for r := range ships {
|
for r := 0; r < ships.Rows; r++ {
|
||||||
for c := range ships[r] {
|
for c := 0; c < ships.Cols; c++ {
|
||||||
if ships[r][c] == "S" {
|
if ships.Get(r, c) == "S" {
|
||||||
alive = true
|
alive = true
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if alive {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !alive {
|
if !alive {
|
||||||
|
|||||||
@@ -2,11 +2,18 @@ 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"`
|
Action string `json:"action"` // "place" or "shoot"
|
||||||
|
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
|
||||||
@@ -14,9 +21,15 @@ type GameRules interface {
|
|||||||
// Assign symbols/colors/pieces at start.
|
// Assign symbols/colors/pieces at start.
|
||||||
AssignPlayerSymbols(players []*structs.Player)
|
AssignPlayerSymbols(players []*structs.Player)
|
||||||
|
|
||||||
|
// Attach Game Metadata
|
||||||
|
AttachGameMetadata(state *structs.MatchState)
|
||||||
// Apply a move.
|
// Apply a move.
|
||||||
// Returns: (changed, gameOver, winnerIndex)
|
// 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?
|
// If a player leaves, who wins?
|
||||||
// Return:
|
// Return:
|
||||||
@@ -24,4 +37,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 {
|
||||||
@@ -25,6 +31,13 @@ func (t *TicTacToeRules) AssignPlayerSymbols(players []*structs.Player) {
|
|||||||
players[1].Metadata["symbol"] = "O"
|
players[1].Metadata["symbol"] = "O"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------
|
||||||
|
// Attach Game Metadata
|
||||||
|
// ------------------------------
|
||||||
|
func (b *TicTacToeRules) AttachGameMetadata(state *structs.MatchState) {
|
||||||
|
// nothing needed for tictactoe
|
||||||
|
}
|
||||||
|
|
||||||
// ValidateMove checks bounds and empty cell.
|
// ValidateMove checks bounds and empty cell.
|
||||||
func (t *TicTacToeRules) ValidateMove(state *structs.MatchState, playerIdx int, payload MovePayload) bool {
|
func (t *TicTacToeRules) ValidateMove(state *structs.MatchState, playerIdx int, payload MovePayload) bool {
|
||||||
|
|
||||||
@@ -45,13 +58,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.
|
||||||
@@ -59,24 +77,33 @@ func (t *TicTacToeRules) ApplyMove(
|
|||||||
state *structs.MatchState,
|
state *structs.MatchState,
|
||||||
playerIdx int,
|
playerIdx int,
|
||||||
payload MovePayload,
|
payload MovePayload,
|
||||||
) (bool, bool, int) {
|
) (bool, bool, int, bool) {
|
||||||
|
b := state.Boards["tictactoe"]
|
||||||
|
if b == nil {
|
||||||
|
return false, false, -1, false
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
return true, over, winner
|
return true, over, winner, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 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 +115,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,11 +124,15 @@ 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,
|
||||||
|
Metadata: map[string]interface{}{},
|
||||||
}
|
}
|
||||||
|
state.Metadata["game"] = m.GameName
|
||||||
|
state.Metadata["mode"] = m.Mode
|
||||||
|
m.Rules.AttachGameMetadata(state)
|
||||||
|
|
||||||
label := fmt.Sprintf("%s:%s", m.GameName, m.Mode)
|
label := fmt.Sprintf("%s:%s", m.GameName, m.Mode)
|
||||||
|
|
||||||
@@ -217,6 +209,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 {
|
||||||
@@ -321,13 +316,14 @@ func (m *GenericMatch) MatchLoop(
|
|||||||
|
|
||||||
// Turn enforcement — keep this here for turn-based games. If you want per-game control,
|
// Turn enforcement — keep this here for turn-based games. If you want per-game control,
|
||||||
// move this check into the game's ApplyMove implementation or toggle via config.
|
// move this check into the game's ApplyMove implementation or toggle via config.
|
||||||
if playerIdx != s.Turn {
|
phase := s.Metadata["phase"]
|
||||||
|
if phase == "battle" && playerIdx != s.Turn {
|
||||||
logger.Warn("Move rejected: not player's turn (idx=%d turn=%d)", playerIdx, s.Turn)
|
logger.Warn("Move rejected: not player's turn (idx=%d turn=%d)", playerIdx, s.Turn)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delegate to rules.ApplyMove which returns (changed, gameOver, winnerIndex)
|
// 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 {
|
if stateChanged {
|
||||||
changed = true
|
changed = true
|
||||||
@@ -337,7 +333,7 @@ func (m *GenericMatch) MatchLoop(
|
|||||||
s.GameOver = true
|
s.GameOver = true
|
||||||
s.Winner = winnerIdx
|
s.Winner = winnerIdx
|
||||||
} else {
|
} else {
|
||||||
if len(s.Players) > 0 {
|
if !keepTurn && len(s.Players) > 0 {
|
||||||
s.Turn = (s.Turn + 1) % len(s.Players)
|
s.Turn = (s.Turn + 1) % len(s.Players)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -347,6 +343,7 @@ func (m *GenericMatch) MatchLoop(
|
|||||||
|
|
||||||
if changed {
|
if changed {
|
||||||
if data, err := json.Marshal(s); err == nil {
|
if data, err := json.Marshal(s); err == nil {
|
||||||
|
logger.Info("Broadcasting state update (op=%d): %v", OpState, data)
|
||||||
if err := dispatcher.BroadcastMessage(OpState, data, nil, nil, true); err != nil {
|
if err := dispatcher.BroadcastMessage(OpState, data, nil, nil, true); err != nil {
|
||||||
logger.Error("BroadcastMessage failed: %v", err)
|
logger.Error("BroadcastMessage failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ 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
|
||||||
|
Metadata map[string]interface{} `json:"metadata"` // metadata
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user