39 Commits

Author SHA1 Message Date
ae5628f370 proper pipeline
All checks were successful
continuous-integration/drone/tag Build is passing
2025-11-30 01:26:56 +05:30
e8e2419537 proper pipeline
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/tag Build is failing
2025-11-30 01:20:02 +05:30
4ee6027612 fixes
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-29 23:58:52 +05:30
389e77e2ae fixes
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-29 23:54:51 +05:30
7ce860db96 copy local.yml
Some checks failed
continuous-integration/drone/push Build is failing
2025-11-29 23:51:50 +05:30
92f307d33d copy local.yml
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-29 23:47:57 +05:30
6e7d2d9f14 fixes
All checks were successful
continuous-integration/drone Build is passing
2025-11-29 22:32:59 +05:30
c91dba475c fixes 2025-11-29 22:23:43 +05:30
04d988c584 fixes 2025-11-29 22:23:37 +05:30
27abc56a00 fixes
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-29 22:23:23 +05:30
333b48ad60 fixes
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-29 22:16:25 +05:30
4dee0bfb0a fixes
Some checks failed
continuous-integration/drone/push Build is failing
2025-11-29 22:12:52 +05:30
1e91825808 buildx no push and use arm64 everywhere
Some checks failed
continuous-integration/drone/push Build is failing
2025-11-29 22:10:33 +05:30
c5cb1047ae buildx no push and use
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-29 21:59:49 +05:30
18f9eed71d no buildx and push
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-29 21:45:50 +05:30
02de328bcd fixes
Some checks reported errors
continuous-integration/drone/push Build is failing
continuous-integration/drone Build was killed
2025-11-29 21:35:36 +05:30
31f1a66fed arm64 images
Some checks failed
continuous-integration/drone/push Build is failing
2025-11-29 21:28:26 +05:30
5065145c6f direct deploy
Some checks reported errors
continuous-integration/drone/push Build was killed
2025-11-29 21:27:09 +05:30
8e0475c8a7 build arm64 image
Some checks failed
continuous-integration/drone/tag Build is failing
2025-11-29 21:18:54 +05:30
1359a75214 allow games.aetoskia.com
All checks were successful
continuous-integration/drone/tag Build is passing
2025-11-29 19:41:33 +05:30
6786547950 enabled tag based deployment
All checks were successful
continuous-integration/drone/tag Build is passing
2025-11-29 19:06:11 +05:30
17a2caea49 fixes
Some checks failed
continuous-integration/drone Build is failing
2025-11-29 19:02:01 +05:30
8ff199ca10 fixes
Some checks failed
continuous-integration/drone Build is failing
2025-11-29 18:59:02 +05:30
62c1f3920b fixes
Some checks failed
continuous-integration/drone/tag Build is failing
2025-11-29 18:42:41 +05:30
73e3f0a7ac fixes
Some checks failed
continuous-integration/drone/tag Build is failing
2025-11-29 18:38:13 +05:30
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 507 additions and 98 deletions

163
.drone.yml Normal file
View File

@@ -0,0 +1,163 @@
---
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 Tags
# -----------------------------------------------------
- name: fetch-tags
image: docker:24
volumes:
- name: dockersock
path: /var/run/docker.sock
commands:
- apk add --no-cache git
- git fetch --tags
- |
# Get latest Git tag and trim newline
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null | tr -d '\n')
echo "Latest Git tag fetched: $LATEST_TAG"
# Save to file for downstream steps
echo "$LATEST_TAG" > /drone/src/LATEST_TAG.txt
# Read back for verification
IMAGE_TAG=$(cat /drone/src/LATEST_TAG.txt | tr -d '\n')
echo "Image tag read from file: $IMAGE_TAG"
# Validate
if [ -z "$IMAGE_TAG" ]; then
echo "❌ No git tags found! Cannot continue."
exit 1
fi
# -----------------------------------------------------
# 2. Check existing Nakama Docker image
# -----------------------------------------------------
- name: check-remote-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 "Checking if lila-games/nakama-server:$IMAGE_TAG exists on remote Docker..."
- echo "Existing Docker tags for lila-games/nakama-server:"
- docker images --format "{{.Repository}}:{{.Tag}}" | grep "^lila-games/nakama-server" || echo "(none)"
- |
if docker image inspect lila-games/nakama-server:$IMAGE_TAG > /dev/null 2>&1; then
echo "✅ Docker image lila-games/nakama-server:$IMAGE_TAG already exists — skipping build"
exit 78
else
echo "⚙️ Docker image lila-games/nakama-server:$IMAGE_TAG not found — proceeding to 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:latest"
- |
docker build --network=host \
-t lila-games/nakama-server:$IMAGE_TAG \
-t lila-games/nakama-server:latest \
/drone/src
# -----------------------------------------------------
# 4. Push Nakama Image
# -----------------------------------------------------
- name: push-image
image: docker:24
environment:
REGISTRY_HOST:
from_secret: REGISTRY_HOST
REGISTRY_USER:
from_secret: REGISTRY_USER
REGISTRY_PASS:
from_secret: REGISTRY_PASS
volumes:
- name: dockersock
path: /var/run/docker.sock
commands:
- IMAGE_TAG=$(cat /drone/src/LATEST_TAG.txt | tr -d '\n')
- echo "🔑 Logging into registry $REGISTRY_HOST ..."
- echo "$REGISTRY_PASS" | docker login $REGISTRY_HOST -u "$REGISTRY_USER" --password-stdin
- echo "🏷️ Tagging images with registry prefix..."
- 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 lila-games/nakama-server:$IMAGE_TAG ..."
- docker push $REGISTRY_HOST/lila-games/nakama-server:$IMAGE_TAG
- echo "📤 Pushing lila-games/nakama-server:latest ..."
- 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" \
lila-games/nakama-server:latest
# -----------------------------------------------------
# Pipeline trigger
# -----------------------------------------------------
trigger:
event:
- tag

3
.gitignore vendored
View File

@@ -2,4 +2,5 @@
.run
.venv
/vendor/
/build/
/build/
.env

35
Dockerfile Normal file
View File

@@ -0,0 +1,35 @@
# -----------------------------------------------------
# 1. Build the Nakama plugin
# -----------------------------------------------------
FROM --platform=linux/arm64 golang:1.21.6 AS plugin_builder
RUN apt-get update && apt-get install -y \
build-essential \
git
WORKDIR /workspace
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN mkdir -p build && \
CGO_ENABLED=1 go build \
--trimpath \
--buildmode=plugin \
-o build/main.so \
./plugins
# -----------------------------------------------------
# 2. Build final Nakama image
# -----------------------------------------------------
FROM heroiclabs/nakama:3.21.0-arm
# Copy plugin from builder stage
COPY --from=plugin_builder /workspace/build/main.so /nakama/data/modules/main.so
#COPY --from=plugin_builder local.yml /nakama/data/config.yml
# Default Nakama startup (runs migrations + server)
ENTRYPOINT exec /bin/sh -ecx "/nakama/nakama migrate up --database.address \"$DB_ADDR\" && exec /nakama/nakama --database.address \"$DB_ADDR\" --socket.server_key=\"$SERVER_KEY\""

266
README.md
View File

@@ -1,38 +1,42 @@
# ✅ 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 |
| Real-time WebSocket communication | ✅ Completed |
| 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 |
| Google Cloud deployment | 🟡 Not Started |
| Requirement | Status |
|----------------------------------------------|--------------------------|
| Install Nakama + PostgreSQL | ✅ Completed |
| Custom Go server plugins | ✅ Completed |
| Server-authoritative Tic-Tac-Toe | ✅ Completed |
| Real-time WebSocket communication | ✅ Completed |
| Device-based authentication | ✅ Completed |
| JWT-based session management | ✅ Completed |
| Match creation & joining | ✅ Completed |
| **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 |
@@ -159,33 +238,50 @@ 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 |
| Risk | Mitigation |
|--------------------------------|--------------------------------|
| 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 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

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
}