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:
110
plugins/main.go
110
plugins/main.go
@@ -5,61 +5,87 @@ import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/heroiclabs/nakama-common/runtime"
|
||||
|
||||
// Adjust these imports to match your project structure
|
||||
"localrepo/plugins/modules"
|
||||
"localrepo/plugins/games"
|
||||
)
|
||||
|
||||
// Example RPC
|
||||
func HelloWorld(
|
||||
func InitModule(
|
||||
ctx context.Context,
|
||||
logger runtime.Logger,
|
||||
db *sql.DB,
|
||||
nk runtime.NakamaModule,
|
||||
payload string,
|
||||
) (string, error) {
|
||||
logger.Info("HelloWorld RPC called — payload: %s", payload)
|
||||
return `{"message": "Hello from Go RPC!"}`, nil
|
||||
}
|
||||
|
||||
// Required module initializer
|
||||
func InitModule(
|
||||
ctx context.Context,
|
||||
logger runtime.Logger,
|
||||
db *sql.DB,
|
||||
nk runtime.NakamaModule,
|
||||
initializer runtime.Initializer,
|
||||
initializer runtime.Initializer,
|
||||
) error {
|
||||
|
||||
//--------------------------------------------------------
|
||||
// 1. Register RPCs
|
||||
//--------------------------------------------------------
|
||||
if err := initializer.RegisterRpc("hello_world", HelloWorld); err != nil {
|
||||
logger.Error("Failed to register RPC: %v", err)
|
||||
return err
|
||||
}
|
||||
if err := initializer.RegisterMatch("tictactoe", NewMatch); err != nil {
|
||||
logger.Error("Failed to register RPC: %v", err)
|
||||
return err
|
||||
}
|
||||
// Match making
|
||||
if err := initializer.RegisterRpc("leave_matchmaking", rpcLeaveMatchmaking); err != nil {
|
||||
logger.Error("RegisterRpc leave_matchmaking failed: %v", err)
|
||||
return err
|
||||
}
|
||||
if err := initializer.RegisterMatchmakerMatched(MatchmakerMatched); err != nil {
|
||||
logger.Error("RegisterMatchmakerMatched failed: %v", err)
|
||||
logger.Error("Failed to register RPC hello_world: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
err := nk.LeaderboardCreate(
|
||||
ctx,
|
||||
"tictactoe", // id
|
||||
true, // authoritative
|
||||
"desc", // sortOrder
|
||||
"incr", // operator
|
||||
"", // resetSchedule
|
||||
map[string]interface{}{}, // metadata
|
||||
)
|
||||
if err := initializer.RegisterRpc("leave_matchmaking", modules.RpcLeaveMatchmaking); err != nil {
|
||||
logger.Error("Failed to register RPC leave_matchmaking: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err != nil && err.Error() != "Leaderboard ID already exists" {
|
||||
return err
|
||||
}
|
||||
//--------------------------------------------------------
|
||||
// 2. Register Matchmaker Handler
|
||||
//--------------------------------------------------------
|
||||
if err := initializer.RegisterMatchmakerMatched(modules.MatchmakerMatched); err != nil {
|
||||
logger.Error("Failed to register MatchmakerMatched: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
//--------------------------------------------------------
|
||||
// 3. Register MATCHES for ALL games
|
||||
//--------------------------------------------------------
|
||||
|
||||
// Build registry: game name → GameRules implementation
|
||||
registry := map[string]game.GameRules{
|
||||
"tictactoe": &game.TicTacToeRules{},
|
||||
"battleship": &game.BattleshipRules{},
|
||||
}
|
||||
|
||||
// Register a Generic Match Handler that can run ANY game from registry
|
||||
if err := initializer.RegisterMatch("generic", modules.NewGenericMatch(registry)); err != nil {
|
||||
logger.Error("Failed to register generic match: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
//--------------------------------------------------------
|
||||
// 4. Register Leaderboards dynamically (optional)
|
||||
//--------------------------------------------------------
|
||||
|
||||
leaderboards := []string{
|
||||
"tictactoe_classic",
|
||||
"tictactoe_ranked",
|
||||
"battleship_classic",
|
||||
"battleship_ranked",
|
||||
}
|
||||
|
||||
for _, lb := range leaderboards {
|
||||
err := nk.LeaderboardCreate(
|
||||
ctx,
|
||||
lb, // leaderboard ID
|
||||
true, // authoritative
|
||||
"desc", // sort order
|
||||
"incr", // operator
|
||||
"", // reset schedule (none)
|
||||
map[string]interface{}{}, // metadata
|
||||
)
|
||||
|
||||
if err != nil && err.Error() != "Leaderboard ID already exists" {
|
||||
logger.Error("Failed to create leaderboard %s: %v", lb, err)
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info("Leaderboard ready: %s", lb)
|
||||
}
|
||||
|
||||
logger.Info("Leaderboard tictactoe ready")
|
||||
logger.Info("Go module loaded successfully!")
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user