diff --git a/plugins/games/battleship.go b/plugins/games/battleship.go index 12333ba..f09f908 100644 --- a/plugins/games/battleship.go +++ b/plugins/games/battleship.go @@ -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 } // ------------------------------ diff --git a/plugins/games/rules.go b/plugins/games/rules.go index 8e9e7ac..dda93cd 100644 --- a/plugins/games/rules.go +++ b/plugins/games/rules.go @@ -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) diff --git a/plugins/games/tic_tac_toe.go b/plugins/games/tic_tac_toe.go index 5b8eeef..8a4ac23 100644 --- a/plugins/games/tic_tac_toe.go +++ b/plugins/games/tic_tac_toe.go @@ -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) { diff --git a/plugins/main.go b/plugins/main.go index 36bae2a..6a5bf0a 100644 --- a/plugins/main.go +++ b/plugins/main.go @@ -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 diff --git a/plugins/modules/match.go b/plugins/modules/match.go index be7e171..3a9d4ea 100644 --- a/plugins/modules/match.go +++ b/plugins/modules/match.go @@ -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 {