14 Commits

Author SHA1 Message Date
21f6698b89 fixes
Some checks failed
continuous-integration/drone/tag Build is failing
2025-11-29 18:33:15 +05:30
06904143d6 drone deployment
Some checks reported errors
continuous-integration/drone/push Build encountered an error
continuous-integration/drone/tag Build encountered an error
2025-11-29 18:27:26 +05:30
ba05aa7c76 removed reference to local.yml as it's never used and passing server_key 2025-11-29 18:27:10 +05:30
bfcf977e13 ignoring .env as it'll be secret 2025-11-29 17:34:24 +05:30
6eca090046 deployment changes for DB_ADDR via env var 2025-11-29 17:25:13 +05:30
07cb519f55 added dockerfile for nakama 2025-11-29 16:59:33 +05:30
840d7701ca UI updated 2025-11-28 20:09:15 +05:30
f1f40f5799 backend update 2025-11-28 19:52:16 +05:30
0d8d3785fc username in leaderboard 2025-11-28 19:42:06 +05:30
65b5ef4660 leaderboard 2025-11-28 19:26:47 +05:30
95381c2a56 feat(matchmaking): enable mode-based matchmaking and improve match initialization
- Added matchmaker configuration to local.yml with support for string property "mode"
- Enabled faster matchmaking via interval_ms and large ticket limit
- Broadcast initial match state when both players join
- Added detailed validation and logging for move processing
- Broadcast game-over and forfeit states immediately on player leave
- Improved MatchLoop robustness with change tracking and clearer diagnostics
2025-11-28 14:14:03 +05:30
ead7ad2c35 added check for allowing only class and blitz game mode at server 2025-11-27 16:10:04 +05:30
4a833dc258 fixes 2025-11-27 15:36:39 +05:30
37c090cf64 fixes 2025-11-27 15:15:57 +05:30
10 changed files with 503 additions and 98 deletions

154
.drone.yml Normal file
View File

@@ -0,0 +1,154 @@
---
kind: pipeline
type: docker
name: nakama-server
platform:
os: linux
arch: arm64
workspace:
path: /drone/src
volumes:
- name: dockersock
host:
path: /var/run/docker.sock
steps:
# -----------------------------------------------------
# 1. Fetch latest Git tag
# -----------------------------------------------------
- name: fetch-tags
image: docker:24
volumes:
- name: dockersock
path: /var/run/docker.sock
commands:
- apk add --no-cache git
- git fetch --tags
- |
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null | tr -d '\n')
echo "Latest Git tag: $LATEST_TAG"
echo "$LATEST_TAG" > /drone/src/LATEST_TAG.txt
if [ -z "$LATEST_TAG" ]; then
echo "❌ No git tags found. Cannot continue."
exit 1
fi
# -----------------------------------------------------
# 2. Check if remote image already exists
# -----------------------------------------------------
- name: check-remote-image
image: docker:24
volumes:
- name: dockersock
path: /var/run/docker.sock
environment:
REGISTRY_HOST:
from_secret: REGISTRY_HOST
commands:
- IMAGE_TAG=$(cat /drone/src/LATEST_TAG.txt | tr -d '\n')
- echo "Checking if $REGISTRY_HOST/lila-games/nakama-server:$IMAGE_TAG exists..."
- |
if docker pull $REGISTRY_HOST/lila-games/nakama-server:$IMAGE_TAG > /dev/null 2>&1; then
echo "✅ Image already exists: $REGISTRY_HOST/lila-games/nakama-server:$IMAGE_TAG"
exit 78
else
echo "⚙️ Image does not exist. Will build."
fi
# -----------------------------------------------------
# 3. Build Nakama Docker image
# -----------------------------------------------------
- name: build-image
image: docker:24
volumes:
- name: dockersock
path: /var/run/docker.sock
commands:
- IMAGE_TAG=$(cat /drone/src/LATEST_TAG.txt | tr -d '\n')
- echo "🔨 Building Nakama image lila-games/nakama-server:$IMAGE_TAG"
- |
docker build \
--network=host \
-t lila-games/nakama-server:$IMAGE_TAG \
-t lila-games/nakama-server:latest \
/drone/src
# -----------------------------------------------------
# 4. Push Nakama image to registry
# -----------------------------------------------------
- name: push-image
image: docker:24
volumes:
- name: dockersock
path: /var/run/docker.sock
environment:
REGISTRY_HOST:
from_secret: REGISTRY_HOST
REGISTRY_USER:
from_secret: REGISTRY_USER
REGISTRY_PASS:
from_secret: REGISTRY_PASS
commands:
- IMAGE_TAG=$(cat /drone/src/LATEST_TAG.txt | tr -d '\n')
- echo "🔑 Logging into registry..."
- echo "$REGISTRY_PASS" | docker login $REGISTRY_HOST -u "$REGISTRY_USER" --password-stdin
- echo "🏷️ Tagging images..."
- docker tag lila-games/nakama-server:$IMAGE_TAG $REGISTRY_HOST/lila-games/nakama-server:$IMAGE_TAG
- docker tag lila-games/nakama-server:$IMAGE_TAG $REGISTRY_HOST/lila-games/nakama-server:latest
- echo "📤 Pushing images..."
- docker push $REGISTRY_HOST/lila-games/nakama-server:$IMAGE_TAG
- docker push $REGISTRY_HOST/lila-games/nakama-server:latest
# -----------------------------------------------------
# 5. Stop old Nakama container
# -----------------------------------------------------
- name: stop-old
image: docker:24
volumes:
- name: dockersock
path: /var/run/docker.sock
commands:
- echo "🛑 Stopping old Nakama container..."
- docker rm -f nakama-server || true
# -----------------------------------------------------
# 6. Run new Nakama container
# -----------------------------------------------------
- name: run-container
image: docker:24
volumes:
- name: dockersock
path: /var/run/docker.sock
environment:
DB_ADDR:
from_secret: POSTGRES_ADDR
SERVER_KEY:
from_secret: SERVER_KEY
REGISTRY_HOST:
from_secret: REGISTRY_HOST
commands:
- IMAGE_TAG=$(cat /drone/src/LATEST_TAG.txt | tr -d '\n')
- echo "🚀 Starting Nakama server..."
- |
docker run -d \
--name nakama-server \
-p 7350:7350 \
-p 7351:7351 \
-p 7349:7349 \
--restart always \
--add-host private-pi:192.168.1.111 \
-e DB_ADDR="$DB_ADDR" \
-e SERVER_KEY="$SERVER_KEY" \
-v /mnt/omnissiah-vault/data/nakama/modules:/nakama/data/modules \
-v /mnt/omnissiah-vault/data/nakama/storage:/nakama/data \
$REGISTRY_HOST/lila-games/nakama-server:$IMAGE_TAG
# -----------------------------------------------------
# Pipeline trigger
# -----------------------------------------------------
trigger:
event:
- tag

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@
.venv
/vendor/
/build/
.env

40
Dockerfile Normal file
View File

@@ -0,0 +1,40 @@
# -----------------------------------------------------
# 1. Build the Nakama plugin (ARM64)
# -----------------------------------------------------
FROM golang:1.22-alpine AS plugin_builder
# Install dependencies needed for CGO plugin build
RUN apk add --no-cache git build-base
WORKDIR /workspace
# Download module deps first (better caching)
COPY go.mod go.sum ./
RUN go mod download
# Copy source code
COPY . .
# Build plugin
RUN mkdir -p build && \
CGO_ENABLED=1 go build \
--trimpath \
--buildmode=plugin \
-o build/main.so \
./plugins
# -----------------------------------------------------
# 2. Build final Nakama image (ARM64)
# -----------------------------------------------------
FROM --platform=linux/arm64 heroiclabs/nakama:3.21.0 AS nakama
# Copy plugin from builder stage
COPY --from=plugin_builder /workspace/build/main.so /nakama/data/modules/main.so
# Default Nakama startup (runs migrations + server)
ENTRYPOINT [
"/bin/sh", "-ecx", "\
/nakama/nakama migrate up --database.address \"$DB_ADDR\" && \
exec /nakama/nakama --database.address \"$DB_ADDR\" --socket.server_key=\"$SERVER_KEY\""
]

246
README.md
View File

@@ -1,24 +1,27 @@
# ✅ Project Status Report
## Multiplayer Tic-Tac-Toe Platform
**To:** CTO & Technical Interview Panel
**Date:** November 25, 2025
**Date:** November 28, 2025
**Version:** v0.0.1
**Version:** v0.2.0
---
## **1. Objective**
Design and implement a lightweight, scalable, server-authoritative multiplayer game system using **Nakama + Go plugins**, supporting authentication, matchmaking, real-time gameplay, and leaderboards — deployable to Google Cloud for demonstration.
Design and implement a lightweight, scalable, server-authoritative multiplayer game system using **Nakama + Go plugins
**, supporting authentication, matchmaking, authoritative gameplay, leaderboards, and a functional UI — deployable to
Google Cloud for demonstration.
---
## **2. Current Completion Summary**
| Requirement | Status |
| ---------------------------------------- | -------------------------------- |
|----------------------------------------------|--------------------------|
| Install Nakama + PostgreSQL | ✅ Completed |
| Custom Go server plugins | ✅ Completed |
| Server-authoritative Tic-Tac-Toe | ✅ Completed |
@@ -26,13 +29,14 @@ Design and implement a lightweight, scalable, server-authoritative multiplayer g
| Device-based authentication | ✅ Completed |
| JWT-based session management | ✅ Completed |
| Match creation & joining | ✅ Completed |
| Matchmaking queue support | 🟡 Not Started |
| Game state validation & turn enforcement | 🟡 Not Started |
| Leaderboard/tracking foundation | 🟡 Not Started |
| UI Game Client | 🟡 Not Started |
| **Matchmaking queue support** | ✅ Completed |
| **Game state validation & turn enforcement** | ✅ Completed |
| **Leaderboard system** | ✅ Completed |
| **UI Game Client** | 🟡 Partially Implemented |
| Google Cloud deployment | 🟡 Not Started |
**Core backend functionality is complete and stable**
**Backend is fully authoritative and complete**
🟡 **UI functional but missing polish, UX, and failure handling**
---
@@ -40,113 +44,188 @@ Design and implement a lightweight, scalable, server-authoritative multiplayer g
* **Backend Framework:** Nakama 3.x
* **Business Logic:** Custom Go runtime module
* **Frontend:** React + Vite + Nakama JS
* **Database:** PostgreSQL 14
* **Transport:** WebSockets (real-time)
* **Authentication:** Device-ID based auth → JWT session returned
* **State Management:** Server-authoritative, deterministic
* **Protocol:** Nakama RT JSON envelopes
* **State Management:** Fully server-authoritative
* **Build & Deployment:** Docker-based
---
## **4. Authentication**
Implemented **Nakama device authentication flow**:
### Backend
1. Client provides device UUID
2. Nakama validates & creates account if needed
3. Server responds with **JWT session token**
4. Client uses JWT for all WebSocket connections
* Device authentication, auto-account creation
* JWT returned and used for RT connections
### UI
* Generates a device UUID and authenticates via `client.authenticateDevice()`
* Stores and manages session state in React context
---
## **5. Game Server Logic**
## **5. Game Server Logic (Go)**
Implemented as Go match module:
Significant enhancements made:
* Turn-based validation
* Board occupancy checks
* Win/draw/forfeit detection
* Automatic broadcast of updated state
* Graceful match termination
* Prevents cheating & client-side manipulation
### **✔ Initial State Broadcast**
* When the second player joins, the server immediately sends the full authoritative state.
### **✔ Complete Turn + Move Validation**
Rejects:
* out-of-bounds moves
* occupied cells
* wrong player's turn
* invalid payloads
### **✔ Forfeit Handling**
* When a user disconnects or leaves, match ends in `forfeit`
* Final state broadcast to remaining player
### **✔ Authoritative State Updates**
* Only broadcasts when state actually changes
* Robust structured logging
Result:
✅ Entire game lifecycle enforced server-side.
**Absolute server authority, zero trust in client.**
---
## **6. Real-Time Networking**
Clients communicate via:
Communication validated end-to-end:
* `match_create`
* `match_join`
* `match_data_send` (OpCode 1) → moves
* Broadcast state updates (OpCode 2)
* `matchmaker_add` / `matchmaker_matched`
* `match_data_send` for moves (OpCode 1)
* Server broadcasts state (OpCode 2)
Python WebSocket simulation confirms:
✅ Move sequencing
✅ Session isolation
✅ Messaging reliability
✅ Auto-cleanup on disconnect
Python simulators and UI both confirm:
* move ordering
* correct enforcement of turn rules
* correct state sync
* stable WebSocket behavior
---
## **7. Matchmaking**
## **7. Matchmaking System (Go + UI)**
**NOT STARTED**
### **Backend (Go)**
* Implements `MatchmakerMatched` hook
* Ensures both players:
* have valid `mode`
* match modes
* exactly two players
* Creates authoritative matches server-side
* RPC `leave_matchmaking` added
### **UI**
* Integrates matchmaking: mode selection → queue → ticket → matched → auto-join
* Uses Nakama JS `socket.addMatchmaker()`
**Status:** Fully functional end-to-end
---
## **8. Testing & Validation**
## **8. Leaderboard System (Go + UI)**
Performed using:
### **Backend (Go)**
* Automated two-client WebSocket game flow - Happy path
* Leaderboard auto-created on startup
* On win:
**PENDING STRESS AND EDGE CASE TESTING**
* winner identified
* username resolved
* score incremented (+1 win)
* metadata logged
### **UI**
* Implements leaderboard view using `client.listLeaderboardRecords()`
* Read-only UI display works
### **Remaining**
* UI sorting, layout, styling
* No leaderboard write actions needed from UI
---
## **9. UI Status**
## **9. UI Implementation Status (React + Vite)**
Not implemented yet — intentionally deferred.
### **What Is Implemented**
Planned:
- ✔ Authentication flow (device auth)
- ✔ WebSocket session handling
- ✔ Matchmaking (classic/blitz modes)
- ✔ Automatic match join
- ✔ Move sending (OpCode 1)
- ✔ State updates (OpCode 2)
- ✔ Board rendering and interactive cells
- ✔ End-of-game messaging
- ✔ Leaderboard display
* Simple browser/mobile client
* Display board, turns, win state
* WebSocket integration
* Leaderboard screen
### **Partially Implemented**
Estimated time: **610 hours**
- 🟡 Match mode UI selection wired but not visually emphasized
- 🟡 Context handles all RT states but missing error handling
### **Not Implemented / Missing**
- 🔴 Reconnect flow (UI does not recover session after WS drop)
- 🔴 No error UI for: matchmaking failure, move rejection, disconnects
- 🔴 No UI for leaving match or returning to lobby
- 🔴 No rematch button / flow
- 🔴 No transitions, animations, or mobile layout
- 🔴 No global app shell (Routing, Home Screen, etc.)
**Summary:** UI is *functional* and capable of playing full authoritative games, but lacks UX polish and failure
handling.
---
## **10. Leaderboard System**
## **10. Testing & Validation**
Backend-ready but not finalized:
### Backend
✅ Database & Nakama leaderboard APIs available
✅ Game result reporting planned
🟡 Ranking, ELO, win streak logic pending
* Extensive scenario tests: draws, wins, illegal moves, disconnects
* Matchmaking simulation across N clients
Estimated time: **46 hours**
### UI
* Verified:
* matchmaking
* game correctness
* leaderboard retrieval
* state sync
* Missing:
* stress testing
* reconnection scenarios
* mobile layout testing
---
## **11. Google Cloud Deployment (Interview-Scope)**
## **11. Google Cloud Deployment**
Goal: **Simple, affordable, demo-ready deployment**
### Planned architecture:
* Highly subjective as of now
Planned architecture remains:
| Component | GCP Service |
| ------------- | ----------------------------- |
|---------------|-------------------------------|
| Nakama server | Compute Engine VM (Docker) |
| PostgreSQL | Cloud SQL (shared tier) |
| Game UI | Cloud Run or Firebase Hosting |
@@ -160,32 +239,49 @@ Estimated setup time: **68 hours**
## **12. Risks & Considerations**
| Risk | Mitigation |
| ------------------------ | ------------------------- |
| No UI yet | Prioritized next |
| Only happy path tested | In parallel with UI work |
| Matchmaking incomplete | Clear implementation plan |
| Leaderboard incomplete | Clear implementation plan |
|--------------------------------|--------------------------------|
| UI lacks error/reconnect logic | Add retry + reconnection flows |
| No rematch or lobby UX | Add match lifecycle UI |
| No mobile layout | Add responsive CSS |
| Cloud deployment pending | Prioritize after UI polish |
| Matchmaking UX is minimal | Add feedback, loading states |
None block demonstration or evaluation.
None of these affect core backend stability.
---
## **13. Next Steps**
1. Implement browser/mobile UI
2. Stress, load and Edge case testing
3. Complete match making, leaderboard scoring
4. Deploy to Google Cloud for public access
5. Record demo video + documentation
### **UI Tasks**
Estimated remaining effort: **1.52.5 days**
1. Add reconnect + error-handling UI
2. Create lobby → gameplay → results, flow
3. Add rematch capability
4. Add responsive + polished UI
5. Add loading indicators & animations
Estimated remaining effort: **6 to 8 hours**
---
## **Executive Summary**
The foundational backend for the multiplayer Tic-Tac-Toe platform is fully implemented, stable, and validated over real-time WebSocket communication. Core features—authentication, session management, game state handling, and authoritative gameplay—are complete and functioning reliably.
### Issues faced
Several unexpected integration challenges during UI setup contributed to the additional day of work. These included:
- Aligning the UIs matchmaking flow with the new authoritative Go-based matchmaking logic.
- Handling Nakama JS WebSocket behaviors, especially around session timing, matchmaker ticket handling, and match join events.
- Ensuring OpCode handling and server-produced state updates matched the servers authoritative model.
- Resolving environment-related issues (Vite dev server, Node version mismatches, and WebSocket URL configuration).
- Debugging cross-origin and connection-reset issues during early WebSocket initialization.
Remaining deliverables, including UI development, matchmaking, extended test coverage, leaderboard logic, and Google Cloud deployment, are intentionally pending to align effort with interview scope and timelines. These are well-defined, low-risk, and can be completed within the estimated timeframe.
These challenges required deeper synchronization between the backend and frontend layers, resulting in an additional **+1 day** of engineering time.
**The project is technically strong, progressing as planned, and positioned for successful final delivery and demonstration.**
### Project Progress
The system now features a fully authoritative backend with matchmaking, gameplay logic, and leaderboards implemented
completely in Go. The UI is functional and integrates correctly with all backend systems, supporting end-to-end
matchmaking and gameplay.
Key remaining work involves UI polish, recovery/error handling, and deployment setup. No major architectural risks
remain.
**The project is now fully playable, technically solid, and ready for final UI enhancements and cloud deployment.**

View File

@@ -1,4 +1,3 @@
version: '3'
services:
postgres:
command: postgres -c shared_preload_libraries=pg_stat_statements -c pg_stat_statements.track=all
@@ -44,8 +43,8 @@ services:
- "/bin/sh"
- "-ecx"
- >
/nakama/nakama migrate up --database.address postgres:localdb@postgres:5432/nakama?sslmode=disable &&
exec /nakama/nakama --config /nakama/data/local.yml --database.address postgres:localdb@postgres:5432/nakama?sslmode=disable
/nakama/nakama migrate up --database.address "$DB_ADDR" &&
exec /nakama/nakama --database.address "$DB_ADDR" --socket.server_key="$SERVER_KEY"
volumes:
- ./local.yml:/nakama/data/local.yml
- ./build:/nakama/data/modules

View File

@@ -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

View File

@@ -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:

View File

@@ -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
}

View File

@@ -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 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

View File

@@ -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
}