Files
tic-tac-toe/plugins/games/battleship.go
Vishesh 'ironeagle' Bangotra eeb0a8175f 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.
2025-12-01 15:28:54 +05:30

179 lines
4.4 KiB
Go

package game
import (
"fmt"
"localrepo/plugins/structs"
)
//
// BATTLESHIP RULES IMPLEMENTATION
//
// NOTES:
// - 2 players
// - Each player has 2 boards:
// 1. Their own ship board (state.Board is not reused here)
// 2. Their "shots" board (hits/misses on opponent)
// - We store boards in Player.Metadata as JSON strings
// (simplest method without changing your structs).
//
// ShipBoard and ShotBoard are encoded inside Metadata:
//
// Metadata["ship_board"] = JSON string of [][]string
// Metadata["shot_board"] = JSON string of [][]string
//
// ------------------------------
// Helpers: encode/decode
// ------------------------------
func encodeBoard(b [][]string) string {
out := "["
for i, row := range b {
out += "["
for j, col := range row {
out += fmt.Sprintf("%q", col)
if j < len(row)-1 {
out += ","
}
}
out += "]"
if i < len(b)-1 {
out += ","
}
}
out += "]"
return out
}
func decodeBoard(s string) [][]string {
var out [][]string
// should never fail; safe fallback
_ = json.Unmarshal([]byte(s), &out)
return out
}
// ------------------------------
// BattleshipRules
// ------------------------------
type BattleshipRules struct{}
func (b *BattleshipRules) MaxPlayers() int { return 2 }
// ------------------------------
// Assign player boards
// ------------------------------
func (b *BattleshipRules) AssignPlayerSymbols(players []*Player) {
// Battleship has no symbols like X/O,
// but we use this hook to initialize per-player boards.
for _, p := range players {
// 10x10 boards
empty := make([][]string, 10)
for r := range empty {
empty[r] = make([]string, 10)
}
// 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)
}
}
// ------------------------------
// ValidateMove
// payload.data = { "row": int, "col": int }
// ------------------------------
func (b *BattleshipRules) ValidateMove(state *structs.MatchState, playerIdx int, payload MovePayload) bool {
rF, ok1 := payload.Data["row"].(float64)
cF, ok2 := payload.Data["col"].(float64)
if !ok1 || !ok2 {
return false
}
r := int(rF)
c := int(cF)
if r < 0 || r > 9 || c < 0 || c > 9 {
return false
}
// Check if this spot was already shot before
shotBoard := decodeBoard(state.Players[playerIdx].Metadata["shot_board"])
return shotBoard[r][c] == ""
}
// ------------------------------
// ApplyMove
// ------------------------------
func (b *BattleshipRules) ApplyMove(state *structs.MatchState, playerIdx int, payload MovePayload) {
attacker := state.Players[playerIdx]
defenderIdx := 1 - playerIdx
defender := state.Players[defenderIdx]
r := int(payload.Data["row"].(float64))
c := int(payload.Data["col"].(float64))
shotBoard := decodeBoard(attacker.Metadata["shot_board"])
shipBoard := decodeBoard(defender.Metadata["ship_board"])
if shipBoard[r][c] == "S" {
// hit
shotBoard[r][c] = "H"
shipBoard[r][c] = "X" // ship cell destroyed
} else {
// miss
shotBoard[r][c] = "M"
}
// Save back
attacker.Metadata["shot_board"] = encodeBoard(shotBoard)
defender.Metadata["ship_board"] = encodeBoard(shipBoard)
}
// ------------------------------
// CheckGameOver
// ------------------------------
func (b *BattleshipRules) CheckGameOver(state *structs.MatchState) (bool, int) {
for i, p := range state.Players {
ships := decodeBoard(p.Metadata["ship_board"])
alive := false
for r := range ships {
for c := range ships[r] {
if ships[r][c] == "S" {
alive = true
}
}
}
if !alive {
// this player has no ships left → opponent wins
return true, 1 - i
}
}
return false, -1
}
// ------------------------------
// Forfeit Winner
// ------------------------------
func (b *BattleshipRules) ForfeitWinner(state *structs.MatchState, leaverIndex int) int {
// If player leaves, opponent automatically wins.
if leaverIndex == 0 {
return 1
}
if leaverIndex == 1 {
return 0
}
return -1
}