### 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.
102 lines
2.5 KiB
Go
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
|
|
}
|