feat: refactor Nakama plugin into generic multi-game match engine
### Highlights
- Introduced generic match engine (`generic_match.go`) implementing dynamic GameRules-based runtime.
- Added modular structure under `/plugins`:
- /plugins/game → GameRules interface + TicTacToe + Battleship rule sets
- /plugins/structs → Board, Player, MatchState generic structs
- /plugins/modules → matchmaking + RPC handlers + match engine
- Migrated TicTacToe logic into reusable rule implementation.
- Added Battleship game support using same engine.
- Updated matchmaking to accept { game, mode } for multi-game routing.
- Updated UI contract: clients must send `game` (and optional `mode`) when joining matchmaking.
- Removed hardcoded TicTacToe match registration.
- Registered a single “generic” authoritative match with ruleset registry.
- Normalized imports under local dev module path.
- Ensured MatchState and Board are now generic and reusable across games.
- Added strict requirement for `game` metadata in match flow (error if missing).
- Cleaned initial state creation into MatchInit with flexible board dimensions.
- Improved MatchLeave for proper forfeit handling through GameRules.
### Result
The server now supports an unlimited number of turn-based board games
via swappable rulesets while keeping a single authoritative Nakama match loop.
This commit is contained in:
143
plugins/games/tic_tac_toe.go
Normal file
143
plugins/games/tic_tac_toe.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"localrepo/plugins/structs"
|
||||
)
|
||||
|
||||
// TicTacToeRules implements GameRules for 3x3 Tic Tac Toe.
|
||||
type TicTacToeRules struct{}
|
||||
|
||||
// -------------------------------
|
||||
// GameRules Implementation
|
||||
// -------------------------------
|
||||
|
||||
func (t *TicTacToeRules) MaxPlayers() int {
|
||||
return 2
|
||||
}
|
||||
|
||||
// Assign player symbols: X and O
|
||||
func (t *TicTacToeRules) AssignPlayerSymbols(players []*Player) {
|
||||
if len(players) < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
players[0].Metadata["symbol"] = "X"
|
||||
players[1].Metadata["symbol"] = "O"
|
||||
}
|
||||
|
||||
// ValidateMove checks bounds and empty cell.
|
||||
func (t *TicTacToeRules) ValidateMove(state *structs.MatchState, playerIdx int, payload MovePayload) bool {
|
||||
|
||||
rowVal, ok1 := payload.Data["row"]
|
||||
colVal, ok2 := payload.Data["col"]
|
||||
|
||||
if !ok1 || !ok2 {
|
||||
return false
|
||||
}
|
||||
|
||||
row, ok3 := rowVal.(float64)
|
||||
col, ok4 := colVal.(float64)
|
||||
|
||||
if !ok3 || !ok4 {
|
||||
return false
|
||||
}
|
||||
|
||||
r := int(row)
|
||||
c := int(col)
|
||||
|
||||
// bounds
|
||||
if !state.Board.InBounds(r, c) {
|
||||
return false
|
||||
}
|
||||
|
||||
// empty?
|
||||
return state.Board.IsEmpty(r, c)
|
||||
}
|
||||
|
||||
// ApplyMove writes X or O to the board.
|
||||
func (t *TicTacToeRules) ApplyMove(state *structs.MatchState, playerIdx int, payload MovePayload) {
|
||||
|
||||
symbol := state.Players[playerIdx].Metadata["symbol"]
|
||||
|
||||
r := int(payload.Data["row"].(float64))
|
||||
c := int(payload.Data["col"].(float64))
|
||||
|
||||
state.Board.Set(r, c, symbol)
|
||||
}
|
||||
|
||||
// CheckGameOver determines win/draw state.
|
||||
func (t *TicTacToeRules) CheckGameOver(state *structs.MatchState) (bool, int) {
|
||||
|
||||
winnerSymbol := t.findWinner(state.Board)
|
||||
|
||||
if winnerSymbol != "" {
|
||||
// find the player with this symbol
|
||||
for _, p := range state.Players {
|
||||
if p.Metadata["symbol"] == winnerSymbol {
|
||||
return true, p.Index
|
||||
}
|
||||
}
|
||||
return true, -1
|
||||
}
|
||||
|
||||
if state.Board.Full() {
|
||||
return true, -1 // draw
|
||||
}
|
||||
|
||||
return false, -1
|
||||
}
|
||||
|
||||
// OnForfeit: whoever leaves loses instantly
|
||||
func (t *TicTacToeRules) ForfeitWinner(state *structs.MatchState, leaverIndex int) int {
|
||||
|
||||
// If player 0 leaves, player 1 wins.
|
||||
if leaverIndex == 0 && len(state.Players) > 1 {
|
||||
return 1
|
||||
}
|
||||
|
||||
// If player 1 leaves, player 0 wins.
|
||||
if leaverIndex == 1 && len(state.Players) > 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Otherwise draw.
|
||||
return -1
|
||||
}
|
||||
|
||||
// -------------------------------
|
||||
// Helper: winner detection
|
||||
// -------------------------------
|
||||
|
||||
func (t *TicTacToeRules) findWinner(b *structs.Board) string {
|
||||
|
||||
lines := [][][2]int{
|
||||
// rows
|
||||
{{0, 0}, {0, 1}, {0, 2}},
|
||||
{{1, 0}, {1, 1}, {1, 2}},
|
||||
{{2, 0}, {2, 1}, {2, 2}},
|
||||
// cols
|
||||
{{0, 0}, {1, 0}, {2, 0}},
|
||||
{{0, 1}, {1, 1}, {2, 1}},
|
||||
{{0, 2}, {1, 2}, {2, 2}},
|
||||
// diagonals
|
||||
{{0, 0}, {1, 1}, {2, 2}},
|
||||
{{0, 2}, {1, 1}, {2, 0}},
|
||||
}
|
||||
|
||||
for _, line := range lines {
|
||||
r1, c1 := line[0][0], line[0][1]
|
||||
r2, c2 := line[1][0], line[1][1]
|
||||
r3, c3 := line[2][0], line[2][1]
|
||||
|
||||
v1 := b.Get(r1, c1)
|
||||
if v1 != "" &&
|
||||
v1 == b.Get(r2, c2) &&
|
||||
v1 == b.Get(r3, c3) {
|
||||
return v1
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user