mirror of
https://github.com/tiennm99/gomoku.git
synced 2026-08-16 22:23:29 +00:00
feat: add documentation, code comments, and update Docker config
Rewrite README with usage guide, deployment instructions, and protocol docs. Update CLAUDE.md to reflect gomoku-only architecture. Add English doc comments to all key server Go files, replacing Chinese comments. Create docs/system-architecture.md (state machine, protocol, database schema) and docs/deployment-guide.md (local dev, Docker, production nginx, resource requirements). Update Dockerfile to Go 1.22 with repo-root build context to include web client. Update docker-compose to match.
This commit is contained in:
@@ -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":"<base64>"}` 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":"<base64>"}`)
|
||||
- `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":"<base64-encoded-payload>"}`.
|
||||
|
||||
**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.
|
||||
|
||||
@@ -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 <id>` or `k <id>` | Kick a player by ID (owner only) |
|
||||
| `set pwd <password>` | 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":"<base64-encoded-payload>"}` — 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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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":"<base64-encoded-payload>"}`
|
||||
- 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 `<script>` tags:
|
||||
|
||||
| Module | Responsibility |
|
||||
|--------|---------------|
|
||||
| `ws-client.js` | WebSocket connection, base64 encode/decode, auth, transaction markers |
|
||||
| `state-machine.js` | Detects server state from message text patterns, shows/hides UI panels |
|
||||
| `board-renderer.js` | Canvas 15x15 board, gradient stones, click-to-place, hover ghost, game-over overlay |
|
||||
| `game-controller.js` | Bridges WS client and board renderer — routes game JSON messages |
|
||||
| `app.js` | Entry point — wires modules, binds DOM events |
|
||||
|
||||
The web client is fully server-authoritative: clicking the board sends `"row,col"` to the server and waits for the server's board broadcast before rendering.
|
||||
+19
-30
@@ -1,56 +1,45 @@
|
||||
# 构建阶段
|
||||
FROM golang:1.17-alpine AS builder
|
||||
# Build stage
|
||||
FROM golang:1.22-alpine AS builder
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 安装必要的构建工具
|
||||
RUN apk add --no-cache git
|
||||
|
||||
# 复制 go.mod 和 go.sum 文件
|
||||
COPY go.mod go.sum ./
|
||||
# Copy server source and build
|
||||
COPY server/go.mod server/go.sum ./server/
|
||||
RUN cd server && go mod download
|
||||
|
||||
# 下载依赖
|
||||
RUN go mod download
|
||||
COPY server/ ./server/
|
||||
RUN cd server && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o /app/gomoku-server main.go
|
||||
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
# Copy web client
|
||||
COPY web/ ./web/
|
||||
|
||||
# 构建应用
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o ratel-server main.go
|
||||
|
||||
# 运行阶段
|
||||
# Runtime stage
|
||||
FROM alpine:latest
|
||||
|
||||
# 安装必要的运行时依赖
|
||||
RUN apk --no-cache add ca-certificates tzdata
|
||||
|
||||
# 设置时区为上海
|
||||
ENV TZ=Asia/Shanghai
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
# 创建非root用户
|
||||
RUN addgroup -g 1000 -S ratel && \
|
||||
adduser -u 1000 -S ratel -G ratel
|
||||
RUN addgroup -g 1000 -S gomoku && \
|
||||
adduser -u 1000 -S gomoku -G gomoku
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 从构建阶段复制二进制文件
|
||||
COPY --from=builder /app/ratel-server .
|
||||
COPY --from=builder /app/gomoku-server .
|
||||
COPY --from=builder /app/web ./web
|
||||
|
||||
# 更改文件所有权
|
||||
RUN chown -R ratel:ratel /app
|
||||
RUN chown -R gomoku:gomoku /app
|
||||
|
||||
# 切换到非root用户
|
||||
USER ratel
|
||||
USER gomoku
|
||||
|
||||
# 暴露端口
|
||||
# WebSocket + web UI on 9998, TCP on 9999
|
||||
EXPOSE 9998 9999
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD nc -z localhost 9998 && nc -z localhost 9999 || exit 1
|
||||
|
||||
# 启动应用
|
||||
CMD ["./ratel-server"]
|
||||
# -s ./web serves web client at http://host:9998/
|
||||
CMD ["./gomoku-server", "-w", "9998", "-t", "9999", "-s", "./web"]
|
||||
|
||||
+12
-10
@@ -1,3 +1,5 @@
|
||||
// Package bot provides optional QQ group messaging via the Milky SDK.
|
||||
// Used to send game notifications to a QQ chat group. Not required for gameplay.
|
||||
package bot
|
||||
|
||||
import (
|
||||
@@ -6,13 +8,13 @@ import (
|
||||
"github.com/ratel-online/core/log"
|
||||
)
|
||||
|
||||
// Session 全局机器人会话
|
||||
// Session is the global bot connection. Nil when bot is not configured.
|
||||
var Session *Milky_go_sdk.Session
|
||||
|
||||
// GroupID 群ID
|
||||
// GroupID is the QQ group to send messages to.
|
||||
var GroupID int64
|
||||
|
||||
// Logger 实现 Milky_go_sdk 的 Logger 接口
|
||||
// Logger adapts the core logger to the Milky SDK's Logger interface.
|
||||
type Logger struct{}
|
||||
|
||||
func (l *Logger) Infof(format string, args ...interface{}) {
|
||||
@@ -48,7 +50,7 @@ func (l *Logger) Warn(args ...interface{}) {
|
||||
log.Info(fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
// SendGroupMessage 发送群消息
|
||||
// SendGroupMessage sends a text message to the configured QQ group.
|
||||
func SendGroupMessage(groupID int64, content string) error {
|
||||
if Session == nil {
|
||||
return fmt.Errorf("bot not connected")
|
||||
@@ -59,25 +61,25 @@ func SendGroupMessage(groupID int64, content string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Connect 连接机器人
|
||||
// Connect establishes a WebSocket connection to the Milky bot service.
|
||||
func Connect(addr, token string, groupID int64) error {
|
||||
GroupID = groupID
|
||||
m, err := Milky_go_sdk.New("ws://"+addr+"/event", "http://"+addr+"/api", token, &Logger{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建Bot会话失败: %v", err)
|
||||
return fmt.Errorf("failed to create bot session: %v", err)
|
||||
}
|
||||
err = m.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("连接Bot失败: %v", err)
|
||||
return fmt.Errorf("failed to connect bot: %v", err)
|
||||
}
|
||||
Session = m
|
||||
log.Infof("Bot已连接: %s", addr)
|
||||
log.Infof("Bot connected: %s", addr)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close 关闭机器人连接
|
||||
// Close gracefully shuts down the bot connection.
|
||||
func Close() {
|
||||
if Session != nil {
|
||||
Session.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+14
-16
@@ -16,13 +16,14 @@ import (
|
||||
"github.com/ratel-online/server/consts"
|
||||
)
|
||||
|
||||
// In-memory data store. All state is volatile — server restart clears everything.
|
||||
var roomIds int64 = 0
|
||||
var players = hashmap.New() // 存储连接过服务器的全部用户
|
||||
var connPlayers = hashmap.New()
|
||||
var rooms = hashmap.New()
|
||||
var roomPlayers = hashmap.New()
|
||||
var roomSpectators = hashmap.New()
|
||||
var roomKickedPlayers = hashmap.New()
|
||||
var players = hashmap.New() // all players ever connected (by ID)
|
||||
var connPlayers = hashmap.New() // currently connected players
|
||||
var rooms = hashmap.New() // active rooms (by room ID)
|
||||
var roomPlayers = hashmap.New() // map[roomID] -> map[playerID]bool
|
||||
var roomSpectators = hashmap.New() // map[roomID] -> map[playerID]int (join order)
|
||||
var roomKickedPlayers = hashmap.New() // map[roomID] -> map[playerID]bool
|
||||
var roomPropsSetter = map[string]func(r *Room, v string){
|
||||
consts.RoomPropsPassword: func(r *Room, v string) {
|
||||
if v == "off" {
|
||||
@@ -59,9 +60,9 @@ func Connected(conn *network.Conn, info *modelx.AuthInfo) *Player {
|
||||
Name: strings.Desensitize(info.Name),
|
||||
Amount: 2000,
|
||||
}
|
||||
player.Conn(conn) // 初始化play对象
|
||||
players.Set(conn.ID(), player) // 写入用户池
|
||||
connPlayers.Set(conn.ID(), player) // 写入连接用户池
|
||||
player.Conn(conn)
|
||||
players.Set(conn.ID(), player)
|
||||
connPlayers.Set(conn.ID(), player)
|
||||
return player
|
||||
}
|
||||
|
||||
@@ -122,12 +123,11 @@ func getPlayer(playerId int64) *Player {
|
||||
}
|
||||
|
||||
func SetRoomProps(room *Room, k, v string) {
|
||||
// 根据房间类型限制可设置的属性
|
||||
// Only allow properties appropriate for the game type
|
||||
allowedProps := getAllowedPropsByGameType(room.Type)
|
||||
|
||||
// 检查属性是否允许设置
|
||||
if !allowedProps[k] {
|
||||
return // 不允许的属性直接返回,不执行设置
|
||||
return
|
||||
}
|
||||
|
||||
if setter, ok := roomPropsSetter[k]; ok {
|
||||
@@ -181,9 +181,9 @@ func IsValidPlayer(roomId, playerId int64) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// 加入房间
|
||||
// JoinRoom adds a player to a room. If the room is full or running, the player
|
||||
// becomes a spectator instead. Returns an error if the player was previously kicked.
|
||||
func JoinRoom(roomId, playerId int64) error {
|
||||
// 资源检查
|
||||
player := getPlayer(playerId)
|
||||
if player == nil {
|
||||
return consts.ErrorsExist
|
||||
@@ -193,7 +193,6 @@ func JoinRoom(roomId, playerId int64) error {
|
||||
return consts.ErrorsRoomInvalid
|
||||
}
|
||||
|
||||
// 加锁防止并发异常
|
||||
room.Lock()
|
||||
defer room.Unlock()
|
||||
|
||||
@@ -203,7 +202,6 @@ func JoinRoom(roomId, playerId int64) error {
|
||||
|
||||
room.ActiveTime = time.Now()
|
||||
|
||||
//房间人数及状态检查
|
||||
if room.Players >= room.MaxPlayers || room.State == consts.RoomStateRunning {
|
||||
spectatorsIds := getRoomSpectators(roomId)
|
||||
spectatorsIds[playerId] = len(spectatorsIds)
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/ratel-online/server/consts"
|
||||
)
|
||||
|
||||
// Role represents a player's role within a room.
|
||||
type Role string
|
||||
|
||||
const (
|
||||
@@ -22,6 +23,12 @@ const (
|
||||
RoleSpectator Role = "spectator"
|
||||
)
|
||||
|
||||
// Player represents a connected user. Public fields are serializable state;
|
||||
// private fields manage the network connection and state machine lifecycle.
|
||||
//
|
||||
// The `data` channel receives packets from the client when `read` is true
|
||||
// (between StartTransaction/StopTransaction). The state machine goroutine
|
||||
// reads from this channel via AskFor* methods.
|
||||
type Player struct {
|
||||
ID int64 `json:"id"`
|
||||
IP string `json:"ip"`
|
||||
@@ -32,11 +39,11 @@ type Player struct {
|
||||
RoomID int64 `json:"roomId"`
|
||||
Role Role `json:"role"`
|
||||
|
||||
conn *network.Conn
|
||||
data chan *protocol.Packet
|
||||
read bool
|
||||
state consts.StateID
|
||||
online bool
|
||||
conn *network.Conn // underlying network connection
|
||||
data chan *protocol.Packet // buffered channel for client input
|
||||
read bool // true when accepting input (inside a transaction)
|
||||
state consts.StateID // current state machine state
|
||||
online bool // false after disconnect
|
||||
}
|
||||
|
||||
func (p *Player) Write(bytes []byte) error {
|
||||
@@ -49,6 +56,8 @@ func (p *Player) IsOnline() bool {
|
||||
return p.online
|
||||
}
|
||||
|
||||
// Offline marks the player as disconnected, closes the connection, and
|
||||
// cleans up room membership. Called when the network read loop exits.
|
||||
func (p *Player) Offline() {
|
||||
p.online = false
|
||||
_ = p.conn.Close()
|
||||
@@ -65,6 +74,9 @@ func (p *Player) Offline() {
|
||||
}
|
||||
}
|
||||
|
||||
// Listening is the main read loop. It reads packets from the network connection
|
||||
// and forwards them to the data channel when the state machine is accepting input.
|
||||
// Blocks until the connection is closed or errors.
|
||||
func (p *Player) Listening() error {
|
||||
loopCount := 0
|
||||
for {
|
||||
@@ -83,7 +95,8 @@ func (p *Player) Listening() error {
|
||||
}
|
||||
}
|
||||
|
||||
// 向客户端发生消息
|
||||
// WriteString sends a text message to the client. The 30ms sleep prevents
|
||||
// message flooding when the server sends multiple messages in rapid succession.
|
||||
func (p *Player) WriteString(data string) error {
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
return p.conn.Write(protocol.Packet{
|
||||
@@ -165,11 +178,14 @@ func (p *Player) AskForStringWithoutTransaction(timeout ...time.Duration) (strin
|
||||
return packet.String(), nil
|
||||
}
|
||||
|
||||
// StartTransaction enables input acceptance and sends the INTERACTIVE_SIGNAL_START
|
||||
// marker to the client. The web client uses this to know when to enable the input field.
|
||||
func (p *Player) StartTransaction() {
|
||||
p.read = true
|
||||
_ = p.WriteString(consts.IsStart)
|
||||
}
|
||||
|
||||
// StopTransaction disables input acceptance and sends INTERACTIVE_SIGNAL_STOP.
|
||||
func (p *Player) StopTransaction() {
|
||||
p.read = false
|
||||
_ = p.WriteString(consts.IsStop)
|
||||
@@ -200,10 +216,14 @@ func (p Player) String() string {
|
||||
return fmt.Sprintf("%s[%d]", p.Name, p.ID)
|
||||
}
|
||||
|
||||
// RoomGame is implemented by each game type's state struct (e.g., Gomoku).
|
||||
// Clean() is called when the room is deleted to close channels and free resources.
|
||||
type RoomGame interface {
|
||||
Clean()
|
||||
}
|
||||
|
||||
// Room represents a game room. Players join a room, wait for enough players,
|
||||
// then the owner starts the game. The Game field holds the active game state.
|
||||
type Room struct {
|
||||
sync.Mutex
|
||||
|
||||
|
||||
+11
-16
@@ -1,24 +1,22 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
ratel-server:
|
||||
gomoku-server:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: ratel-server:latest
|
||||
container_name: ratel-server
|
||||
# Build from repo root so Dockerfile can access both server/ and web/
|
||||
context: ..
|
||||
dockerfile: server/Dockerfile
|
||||
image: gomoku-server:latest
|
||||
container_name: gomoku-server
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "9998:9998" # WebSocket端口
|
||||
- "9999:9999" # TCP端口
|
||||
- "9998:9998" # WebSocket + web UI
|
||||
- "9999:9999" # TCP (CLI client)
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
# volumes:
|
||||
# # 如果需要持久化日志,可以取消下面的注释
|
||||
# - ./logs:/app/logs
|
||||
networks:
|
||||
- ratel-network
|
||||
command: ["./ratel-server", "-w", "9998", "-t", "9999"]
|
||||
- gomoku-network
|
||||
command: ["./gomoku-server", "-w", "9998", "-t", "9999", "-s", "./web"]
|
||||
healthcheck:
|
||||
test: ["CMD", "nc", "-z", "localhost", "9998"]
|
||||
interval: 30s
|
||||
@@ -40,8 +38,5 @@ services:
|
||||
memory: 128M
|
||||
|
||||
networks:
|
||||
ratel-network:
|
||||
gomoku-network:
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.20.0.0/16
|
||||
+2
-2
@@ -29,13 +29,13 @@ func main() {
|
||||
flag.Int64Var(&BotGroup, "bot-group", 0, "Bot group ID")
|
||||
|
||||
flag.Parse()
|
||||
// 连接机器人
|
||||
// Connect QQ bot if configured
|
||||
if BotAddr != "" && BotToken != "" && BotGroup != 0 {
|
||||
err := bot.Connect(BotAddr, BotToken, BotGroup)
|
||||
if err != nil {
|
||||
log.Panic(fmt.Sprintf("连接Bot失败: %v", err))
|
||||
}
|
||||
// 发送测试消息到 BotGroup 群
|
||||
// Send test message to the bot group
|
||||
err = bot.SendGroupMessage(BotGroup, "Server started!")
|
||||
if err != nil {
|
||||
log.Errorf("发送群消息失败: %v", err)
|
||||
|
||||
@@ -17,8 +17,9 @@ type Network interface {
|
||||
Serve() error
|
||||
}
|
||||
|
||||
// handle processes a new connection: wraps it, authenticates within 3 seconds,
|
||||
// creates a Player, starts the state machine goroutine, and blocks on Listening.
|
||||
func handle(rwc protocol.ReadWriteCloser) error {
|
||||
// 给新进入的用户分配资源
|
||||
c := network.Wrapper(rwc)
|
||||
defer func() {
|
||||
err := c.Close()
|
||||
@@ -39,7 +40,8 @@ func handle(rwc protocol.ReadWriteCloser) error {
|
||||
return player.Listening()
|
||||
}
|
||||
|
||||
// 登陆验签
|
||||
// loginAuth reads an AuthInfo JSON packet from the connection within 3 seconds.
|
||||
// If the client doesn't authenticate in time, it returns ErrorsAuthFail.
|
||||
func loginAuth(c *network.Conn) (*model.AuthInfo, error) {
|
||||
authChan := make(chan *model.AuthInfo)
|
||||
defer close(authChan)
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"github.com/ratel-online/server/database"
|
||||
)
|
||||
|
||||
// create handles room creation. Prompts for game type, creates the room, and
|
||||
// automatically joins the creator. Transitions to waiting state.
|
||||
type create struct{}
|
||||
|
||||
func (*create) Next(player *database.Player) (consts.StateID, error) {
|
||||
@@ -16,7 +18,6 @@ func (*create) Next(player *database.Player) (consts.StateID, error) {
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
// 创建房间
|
||||
room := database.CreateRoom(player.ID, gameType)
|
||||
err = player.WriteString(fmt.Sprintf("Create room successful, id : %d\n", room.ID))
|
||||
if err != nil {
|
||||
@@ -33,7 +34,7 @@ func (*create) Exit(_ *database.Player) consts.StateID {
|
||||
return consts.StateHome
|
||||
}
|
||||
|
||||
// 询问游戏类型
|
||||
// askForGameType displays the list of available game types and waits for the player's selection.
|
||||
func askForGameType(player *database.Player) (gameType int, err error) {
|
||||
buf := bytes.Buffer{}
|
||||
buf.WriteString("Please select game type\n")
|
||||
@@ -52,7 +53,6 @@ func askForGameType(player *database.Player) (gameType int, err error) {
|
||||
_ = player.WriteError(consts.ErrorsGameTypeInvalid)
|
||||
return 0, consts.ErrorsGameTypeInvalid
|
||||
}
|
||||
// check game type.
|
||||
if _, ok := consts.GameTypes[gameType]; !ok {
|
||||
_ = player.WriteError(consts.ErrorsGameTypeInvalid)
|
||||
return 0, consts.ErrorsGameTypeInvalid
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/ratel-online/server/database"
|
||||
)
|
||||
|
||||
// home is the main menu. Player chooses to join an existing room or create a new one.
|
||||
type home struct{}
|
||||
|
||||
func (*home) Next(player *database.Player) (consts.StateID, error) {
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// join displays the room list and lets the player pick one to join.
|
||||
// If the room has a password, the player must enter it before joining.
|
||||
type join struct{}
|
||||
|
||||
func (s *join) Next(player *database.Player) (consts.StateID, error) {
|
||||
@@ -44,7 +46,6 @@ func (s *join) Next(player *database.Player) (consts.StateID, error) {
|
||||
return 0, player.WriteError(consts.ErrorsRoomInvalid)
|
||||
}
|
||||
|
||||
//房间存在密码,要求输入密码
|
||||
pwd := room.Password
|
||||
if pwd != "" {
|
||||
err = verifyPassword(player, pwd)
|
||||
@@ -68,7 +69,7 @@ func (*join) Exit(player *database.Player) consts.StateID {
|
||||
return consts.StateHome
|
||||
}
|
||||
|
||||
// 校验密码
|
||||
// verifyPassword prompts the player for the room password and validates it.
|
||||
func verifyPassword(player *database.Player, pwd string) error {
|
||||
err := player.WriteString("Please input room password: \n")
|
||||
if err != nil {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/ratel-online/server/database"
|
||||
)
|
||||
|
||||
// welcome is the initial state. Sends a greeting and transitions to home.
|
||||
type welcome struct{}
|
||||
|
||||
func (*welcome) Next(player *database.Player) (consts.StateID, error) {
|
||||
|
||||
Reference in New Issue
Block a user