Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d8d3785fc | |||
| 65b5ef4660 | |||
| 95381c2a56 | |||
| ead7ad2c35 | |||
| 4a833dc258 | |||
| 37c090cf64 |
@@ -1,4 +1,3 @@
|
||||
version: '3'
|
||||
services:
|
||||
postgres:
|
||||
command: postgres -c shared_preload_libraries=pg_stat_statements -c pg_stat_statements.track=all
|
||||
|
||||
@@ -7,3 +7,10 @@ session:
|
||||
socket:
|
||||
max_message_size_bytes: 4096 # reserved buffer
|
||||
max_request_size_bytes: 131072
|
||||
matchmaker:
|
||||
max_tickets: 10000
|
||||
interval_ms: 100
|
||||
query:
|
||||
properties:
|
||||
string:
|
||||
mode: true
|
||||
|
||||
@@ -54,7 +54,7 @@ async def simulate_matchmaking(num_players: int = 6):
|
||||
elif p not in matches[p.match_id]:
|
||||
matches[p.match_id].append(p)
|
||||
# print(f'player = {p.label} for match = {p.match_id}')
|
||||
print(f'players = {len(matches[p.match_id])} for match = {p.match_id}')
|
||||
# print(f'players = {len(matches[p.match_id])} for match = {p.match_id}')
|
||||
|
||||
# stop early if all assigned
|
||||
if sum(len(v) for v in matches.values()) >= num_players:
|
||||
|
||||
@@ -45,6 +45,21 @@ func InitModule(
|
||||
return err
|
||||
}
|
||||
|
||||
err := nk.LeaderboardCreate(
|
||||
ctx,
|
||||
"tictactoe", // id
|
||||
true, // authoritative
|
||||
"desc", // sortOrder
|
||||
"incr", // operator
|
||||
"", // resetSchedule
|
||||
map[string]interface{}{}, // metadata
|
||||
)
|
||||
|
||||
if err != nil && err.Error() != "Leaderboard ID already exists" {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info("Leaderboard tictactoe ready")
|
||||
logger.Info("Go module loaded successfully!")
|
||||
return nil
|
||||
}
|
||||
|
||||
106
plugins/match.go
106
plugins/match.go
@@ -106,6 +106,21 @@ func (m *TicTacToeMatch) MatchJoin(
|
||||
}
|
||||
|
||||
logger.Info("MatchJoin: now %d players", len(s.Players))
|
||||
|
||||
// If we have enough players to start, broadcast initial state immediately
|
||||
if len(s.Players) == 2 {
|
||||
stateJSON, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
logger.Error("Failed to marshal state on join: %v", err)
|
||||
} else {
|
||||
if err := dispatcher.BroadcastMessage(OpState, stateJSON, nil, nil, true); err != nil {
|
||||
logger.Error("BroadcastMessage (initial state) failed: %v", err)
|
||||
} else {
|
||||
logger.Info("Broadcasted initial state to players")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -128,6 +143,18 @@ func (m *TicTacToeMatch) MatchLeave(
|
||||
s.GameOver = true
|
||||
s.Winner = "forfeit"
|
||||
logger.Info("MatchLeave: game ended by forfeit")
|
||||
|
||||
// broadcast final state so clients see the forfeit
|
||||
stateJSON, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
logger.Error("Failed to marshal state on leave: %v", err)
|
||||
} else {
|
||||
if err := dispatcher.BroadcastMessage(OpState, stateJSON, nil, nil, true); err != nil {
|
||||
logger.Error("BroadcastMessage (forfeit) failed: %v", err)
|
||||
} else {
|
||||
logger.Info("Broadcasted forfeit state to remaining players")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return s
|
||||
@@ -151,8 +178,11 @@ func (m *TicTacToeMatch) MatchLoop(
|
||||
return s
|
||||
}
|
||||
|
||||
changed := false
|
||||
|
||||
for _, msg := range messages {
|
||||
if msg.GetOpCode() != OpMove {
|
||||
logger.Debug("Ignoring non-move opcode: %d", msg.GetOpCode())
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -162,46 +192,108 @@ func (m *TicTacToeMatch) MatchLoop(
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(msg.GetData(), &move); err != nil {
|
||||
logger.Warn("Invalid move payload: %v", err)
|
||||
logger.Warn("Invalid move payload from %s: %v", msg.GetUserId(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
playerID := msg.GetUserId()
|
||||
playerIdx := indexOf(s.Players, playerID)
|
||||
logger.Info("Received move from %s (playerIdx=%d): row=%d col=%d", playerID, playerIdx, move.Row, move.Col)
|
||||
|
||||
if playerIdx == -1 {
|
||||
logger.Warn("Move rejected: player %s not in player list", playerID)
|
||||
continue
|
||||
}
|
||||
|
||||
if playerIdx != s.Turn {
|
||||
// not your turn
|
||||
logger.Warn("Move rejected: not player's turn (playerIdx=%d turn=%d)", playerIdx, s.Turn)
|
||||
continue
|
||||
}
|
||||
|
||||
if move.Row < 0 || move.Row > 2 || move.Col < 0 || move.Col > 2 {
|
||||
logger.Warn("Move rejected: out of bounds (%d,%d)", move.Row, move.Col)
|
||||
continue
|
||||
}
|
||||
|
||||
if s.Board[move.Row][move.Col] != "" {
|
||||
logger.Warn("Move rejected: cell already occupied (%d,%d)", move.Row, move.Col)
|
||||
continue
|
||||
}
|
||||
|
||||
symbols := []string{"X", "O"}
|
||||
if playerIdx < 0 || playerIdx >= len(symbols) {
|
||||
logger.Warn("Move rejected: invalid player index %d", playerIdx)
|
||||
continue
|
||||
}
|
||||
s.Board[move.Row][move.Col] = symbols[playerIdx]
|
||||
|
||||
// Apply move
|
||||
s.Board[move.Row][move.Col] = symbols[playerIdx]
|
||||
changed = true
|
||||
logger.Info("Move applied for player %s -> %s at (%d,%d)", playerID, symbols[playerIdx], move.Row, move.Col)
|
||||
|
||||
// Check win/draw
|
||||
if winner := checkWinner(s.Board); winner != "" {
|
||||
s.Winner = winner
|
||||
s.GameOver = true
|
||||
logger.Info("Game over! Winner: %s", winner)
|
||||
} else if fullBoard(s.Board) {
|
||||
s.Winner = "draw"
|
||||
s.GameOver = true
|
||||
logger.Info("Game over! Draw")
|
||||
} else {
|
||||
s.Turn = 1 - s.Turn
|
||||
logger.Info("Turn advanced to %d", s.Turn)
|
||||
}
|
||||
if s.GameOver {
|
||||
if s.Winner != "" && s.Winner != "draw" && s.Winner != "forfeit" {
|
||||
// winner = "X" or "O"
|
||||
winningIndex := 0
|
||||
if s.Winner == "O" {
|
||||
winningIndex = 1
|
||||
}
|
||||
|
||||
winnerUserId := s.Players[winningIndex]
|
||||
account, acc_err := nk.AccountGetId(ctx, winnerUserId)
|
||||
winnerUsername := ""
|
||||
if acc_err != nil {
|
||||
logger.Error("Failed to fetch username for winner %s: %v", winnerUserId, acc_err)
|
||||
} else {
|
||||
winnerUsername = account.GetUser().GetUsername()
|
||||
}
|
||||
|
||||
logger.Info("Winner username=%s userId=%s", winnerUsername, winnerUserId)
|
||||
// Write +1 win
|
||||
_, err := nk.LeaderboardRecordWrite(
|
||||
ctx,
|
||||
"tictactoe", // leaderboard ID
|
||||
winnerUserId, // owner ID
|
||||
winnerUsername, // username
|
||||
int64(1), // score
|
||||
int64(0), // subscore
|
||||
map[string]interface{}{"result": "win"},
|
||||
nil, // overrideOperator
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error("Failed to write leaderboard win: %v", err)
|
||||
} else {
|
||||
logger.Info("Leaderboard updated for: %s", winnerUserId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
// If anything changed (or periodically if you want), broadcast updated state to everyone
|
||||
if changed {
|
||||
stateJSON, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
logger.Error("Failed to marshal state: %v", err)
|
||||
} else {
|
||||
if err := dispatcher.BroadcastMessage(OpState, stateJSON, nil, nil, true); err != nil {
|
||||
logger.Error("BroadcastMessage failed: %v", err)
|
||||
} else {
|
||||
logger.Info("Broadcasted updated state to players")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return s
|
||||
|
||||
@@ -29,11 +29,12 @@ func MatchmakerMatched(
|
||||
|
||||
propsA := entries[0].GetProperties()
|
||||
propsB := entries[1].GetProperties()
|
||||
validModes := map[string]bool{"classic": true, "blitz": true}
|
||||
|
||||
modeA, okA := propsA["mode"].(string)
|
||||
modeB, okB := propsB["mode"].(string)
|
||||
|
||||
if !okA || !okB {
|
||||
if !okA || !okB || !validModes[modeA] || !validModes[modeB] {
|
||||
logger.Warn("MatchmakerMatched missing mode property — ignoring")
|
||||
return "", nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user