feat(games): unify ApplyMove signatures + remove unused player conversion
Updated TicTacToeRules and BattleshipRules to implement ApplyMove(state, playerIdx, payload) (bool, bool, int) as required by GameRules. Added win/draw resolution logic directly inside each game’s ApplyMove return. Removed obsolete convertToGamePlayers helper. Updated GenericMatch to call AssignPlayerSymbols with []*structs.Player directly. Ensured all rule implementations now fully satisfy the GameRules interface.
This commit is contained in:
@@ -2,6 +2,7 @@ package games
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"encoding/json"
|
||||
"localrepo/plugins/structs"
|
||||
)
|
||||
|
||||
@@ -64,7 +65,7 @@ func (b *BattleshipRules) MaxPlayers() int { return 2 }
|
||||
// ------------------------------
|
||||
// Assign player boards
|
||||
// ------------------------------
|
||||
func (b *BattleshipRules) AssignPlayerSymbols(players []*Player) {
|
||||
func (b *BattleshipRules) AssignPlayerSymbols(players []*structs.Player) {
|
||||
// Battleship has no symbols like X/O,
|
||||
// but we use this hook to initialize per-player boards.
|
||||
|
||||
@@ -110,30 +111,34 @@ func (b *BattleshipRules) ValidateMove(state *structs.MatchState, playerIdx int,
|
||||
// ------------------------------
|
||||
// ApplyMove
|
||||
// ------------------------------
|
||||
func (b *BattleshipRules) ApplyMove(state *structs.MatchState, playerIdx int, payload MovePayload) {
|
||||
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]
|
||||
|
||||
attacker := state.Players[playerIdx]
|
||||
defenderIdx := 1 - playerIdx
|
||||
defender := state.Players[defenderIdx]
|
||||
r := int(payload.Data["row"].(float64))
|
||||
c := int(payload.Data["col"].(float64))
|
||||
|
||||
r := int(payload.Data["row"].(float64))
|
||||
c := int(payload.Data["col"].(float64))
|
||||
shotBoard := decodeBoard(attacker.Metadata["shot_board"])
|
||||
shipBoard := decodeBoard(defender.Metadata["ship_board"])
|
||||
|
||||
shotBoard := decodeBoard(attacker.Metadata["shot_board"])
|
||||
shipBoard := decodeBoard(defender.Metadata["ship_board"])
|
||||
if shipBoard[r][c] == "S" {
|
||||
shotBoard[r][c] = "H"
|
||||
shipBoard[r][c] = "X"
|
||||
} else {
|
||||
shotBoard[r][c] = "M"
|
||||
}
|
||||
|
||||
if shipBoard[r][c] == "S" {
|
||||
// hit
|
||||
shotBoard[r][c] = "H"
|
||||
shipBoard[r][c] = "X" // ship cell destroyed
|
||||
} else {
|
||||
// miss
|
||||
shotBoard[r][c] = "M"
|
||||
}
|
||||
attacker.Metadata["shot_board"] = encodeBoard(shotBoard)
|
||||
defender.Metadata["ship_board"] = encodeBoard(shipBoard)
|
||||
|
||||
// Save back
|
||||
attacker.Metadata["shot_board"] = encodeBoard(shotBoard)
|
||||
defender.Metadata["ship_board"] = encodeBoard(shipBoard)
|
||||
over, winner := b.CheckGameOver(state)
|
||||
|
||||
return true, over, winner
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
|
||||
@@ -12,7 +12,7 @@ type GameRules interface {
|
||||
MaxPlayers() int
|
||||
|
||||
// Assign symbols/colors/pieces at start.
|
||||
AssignPlayerSymbols(players []*Player)
|
||||
AssignPlayerSymbols(players []*structs.Player)
|
||||
|
||||
// Apply a move.
|
||||
// Returns: (changed, gameOver, winnerIndex)
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package games
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"localrepo/plugins/structs"
|
||||
)
|
||||
|
||||
@@ -18,7 +16,7 @@ func (t *TicTacToeRules) MaxPlayers() int {
|
||||
}
|
||||
|
||||
// Assign player symbols: X and O
|
||||
func (t *TicTacToeRules) AssignPlayerSymbols(players []*Player) {
|
||||
func (t *TicTacToeRules) AssignPlayerSymbols(players []*structs.Player) {
|
||||
if len(players) < 2 {
|
||||
return
|
||||
}
|
||||
@@ -57,16 +55,24 @@ func (t *TicTacToeRules) ValidateMove(state *structs.MatchState, playerIdx int,
|
||||
}
|
||||
|
||||
// ApplyMove writes X or O to the board.
|
||||
func (t *TicTacToeRules) ApplyMove(state *structs.MatchState, playerIdx int, payload MovePayload) {
|
||||
func (t *TicTacToeRules) ApplyMove(
|
||||
state *structs.MatchState,
|
||||
playerIdx int,
|
||||
payload MovePayload,
|
||||
) (bool, bool, int) {
|
||||
symbol := state.Players[playerIdx].Metadata["symbol"]
|
||||
|
||||
symbol := state.Players[playerIdx].Metadata["symbol"]
|
||||
r := int(payload.Data["row"].(float64))
|
||||
c := int(payload.Data["col"].(float64))
|
||||
|
||||
r := int(payload.Data["row"].(float64))
|
||||
c := int(payload.Data["col"].(float64))
|
||||
state.Board.Set(r, c, symbol)
|
||||
|
||||
state.Board.Set(r, c, symbol)
|
||||
over, winner := t.CheckGameOver(state)
|
||||
|
||||
return true, over, winner
|
||||
}
|
||||
|
||||
|
||||
// CheckGameOver determines win/draw state.
|
||||
func (t *TicTacToeRules) CheckGameOver(state *structs.MatchState) (bool, int) {
|
||||
|
||||
|
||||
@@ -22,11 +22,6 @@ func InitModule(
|
||||
//--------------------------------------------------------
|
||||
// 1. Register RPCs
|
||||
//--------------------------------------------------------
|
||||
if err := initializer.RegisterRpc("hello_world", HelloWorld); err != nil {
|
||||
logger.Error("Failed to register RPC hello_world: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := initializer.RegisterRpc("leave_matchmaking", modules.RpcLeaveMatchmaking); err != nil {
|
||||
logger.Error("Failed to register RPC leave_matchmaking: %v", err)
|
||||
return err
|
||||
|
||||
@@ -19,6 +19,9 @@ const (
|
||||
// GenericMatch is a match implementation that delegates game-specific logic
|
||||
// to a game.GameRules implementation chosen by the match params ("game").
|
||||
type GenericMatch struct {
|
||||
// Registry provided when creating the match factory. Keeps available rules.
|
||||
Registry map[string]games.GameRules
|
||||
|
||||
GameName string
|
||||
Mode string
|
||||
Config games.GameConfiguration
|
||||
@@ -28,7 +31,7 @@ type GenericMatch struct {
|
||||
// NewGenericMatch returns a factory function suitable for RegisterMatch.
|
||||
// Provide a registry mapping game names (strings) to implementations.
|
||||
func NewGenericMatch(
|
||||
registry map[string]games.GameRules
|
||||
registry map[string]games.GameRules,
|
||||
) func(
|
||||
ctx context.Context,
|
||||
logger runtime.Logger,
|
||||
@@ -36,9 +39,14 @@ func NewGenericMatch(
|
||||
nk runtime.NakamaModule,
|
||||
) (runtime.Match, error) {
|
||||
|
||||
return func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule) (runtime.Match, error) {
|
||||
// Empty match instance; MatchInit will fill real config.
|
||||
return &GenericMatch{}, nil
|
||||
return func(
|
||||
ctx context.Context,
|
||||
logger runtime.Logger,
|
||||
db *sql.DB,
|
||||
nk runtime.NakamaModule,
|
||||
) (runtime.Match, error) {
|
||||
// The factory stores the registry on each match instance so MatchInit can use it.
|
||||
return &GenericMatch{Registry: registry}, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +79,7 @@ func newEmptyBoard(rows, cols int) *structs.Board {
|
||||
// Match interface methods
|
||||
// -------------------------
|
||||
|
||||
// MatchInit: create initial state. Expects params to include "game" (string) and optionally "mode" and board size.
|
||||
// MatchInit: create initial state. Expects params to include "game" (string) and optionally "mode".
|
||||
func (m *GenericMatch) MatchInit(
|
||||
ctx context.Context,
|
||||
logger runtime.Logger,
|
||||
@@ -92,31 +100,40 @@ func (m *GenericMatch) MatchInit(
|
||||
return nil, 0, ""
|
||||
}
|
||||
|
||||
// ---- 2. config lookup ----
|
||||
cfg, found := games.GameConfig[gameName]
|
||||
if !found {
|
||||
logger.Error("MatchInit ERROR: game '%s' not in GameConfig", gameName)
|
||||
return nil, 0, ""
|
||||
}
|
||||
|
||||
rules, found := games.RulesRegistry[gameName]
|
||||
if !found {
|
||||
logger.Warn("GenericMatch MatchInit: unknown game '%s' — match will start but no rules attached", gameName)
|
||||
// ---- 3. rules lookup from registry (factory-provided) ----
|
||||
var rules games.GameRules
|
||||
if m.Registry != nil {
|
||||
if r, ok := m.Registry[gameName]; ok {
|
||||
rules = r
|
||||
}
|
||||
}
|
||||
|
||||
if rules == nil {
|
||||
// no rules — abort match creation
|
||||
logger.Error("MatchInit ERROR: no rules registered for game '%s'", gameName)
|
||||
return nil, 0, ""
|
||||
}
|
||||
|
||||
// ---- 2. mode (optional) ----
|
||||
// ---- 4. mode (optional) ----
|
||||
mode := "default"
|
||||
if md, ok := params["mode"].(string); ok && md != "" {
|
||||
mode = md
|
||||
}
|
||||
|
||||
// ---- 3. build match instance fields ----
|
||||
// ---- 5. build match instance fields ----
|
||||
m.GameName = gameName
|
||||
m.Mode = mode
|
||||
m.Config = cfg
|
||||
m.Rules = rules
|
||||
|
||||
// ---- 4. create initial state ----
|
||||
// ---- 6. create initial state (board from config) ----
|
||||
state := &structs.MatchState{
|
||||
Players: []*structs.Player{},
|
||||
Board: newEmptyBoard(cfg.Board.Rows, cfg.Board.Cols),
|
||||
@@ -128,13 +145,12 @@ func (m *GenericMatch) MatchInit(
|
||||
label := fmt.Sprintf("%s:%s", m.GameName, m.Mode)
|
||||
|
||||
logger.Info("MatchInit OK — game=%s mode=%s players=%d board=%dx%d", m.GameName, m.Mode, cfg.Players, cfg.Board.Rows, cfg.Board.Cols)
|
||||
// Store the gameName in the match's Registry via a reserved key? Simpler: keep label informative and rely on Registry lookup by gameName later.
|
||||
|
||||
// Tick rate 5 (200ms) is a sensible default; can be tuned per game.
|
||||
return state, 5, label
|
||||
}
|
||||
|
||||
// MatchJoinAttempt: basic capacity check using rules.MaxPlayers()
|
||||
// MatchJoinAttempt: basic capacity check using config.Players
|
||||
func (m *GenericMatch) MatchJoinAttempt(
|
||||
ctx context.Context,
|
||||
logger runtime.Logger,
|
||||
@@ -149,6 +165,12 @@ func (m *GenericMatch) MatchJoinAttempt(
|
||||
|
||||
s := state.(*structs.MatchState)
|
||||
|
||||
if m.Config.Players <= 0 {
|
||||
// defensive: require init to have populated config
|
||||
logger.Error("MatchJoinAttempt ERROR: match config not initialized")
|
||||
return s, false, "server error"
|
||||
}
|
||||
|
||||
if len(s.Players) >= m.Config.Players {
|
||||
return s, false, "match full"
|
||||
}
|
||||
@@ -189,16 +211,20 @@ func (m *GenericMatch) MatchJoin(
|
||||
})
|
||||
}
|
||||
|
||||
if len(s.Players) == m.Config.Players {
|
||||
// Assign player symbols/colors/etc.
|
||||
logger.Info("MatchJoin: now %d players (need %d)", len(s.Players), m.Config.Players)
|
||||
|
||||
if m.Rules != nil && len(s.Players) == m.Config.Players {
|
||||
// Assign player symbols/colors/etc. Pass structs.Player directly.
|
||||
m.Rules.AssignPlayerSymbols(s.Players)
|
||||
|
||||
// Broadcast initial state
|
||||
if data, err := json.Marshal(s); err == nil {
|
||||
if err := dispatcher.BroadcastMessage(OpState, data, nil, nil, true); err != nil {
|
||||
logger.Error("BroadcastMessage (initial state) failed: %v", err)
|
||||
}
|
||||
}
|
||||
if err := dispatcher.BroadcastMessage(OpState, data, nil, nil, true); err != nil {
|
||||
logger.Error("BroadcastMessage (initial state) failed: %v", err)
|
||||
}
|
||||
} else {
|
||||
logger.Error("Failed to marshal initial state: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return s
|
||||
@@ -228,26 +254,29 @@ func (m *GenericMatch) MatchLeave(
|
||||
leaverIdx = indexOfPlayerByID(s.Players, presences[0].GetUserId())
|
||||
}
|
||||
|
||||
winner := m.Rules.ForfeitWinner(s, leaverIdx)
|
||||
s.Winner = winner
|
||||
s.GameOver = true
|
||||
if m.Rules != nil {
|
||||
winner := m.Rules.ForfeitWinner(s, leaverIdx)
|
||||
s.Winner = winner
|
||||
s.GameOver = true
|
||||
} else {
|
||||
// fallback: end match as forfeit
|
||||
s.GameOver = true
|
||||
s.Winner = -1
|
||||
}
|
||||
|
||||
// broadcast final state
|
||||
if data, err := json.Marshal(s); err == nil {
|
||||
if err := dispatcher.BroadcastMessage(OpState, data, nil, nil, true); err != nil {
|
||||
logger.Error("BroadcastMessage (forfeit) failed: %v", err)
|
||||
}
|
||||
} else {
|
||||
logger.Error("Failed to marshal forfeit state: %v", err)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
//
|
||||
// ──────────────────────────────────────────────────────────
|
||||
// MatchLoop
|
||||
// ──────────────────────────────────────────────────────────
|
||||
//
|
||||
|
||||
// MatchLoop: handle incoming move messages, delegate to the GameRules implementation
|
||||
func (m *GenericMatch) MatchLoop(
|
||||
ctx context.Context,
|
||||
logger runtime.Logger,
|
||||
@@ -267,6 +296,11 @@ func (m *GenericMatch) MatchLoop(
|
||||
|
||||
changed := false
|
||||
|
||||
if m.Rules == nil {
|
||||
logger.Warn("MatchLoop: no rules present for game '%s' -- ignoring messages", m.GameName)
|
||||
return s
|
||||
}
|
||||
|
||||
for _, msg := range messages {
|
||||
if msg.GetOpCode() != OpMove {
|
||||
continue
|
||||
@@ -285,13 +319,15 @@ func (m *GenericMatch) MatchLoop(
|
||||
continue
|
||||
}
|
||||
|
||||
// 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.
|
||||
if playerIdx != s.Turn {
|
||||
logger.Warn("Move rejected: not player's turn (idx=%d turn=%d)", playerIdx, s.Turn)
|
||||
continue
|
||||
}
|
||||
|
||||
// Delegate to rules.ApplyMove which returns (changed, gameOver, winnerIndex)
|
||||
stateChanged, gameOver, winnerIdx := rules.ApplyMove(s, playerIdx, payload)
|
||||
stateChanged, gameOver, winnerIdx := m.Rules.ApplyMove(s, playerIdx, payload)
|
||||
|
||||
if stateChanged {
|
||||
changed = true
|
||||
@@ -301,13 +337,13 @@ func (m *GenericMatch) MatchLoop(
|
||||
s.GameOver = true
|
||||
s.Winner = winnerIdx
|
||||
} else {
|
||||
s.Turn = (s.Turn + 1) % len(s.Players)
|
||||
if len(s.Players) > 0 {
|
||||
s.Turn = (s.Turn + 1) % len(s.Players)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// optional: if gameOver and winnerIdx >= 0, write leaderboard here if desired
|
||||
// This code left intentionally out — you can add leaderboard writes by asking for that feature.
|
||||
}
|
||||
// Optional: handle leaderboard logic here if needed
|
||||
|
||||
if changed {
|
||||
if data, err := json.Marshal(s); err == nil {
|
||||
|
||||
Reference in New Issue
Block a user