Files
tic-tac-toe/plugins/modules/matchmaking.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

102 lines
2.5 KiB
Go

package modules
import (
"context"
"database/sql"
"encoding/json"
"github.com/heroiclabs/nakama-common/runtime"
)
type MatchmakingTicket struct {
Game string `json:"game"` // e.g. "tictactoe", "battleship"
Mode string `json:"mode"` // e.g. "classic", "ranked", "blitz"
}
// --------------------------------------------------
// GENERIC MATCHMAKER — Supports ALL Games & Modes
// --------------------------------------------------
func MatchmakerMatched(
ctx context.Context,
logger runtime.Logger,
db *sql.DB,
nk runtime.NakamaModule,
entries []runtime.MatchmakerEntry,
) (string, error) {
if len(entries) == 0 {
return "", nil
}
// Extract the first player's desired properties
props0 := entries[0].GetProperties()
game, okGame := props0["game"].(string)
mode, okMode := props0["mode"].(string)
if !okGame || !okMode {
logger.Warn("MatchmakerMatched: Missing 'game' or 'mode' properties.")
return "", nil
}
// Ensure ALL players match game + mode
for _, e := range entries {
p := e.GetProperties()
g, okG := p["game"].(string)
m, okM := p["mode"].(string)
if !okG || !okM || g != game || m != mode {
logger.Warn("MatchmakerMatched: Player properties do not match — retrying matchmaking.")
return "", nil
}
}
// Create the correct authoritative match handler.
// This depends on how "game" was registered in main.go.
// Example: initializer.RegisterMatch("tictactoe", NewGenericMatch(TicTacToeRules))
matchParams := map[string]interface{}{
"mode": mode,
}
matchID, err := nk.MatchCreate(ctx, game, matchParams)
if err != nil {
logger.Error("MatchmakerMatched: MatchCreate failed: %v", err)
return "", runtime.NewError("failed to create match", 13)
}
logger.Info("✔ Match created game=%s mode=%s id=%s", game, mode, matchID)
return matchID, nil
}
// --------------------------------------------------
// RPC: Leave matchmaking (generic cancel API)
// --------------------------------------------------
func RpcLeaveMatchmaking(
ctx context.Context,
logger runtime.Logger,
db *sql.DB,
nk runtime.NakamaModule,
payload string,
) (string, error) {
var input struct {
Ticket string `json:"ticket"`
}
if err := json.Unmarshal([]byte(payload), &input); err != nil {
return "", runtime.NewError("invalid JSON", 3)
}
if input.Ticket == "" {
return "", runtime.NewError("missing ticket", 3)
}
// Client removes ticket locally — server doesn't need to do anything
logger.Info("✔ Player left matchmaking: ticket=%s", input.Ticket)
return "{}", nil
}