diff --git a/CLAUDE.md b/CLAUDE.md index bc9d357..ee65324 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,112 +4,114 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -Monorepo for a multiplayer gomoku (and other board/card games) platform, forked from [ratel-online](https://github.com/ratel-online). Four independent modules: three Go modules and one Java/Spring Boot API. +Multiplayer Gomoku (Five-in-a-Row) platform. Go game server with WebSocket/TCP support, vanilla JS web client with HTML5 Canvas board, and a shared Go core library. Forked from [ratel-online](https://github.com/ratel-online) with card games removed — gomoku is the only game type. ## Architecture ``` -client (Go CLI) ──TCP/WS──► server (Go game server) - │ - ├── state machine (per-player) - ├── in-memory database (hashmaps) - └── protocol abstraction (TCP + WebSocket) +web/ (browser) ──WS──► server/ (:9998, serves static files + /ws endpoint) +client/ (CLI) ──TCP──► server/ (:9999) + │ + ├── state machine (per-player goroutine) + │ welcome → home → join/create → waiting → game + ├── in-memory database (hashmaps, no persistence) + ├── gomoku engine (15x15 board, win detection) + └── protocol abstraction (TCP + WebSocket) -core (Go shared lib) ◄── imported by both server and client - -api (Java/Spring Boot) ── standalone REST API (user auth, MySQL, Redis) +core/ (Go shared lib) ◄── imported by server and client +api/ (Java/Spring Boot) ── standalone REST API (optional, not needed for play) ``` **Key design patterns:** - **State machine per player** (`server/state/`): each state implements `Next(player) -> (nextStateID, error)`. States registered in `state.go` via `Register()`. -- **Protocol abstraction** (`core/protocol/`): `ReadWriteCloser` interface wraps both TCP (4-byte length-prefixed binary) and WebSocket (JSON). Server and client share same interface. +- **Protocol abstraction** (`core/protocol/`): `ReadWriteCloser` interface wraps both TCP (4-byte length-prefixed binary) and WebSocket (JSON `{"data":""}` packets). - **In-memory game store** (`server/database/`): volatile hashmaps for players, rooms, spectators. No persistence — restart loses all state. -- **Channel-based game sync**: game states communicated via `States[playerID] chan int`, not locks. - -**Dependency flow:** `server → core`, `client → core`, `api` is isolated (Java). +- **Channel-based game sync**: game turns communicated via `States[playerID] chan int`, not locks. Mutex protects board reads/writes. +- **Server-authoritative**: all move validation and win detection server-side. Web client never modifies board locally. ## Build & Run ### Server (Go) ```bash cd server -go build -o ratel-server main.go # build -go run main.go -w 9998 -t 9999 # run (WS + TCP) +go build -o gomoku-server main.go +go run main.go -w 9998 -t 9999 -s ../web +# Web UI at http://localhost:9998, WebSocket at ws://localhost:9998/ws ``` -Server flags: `-w` (WebSocket port, default 9998), `-t` (TCP port, default 9999), `-bot`, `-bot-token`, `-bot-group` (QQ bot). +Server flags: `-w` (WebSocket port, default 9998), `-t` (TCP port, default 9999), `-s` (static files dir, default `../web`), `-bot`, `-bot-token`, `-bot-group` (QQ bot). -### Client (Go) +### Client (Go CLI) ```bash cd client go run main.go -h 127.0.0.1 -p 9999 # TCP -go run main.go -h 127.0.0.1 -p 9998 # WebSocket (auto-detected by port) -``` - -Client flags: `-h` (host), `-p` (port), `-n` (player name, auto-generated if empty). - -### Cross-compilation -```bash -cd server && bash build.sh # Linux/macOS — outputs to target/ -cd server && powershell ./build.ps1 # Windows — outputs to target/ +go run main.go -h 127.0.0.1 -p 9998 # WebSocket ``` ### Docker ```bash cd server -make build && make run # dev: ports 9998 + 9999 -make prod-build && make prod-up # prod: adds nginx on port 80 -make logs # view logs -make stop # stop +make build && make run # dev: ports 9998 + 9999 +make logs # view logs +make stop # stop ``` -### API (Java) -```bash -cd api -mvn spring-boot:run # requires MySQL + Redis configured -``` -Config in `api/src/main/resources/application.properties`. Database schema in `api/ratel.sql`. - ### Tests ```bash cd server && go test ./... cd core && go test ./... -cd client && go test ./... ``` ## Module Details ### server/ - `main.go`: entry point, starts TCP + WS listeners concurrently -- `state/`: state machine — `welcome → home → join/create → waiting → game` -- `state/game/`: game implementations (dou dizhu, texas, mahjong, uno, liar, runfast) -- `network/`: TCP (`tcp.go`) and WebSocket (`wss.go`) listeners, both call shared `handle()` -- `database/`: in-memory player/room store, game-specific record types -- `consts/`: state IDs, game types, timeouts (20s rob, 40s play, 60s bet), room properties -- `rule/`: game rule validation -- `render/`: terminal UI rendering -- `skill/`: special ability system for skill-mode games +- `consts/const.go`: state IDs (`StateWelcome` through `StateGomokuGame`), `GameTypeGomoku=1`, board size (15), timeouts, error constants +- `state/state.go`: state machine runner — registers states, loops `Next()` calls per player +- `state/welcome.go`, `home.go`, `join.go`, `create.go`: menu navigation states +- `state/waiting.go`: room lobby — start game, kick players, set room props +- `state/game/gomoku.go`: gomoku game loop — turn handling, move validation, win detection (4 directions), board broadcast +- `database/model.go`: `Player`, `Room`, `RoomGame` interface — core domain types +- `database/gomoku.go`: `Gomoku` struct — board state, player IDs, turn tracking, channel-based sync +- `database/database.go`: in-memory store — room CRUD, player management, broadcast helpers +- `network/network.go`: connection handler — auth, state machine bootstrap +- `network/wss.go`: WebSocket server + static file serving +- `network/tcp.go`: TCP server +- `bot/bot.go`: QQ bot integration (optional) ### core/ -- `protocol/`: `Packet` struct + `ReadWriteCloser` interface (TCP and WS implementations) -- `model/`: shared types — `AuthInfo`, `Player`, `Room`, `GameEvent` +- `protocol/protocol.go`: `Packet` struct + `ReadWriteCloser` interface +- `protocol/websocket.go`: WebSocket implementation (JSON `{"data":""}`) +- `protocol/tcp.go`: TCP implementation (4-byte length-prefixed binary) +- `model/model.go`: shared types — `AuthInfo`, `Player`, `Room` - `network/`: `Conn` wrapper with auto-assigned IDs and handler loop -- `util/poker/`: card evaluation, hand ranking for multiple game types -- `pkg/holdem/`: Texas hold'em hand evaluation + +### web/ +- `index.html`: HTML shell with panels (connect, home, create, waiting, game, log sidebar) +- `js/ws-client.js`: WebSocket client — base64 encode/decode, auth, transaction markers +- `js/state-machine.js`: detects server state from message patterns, routes UI panels +- `js/board-renderer.js`: Canvas 15x15 board — gradient stones, click handling, hover ghost, game-over overlay +- `js/game-controller.js`: bridges WS client + board renderer — handles turn/board/gameover messages +- `js/app.js`: entry point — wires all modules, DOM event bindings ### client/ - `main.go`: CLI entry, connects via TCP or WS based on port -- `ctx/`: connection context, auth flow, packet listener with start/stop markers +- `ctx/`: connection context, auth flow, packet listener - `shell/`: wraps context, manages player session -- `api/`: login HTTP calls to the Java API (port 9088) ### api/ (Java) -- Spring Boot 2.3.1, MySQL + Redis + MyBatis -- Controllers: `AuthController`, `UserController`, `MailController` -- Single `user` table (see `ratel.sql`) +- Spring Boot 2.3.1, MySQL + Redis + MyBatis (optional, not required for gameplay) ## Networking Protocol -Auth flow: client sends `AuthInfo` JSON within 3 seconds → server creates player → state machine starts. +**Auth flow**: client sends `AuthInfo{id, name, score}` JSON within 3 seconds → server creates player → state machine starts. -Packet format (TCP): `[4-byte big-endian length][JSON payload]`. WebSocket uses native JSON messages. +**Packet format**: TCP uses `[4-byte big-endian length][payload]`. WebSocket uses JSON `{"data":""}`. + +**Game messages** (server → client, JSON strings): +- `{"type":"info","color":1}` — player color assignment (1=black, 2=white) +- `{"type":"board","board":[[...]],"last":[r,c],"turn":1}` — full board state +- `{"type":"turn","color":1}` — it's your turn +- `{"type":"gameover","winner":"Name"}` or `{"type":"gameover","draw":true}` + +**Player input** (client → server): plain strings — `"1"` for join, `"2"` for new, `"7,7"` for moves. diff --git a/README.md b/README.md index cd95c04..29bf283 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,202 @@ -# gomoku +# Gomoku Online -A simple Gomoku game with networking. Based on [Ratel](https://github.com/ratel-online/). +A multiplayer Gomoku (Five-in-a-Row) game with a Go server and web client. Players connect via WebSocket, create or join rooms, and play on a 15x15 board rendered on HTML5 Canvas. + +Based on [Ratel Online](https://github.com/ratel-online/). + +## Quick Start + +```bash +# Build and run the server +cd server +go build -o gomoku-server main.go +./gomoku-server -w 9998 -t 9999 -s ../web + +# Open http://localhost:9998 in your browser +# Open a second tab to play against yourself +``` ## Project Structure -This is a monorepo containing the following components: +``` +gomoku/ + server/ Go game server (WebSocket + TCP, state machine, in-memory DB) + core/ Shared Go library (protocol, models, networking) + client/ Go CLI client (terminal-based) + web/ Browser client (vanilla JS, HTML5 Canvas) + api/ Java REST API (user auth, MySQL — optional, not required for play) +``` -| Directory | Description | Language | Source | -|-----------|-------------|----------|--------| -| `server/` | Game server with multiplayer support | Go | [ratel-online/server](https://github.com/ratel-online/server) | -| `core/` | Shared core logic, models, and networking | Go | [ratel-online/core](https://github.com/ratel-online/core) | -| `client/` | CLI game client | Go | [ratel-online/client](https://github.com/ratel-online/client) | -| `api/` | API definitions | Java | [ratel-online/api](https://github.com/ratel-online/api) | +## Architecture + +``` +Browser (web/) ──WebSocket──> server/ (:9998) + | +CLI (client/) ──TCP/WS─────> |── state machine (per player) + |── in-memory database (rooms, players) + |── gomoku game engine (15x15, win detection) + | + |── serves web/ as static files at / + |── WebSocket endpoint at /ws + +core/ ── shared protocol, models, networking (imported by server + client) +``` + +**Key design decisions:** +- **Server-authoritative**: All move validation and win detection happens server-side. The web client never updates the board locally — it waits for the server's broadcast. +- **State machine per player**: Each connected player runs through states: `welcome -> home -> join/create -> waiting -> game`. States are registered in `server/state/state.go`. +- **In-memory store**: All game state lives in memory. Server restart loses all rooms and games. This is intentional — gomoku games are short-lived. +- **No build step for web client**: Vanilla JS, no bundler, no npm. Just serve the files. + +## How to Play + +### Web Client (recommended) + +1. Start the server (see Quick Start above) +2. Open `http://localhost:9998` in your browser +3. Enter a player name and click **Connect** +4. Click **New Room** to create a gomoku room +5. Select **Gomoku** from the game type list +6. Share the room with another player (they click **Join Room** and select the room ID) +7. Room owner clicks **Start Game** when both players are in +8. Black plays first — click on a board intersection to place a stone +9. First player to get 5 in a row (horizontal, vertical, or diagonal) wins + +### CLI Client + +```bash +cd client +go run main.go -h 127.0.0.1 -p 9999 # TCP connection +go run main.go -h 127.0.0.1 -p 9998 # WebSocket connection +``` + +In the CLI, type numbers to navigate menus and `row,col` to place stones (e.g., `7,7`). + +### Room Commands (in waiting state) + +| Command | Description | +|---------|-------------| +| `start` or `s` | Start the game (owner only, needs 2 players) | +| `ls` or `v` | View room players and settings | +| `kicking ` or `k ` | Kick a player by ID (owner only) | +| `set pwd ` | Set room password | +| `set ip on/off` | Show/hide player IPs | +| `exit` or `e` | Leave the room | + +## Server Configuration + +```bash +./gomoku-server [flags] +``` + +| Flag | Default | Description | +|------|---------|-------------| +| `-w` | `9998` | WebSocket port (also serves web client) | +| `-t` | `9999` | TCP port (for CLI client) | +| `-s` | `../web` | Path to web client static files directory | +| `-bot` | (empty) | QQ bot connection address (optional) | +| `-bot-token` | (empty) | QQ bot authentication token | +| `-bot-group` | `0` | QQ bot group ID for notifications | + +## Deployment + +### Docker + +```bash +cd server + +# Development +make build && make run +# -> WebSocket + Web UI at http://localhost:9998 +# -> TCP at localhost:9999 + +# View logs +make logs + +# Stop +make stop + +# Clean up +make clean +``` + +### Docker Compose (manual) + +```bash +cd server +docker compose up -d +``` + +This exposes: +- Port `9998`: WebSocket server + web client UI +- Port `9999`: TCP server for CLI clients + +### Production + +For production deployment behind a reverse proxy: + +1. Build the Docker image: `docker build -t gomoku-server:latest server/` +2. Run with your orchestrator (Docker Compose, Kubernetes, etc.) +3. Point a reverse proxy (nginx, Caddy) at port `9998` with WebSocket upgrade support +4. Ensure the `/ws` path proxies WebSocket connections correctly + +Example nginx config: +```nginx +server { + listen 80; + server_name gomoku.example.com; + + location / { + proxy_pass http://localhost:9998; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + } +} +``` + +### Cross-compilation + +```bash +cd server +bash build.sh # Linux/macOS — outputs to target/ +powershell ./build.ps1 # Windows — outputs to target/ +``` + +## Development + +### Prerequisites + +- Go 1.22+ +- Modern browser (Chrome, Firefox, Edge) for web client +- Docker + Docker Compose (optional, for containerized deployment) + +### Build + +```bash +cd server && go build ./... +cd core && go build ./... +cd client && go build ./... +``` + +### Tests + +```bash +cd server && go test ./... +cd core && go test ./... +``` + +### Protocol + +The server uses a JSON-over-WebSocket protocol: + +- **Wire format**: `{"data":""}` — Go's `[]byte` serializes as base64 in JSON +- **Auth flow**: Client sends `{"id":0, "name":"PlayerName", "score":0}` within 3 seconds of connecting +- **State signals**: `INTERACTIVE_SIGNAL_START` / `INTERACTIVE_SIGNAL_STOP` control when the client can send input +- **Game messages** (JSON): `{"type":"board",...}`, `{"type":"turn",...}`, `{"type":"gameover",...}` +- **Text messages**: Plain strings for menus, errors, and chat ## Credits -This project is built upon the work of the [Ratel Online](https://github.com/ratel-online) organization. The original repositories are licensed under the **MIT License**. - -### Original Authors & Contributors - -- [ainilili](https://github.com/ainilili) — Primary author -- [feel-easy](https://github.com/feel-easy) -- [mlmdflr](https://github.com/mlmdflr) -- [mikodream](https://github.com/mikodream) -- [EldersJavas](https://github.com/EldersJavas) - -See the `LICENSE` file in each subdirectory for the original MIT license terms. +Built upon [Ratel Online](https://github.com/ratel-online) by [ainilili](https://github.com/ainilili) and contributors. Licensed under MIT — see `LICENSE` files in each subdirectory. diff --git a/docs/deployment-guide.md b/docs/deployment-guide.md new file mode 100644 index 0000000..2e9a5dc --- /dev/null +++ b/docs/deployment-guide.md @@ -0,0 +1,165 @@ +# Deployment Guide + +## Prerequisites + +| Requirement | Version | Purpose | +|-------------|---------|---------| +| Go | 1.22+ | Build server and CLI client | +| Docker | 20+ | Containerized deployment | +| Docker Compose | v2+ | Orchestration | +| Modern browser | Chrome/Firefox/Edge | Web client | + +## Local Development + +### 1. Build and Run Server + +```bash +cd server +go build -o gomoku-server main.go +./gomoku-server -w 9998 -t 9999 -s ../web +``` + +This starts: +- **Port 9998**: WebSocket server + web client UI at `http://localhost:9998` +- **Port 9999**: TCP server for CLI clients + +### 2. Open Web Client + +Open `http://localhost:9998` in two browser tabs. Enter names, connect, create a room, join it, and start playing. + +### 3. CLI Client (optional) + +```bash +cd client +go run main.go -h 127.0.0.1 -p 9999 +``` + +### Server Flags + +| Flag | Default | Description | +|------|---------|-------------| +| `-w` | `9998` | WebSocket port (serves web UI + `/ws` endpoint) | +| `-t` | `9999` | TCP port (CLI client) | +| `-s` | `../web` | Static files directory for web client | +| `-bot` | *(empty)* | QQ bot address (optional) | +| `-bot-token` | *(empty)* | QQ bot token | +| `-bot-group` | `0` | QQ group ID for notifications | + +## Docker Deployment + +### Development + +```bash +cd server +make build # builds Docker image from repo root context +make run # starts container, maps ports 9998 + 9999 +make logs # tail container logs +make stop # stop container +make clean # remove container and image +``` + +### Manual Docker Compose + +```bash +cd server +docker compose up -d +``` + +The `docker-compose.yaml` sets build context to the repo root (`..`) so the Dockerfile can access both `server/` and `web/`. + +### Standalone Docker Build + +From the **repo root**: + +```bash +docker build -t gomoku-server:latest -f server/Dockerfile . +docker run -d -p 9998:9998 -p 9999:9999 --name gomoku gomoku-server:latest +``` + +## Production Deployment + +### Reverse Proxy (nginx) + +The server handles both HTTP (static files) and WebSocket (`/ws`) on the same port. Your reverse proxy must support WebSocket upgrade: + +```nginx +server { + listen 80; + server_name gomoku.example.com; + + location / { + proxy_pass http://gomoku-server:9998; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} +``` + +### HTTPS / TLS + +Add TLS termination at the reverse proxy layer. The Go server itself does not handle TLS. Clients will connect via `wss://` automatically when the page is served over HTTPS. + +### Resource Requirements + +The server is lightweight — all state is in-memory: + +| Metric | Estimate | +|--------|----------| +| Memory | ~50MB base + ~1KB per active player | +| CPU | Minimal — one goroutine per player | +| Disk | None (no persistence) | +| Network | ~1KB per move (JSON board broadcast) | + +### Container Limits (docker-compose defaults) + +- CPU: 2 cores max, 0.5 reserved +- Memory: 512MB max, 128MB reserved +- Log rotation: 10MB per file, 3 files max + +### Health Checks + +The Docker container includes a health check that tests TCP connectivity to port 9998. For external monitoring: + +```bash +# WebSocket port +curl -s -o /dev/null -w "%{http_code}" http://localhost:9998 +# Should return 200 (serves index.html) + +# TCP port +nc -z localhost 9999 +``` + +## Cross-Compilation + +Build server binaries for multiple platforms: + +```bash +cd server + +# Linux/macOS +bash build.sh # outputs to target/ + +# Windows +powershell ./build.ps1 # outputs to target/ +``` + +## Important Notes + +### No Persistence + +All game state (rooms, players, active games) is stored in memory. A server restart clears everything. This is by design — gomoku games are short-lived sessions, not long-running persistent state. + +### No Authentication Required + +The web client sends `id: 0` during auth — the server assigns a real ID from its connection counter. No database, no passwords, no sessions. The optional Java `api/` module provides user auth via MySQL if needed, but it is not required for gameplay. + +### Single-Process Architecture + +The server runs as a single Go process. There is no clustering or horizontal scaling — a single instance handles all connections. For most use cases (dozens of concurrent games), this is sufficient. + +### WebSocket Path + +The WebSocket endpoint is always at `/ws`. The static file server handles all other paths. Do not configure your reverse proxy to intercept `/ws` — it must pass through to the Go server. diff --git a/docs/system-architecture.md b/docs/system-architecture.md new file mode 100644 index 0000000..f74d57f --- /dev/null +++ b/docs/system-architecture.md @@ -0,0 +1,138 @@ +# System Architecture + +## Overview + +Gomoku Online is a multiplayer game platform with a Go server, vanilla JS web client, and optional CLI client. The server manages all game state in memory and communicates with clients over WebSocket (web) and TCP (CLI). + +## Component Diagram + +``` ++-------------------+ WebSocket (:9998) +-------------------+ +| Web Client | ◄──────────────────────► | Game Server | +| (browser) | /ws endpoint | (Go) | +| | | | +| ws-client.js | HTTP (:9998) | main.go | +| state-machine.js | ◄────── static files ──── | network/wss.go | +| board-renderer.js| / endpoint | network/tcp.go | +| game-controller.js | network/network.go +| app.js | | | ++-------------------+ | state/ | + | state.go | ++-------------------+ TCP (:9999) | welcome.go | +| CLI Client | ◄──────────────────────► | home.go | +| (Go terminal) | binary protocol | join.go | ++-------------------+ | create.go | + | waiting.go | + | game/gomoku.go | + | | + | database/ | + | model.go | + | database.go | + | gomoku.go | + | | + | consts/const.go | + +-------------------+ +``` + +## State Machine + +Every connected player runs a per-goroutine state machine. States are registered in `state/state.go` and each implements the `State` interface: + +```go +type State interface { + Next(player *database.Player) (consts.StateID, error) + Exit(player *database.Player) consts.StateID +} +``` + +### State Flow + +``` +Welcome ──► Home ──► Join ──► Waiting ──► GomokuGame ──► Waiting + └── Create ──► Waiting (game ends) + │ + └── Home (player exits room) +``` + +| State | ID | Description | +|-------|-----|-------------| +| Welcome | 1 | Sends greeting, auto-transitions to Home | +| Home | 2 | Menu: "1.Join" or "2.New" | +| Join | 3 | Lists rooms, player picks one by ID | +| Create | 4 | Lists game types, creates room, auto-joins | +| Waiting | 5 | Room lobby: owner starts game, players chat/wait | +| GomokuGame | 6 | Active gomoku game loop | + +## Networking Protocol + +### Wire Formats + +**WebSocket** (`/ws` on port 9998): +- JSON packets: `{"data":""}` +- Go's `[]byte` serializes as base64 in JSON, so the web client must `btoa()`/`atob()` + +**TCP** (port 9999): +- Binary: `[4-byte big-endian length][payload bytes]` +- Same logical `Packet{Body}` struct, different encoding + +### Auth Flow + +1. Client connects (WS or TCP) +2. Server waits up to 3 seconds for an `AuthInfo` JSON packet +3. `AuthInfo` fields: `id` (0 for web clients, server assigns real ID), `name`, `score` +4. Server creates a `Player`, starts the state machine goroutine +5. Connection enters the read loop (`Player.Listening()`) + +### Transaction Markers + +The server uses `INTERACTIVE_SIGNAL_START` and `INTERACTIVE_SIGNAL_STOP` to tell the client when input is expected. The web client enables/disables interactivity based on these signals. + +### Gomoku Game Messages + +Server sends JSON strings (not wrapped in the usual `Packet` format — they're the `Body` content): + +| Message | Direction | Description | +|---------|-----------|-------------| +| `{"type":"info","color":1}` | server→client | Your color assignment (1=black, 2=white) | +| `{"type":"board","board":[[...]],"last":[r,c],"turn":1}` | server→client | Full 15x15 board state after each move | +| `{"type":"turn","color":1}` | server→client | It's your turn to play | +| `{"type":"gameover","winner":"Name"}` | server→client | Game ended with a winner | +| `{"type":"gameover","draw":true}` | server→client | Game ended in a draw | +| `"7,7"` | client→server | Place stone at row 7, column 7 | + +## In-Memory Database + +All state lives in concurrent hashmaps (no persistence): + +| Store | Key | Value | Purpose | +|-------|-----|-------|---------| +| `players` | player ID | `*Player` | All players ever connected | +| `connPlayers` | player ID | `*Player` | Currently connected players | +| `rooms` | room ID | `*Room` | Active game rooms | +| `roomPlayers` | room ID | `map[playerID]bool` | Players in each room | +| `roomSpectators` | room ID | `map[playerID]int` | Spectators (with join order) | + +Rooms are auto-cleaned after 24 hours of inactivity or when all players disconnect. + +## Gomoku Game Engine + +- **Board**: 15x15 integer array (0=empty, 1=black, 2=white) +- **Turn sync**: channel-based — `States[playerID] chan int` with buffer size 2 +- **Move validation**: bounds check, cell empty, correct turn (all server-side) +- **Win detection**: checks 4 directions (horizontal, vertical, 2 diagonals) from last placed stone, counting consecutive same-color stones in both directions +- **Draw**: board full (225 moves) with no winner +- **Concurrency**: `sync.Mutex` on the `Gomoku` struct protects board reads/writes + +## Web Client Architecture + +Vanilla JavaScript, no framework, no build step. Five modules loaded via `