match.go for tictactoe
This commit is contained in:
@@ -30,6 +30,10 @@ func InitModule(
|
||||
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
|
||||
}
|
||||
|
||||
logger.Info("Go module loaded successfully!")
|
||||
return nil
|
||||
|
||||
286
plugins/match.go
Normal file
286
plugins/match.go
Normal file
@@ -0,0 +1,286 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/heroiclabs/nakama-common/runtime"
|
||||
)
|
||||
|
||||
const (
|
||||
OpMove int64 = 1
|
||||
OpState int64 = 2
|
||||
)
|
||||
|
||||
// Server-side game state
|
||||
type MatchState struct {
|
||||
Board [3][3]string `json:"board"`
|
||||
Players []string `json:"players"`
|
||||
Turn int `json:"turn"` // index in Players
|
||||
Winner string `json:"winner"` // "X", "O", "draw", "forfeit"
|
||||
GameOver bool `json:"game_over"` // true when finished
|
||||
}
|
||||
|
||||
// Struct that implements runtime.Match
|
||||
type TicTacToeMatch struct{}
|
||||
|
||||
// Factory for RegisterMatch
|
||||
func NewMatch(
|
||||
ctx context.Context,
|
||||
logger runtime.Logger,
|
||||
db *sql.DB,
|
||||
nk runtime.NakamaModule,
|
||||
) (runtime.Match, error) {
|
||||
logger.Info("TicTacToe NewMatch factory called")
|
||||
return &TicTacToeMatch{}, nil
|
||||
}
|
||||
|
||||
// ---- MatchInit ----
|
||||
// Return initial state, tick rate (ticks/sec), and label
|
||||
func (m *TicTacToeMatch) MatchInit(
|
||||
ctx context.Context,
|
||||
logger runtime.Logger,
|
||||
db *sql.DB,
|
||||
nk runtime.NakamaModule,
|
||||
params map[string]interface{},
|
||||
) (interface{}, int, string) {
|
||||
|
||||
state := &MatchState{
|
||||
Board: [3][3]string{},
|
||||
Players: []string{},
|
||||
Turn: 0,
|
||||
Winner: "",
|
||||
GameOver: false,
|
||||
}
|
||||
|
||||
tickRate := 5 // 5 ticks per second (~200ms)
|
||||
label := "tictactoe"
|
||||
|
||||
logger.Info("TicTacToe MatchInit: tickRate=%v label=%s", tickRate, label)
|
||||
return state, tickRate, label
|
||||
}
|
||||
|
||||
// ---- MatchJoinAttempt ----
|
||||
func (m *TicTacToeMatch) MatchJoinAttempt(
|
||||
ctx context.Context,
|
||||
logger runtime.Logger,
|
||||
db *sql.DB,
|
||||
nk runtime.NakamaModule,
|
||||
dispatcher runtime.MatchDispatcher,
|
||||
tick int64,
|
||||
state interface{},
|
||||
presence runtime.Presence,
|
||||
metadata map[string]string,
|
||||
) (interface{}, bool, string) {
|
||||
|
||||
s := state.(*MatchState)
|
||||
|
||||
if len(s.Players) >= 2 {
|
||||
return s, false, "match full"
|
||||
}
|
||||
|
||||
return s, true, ""
|
||||
}
|
||||
|
||||
// ---- MatchJoin ----
|
||||
func (m *TicTacToeMatch) MatchJoin(
|
||||
ctx context.Context,
|
||||
logger runtime.Logger,
|
||||
db *sql.DB,
|
||||
nk runtime.NakamaModule,
|
||||
dispatcher runtime.MatchDispatcher,
|
||||
tick int64,
|
||||
state interface{},
|
||||
presences []runtime.Presence,
|
||||
) interface{} {
|
||||
|
||||
s := state.(*MatchState)
|
||||
|
||||
for _, p := range presences {
|
||||
userID := p.GetUserId()
|
||||
// avoid duplicates
|
||||
if indexOf(s.Players, userID) == -1 {
|
||||
s.Players = append(s.Players, userID)
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("MatchJoin: now %d players", len(s.Players))
|
||||
return s
|
||||
}
|
||||
|
||||
// ---- MatchLeave ----
|
||||
func (m *TicTacToeMatch) MatchLeave(
|
||||
ctx context.Context,
|
||||
logger runtime.Logger,
|
||||
db *sql.DB,
|
||||
nk runtime.NakamaModule,
|
||||
dispatcher runtime.MatchDispatcher,
|
||||
tick int64,
|
||||
state interface{},
|
||||
presences []runtime.Presence,
|
||||
) interface{} {
|
||||
|
||||
s := state.(*MatchState)
|
||||
|
||||
// End the game if anyone leaves
|
||||
if !s.GameOver {
|
||||
s.GameOver = true
|
||||
s.Winner = "forfeit"
|
||||
logger.Info("MatchLeave: game ended by forfeit")
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// ---- MatchLoop ----
|
||||
func (m *TicTacToeMatch) MatchLoop(
|
||||
ctx context.Context,
|
||||
logger runtime.Logger,
|
||||
db *sql.DB,
|
||||
nk runtime.NakamaModule,
|
||||
dispatcher runtime.MatchDispatcher,
|
||||
tick int64,
|
||||
state interface{},
|
||||
messages []runtime.MatchData,
|
||||
) interface{} {
|
||||
|
||||
s := state.(*MatchState)
|
||||
|
||||
if s.GameOver {
|
||||
return s
|
||||
}
|
||||
|
||||
for _, msg := range messages {
|
||||
if msg.GetOpCode() != OpMove {
|
||||
continue
|
||||
}
|
||||
|
||||
var move struct {
|
||||
Row int `json:"row"`
|
||||
Col int `json:"col"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(msg.GetData(), &move); err != nil {
|
||||
logger.Warn("Invalid move payload: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
playerID := msg.GetUserId()
|
||||
playerIdx := indexOf(s.Players, playerID)
|
||||
if playerIdx != s.Turn {
|
||||
// not your turn
|
||||
continue
|
||||
}
|
||||
|
||||
if move.Row < 0 || move.Row > 2 || move.Col < 0 || move.Col > 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
if s.Board[move.Row][move.Col] != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
symbols := []string{"X", "O"}
|
||||
if playerIdx < 0 || playerIdx >= len(symbols) {
|
||||
continue
|
||||
}
|
||||
s.Board[move.Row][move.Col] = symbols[playerIdx]
|
||||
|
||||
if winner := checkWinner(s.Board); winner != "" {
|
||||
s.Winner = winner
|
||||
s.GameOver = true
|
||||
} else if fullBoard(s.Board) {
|
||||
s.Winner = "draw"
|
||||
s.GameOver = true
|
||||
} else {
|
||||
s.Turn = 1 - s.Turn
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast updated state to everyone
|
||||
stateJSON, _ := json.Marshal(s)
|
||||
if err := dispatcher.BroadcastMessage(OpState, stateJSON, nil, nil, true); err != nil {
|
||||
logger.Error("BroadcastMessage failed: %v", err)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// ---- MatchTerminate ----
|
||||
func (m *TicTacToeMatch) MatchTerminate(
|
||||
ctx context.Context,
|
||||
logger runtime.Logger,
|
||||
db *sql.DB,
|
||||
nk runtime.NakamaModule,
|
||||
dispatcher runtime.MatchDispatcher,
|
||||
tick int64,
|
||||
state interface{},
|
||||
graceSeconds int,
|
||||
) interface{} {
|
||||
|
||||
logger.Info("MatchTerminate: grace=%d", graceSeconds)
|
||||
return state
|
||||
}
|
||||
|
||||
// ---- MatchSignal (not used, but required) ----
|
||||
func (m *TicTacToeMatch) MatchSignal(
|
||||
ctx context.Context,
|
||||
logger runtime.Logger,
|
||||
db *sql.DB,
|
||||
nk runtime.NakamaModule,
|
||||
dispatcher runtime.MatchDispatcher,
|
||||
tick int64,
|
||||
state interface{},
|
||||
data string,
|
||||
) (interface{}, string) {
|
||||
|
||||
logger.Info("MatchSignal: %s", data)
|
||||
// no-op; just echo back
|
||||
return state, ""
|
||||
}
|
||||
|
||||
// ---- Helpers ----
|
||||
func indexOf(arr []string, v string) int {
|
||||
for i, s := range arr {
|
||||
if s == v {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func checkWinner(b [3][3]string) string {
|
||||
lines := [][][2]int{
|
||||
{{0, 0}, {0, 1}, {0, 2}},
|
||||
{{1, 0}, {1, 1}, {1, 2}},
|
||||
{{2, 0}, {2, 1}, {2, 2}},
|
||||
{{0, 0}, {1, 0}, {2, 0}},
|
||||
{{0, 1}, {1, 1}, {2, 1}},
|
||||
{{0, 2}, {1, 2}, {2, 2}},
|
||||
{{0, 0}, {1, 1}, {2, 2}},
|
||||
{{0, 2}, {1, 1}, {2, 0}},
|
||||
}
|
||||
|
||||
for _, l := range lines {
|
||||
a, b2, c := l[0], l[1], l[2]
|
||||
if b[a[0]][a[1]] != "" &&
|
||||
b[a[0]][a[1]] == b[b2[0]][b2[1]] &&
|
||||
b[a[0]][a[1]] == b[c[0]][c[1]] {
|
||||
return b[a[0]][a[1]]
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func fullBoard(b [3][3]string) bool {
|
||||
for _, row := range b {
|
||||
for _, v := range row {
|
||||
if v == "" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user