45 Commits

Author SHA1 Message Date
70669fc856 Update README.md 2025-12-01 08:33:28 +00:00
1752bfaed4 Update README.md 2025-12-01 08:33:01 +00:00
74e75de577 Update README.md 2025-12-01 08:32:29 +00:00
7983be56e4 Update README.md 2025-12-01 08:27:57 +00:00
e9eb37f665 fixes 2025-11-30 02:06:50 +05:30
7a7c6adc47 updated report 2025-11-30 02:04:41 +05:30
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 526 additions and 132 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

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@
.venv .venv
/vendor/ /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\""

319
README.md
View File

@@ -1,191 +1,272 @@
# ✅ Project Status Report # tic-tac-toe — Authoritative Multiplayer Game Server (Nakama + Go)
## Multiplayer Tic-Tac-Toe Platform
**To:** CTO & Technical Interview Panel A production-grade, server-authoritative multiplayer backend built using **Nakama 3**, **Go plugins**, and **PostgreSQL**, deployed via **Drone CI/CD** and exposed securely through **Traefik**.
**Date:** November 25, 2025
**Version:** v0.0.1
--- ---
## **1. Objective** ## 🚀 Overview
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. This repository contains a fully authoritative Tic-Tac-Toe backend built on **Nakama 3.21.0 ARM**, using a custom **Go plugin** to implement game logic, matchmaking, validation, leaderboards, and session management.
The server ensures:
* **Zero client trust**
* **Deterministic gameplay**
* **Secure matchmaking**
* **Validated moves**
* **Authoritative outcomes**
Designed for scalability, extensibility, and cloud deployment (GCP, AWS, or ARM homelab environments).
--- ---
## **2. Current Completion Summary** ## ⭐ Key Features
| Requirement | Status | * **Authoritative Go match handler** (`tictactoe`)
| ---------------------------------------- | -------------------------------- | * Device-ID authentication + JWT session
| Install Nakama + PostgreSQL | ✅ Completed | * Matchmaking queue support
| Custom Go server plugins | ✅ Completed | * Deterministic game validation
| Server-authoritative Tic-Tac-Toe | ✅ Completed | * Match lifecycle handling
| Real-time WebSocket communication | ✅ Completed | * Disconnect handling & forfeit detection
| Device-based authentication | ✅ Completed | * Leaderboards creation + scoring
| JWT-based session management | ✅ Completed | * Production-grade CI/CD with Drone
| Match creation & joining | ✅ Completed | * Automated ARM Docker builds
| Matchmaking queue support | 🟡 Not Started | * Full Traefik routing (HTTP + WebSocket)
| Game state validation & turn enforcement | 🟡 Not Started | * Compatible with x86/ARM/GCP
| Leaderboard/tracking foundation | 🟡 Not Started |
| UI Game Client | 🟡 Not Started |
| Google Cloud deployment | 🟡 Not Started |
**Core backend functionality is complete and stable**
--- ---
## **3. Core Technical Architecture** ## 🧩 Architecture
* **Backend Framework:** Nakama 3.x ### High-Level System Diagram
* **Business Logic:** Custom Go runtime module
* **Database:** PostgreSQL 14 ```mermaid
* **Transport:** WebSockets (real-time) flowchart LR
* **Authentication:** Device-ID based auth → JWT session returned UI[(React + Vite)] --> Traefik
* **State Management:** Server-authoritative, deterministic Traefik --> Nakama[Nakama Server]
* **Protocol:** Nakama RT JSON envelopes Nakama --> Plugin[Go Plugin]
* **Build & Deployment:** Docker-based Nakama --> Postgres[(PostgreSQL)]
Drone[Drone CI/CD] --> Registry[Private Docker Registry]
Registry --> Nakama
```
--- ---
## **4. Authentication** ## 🛠 Tech Stack
Implemented **Nakama device authentication flow**: **Backend**
1. Client provides device UUID * Nakama 3.21.0 ARM
2. Nakama validates & creates account if needed * Go 1.21.6 (plugin ABI compatible)
3. Server responds with **JWT session token** * PostgreSQL 16
4. Client uses JWT for all WebSocket connections * Docker / multi-stage builds
* Drone CI/CD
* Traefik reverse proxy
**Cloud/Infra**
* GCP-ready
* ARM homelab deployments
* Private registry support
--- ---
## **5. Game Server Logic** ## 📦 Repository Structure
Implemented as Go match module: ```
tic-tac-toe/
* Turn-based validation │── plugins/
* Board occupancy checks │ ├── main.go
* Win/draw/forfeit detection │ ├── match.go
* Automatic broadcast of updated state │ └── matchmaking.go
* Graceful match termination
* Prevents cheating & client-side manipulation │── Dockerfile
│── go.mod
Result: │── go.sum
✅ Entire game lifecycle enforced server-side. │── README.md
```
--- ---
## **6. Real-Time Networking** ## 🔌 Registered Server Components
Clients communicate via: ### 📌 Match Handler
* `match_create` Name: **`tictactoe`**
* `match_join`
* `match_data_send` (OpCode 1) → moves
* Broadcast state updates (OpCode 2)
Python WebSocket simulation confirms: Handles:
✅ Move sequencing
✅ Session isolation * Turn validation
✅ Messaging reliability * State updates
✅ Auto-cleanup on disconnect * Win/loss/draw
* Disconnect forfeit
* Broadcasting authoritative state
### 📌 RPCs
| RPC Name | Purpose |
| ----------------------- | ---------------------------------------- |
| **`leave_matchmaking`** | Allows clients to exit matchmaking queue |
### 📌 Matchmaker Hook
| Hook | Purpose |
| ----------------------- | --------------------------------------------------------- |
| **`MatchmakerMatched`** | Validates matchmaker tickets and instantiates a new match |
--- ---
## **7. Matchmaking** ## 🎮 Gameplay Protocol
**NOT STARTED** ### OpCodes
| OpCode | Direction | Meaning |
| ------ | --------------- | ----------------------------- |
| **1** | Client → Server | Submit move `{x, y}` |
| **2** | Server → Client | Full authoritative game state |
--- ---
## **8. Testing & Validation** ## 🧠 Authoritative Flow Diagram
Performed using: ```mermaid
sequenceDiagram
participant C1 as Client 1
participant C2 as Client 2
participant S as Nakama + Go Plugin
* Automated two-client WebSocket game flow - Happy path C1->>S: Matchmaker Join
C2->>S: Matchmaker Join
**PENDING STRESS AND EDGE CASE TESTING** S->>S: MatchmakerMatched()
S-->>C1: Matched
S-->>C2: Matched
C1->>S: OpCode 1 (Move)
S->>S: Validate Move
S-->>C1: OpCode 2 (Updated State)
S-->>C2: OpCode 2 (Updated State)
```
--- ---
## **9. UI Status** ## ⚙️ Build & Development
Not implemented yet — intentionally deferred. ### Build Go Plugin
Planned: ```
CGO_ENABLED=1 go build \
--trimpath \
--buildmode=plugin \
-o build/main.so \
./plugins
```
* Simple browser/mobile client ### Run Nakama Locally
* Display board, turns, win state
* WebSocket integration
* Leaderboard screen
Estimated time: **610 hours** ```
nakama migrate up --database.address "$DB_ADDR"
nakama \
--database.address "$DB_ADDR" \
--socket.server_key="$SERVER_KEY"
```
--- ---
## **10. Leaderboard System** ## 🐳 Docker Build (Multi-Stage)
Backend-ready but not finalized: ```dockerfile
FROM --platform=linux/arm64 golang:1.21.6 AS plugin_builder
✅ Database & Nakama leaderboard APIs available RUN go mod download
✅ Game result reporting planned COPY . .
🟡 Ranking, ELO, win streak logic pending RUN CGO_ENABLED=1 go build --buildmode=plugin -o build/main.so ./plugins
Estimated time: **46 hours** FROM heroiclabs/nakama:3.21.0-arm
COPY --from=plugin_builder /workspace/build/main.so /nakama/data/modules/main.so
ENTRYPOINT ...
```
--- ---
## **11. Google Cloud Deployment (Interview-Scope)** ## 🤖 CI/CD — Drone Pipeline
Goal: **Simple, affordable, demo-ready deployment** Drone performs:
### Planned architecture: 1. Fetch latest Git tag
2. Check if Docker image already exists
3. Build Nakama + plugin
4. Push to private registry
5. Stop old container
6. Deploy new Nakama server automatically
* Highly subjective as of now Full pipeline included in repository (`.drone.yml`).
| Component | GCP Service |
| ------------- | ----------------------------- |
| Nakama server | Compute Engine VM (Docker) |
| PostgreSQL | Cloud SQL (shared tier) |
| Game UI | Cloud Run or Firebase Hosting |
| Networking | Static IP + HTTPS |
| Auth secrets | Secret Manager (optional) |
Estimated setup time: **68 hours**
--- ---
## **12. Risks & Considerations** ## 🌐 Traefik Routing
| Risk | Mitigation | ### HTTPS
| ------------------------ | ------------------------- |
| No UI yet | Prioritized next |
| Only happy path tested | In parallel with UI work |
| Matchmaking incomplete | Clear implementation plan |
| Leaderboard incomplete | Clear implementation plan |
None block demonstration or evaluation. ```
nakama.aetoskia.com
/ → HTTP API
/ws → WebSocket (real-time)
```
Includes:
* CORS rules
* WebSocket upgrade headers
* Certificate resolver
* Secure default middlewares
--- ---
## **13. Next Steps** ## 🔧 Environment Variables
1. Implement browser/mobile UI | Variable | Description |
2. Stress, load and Edge case testing | --------------- | ---------------------------- |
3. Complete match making, leaderboard scoring | `DB_ADDR` | PostgreSQL connection string |
4. Deploy to Google Cloud for public access | `SERVER_KEY` | Nakama server key |
5. Record demo video + documentation | `REGISTRY_HOST` | Private registry |
Estimated remaining effort: **1.52.5 days**
--- ---
## **Executive Summary** ## 📊 Leaderboards
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. * Created during server start
* Score: `+1` on win
* Metadata logged (mode, player IDs)
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. ---
**The project is technically strong, progressing as planned, and positioned for successful final delivery and demonstration.** ## 🧪 Testing
* Win/loss/draw simulation
* Invalid move rejection
* Disconnect → forfeit
* Load testing matchmaking
---
## 📈 Production Deployment
Supported on:
* ARM homelabs (Raspberry Pi)
* Google Cloud (Compute Engine + Cloud SQL)
* AWS EC2
* Kubernetes (optional)
---
## 🤝 Contributing
1. Fork repo
2. Create feature branch
3. Write tests
4. Submit PR
Coding style: Go fmt + idiomatic Go; follow Nakama plugin constraints.
---

View File

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

View File

@@ -7,3 +7,10 @@ session:
socket: socket:
max_message_size_bytes: 4096 # reserved buffer max_message_size_bytes: 4096 # reserved buffer
max_request_size_bytes: 131072 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]: elif p not in matches[p.match_id]:
matches[p.match_id].append(p) matches[p.match_id].append(p)
# print(f'player = {p.label} for match = {p.match_id}') # 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 # stop early if all assigned
if sum(len(v) for v in matches.values()) >= num_players: if sum(len(v) for v in matches.values()) >= num_players:

View File

@@ -45,6 +45,21 @@ func InitModule(
return err 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!") logger.Info("Go module loaded successfully!")
return nil return nil
} }

View File

@@ -106,6 +106,21 @@ func (m *TicTacToeMatch) MatchJoin(
} }
logger.Info("MatchJoin: now %d players", len(s.Players)) 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 return s
} }
@@ -128,6 +143,18 @@ func (m *TicTacToeMatch) MatchLeave(
s.GameOver = true s.GameOver = true
s.Winner = "forfeit" s.Winner = "forfeit"
logger.Info("MatchLeave: game ended by 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 return s
@@ -151,8 +178,11 @@ func (m *TicTacToeMatch) MatchLoop(
return s return s
} }
changed := false
for _, msg := range messages { for _, msg := range messages {
if msg.GetOpCode() != OpMove { if msg.GetOpCode() != OpMove {
logger.Debug("Ignoring non-move opcode: %d", msg.GetOpCode())
continue continue
} }
@@ -162,46 +192,108 @@ func (m *TicTacToeMatch) MatchLoop(
} }
if err := json.Unmarshal(msg.GetData(), &move); err != nil { 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 continue
} }
playerID := msg.GetUserId() playerID := msg.GetUserId()
playerIdx := indexOf(s.Players, playerID) 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 { if playerIdx != s.Turn {
// not your turn logger.Warn("Move rejected: not player's turn (playerIdx=%d turn=%d)", playerIdx, s.Turn)
continue continue
} }
if move.Row < 0 || move.Row > 2 || move.Col < 0 || move.Col > 2 { 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 continue
} }
if s.Board[move.Row][move.Col] != "" { if s.Board[move.Row][move.Col] != "" {
logger.Warn("Move rejected: cell already occupied (%d,%d)", move.Row, move.Col)
continue continue
} }
symbols := []string{"X", "O"} symbols := []string{"X", "O"}
if playerIdx < 0 || playerIdx >= len(symbols) { if playerIdx < 0 || playerIdx >= len(symbols) {
logger.Warn("Move rejected: invalid player index %d", playerIdx)
continue 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 != "" { if winner := checkWinner(s.Board); winner != "" {
s.Winner = winner s.Winner = winner
s.GameOver = true s.GameOver = true
logger.Info("Game over! Winner: %s", winner)
} else if fullBoard(s.Board) { } else if fullBoard(s.Board) {
s.Winner = "draw" s.Winner = "draw"
s.GameOver = true s.GameOver = true
logger.Info("Game over! Draw")
} else { } else {
s.Turn = 1 - s.Turn 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 // If anything changed (or periodically if you want), broadcast updated state to everyone
stateJSON, _ := json.Marshal(s) if changed {
if err := dispatcher.BroadcastMessage(OpState, stateJSON, nil, nil, true); err != nil { stateJSON, err := json.Marshal(s)
logger.Error("BroadcastMessage failed: %v", err) 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 return s

View File

@@ -29,11 +29,12 @@ func MatchmakerMatched(
propsA := entries[0].GetProperties() propsA := entries[0].GetProperties()
propsB := entries[1].GetProperties() propsB := entries[1].GetProperties()
validModes := map[string]bool{"classic": true, "blitz": true}
modeA, okA := propsA["mode"].(string) modeA, okA := propsA["mode"].(string)
modeB, okB := propsB["mode"].(string) modeB, okB := propsB["mode"].(string)
if !okA || !okB { if !okA || !okB || !validModes[modeA] || !validModes[modeB] {
logger.Warn("MatchmakerMatched missing mode property — ignoring") logger.Warn("MatchmakerMatched missing mode property — ignoring")
return "", nil return "", nil
} }