mirror of
https://github.com/tiennm99/gomoku.git
synced 2026-07-16 16:17:21 +00:00
cbc74c7813decf06591cda98246abd346007e691
Fixes the server log "gameover player N: unexpected Request_CreateRoom". Root cause: when ANY player leaves a room, leaveRoom broadcasts ClientExitResponse to ALL players in room.Players (so peers see who left). The broadcast fired CLIENT_EXIT on both the exiting client AND its peer. But the client's handlers treated every CLIENT_EXIT as "I exited" — transitioning to lobby, clearing gameState.roomId, hiding the HUD. Meanwhile the peer's server-side state machine was still in gameoverState. The peer then clicked Create Room from the (now wrongly shown) lobby, and the request arrived at gameoverState's default case. Client: - New services/client-exit-helpers.js with isSelfExit(data, clientId). exitClientId === 0 or missing → bare self-ack (home.ClientExit / watching.ClientExit / room-closed eject). exitClientId === clientId → our own leaveRoom broadcast loopback. Anything else → a peer left; stay in state. - game-state-service CLIENT_EXIT handler: only reset() when isSelfExit. - game-scene._onClientExit: only transition to MenuScene when isSelfExit; peer exits render a toast "<nickname> left the room" instead. - menu-ui.js module CLIENT_EXIT handler: only showLobby() when isSelfExit. Server: the client fix alone leaves a new hole — after a peer leaves a finished PVP room, the remaining player sits in gameoverState with a game-over modal and two buttons. "Play Again" would try to restart a PVP game with only one human; "Leave" still works. To close this hole cleanly: - state/waiting.go: add kickStaleRoomPeers helper. After a leaveRoom in gameoverState, if the room is a PVP room with < 2 remaining human players it's unplayable — push a synthetic Request_ClientExit onto each remaining peer's CmdCh. Their gameoverState goroutine wakes up, processes the synthetic exit, and returns StateHome in lockstep. - state/gameover.go: call kickStaleRoomPeers after leaveRoom in the ClientExit case. Tests: - Rename TestFlow_CreateRoomAfterOpponentForfeit → TestFlow_OpponentExit_KicksPeerFromGameover and rewrite to assert the auto-kick behavior: black leaves → white's gameoverState must return StateHome WITHOUT any test-driven CmdCh push. Then both players can create fresh rooms. Full suite + client build green.
Gomoku (Five-in-a-Row)
Multiplayer online Gomoku with PVP, PVE (3 AI difficulties), and spectator mode. Go server + Phaser 3 browser client communicating over protobuf WebSocket binary frames.
Forked from ratel-online by ainilili and contributors. Card games removed; gomoku rewritten with a caro-compatible protobuf protocol and Phaser 3 client (feature set ported from tiennm99/caro).
Features
- PVP — two human players in a room
- PVE — three AI difficulties:
- Easy — random legal move
- Medium — heuristic threat scoring
- Hard — minimax depth 3 with alpha-beta pruning
- Spectator mode — watch any in-progress game in real time
- Owner-controlled start — room owner starts when ready; other players can join before start
- Live lobby — room list updates as rooms open / close
- Heartbeat / reconnect — client pings every 5 s; missed pings close the connection cleanly
Quick Start
Docker (recommended)
docker compose up -d
- Browser client: http://localhost:8080
- WebSocket endpoint: ws://localhost:1999/gomoku
Tear down:
docker compose down
Local Development
Prerequisites: Go 1.23+, Node 22+
# Terminal 1 — Go server
go -C server run . -p 1999
# Terminal 2 — Vite dev server
npm --prefix client install
npm --prefix client run dev
Open http://localhost:5173 in two browser tabs to play against yourself.
Architecture
Browser tab A ─────────────────────────────────────────────────┐
Browser tab B ──► http://localhost:8080 (nginx / client:8080) │
│ │
Phaser 3 + protobufjs │
│ WS binary frames │
▼ │
ws://localhost:1999/gomoku (Go server) │
│ │
┌──────────┴──────────┐ │
│ state machine │ one goroutine per player │
│ per-player │ lobby → waiting → game │
├─────────────────────┤ │
│ in-memory domain │ rooms, players (no DB) │
├─────────────────────┤ │
│ game engine │ 15×15 board, win detect │
└─────────────────────┘ │
│ │
common/proto/*.proto (single source of truth)────┘
Key design decisions:
- Server-authoritative — all move validation and win detection server-side; browser never modifies board locally
- Protobuf binary frames over WebSocket (
gorilla/websocket);common/proto/is the single source of truth - State machine per player —
lobby → waiting → game; states registered inserver/state/ - In-memory store — rooms and players live in hashmaps; restart loses all state (intentional — games are short-lived)
- Channel-based turn sync — game turns flow via
chanper player, no polling
Project Structure
gomoku/
├── server/ Go game server (WebSocket :1999, WS endpoint /gomoku)
│ ├── main.go
│ ├── consts/ state IDs, game constants
│ ├── database/ in-memory room + player store
│ ├── game/ gomoku engine (board, win detection, AI)
│ ├── network/ WebSocket server, connection handler
│ ├── protocol/ protobuf encode/decode helpers
│ ├── state/ state machine (lobby, waiting, game)
│ ├── Dockerfile multi-stage Go build → distroless runtime
│ └── Makefile build / test / docker targets
│
├── client/ Phaser 3 browser client (Vite, served by nginx :8080)
│ ├── src/
│ │ ├── scenes/ Phaser scenes (lobby, game, spectate)
│ │ ├── services/ connection, room, game, AI services
│ │ └── generated/ protobuf JS stubs (committed, no build-time protoc)
│ ├── Dockerfile Vite build → nginx:1.27-alpine
│ └── nginx.conf SPA fallback, gzip, aggressive asset caching
│
├── common/
│ └── proto/ request.proto / response.proto (shared by server + client)
│
├── docker-compose.yml two services: server :1999, client :8080
└── docs/ architecture, deployment, code standards
Game Rules
- Board: 15 × 15 intersections
- Black moves first
- First player to place 5 stones in a row (horizontal, vertical, or diagonal) wins
- Full board with no winner is a draw
Protocol
- Transport: WebSocket binary frames (
ws://<host>:1999/gomoku) - Encoding: protobuf (see
common/proto/request.proto,common/proto/response.proto) - Auth: client sends
AuthRequest{name}immediately after connect; server replies withAuthResponse{playerId, color} - Heartbeat: client →
HeartbeatRequestevery 5 s; server repliesHeartbeatResponse; 3 missed → disconnect - Auto-reconnect: client retries with exponential back-off on connection loss
Deployment Notes
See docs/deployment-guide.md for:
- Docker Compose quick start
- Building individual images
- Reverse proxy / TLS setup (Caddy, Traefik, nginx)
Credits
- ratel-online by ainilili — original Go game server framework (MIT)
- tiennm99/caro — feature-set reference: protobuf protocol, AI engine, Phaser 3 client
- Phaser 3 — HTML5 game framework
- gorilla/websocket — WebSocket library
- protobuf + protobufjs — binary protocol
Implementation history: plans/260411-1101-port-caro-feature-set/plan.md
License
Apache 2.0 — see LICENSE.
Description
A simple Gomoku game with networking. Base on [Ratel](https://github.com/ratel-online/)
Readme
Apache-2.0
1.3 MiB
Languages
JavaScript
73.4%
Go
25.3%
HTML
1%
Makefile
0.2%
Dockerfile
0.1%