Commit Graph
55 Commits
Author SHA1 Message Date
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
caac97b8d8 chore(deps): bump actions/checkout from 4 to 6 (#11)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-23 18:02:54 +07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
23d3d2cfa3 chore(deps): bump protobufjs from 7.5.5 to 7.5.8 in /client (#9)
Bumps [protobufjs](https://github.com/protobufjs/protobuf.js) from 7.5.5 to 7.5.8.
- [Release notes](https://github.com/protobufjs/protobuf.js/releases)
- [Changelog](https://github.com/protobufjs/protobuf.js/blob/protobufjs-v7.5.8/CHANGELOG.md)
- [Commits](https://github.com/protobufjs/protobuf.js/compare/protobufjs-v7.5.5...protobufjs-v7.5.8)

---
updated-dependencies:
- dependency-name: protobufjs
  dependency-version: 7.5.8
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-23 18:02:23 +07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
15ab00424e chore(deps): bump @protobufjs/utf8 from 1.1.0 to 1.1.1 in /client (#8)
Bumps [@protobufjs/utf8](https://github.com/dcodeIO/protobuf.js) from 1.1.0 to 1.1.1.
- [Release notes](https://github.com/dcodeIO/protobuf.js/releases)
- [Changelog](https://github.com/protobufjs/protobuf.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/dcodeIO/protobuf.js/compare/protobufjs-cli-v1.1.0...protobufjs-cli-v1.1.1)

---
updated-dependencies:
- dependency-name: "@protobufjs/utf8"
  dependency-version: 1.1.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-23 18:02:15 +07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
eeb532c5d9 chore(deps-dev): bump protobufjs-cli from 1.2.0 to 1.2.1 in /client (#7)
Bumps [protobufjs-cli](https://github.com/protobufjs/protobuf.js) from 1.2.0 to 1.2.1.
- [Release notes](https://github.com/protobufjs/protobuf.js/releases)
- [Changelog](https://github.com/protobufjs/protobuf.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/protobufjs/protobuf.js/compare/protobufjs-cli-v1.2.0...protobufjs-cli-v1.2.1)

---
updated-dependencies:
- dependency-name: protobufjs-cli
  dependency-version: 1.2.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-23 18:02:05 +07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
321196b087 chore(deps-dev): bump postcss from 8.5.9 to 8.5.14 in /client (#6)
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.9 to 8.5.14.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.9...8.5.14)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.14
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-23 18:01:57 +07:00
tiennm99andGitHub 7ab1a90386 chore: add dependabot config (#10) 2026-05-23 10:48:26 +07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
063782168b chore(deps): bump protobufjs from 7.5.4 to 7.5.5 in /client (#5)
Bumps [protobufjs](https://github.com/protobufjs/protobuf.js) from 7.5.4 to 7.5.5.
- [Release notes](https://github.com/protobufjs/protobuf.js/releases)
- [Changelog](https://github.com/protobufjs/protobuf.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/protobufjs/protobuf.js/compare/protobufjs-v7.5.4...protobufjs-v7.5.5)

---
updated-dependencies:
- dependency-name: protobufjs
  dependency-version: 7.5.5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-07 13:33:13 +07:00
tiennm99 501ef1f1d2 docs: rewrite CLAUDE.md, README, architecture; trim historical baggage
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.
2026-04-11 20:16:47 +07:00
tiennm99 7aae4fc909 chore: remove implemented plans
Both plans fully implemented; directories no longer referenced.

- 260409-2047-web-client-gomoku-engine: marked status: done in
  header. Original pre-caro-port web client scope, superseded by
  the caro port's Phaser 3 rewrite.
- 260411-1101-port-caro-feature-set: header said pending but all
  11 phases are implemented — verified against current layout
  (common/proto/{request,response}.proto, server/{game,lobby,network,
  state}, client/src/{scenes,ui,objects,services,generated,config}).
  Recent bug-fix commits validated every feature end-to-end.
- plans/reports: subagent trace logs from phase executions, no
  active referents.

Note: 260411-* and reports/ were never tracked in git (untracked
working-tree artifacts); only the 260409-* removal shows as deleted
files in this commit.
2026-04-11 20:09:26 +07:00
tiennm99 cbc74c7813 fix(exit): distinguish self vs peer ClientExitResponse
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.
2026-04-11 19:39:24 +07:00
tiennm99 23a15765be fix(client): harden scene teardown to prevent PVP→PVE residue
Defensive cleanup to address the reported symptom "can not play PVE
after PVP". Server-side flow tests pass in every permutation, so the
residue must be in the client scene lifecycle.

game-state-service:
- Reset isSpectating on GAME_STARTING, not just on CLIENT_EXIT. If a
  prior watch session ever left the flag set (e.g., the user left via
  a path that didn't emit CLIENT_EXIT), a fresh game-starting signal
  would incorrectly treat the new player as a spectator and block
  clicks via isMyTurn() returning false.

game-scene._cleanup:
- Explicitly drop Phaser input listeners (pointerdown, pointermove).
  Phaser's scene shutdown is supposed to tear down the input plugin,
  but an explicit off() makes the cleanup obvious and robust against
  any lifecycle quirk in scene restart.
- Null out this.board / this.stones / this.lastMarker / this.hoverGraphic
  / this.hoverPos so the next create() starts from a known-clean state,
  and late-firing events can't accidentally dereference stale Phaser
  game objects that the scene already destroyed.

No functional change to the happy path — just defensive teardown that
makes a scene restart observably idempotent.

npm run build green.
2026-04-11 18:53:23 +07:00
tiennm99 e7b6a5236f test: add PVE-after-PVP flow regression coverage
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.
2026-04-11 18:49:51 +07:00
tiennm99 9de0f97752 feat(pvp): auto-start game on second player join
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.
2026-04-11 18:19:05 +07:00
tiennm99 6434fccb0d fix(client): correct RoomCreateSuccess field names and track roomId
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.
2026-04-11 17:46:47 +07:00
tiennm99 a3506364e7 fix(rematch): sync both player goroutines on GameReset via RematchCh
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.
2026-04-11 17:35:31 +07:00
tiennm99 03758b0e90 fix(exit): route ClientExit through state machine, keep WS alive
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.
2026-04-11 16:51:13 +07:00
tiennm99 944758df64 chore: gitignore Claude Code per-user state 2026-04-11 16:36:27 +07:00
tiennm99 fba29d0cf9 feat(claude): enable gopls-lsp plugin, ignore per-user state 2026-04-11 16:36:14 +07:00
tiennm99 1d80d334e0 refactor(lobby): drop 'New' prefix from Room type and store functions
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.
2026-04-11 16:31:36 +07:00
tiennm99 4c068849c4 refactor(server): rename database package to lobby, drop ratel legacy files
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.
2026-04-11 16:08:46 +07:00
tiennm99 d45b1275af refactor: typed proto enums, dead code cleanup, room-join broadcast fix
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.
2026-04-11 16:03:21 +07:00
tiennm99 971371d544 ci: github actions for server test + client build
Two jobs on push/PR to main (paths-ignore: docs, plans, markdown):
- server: go vet + go test + go build (Go 1.23)
- client: npm ci + npm run build (Node 22)
2026-04-11 15:28:25 +07:00
tiennm99 f75af2b7a6 docs: rewrite README with new architecture and docker quickstart
- Replace ratel-online-era content with current project description
- Add architecture ASCII diagram, features list, protocol section
- Add quick-start (docker compose + local dev)
- Preserve ratel-online credit (ainilili/ratel) and add caro credit
- Update deployment-guide.md: docker compose, individual image builds,
  reverse proxy (Caddy/nginx), env vars, smoke check commands
2026-04-11 15:28:20 +07:00
tiennm99 b2be88597c feat(docker): multi-stage Dockerfiles and compose for server + nginx client
- server/Dockerfile: golang:1.23-alpine build → distroless/static-debian12:nonroot (3.5 MB)
- client/Dockerfile: node:22-alpine Vite build → nginx:1.27-alpine runtime
- client/nginx.conf: SPA fallback, aggressive asset caching, gzip
- docker-compose.yml: two services — server :1999, client :8080
- server/.dockerignore / client/.dockerignore: trim build contexts
- server/Makefile: add docker-build/run/stop/logs/smoke targets
- client/vite.config.js: raise chunkSizeWarningLimit to 2000 (Phaser bundle)
- remove server/docker-compose.yaml (superseded by root compose file)
2026-04-11 15:28:15 +07:00
tiennm99 4fad10ae41 test(state): unit tests for spectator watching flow, snapshot, and auto-eject
9 test cases in watching_test.go:
- TestWatching_ExitReturnsHome
- TestWatching_ClientExitReturnsErrClientExit
- TestWatching_MoveAttemptRejected
- TestWatching_ClosedCmdChReturnsErrClientExit
- TestSnapshot_SendsOwnerStartingAndHistory
- TestHomeWatchGame_RouteToWatching
- TestHomeWatchGame_RoomNotFound
- TestDeleteRoom_EjectsSpectators
- TestRoomSummary_IncludesSpectators
2026-04-11 15:08:21 +07:00
tiennm99 34b0f8f2ce feat(state): watching state with room snapshot replay and spectator-cannot-act
- Add StateWatching to consts/const.go (iota value 8)
- Register watchingState in state.go runner init()
- Add sendRoomSnapshot helper in game_shared.go: sends WatchGameSuccessResponse,
  GameStartingResponse, and one GameMoveSuccessResponse per MoveHistory entry
  using room.Snapshot() to avoid holding lock during Send calls
- Update home.go: handle WatchGameRequest → WatchNewRoom + sendRoomSnapshot →
  StateWatching; RoomPlayFailNotFoundResponse on missing room
- New watching.go: WatchGameExitRequest → UnwatchNewRoom + ShowOptions → StateHome;
  ClientExitRequest → UnwatchNewRoom → ErrClientExit; GameMoveRequest →
  SpectatorCannotActResponse + stay; closed CmdCh → UnwatchNewRoom → ErrClientExit
- Update store.go LeaveNewRoom: when last player leaves, collect and eject spectators
  (clear Spectators map under lock, then send ClientExitResponse + push synthetic
  WatchGameExitRequest to each spectator CmdCh after releasing lock)
2026-04-11 15:08:15 +07:00
tiennm99 e8281e5411 chore(client): split menu-ui into lobby and rooms modules
menu-ui.js exceeded 200-line limit. Extract room-list, PVE difficulty panel,
and waiting-room into menu-ui-rooms.js. menu-ui.js re-exports showRoomList
and showWaiting so menu-scene.js import paths are unchanged.
2026-04-11 14:55:04 +07:00
tiennm99 2d917d5d85 feat(client): dom menu-ui and game-ui with role-aware waiting room
menu-ui.js: nickname screen, lobby, PVP/PVE submenus, room list table,
role-aware showWaiting() — owner gets Start Game button (disabled until
playerCount=2); joiner gets passive wait message. Returns update() handle
for live ROOM_JOIN_SUCCESS pushes from menu-scene. All user strings set
via textContent for XSS safety.

game-ui.js: game HUD with turn indicator dots, scrollable move history,
showGameOver modal with Play Again (sendGameReset) and Leave buttons,
showToast auto-dismissing after 2s into #toast-container. Error toasts
wired for all GAME_MOVE_* and spectator-cannot-act events.
2026-04-11 14:49:04 +07:00
tiennm99 4c59ee0f83 feat(client): phaser board and stone objects with drop-in animation
Replace phase-09 stubs with real Phaser.GameObjects.Graphics implementations.
Board draws wood-textured 15x15 grid with star points and coordinate labels.
Stone renders gradient black/white circle with 180ms Back.easeOut drop tween.
createLastMoveMarker draws a red filled circle at the last-played intersection.
menu-scene.js wired for live waiting-room updates and role-aware showWaiting.
2026-04-11 14:48:55 +07:00
tiennm99 d6c781082a chore(deps): go mod tidy after retiring pkg shims 2026-04-11 14:48:44 +07:00
tiennm99 ca933abeed test(state): unit tests for runner, PVP/PVE game flow, waiting, and gameover
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.
2026-04-11 14:28:58 +07:00
tiennm99 530b96d161 refactor: retire legacy database shim, TCP handler, and server/pkg shims
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.
2026-04-11 14:28:32 +07:00
tiennm99 43ac04ac79 feat(state): rewrite state machine using cmdCh and typed protobuf requests
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).
2026-04-11 14:28:19 +07:00
tiennm99 5ceb7ef924 test(server): unit tests for dispatch and ws network layer
- codec_test.go: roundtrip encode/decode for Request and Response variants,
  malformed input, empty frame
- handlers_stateless_test.go: nickname validation table (9 cases),
  heartbeat no-op, get_rooms response, set_client_info version store,
  dispatch routing (stateless inline vs stateful CmdCh push),
  CmdCh overflow drop without panic
2026-04-11 13:56:29 +07:00
tiennm99 9e14f3a9b3 feat(server): new ws network layer and protobuf dispatch on :1999/gomoku
- Add server/network/{server,codec,reader,writer,dispatch,handlers_stateless}.go
- Single /gomoku WS endpoint; binary protobuf frames only (no JSON/base64)
- Per-connection reader + writer goroutines; write mutex serialises WS writes
- Dispatch type-switch: stateless (heartbeat, set_nickname, get_rooms,
  set_client_info, client_exit) inline; stateful requests → player.CmdCh
- Add Player.SendCh, CmdCh, LastHeartbeat, ClientVersion, Send() to database.Player
- Rewire main.go: NewServer(":1999").Serve() + database.StartCleanup()
- Empty wss.go (old shim superseded by server.go)
2026-04-11 13:56:22 +07:00
tiennm99 2c37c8f4ef feat(client): boot/menu/game scenes wired to event bus and protobuf dispatch
- boot-scene: connects WS, waits for NICKNAME_SET then starts MenuScene
- menu-scene: subscribes to CLIENT_CONNECT/NICKNAME_SET/SHOW_OPTIONS/SHOW_ROOMS/
  ROOM_CREATE_SUCCESS/ROOM_JOIN_SUCCESS/ROOM_JOIN_FAIL_*/GAME_STARTING; delegates
  to menu-ui stubs; proper shutdown() unsubscribes all handlers
- game-scene: Board+Stone stub integration, hover preview, click→sendGameMove,
  GAME_MOVE_SUCCESS/GAME_OVER/CLIENT_EXIT/GAME_STARTING/SPECTATOR_CANNOT_ACT/
  move error events all wired; audio feedback; shutdown() cleanup
2026-04-11 13:41:40 +07:00
tiennm99 6b2035632f feat(client): event-bus, connection-service, game-state-service with typed protobuf
- event-bus: pub/sub singleton with on/once/off/emit
- connection-service: WS wrapper for ws://host:1999/gomoku, binary protobuf
  encode/decode, 50s heartbeat, exponential backoff reconnect (max 10 attempts),
  14 typed send helpers including sendGameStarting() (owner-only explicit start)
- game-state-service: client-side state container wired to all ClientEventCode
  events; exposes isMyTurn(), currentPlayerNickname(), isOccupied()
- menu-ui, game-ui: no-op stubs for phase-10 to replace
- board, stone: minimal stub classes so GameScene compiles without real Phaser objects
2026-04-11 13:41:32 +07:00
tiennm99 39bbcd98bc test(database): unit tests for store and room operations
19 tests covering: unique ID assignment, PVP/PVE room creation, color
randomization, invalid difficulty rejection, join/leave/watch/unwatch,
empty-room cleanup, snapshot independence, ApplyMove turn advancement,
move history recording, win detection, Reset clearing, and IsOwner.
2026-04-11 13:31:04 +07:00
tiennm99 acf08669dd feat(database): new domain model for PVP/PVE/spectator with thread-safe store
- Split monolithic database.go/model.go/gomoku.go into focused files:
  player.go, room.go, store.go, errors.go, events.go, cleanup.go
- NewRoom embeds game.Board directly; carries blackPlayerId, whitePlayerId,
  currentTurn, moveHistory, spectators, owner, type, difficulty from caro domain
- Store singleton with sync.RWMutex: RegisterPlayer, CreatePvpRoom, CreatePveRoom,
  JoinNewRoom, LeaveNewRoom, WatchNewRoom, UnwatchNewRoom, BroadcastToNewRoom
- PVE rooms randomly assign human to Black or White; AI takes opposite side
- legacy.go shim keeps server/state/*.go and state/game/gomoku.go compiling
  without modification until phase-06 rewrites them
- consts/const.go: add RoomTypePvp/Pve, Status*, RoomStatus*, Difficulty* enums
2026-04-11 13:30:58 +07:00
tiennm99 a5acd69b87 feat(client): phaser+vite bootstrap with html shell, game config, and scene stubs
- index.html: dark shell with #game-container (Phaser canvas parent),
  #ui-overlay containing #menu-root, #game-hud-root, #toast-container
- vite.config.js: port 5173, strictPort, sourcemap build, no GitHub Pages base
- src/main.js: instantiates Phaser.Game from gameConfig
- src/config/game-config.js: 800x800 FIT canvas, parent=game-container, scene list
- src/config/protocol-constants.js: ClientEventCode enum aligned to response.proto
  oneofs; adds SPECTATOR_CANNOT_ACT, renames GAME_WATCH_SUCCESSFUL to
  WATCH_GAME_SUCCESS, drops CLIENT_KICK (no proto case)
- src/scenes/boot-scene.js|menu-scene.js|game-scene.js: minimal stubs for Phaser
  scene registry; phase-09 replaces them with real implementations
2026-04-11 12:48:07 +07:00
tiennm99 43b349e09e chore: remove .gitkeep placeholders now that phase-02 and phase-03 populated the directories 2026-04-11 12:42:58 +07:00
tiennm99 a5d1443327 feat(client): initial package.json and generated protobuf bindings
Adds Phaser+Vite+protobufjs package.json with proto:gen script, installs
dependencies, and commits generated protocol.js and protocol.d.ts so
builds work without requiring protoc or pbjs on every dev box.
Also adds node_modules/ to root .gitignore.
2026-04-11 12:41:06 +07:00
tiennm99 c40120d6db feat(server): add protoc-gen-go toolchain and generated proto code
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.
2026-04-11 12:40:53 +07:00
tiennm99 bb799da2c5 feat(proto): port caro protobuf schemas with com.miti99.gomoku.proto namespace
Copies request.proto and response.proto from caro, renames package to
com.miti99.gomoku.proto, drops Java options, adds go_package option,
and appends SpectatorCannotActResponse (field 21) to the Response oneof.
2026-04-11 12:40:45 +07:00
tiennm99 a8b7ab37e4 feat: add gomoku AI with easy/medium/hard difficulty and tests
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).
2026-04-11 12:30:59 +07:00
tiennm99 b20262b85a feat: add gomoku board and helper logic with tests
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.
2026-04-11 12:30:50 +07:00
tiennm99 1ce619ba4c chore: add skeleton directories for future phases
common/proto/, server/game/, server/protocol/, client/src/{config,scenes,
services,objects,ui,generated} — all .gitkeep placeholders.
2026-04-11 12:07:25 +07:00
tiennm99 b089e64955 refactor: delete api/, core/, old client/, web/, server/bot/, server/network/tcp.go
Removed legacy Java API, Go shared core lib, Go CLI client, vanilla-JS web
client, QQ bot integration, and TCP listener. Server is now WS-only.
2026-04-11 12:07:18 +07:00
tiennm99 1b9eec5f7d refactor: rename Go module and copy core pkgs into server/pkg
- Module: github.com/ratel-online/server → github.com/tiennm99/gomoku/server
- Copied core/{log,util/async,util/json,util/strings,model,network,protocol,consts}
  into server/pkg/* (temporary shims until phase-05 replaces protocol/network)
- Rewrote all ratel-online import paths across server/**/*.go
- Trimmed main.go: single -p flag, WS-only on :1999, no TCP/bot/static
- Trimmed network/wss.go: endpoint /gomoku, no static file serving
- Updated Makefile: removed TCP/bot references, port 1999
2026-04-11 12:04:03 +07:00
tiennm99 5ccd4e7ce2 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.
2026-04-09 23:35:46 +07:00