The project-level docs described the pre-caro-port architecture (TCP on
9999, JSON {data:base64} wire format, web/ + core/ + api/ + bot/ dirs,
server flags -w/-t/-s, state strings like '1'/'2'/'7,7') and referenced
files that no longer exist. The README also described an owner-controlled
Start button, 5-second heartbeat, 'database/' directory, AuthRequest
handshake, and linked to now-deleted plan files.
CLAUDE.md
- Rewritten for the current architecture: single WS endpoint at
:1999/gomoku, protobuf binary frames, server/{consts,game,lobby,
network,state,pkg/log}, client/src/{config,scenes,services,ui,
objects,generated}, auto-start PVP semantics, heartbeat 50s/deadline
90s, ClientExit self-vs-peer helper, enum serialization notes, Hard
AI description, and a "When editing" section covering proto regen,
state flow tests, and scene cleanup patterns.
README.md
- Features: owner-controlled start → auto-start on 2nd join.
- Heartbeat: 5s → 50s; add server deadline + reconnect back-off.
- Project structure: database/ → lobby/; services list corrected.
- Protocol: replace AuthRequest story with real SetNickname handshake.
- Credits: trimmed from a multi-bullet block to a single attribution
line ("Based on ratel-online by ainilili"). Removed caro references
and the link to the deleted plans/ implementation history.
docs/system-architecture.md
- Fully rewritten. Drops old TCP+JSON protocol tables, CLI client box,
obsolete state flow diagram. Adds: current state registry
(welcome/setNickname/home/waiting/gamePvp/gamePve/gameover/watching),
cross-goroutine sync channels (StartCh/GameOverCh/RematchCh), typed
enum list, kickStaleRoomPeers note, ClientExit self-vs-peer semantics,
and client module table.
docs/deployment-guide.md
- Remove stale pointer to deleted plans/ directory.
Code comment cleanup (user: "don't mention too much"):
- server/network/handlers_stateless.go handleHeartbeat: drop caro
reference, explain behavior in terms of the reader loop.
- server/game/ai_eval.go: "ported from caro's GomokuAI scoring" →
generic "threat-pattern weights used by the Hard AI".
- server/game/ai.go positionScore: drop caro attribution, describe
the heuristic directly.
- server/lobby/room.go: drop "caro Java domain" annotation, describe
what Room actually embeds.
- client/src/services/game-state-service.js WATCH_GAME_SUCCESS: drop
historical rename comment.
go vet + go test ./... + npm run build all green.
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.
Server-side tests driving state machine through PVP→leave→PVE in every
permutation:
- TestFlow_CreatePveAfterPvpLeave: finished PVP game → ClientExit from
gameoverState → StateHome → CreatePveRoom → verify PVE room with AI.
- TestFlow_CreatePveAfterPvpRematch: the specific residue case flagged
in the bug report. PVP → GameReset (rematch, closes RematchCh + fresh
GameOverCh) → force game 2 finished → ClientExit → StateHome →
CreatePveRoom → verify clean new PVE room state.
- TestFlow_CreatePveAfterPvpForfeit: mid-game forfeit via ClientExit
from gamePvpState → StateHome (signalGameOver + broadcastForfeit +
leaveRoom path) → CreatePveRoom.
All pass. The server state machine has no stale residue that blocks
PVE creation after a PVP session.
If the reported symptom persists after rebuilding both server and
client, it is a client-side issue (stale scene state, event handler
accumulation) that needs a different repro and isolation.
The "Start Game" button in the waiting room is gone — as soon as a
second player joins a PVP room, the server flips to Playing and both
clients transition into the game in lockstep.
Server:
- Proto: remove GameStartingRequest message + oneof field (now unused).
- state/home.go handleJoinRoom: after a successful join that fills the
room, call assignColors, set Status=Playing + CurrentTurn=Black,
close StartCh, broadcast GameStartingResponse, return StateGamePvp
for the joining player. If still only one player, return StateWaiting
as before.
- state/waiting.go: collapse ownerWait + joinerWait into a single
unified loop. Both roles just select on StartCh (auto-start signal)
and CmdCh (for ClientExit). No more GameStartingRequest handling or
PlayerCount checks.
- network/dispatch.go: drop Request_GameStarting from the stateful
routing list.
Client:
- connection-service.js: remove sendGameStarting() helper.
- menu-ui-rooms.js showWaiting: drop the isOwner parameter + Start Game
button + owner/joiner branching. Both views show the same passive
'Waiting for opponent...' message that flips to 'Opponent joined:
<name> - starting game...' when the second player arrives.
- menu-scene.js: update _onRoomCreateSuccess + _onRoomJoinSuccess to
call the simpler showWaiting(ownerNickname, playerCount, onLeave)
signature.
Tests:
- Remove TestWaitingOwnerGameStartingRequiresFullRoom (obsolete).
- Rewrite TestWaitingOwnerStartsWhenFull into TestWaitingOwner
TransitionsOnStartCh: simulate handleJoinRoom closing StartCh,
verify owner transitions to StateGamePvp.
- Add TestFlow_JoinAutoStartsGame: end-to-end drive where owner is in
waitingState, joiner sends JoinRoomRequest from home, and both
goroutines must land in StateGamePvp with colors assigned and
GameStartingResponse broadcast.
Regen Go + JS proto stubs. go vet + go test ./... green.
npm --prefix client run build green.
Two stacked bugs in the client's room-creation path:
1. menu-scene._onRoomCreateSuccess read `data.ownerNickname` but the
proto field is `room_owner` (pbjs camelCase: `roomOwner`). The
waiting room showed an empty owner name after clicking Create Room.
2. game-state-service had no handler at all for ROOM_CREATE_SUCCESS.
After creating a room, gameState.roomId stayed at whatever value
the previous CLIENT_EXIT left (null). The waiting room panel
displayed "Room ID: " (empty) until another player joined and
triggered ROOM_JOIN_SUCCESS. Also broke any code path that relies
on roomId between Create and Join.
Fix:
- menu-scene: read data.roomOwner (the actual proto field name);
updated JSDoc to document all three proto fields (id, roomOwner,
roomType).
- game-state-service: add a ROOM_CREATE_SUCCESS listener that sets
roomId from data.id (the proto field is `id`, not `roomId`, for
RoomCreateSuccessResponse).
Server-side flow tests added in a separate commit (a350636) prove
the server correctly allows create-after-pvp in every permutation
(direct leave, rematch→leave, opponent forfeit). The reported "can
not create room after pvp" symptom matches the field-name bug above:
the waiting room did render, but with blank identifying info, so it
was easy to mistake for "nothing happened".
npm run build green.
Bug: after a PVP game ends, clicking "Play Again" cleared the board on
both clients but subsequent moves did nothing — the game was frozen.
Root cause: both players land in gameoverState, each goroutine blocked
on its own CmdCh. When Player A sent GameResetRequest, A's goroutine
reset the room and transitioned to gamePvpState — but Player B's
goroutine was still blocked in gameoverState. B's moves arrived at
CmdCh, gameoverState switch-case didn't match GameMoveRequest, and the
default branch silently dropped them. The board was reset (visible via
the broadcast) but only A was actually in the game on the server.
Fix: add lobby.Room.RematchCh (pattern mirrors StartCh in waiting state).
First goroutine entering gameoverState lazily creates it. gameoverState
now selects on CmdCh OR RematchCh. handleGameReset closes RematchCh
after resetting the room, waking the peer goroutine so both transition
to gamePvpState in lockstep.
Tests (new):
- server/state/flow_test.go:
* TestFlow_HomeCreateLeaveCreate_OneRoomVisible — drives state machine
home→create→waiting→exit→home→create and asserts the lobby store
contains exactly 1 room (guards against ghost rooms after leave)
* TestFlow_RoomListReflectsCreateLeaveCycle — explicit create/leave/
create assertion at each step
* TestFlow_HomeExit_DoesNotKillSession — verify home.ClientExit stays
in home and emits ClientExitResponse
* TestFlow_Rematch_BothPlayersTransition — two player goroutines in
gameoverState, one sends GameReset, both must reach gamePvpState
within 500ms (regression guard for the reported bug)
* TestFlow_Rematch_NewGameAcceptsMovesFromBoth — after rematch, feed
a GameMove and verify the board is mutated and a GameMoveSuccess
response is emitted
- server/lobby/lobby_flow_test.go:
* 7 flow-level tests covering create/leave/create/find cycles, PVE
leave cleanup, two-player leave preserving the room, idempotent
leave, and list consistency across concurrent users
All tests green: go vet + go test ./... + npm run build.
NOTE on "ghost room after leave": the failing-case tests in this suite
prove the server store + state machine correctly delete empty rooms on
leave. The lobby package never showed a ghost. If the symptom persists
after rebuilding, the residue is client-side DOM caching and needs a
browser refresh.
Bug: after clicking "Leave" on the game-over modal, the client landed
on the nickname entry screen and got stuck — typing a nickname produced
no response for up to 90 seconds.
Root cause chain:
1. Server handleClientExit (stateless) closed SendCh and nilled it but
did not close the WS connection. Writer goroutine drained + exited
without closing the conn; reader stayed blocked on ReadMessage; state
machine stayed blocked on CmdCh. Zombie session.
2. Any subsequent player.Send() returned nil (SendCh was nil), silently
dropping responses. The setNickname reply never reached the client.
3. Client only unstuck when reader's 90 s readDeadline fired, finally
closing the WS and triggering the auto-reconnect path.
4. Client MenuScene.create() always called showNicknameScreen() without
checking gameState.nickname, so even after the reconnect the user
had to re-type their name.
Fix: "exit" now means "leave current room, return to home lobby" —
not "terminate WS session".
Server:
- dispatch.go: move Request_ClientExit from stateless to stateful path.
- handlers_stateless.go: delete handleClientExit entirely.
- waiting.go leaveRoom: broadcast ClientExitResponse via broadcastResponse
BEFORE removing the player, so the exiting player also receives their
own exit confirmation. Removes duplicated self-notification logic.
- waiting/gamePvp/gamePve/gameover/watching: on ClientExitRequest, call
leaveRoom + return consts.StateHome (was return 0, ErrClientExit).
- home.go: on ClientExitRequest, send bare ClientExitResponse and stay
in home (no room to leave).
- Update 4 state tests that asserted ErrClientExit → now assert StateHome.
- Rename TestWatching_ClientExitReturnsErrClientExit → ReturnsHome.
Client:
- menu-scene.js create(): if gameState.nickname is set, skip nickname
screen and call showLobby() directly. Otherwise showNicknameScreen().
- game-scene.js _onClientExit: drop the explicit showLobby() call; let
MenuScene.create() decide the panel based on nickname state. Remove
now-unused menu-ui showLobby import.
Result: click Leave → server sends ClientExitResponse, state machine
transitions to StateHome, WS stays open, client transitions to
MenuScene → sees gameState.nickname is set → goes straight to lobby.
No reconnect, no re-typing, no 90-second stall.
go vet + go test ./... green. npm run build green.
The 'New' prefix was a phase-06 naming compromise to avoid colliding
with a legacy Room type. That legacy type is gone now, so the prefix
is misleading — in Go 'NewX' conventionally reads as a constructor,
not a type.
- NewRoom type → Room
- JoinNewRoom → JoinRoom
- GetNewRoom → GetRoom
- LeaveNewRoom → LeaveRoom
- WatchNewRoom → WatchRoom
- UnwatchNewRoom → UnwatchRoom
- deleteNewRoom → deleteRoom
Also remove dead code uncovered by the pass:
- Delete lobby/events.go entirely — RoomEvent + BroadcastEvent wrapped
a no-op placeholder, never called.
- Delete BroadcastToNewRoom no-op from lobby/store.go — kept only for
the dead BroadcastEvent wrapper.
- Delete TestBroadcastToRoom_SkipsExcludedIDs — theater test that never
actually called BroadcastToNewRoom, just built a manual exclude map.
go vet + go test ./... green.
Rename
- server/database/ → server/lobby/ (17 import sites + qualified references)
- Package was named "database" but there is no database — it's in-memory
hashmaps for players/rooms/spectators. "lobby" accurately names its role:
the multiplayer game lobby state (who is in what room, joining/leaving,
spectating).
Ratel-online legacy cleanup
- Delete server/README.md — 100% Chinese, entirely about ratel card games
(斗地主/跑得快/德州扑克/麻将/骗子酒馆/Uno). Zero relevance to gomoku.
- Delete server/build.sh + server/build.ps1 — multi-platform ratel-server
build scripts with mixed Chinese comments, superseded by Makefile +
Dockerfile.
- Delete server/docs/ — Chinese Docker deployment guide + quickstart,
superseded by root docs/deployment-guide.md and root README.md.
- Delete server/demo.gif — ratel card-game demo screenshot.
Comment fixes: update "database" references in consts/const.go and
game/board.go package docs to point at "lobby" instead.
go vet + go test ./... green.
Protocol type safety
- Add Piece, GameResult, RoomType, RoomStatus proto3 enums; replace
unsafe string fields in GameMoveSuccessResponse, GameOverResponse,
RoomCreateSuccessResponse, RoomSummary, WatchGameSuccessResponse.
- Go compiler now rejects typos like "PVP" or "BLACK". protobufjs
toJSON() serializes enums as UPPERCASE names, so existing client
comparisons keep working.
- Fixes turn-change bug where lowercase "black"/"white" server strings
never matched client 'BLACK'/'WHITE' checks, freezing currentTurn
and blocking the non-starting player's clicks.
- Regenerate Go + JS stubs.
Room-join bug fix
- home.go handleJoinRoom now broadcasts RoomJoinSuccessResponse to all
players in the room (owner + joiner), not just the joiner. Owner
can see the player count flip to 2/2 and the Start Game button
enables. Remove superseded broadcastJoined + GameReadyResponse path.
- client menu-scene _onRoomJoinSuccess: read correct proto camelCase
field names (roomOwner, roomClientCount, clientNickname).
Dead code removal
- Delete GameReadyRequest/Response proto messages entirely; leaveRoom
notifies via ClientExitResponse.
- Delete server/network/{network,wss}.go empty stubs.
- Delete server/pkg/consts/ unused re-export package.
- Slim server/consts/const.go from 117 LOC to 24: drop legacy TCP
constants, duplicated RoomType/RoomStatus enums, Error/NewErr types,
StateJoin/StateCreate=0 type-unsafe aliases, unused GameTypes maps.
- database/player.go: remove unused IP, Mode, Type, Amount, legacy
private state field.
- Fix gopls findings: unused forPlayer, gameOverOnce, runAIMove player,
handleGameReset player params.
go vet + go test ./... green, npm run build green.
27 tests covering: welcome/setNickname/home transitions, runner exit on
ErrClientExit, waiting owner/joiner role split with StartCh signal,
PVP move validation (turn order, bounds, occupancy, win detection with
GameOverCh cross-goroutine sync), PVE human+AI alternation and AI-first
when human is White, gameover reset/rematch for both PVP and PVE.
Remove database/legacy.go (legacy hashmap store, Room/Gomoku types).
Remove state/create.go and state/join.go (collapsed into home.go).
Remove state/game/gomoku.go (replaced by game_pvp.go + game_shared.go).
Remove server/pkg/{model,network,protocol,async,json,strings} packages
that are no longer imported after the state machine rewrite.
Stub out network/network.go (legacy TCP handler retired).
Rewrite database/player.go to remove legacy conn/data fields.
Replace legacy AskForString/AskForPacket state machine with channel-based
runner. Each state reads *protocol.Request from player.CmdCh. Adds PVP
waiting (owner/joiner role split with StartCh signal), gamePvp (GameOverCh
for cross-goroutine game-end sync), gamePve (AI alternates with human,
AI-first when human is White), and gameover (reset/rematch support).
Adds tools.go to pin protoc-gen-go, updates go.mod/go.sum with
google.golang.org/protobuf, generates request.pb.go and response.pb.go,
and adds a make proto target to server/Makefile for regen.
Port GomokuAI.java to Go with three difficulty levels:
- Easy: uniform random (seeded RNG for determinism)
- Medium: immediate win/block heuristic from caro
- Hard: true minimax depth-3 with alpha-beta pruning
evaluatePosition uses caro threat-pattern weights (open-four=100000,
closed-four=10000, open-three=1000, etc.) as leaf evaluator.
candidateMoves limits branching to radius-2 Chebyshev neighbours.
Benchmark: ~71µs/move on 30-stone mid-game board (budget: <1s).
Port Board.java and GomokuHelper.java to idiomatic Go under server/game/.
Board is a value type ([15][15]Piece) enabling zero-allocation Clone().
Helper provides ValidMoves, FormatBoard, WinnerMessage as pure functions.
Covers all 4 win directions, draw, OOB, occupied-cell, and reset cases.
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.
Remove 3,612 lines of card game implementations (Dou Dizhu, Texas
Hold'em, Mahjong, Uno, Liar's Bar, Run Fast), their database models,
rule engine, skill system, and render package.
Simplify Room struct by removing card-game-specific fields, clean up
consts to only gomoku game type, and remove unused dependencies
(mahjong, uno, cast, color).
Add server-side gomoku (five-in-a-row) as game type 9 with 15x15 board,
2-player rooms, move validation, win detection in 4 directions, and
draw detection. Includes channel-based turn sync with mutex-protected
board access.
Add vanilla JS web client served as static files from the Go server:
WebSocket connection with base64 packet encoding, state machine
navigation, Canvas-based board renderer with gradient stones, hover
ghost, last-move highlight, and game-over overlay.
Import source files from ratel-online organization repos into monorepo
structure for gomoku game development. Update README with project
structure and credits for original authors.