mirror of
https://github.com/tiennm99/caro.git
synced 2026-10-03 05:19:23 +00:00
chore: remove shipped plans
All 5 plans implemented and merged. Deleting to keep the plans/ directory focused on active work: - 260409-1701-caro-simplification (shippedd871cc2) - 260409-1812-web-gomoku-client (shipped77e141c, later replaced by Phaser) - 260410-0913-phaser-web-client (shipped22bb9c1) - 260410-1843-refactor-project-structure (shipped c71aa6a/5b68ee9/2d74117/1297b7d) - 260410-2101-websocket-protobuf-migration (shipped945a249through42d94a2) plans/reports/ kept for historical cross-plan reports.
This commit is contained in:
1 parent
42d94a2aed
commit
d5c1318a0f
37 files changed
-5633
No files matched your search
@@ -1,80 +0,0 @@
|
||||
## Phase 1: Delete Dead Files
|
||||
|
||||
### Context Links
|
||||
- [Plan overview](./plan.md)
|
||||
|
||||
### Overview
|
||||
- **Priority:** P1 (do first -- unblocks everything)
|
||||
- **Status:** Pending
|
||||
- **Effort:** 30m
|
||||
|
||||
Pure deletion phase. No logic changes. Every file listed is either landlords-specific code or obsolete documentation.
|
||||
|
||||
### Files to Delete
|
||||
|
||||
**Common module -- Poker domain:**
|
||||
- `landlords-common/src/main/java/org/nico/ratel/landlords/entity/Poker.java`
|
||||
- `landlords-common/src/main/java/org/nico/ratel/landlords/entity/PokerSell.java`
|
||||
- `landlords-common/src/main/java/org/nico/ratel/landlords/enums/PokerLevel.java`
|
||||
- `landlords-common/src/main/java/org/nico/ratel/landlords/enums/PokerType.java`
|
||||
- `landlords-common/src/main/java/org/nico/ratel/landlords/helper/PokerHelper.java`
|
||||
- `landlords-common/src/main/java/org/nico/ratel/landlords/utils/LastCardsUtils.java`
|
||||
|
||||
**Common module -- Old robot system:**
|
||||
- `landlords-common/src/main/java/org/nico/ratel/landlords/robot/AbstractRobotDecisionMakers.java`
|
||||
- `landlords-common/src/main/java/org/nico/ratel/landlords/robot/EasyRobotDecisionMakers.java`
|
||||
- `landlords-common/src/main/java/org/nico/ratel/landlords/robot/MediumRobotDecisionMakers.java`
|
||||
- `landlords-common/src/main/java/org/nico/ratel/landlords/robot/RobotDecisionMakers.java`
|
||||
|
||||
**Common module -- Tests:**
|
||||
- `landlords-common/src/test/java/org/nico/ratel/landlords/helper/tests/PokerHelperTest.java`
|
||||
- `landlords-common/src/test/java/org/nico/ratel/landlords/robot/tests/MediumRobotDecisionMakersTests.java`
|
||||
|
||||
**Common module -- Chinese i18n:**
|
||||
- `landlords-common/src/main/resources/messages_zh_CN.properties`
|
||||
|
||||
**Server module -- Landlord event handlers (6 files):**
|
||||
- `landlords-server/.../event/ServerEventListener_CODE_GAME_LANDLORD_ELECT.java`
|
||||
- `landlords-server/.../event/ServerEventListener_CODE_GAME_POKER_PLAY.java`
|
||||
- `landlords-server/.../event/ServerEventListener_CODE_GAME_POKER_PLAY_PASS.java`
|
||||
- `landlords-server/.../event/ServerEventListener_CODE_GAME_POKER_PLAY_REDIRECT.java`
|
||||
- `landlords-server/.../robot/RobotEventListener_CODE_GAME_LANDLORD_ELECT.java`
|
||||
- `landlords-server/.../robot/RobotEventListener_CODE_GAME_POKER_PLAY.java`
|
||||
|
||||
**Client module -- Landlord event handlers (12 files):**
|
||||
- `landlords-client/.../event/ClientEventListener_CODE_GAME_LANDLORD_CONFIRM.java`
|
||||
- `landlords-client/.../event/ClientEventListener_CODE_GAME_LANDLORD_CYCLE.java`
|
||||
- `landlords-client/.../event/ClientEventListener_CODE_GAME_LANDLORD_ELECT.java`
|
||||
- `landlords-client/.../event/ClientEventListener_CODE_GAME_POKER_PLAY.java`
|
||||
- `landlords-client/.../event/ClientEventListener_CODE_GAME_POKER_PLAY_CANT_PASS.java`
|
||||
- `landlords-client/.../event/ClientEventListener_CODE_GAME_POKER_PLAY_INVALID.java`
|
||||
- `landlords-client/.../event/ClientEventListener_CODE_GAME_POKER_PLAY_LESS.java`
|
||||
- `landlords-client/.../event/ClientEventListener_CODE_GAME_POKER_PLAY_MISMATCH.java`
|
||||
- `landlords-client/.../event/ClientEventListener_CODE_GAME_POKER_PLAY_ORDER_ERROR.java`
|
||||
- `landlords-client/.../event/ClientEventListener_CODE_GAME_POKER_PLAY_PASS.java`
|
||||
- `landlords-client/.../event/ClientEventListener_CODE_GAME_POKER_PLAY_REDIRECT.java`
|
||||
- `landlords-client/.../event/ClientEventListener_CODE_SHOW_POKERS.java`
|
||||
|
||||
**Root-level files:**
|
||||
- `GOMOKU_CONVERSION_SUMMARY.md`
|
||||
- `PROTOCO_CN.md`
|
||||
- `UPDATE.md`
|
||||
- `serverlist.json`
|
||||
- `docker/` (entire directory)
|
||||
|
||||
### Total: ~35 files/dirs deleted
|
||||
|
||||
### Implementation Steps
|
||||
1. Delete all files listed above via `git rm`
|
||||
2. Run `mvn compile` -- expect failures (imports of deleted classes). Those are fixed in Phase 2.
|
||||
3. Commit: `refactor: delete all landlords card game code and obsolete docs`
|
||||
|
||||
### Risk Assessment
|
||||
- **Risk:** Accidentally delete a file still referenced by kept code
|
||||
- **Mitigation:** Phase 2 explicitly fixes all broken imports. Compile verification in Phase 5.
|
||||
- **Likelihood:** Low (all files audited against grep results)
|
||||
|
||||
### Success Criteria
|
||||
- [ ] All listed files removed from repo
|
||||
- [ ] No landlord/poker .java files remain
|
||||
- [ ] Commit is clean and focused
|
||||
@@ -1,92 +0,0 @@
|
||||
## Phase 2: Clean Shared Code (Common Module)
|
||||
|
||||
### Context Links
|
||||
- [Plan overview](./plan.md)
|
||||
- [Phase 1](./phase-01-delete-dead-files.md) (must complete first)
|
||||
|
||||
### Overview
|
||||
- **Priority:** P1
|
||||
- **Status:** Pending
|
||||
- **Effort:** 1h
|
||||
- **Blocked by:** Phase 1
|
||||
|
||||
Remove all landlords references from shared entities, enums, helpers, and printers. After this phase, the common module compiles cleanly with only Gomoku domain code.
|
||||
|
||||
### Key Insights
|
||||
- `Room.java` is already mostly clean (gomoku fields present). But it still has `setCurrentSellClient()` called from server code, and leftover `scoreRate`/`baseScore`/`score` fields.
|
||||
- `ClientSide.java` has no `pokers` field (already removed) but still has `score`, `scoreInc`, `type` (ClientType = LANDLORD/PEASANT), and linked-list `next`/`pre` fields used for 3-player turn order.
|
||||
- `ClientEventCode.java` and `ServerEventCode.java` are already cleaned up -- only Gomoku codes remain. No changes needed.
|
||||
- `ClientRole.java` has `PLAYER, ROBOT, BLACK_PLAYER, WHITE_PLAYER` -- needs simplification.
|
||||
- `ClientStatus.java` has `CALL_LANDLORD` -- remove it.
|
||||
- `ClientType.java` (LANDLORD/PEASANT) -- delete entire enum, not applicable to Gomoku.
|
||||
- `SimplePrinter.java` has `printPokers()` method referencing deleted PokerHelper -- must remove.
|
||||
- `ClientEventListener.java` (client base class) has `lastPokers`, `lastSellClientNickname`, `lastSellClientType` static fields -- must remove.
|
||||
|
||||
### Files to Modify
|
||||
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `entity/Room.java` | Remove `scoreRate`, `baseScore`, `currentSellClient`, `firstSellClient`, `landlordPokers`, `landlordId`, `lastSellClient` and all their getters/setters. Keep gomoku fields. |
|
||||
| `entity/ClientSide.java` | Remove `score`, `scoreInc`, `type` (ClientType), `next`, `pre`, `round` fields + getters/setters. Gomoku doesn't need linked-list player chaining (use Room's clientSideMap). |
|
||||
| `enums/ClientRole.java` | Remove `PLAYER` and `ROBOT`. Keep `BLACK_PLAYER`, `WHITE_PLAYER`. Add `SPECTATOR`. |
|
||||
| `enums/ClientStatus.java` | Remove `CALL_LANDLORD`. Keep `TO_CHOOSE`, `NO_READY`, `READY`, `WAIT`, `PLAYING`. |
|
||||
| `enums/ClientType.java` | **Delete entire file** -- no concept of landlord/peasant in Gomoku. |
|
||||
| `print/SimplePrinter.java` | Remove `printPokers()` method and `import Poker/PokerHelper`. Remove `pokerDisplayFormat` field. |
|
||||
| `helper/I18nHelper.java` | Verify no reference to `messages_zh_CN.properties` (already handles fallback to en_US). |
|
||||
|
||||
### Files to Delete
|
||||
- `landlords-common/src/main/java/org/nico/ratel/landlords/enums/ClientType.java`
|
||||
|
||||
### Implementation Steps
|
||||
|
||||
1. **Delete `ClientType.java`**
|
||||
|
||||
2. **Clean `Room.java`:**
|
||||
- Remove fields: `scoreRate`, `baseScore` (and `getScore()`, `initScoreRate()`, `increaseRate()` methods)
|
||||
- Verify no `landlordPokers`, `currentSellClient`, `firstSellClient`, `lastSellClient`, `landlordId` fields exist (grep confirms Room.java already lacks these -- they exist only in deleted server files that call non-existent setters)
|
||||
- Actually: grep shows `room.setCurrentSellClient()` called from server files, but Room.java has no such method. These are in files being deleted in Phase 1. **No Room.java changes needed for these.**
|
||||
- Remove: `scoreRate`, `baseScore`, `getScore()`, `getBaseScore()`, `setBaseScore()`, `getScoreRate()`, `setScoreRate()`, `initScoreRate()`, `increaseRate()` -- these are unused in Gomoku
|
||||
|
||||
3. **Clean `ClientSide.java`:**
|
||||
- Remove `type` field (ClientType) + getter/setter
|
||||
- Remove `score`, `scoreInc` fields + getter/setter/`addScore()`
|
||||
- Remove `next`, `pre` fields + getter/setter (3-player circular list not needed for 2-player Gomoku; use Room.clientSideMap)
|
||||
- Remove `round`, `resetRound()`, `addRound()`, `getRound()`
|
||||
- Update `init()` to remove references to removed fields
|
||||
|
||||
4. **Clean `ClientRole.java`:**
|
||||
- Remove `PLAYER` and `ROBOT`
|
||||
- Add `SPECTATOR`
|
||||
- Result: `BLACK_PLAYER, WHITE_PLAYER, SPECTATOR`
|
||||
|
||||
5. **Clean `ClientStatus.java`:**
|
||||
- Remove `CALL_LANDLORD`
|
||||
|
||||
6. **Clean `SimplePrinter.java`:**
|
||||
- Remove `import org.nico.ratel.landlords.entity.Poker`
|
||||
- Remove `import org.nico.ratel.landlords.helper.PokerHelper`
|
||||
- Remove `pokerDisplayFormat` static field
|
||||
- Remove `printPokers()` method
|
||||
|
||||
7. **Clean `ClientEventListener.java` (client module base class):**
|
||||
- Remove `import org.nico.ratel.landlords.entity.Poker`
|
||||
- Remove static fields: `lastPokers`, `lastSellClientNickname`, `lastSellClientType`
|
||||
- Remove `initLastSellInfo()` method
|
||||
|
||||
8. Run `mvn compile -pl landlords-common` -- must pass
|
||||
|
||||
### Risk Assessment
|
||||
- **Risk:** Removing `next`/`pre` from ClientSide breaks server event handlers that use circular linked list for turn order
|
||||
- **Mitigation:** Server handlers are rewritten in Phase 3 to use Room's `isPlayerTurn()` / `currentTurn` instead. Phase 3 must not use `client.getNext()`.
|
||||
- **Likelihood:** Medium
|
||||
- **Impact:** Compile error (caught immediately)
|
||||
|
||||
### Security Considerations
|
||||
None -- no auth/data changes.
|
||||
|
||||
### Success Criteria
|
||||
- [ ] `ClientType.java` deleted
|
||||
- [ ] No `import.*Poker` in any kept file
|
||||
- [ ] No `score`/`scoreRate`/`baseScore` in Room or ClientSide
|
||||
- [ ] ClientRole has exactly: `BLACK_PLAYER, WHITE_PLAYER, SPECTATOR`
|
||||
- [ ] `mvn compile -pl landlords-common` passes
|
||||
@@ -1,164 +0,0 @@
|
||||
## Phase 3: Rewrite Server Event Handlers
|
||||
|
||||
### Context Links
|
||||
- [Plan overview](./plan.md)
|
||||
- [Phase 2](./phase-02-clean-shared-code.md) (must complete first)
|
||||
- Key domain files: `Board.java`, `GomokuHelper.java`, `GomokuAI.java`, `Room.java`
|
||||
|
||||
### Overview
|
||||
- **Priority:** P1
|
||||
- **Status:** Pending
|
||||
- **Effort:** 2h
|
||||
- **Blocked by:** Phase 2
|
||||
|
||||
Rewrite the server-side event handlers to implement Gomoku game flow. Create the missing `ServerEventListener_CODE_GAME_MOVE.java`. Fix existing handlers that still contain landlords logic.
|
||||
|
||||
### Data Flow: Gomoku Game Lifecycle
|
||||
|
||||
```
|
||||
Client Server
|
||||
|-- CODE_ROOM_CREATE ---------->| Create room, assign client as BLACK_PLAYER
|
||||
|<-- CODE_ROOM_CREATE_SUCCESS --|
|
||||
| |
|
||||
|-- CODE_ROOM_JOIN ------------>| Join room, assign as WHITE_PLAYER
|
||||
|<-- CODE_ROOM_JOIN_SUCCESS ----| (to both players)
|
||||
| | Auto-start: call CODE_GAME_STARTING
|
||||
|<-- CODE_GAME_STARTING --------| (to both players: board state, who is black/white)
|
||||
| |
|
||||
|-- CODE_GAME_MOVE ------------>| Validate move via GomokuHelper
|
||||
|<-- CODE_GAME_MOVE_SUCCESS ----| (broadcast to both + spectators)
|
||||
| or CODE_GAME_MOVE_INVALID |
|
||||
| or CODE_GAME_MOVE_OCCUPIED |
|
||||
| or CODE_GAME_MOVE_NOT_YOUR_TURN |
|
||||
| |
|
||||
|<-- CODE_GAME_OVER -----------| (when GomokuHelper detects win/draw)
|
||||
```
|
||||
|
||||
### Files to Create
|
||||
|
||||
**`ServerEventListener_CODE_GAME_MOVE.java`** -- the core missing handler
|
||||
|
||||
```
|
||||
Package: org.nico.ratel.landlords.server.event
|
||||
```
|
||||
|
||||
Logic:
|
||||
1. Parse `data` as JSON: `{ "row": int, "col": int }`
|
||||
2. Get room from `ServerContains.getRoom(clientSide.getRoomId())`
|
||||
3. Null-check room -> push `CODE_ROOM_PLAY_FAIL_BY_INEXIST`
|
||||
4. Check `room.isPlayerTurn(clientSide.getId())` -> if false, push `CODE_GAME_MOVE_NOT_YOUR_TURN`
|
||||
5. Check `room.getGameBoard().isValidMove(row, col)`:
|
||||
- Out of bounds -> `CODE_GAME_MOVE_OUT_OF_BOUNDS`
|
||||
- Position occupied -> `CODE_GAME_MOVE_OCCUPIED`
|
||||
6. Call `GomokuHelper.makeMove(room, row, col, clientSide.getId())`
|
||||
7. Build result JSON: `{ row, col, piece, playerId, playerNickname, nextPlayerId }`
|
||||
8. Broadcast `CODE_GAME_MOVE_SUCCESS` to all players + spectators
|
||||
9. Check `GomokuHelper.isGameOver(room)`:
|
||||
- If yes, determine winner, broadcast `CODE_GAME_OVER` with `{ result, winnerNickname, board }`
|
||||
- For PVE: if next turn is AI, trigger AI move via `GomokuAI.getNextMove()` and recurse
|
||||
|
||||
**`RobotEventListener_CODE_GAME_MOVE.java`** -- AI move handler for PVE
|
||||
|
||||
```
|
||||
Package: org.nico.ratel.landlords.server.robot
|
||||
```
|
||||
|
||||
Logic:
|
||||
1. Get room, get AI piece color from room
|
||||
2. Call `GomokuAI.getNextMove(board, difficulty)`
|
||||
3. Delegate to `ServerEventListener_CODE_GAME_MOVE` with the AI's move data
|
||||
|
||||
### Files to Rewrite
|
||||
|
||||
**`ServerEventListener_CODE_GAME_STARTING.java`** -- Currently distributes poker cards. Rewrite for Gomoku:
|
||||
1. Get room
|
||||
2. Assign players: first player = BLACK, second = WHITE
|
||||
3. Set `room.setBlackPlayerId(first.getId())`, `room.setWhitePlayerId(second.getId())`
|
||||
4. Set player roles: `first.setRole(ClientRole.BLACK_PLAYER)`, `second.setRole(ClientRole.WHITE_PLAYER)`
|
||||
5. Set `room.setStatus(RoomStatus.STARTING)`
|
||||
6. Set `room.setCurrentTurn(PieceType.BLACK)`
|
||||
7. Reset board: `room.getGameBoard().reset()`
|
||||
8. Build result JSON: `{ roomId, blackPlayer: {id, nickname}, whitePlayer: {id, nickname}, boardSize: 15 }`
|
||||
9. Push `CODE_GAME_STARTING` to both players + spectators
|
||||
10. For PVE: if AI is BLACK, trigger AI first move
|
||||
|
||||
**`ServerEventListener_CODE_GAME_READY.java`** -- Currently checks for 3 players and uses `ClientRole.PLAYER`. Rewrite:
|
||||
1. Change player count check from 3 to 2
|
||||
2. Replace `ClientRole.PLAYER` references with check for non-null channel (human player)
|
||||
3. Remove Chinese log messages (`"房间状态"`, `"玩家状态"`)
|
||||
4. When all ready, call `CODE_GAME_STARTING`
|
||||
|
||||
**`ServerEventListener_CODE_ROOM_CREATE.java`** -- Currently calls `room.setCurrentSellClient()`. Rewrite:
|
||||
1. Remove `room.setCurrentSellClient()` call
|
||||
2. Set first player as `ClientRole.BLACK_PLAYER`
|
||||
3. Rest is fine
|
||||
|
||||
**`ServerEventListener_CODE_ROOM_CREATE_PVE.java`** -- Currently creates 2 robots for 3-player game. Rewrite:
|
||||
1. Create room with human player
|
||||
2. Create 1 AI robot (not 2) -- Gomoku is 2-player
|
||||
3. Assign human as BLACK, AI as WHITE (or configurable)
|
||||
4. Replace `RobotDecisionMakers.contains()` with simple difficulty range check (1-3)
|
||||
5. Remove `client.setNext()`/`client.setPre()` linked-list wiring
|
||||
6. Auto-start game immediately
|
||||
|
||||
**`ServerEventListener_CODE_ROOM_JOIN.java`** -- Currently allows up to 3 players. Rewrite:
|
||||
1. Change full-room check from `size == 3` to `size == 2`
|
||||
2. Remove `next`/`pre` linked-list wiring
|
||||
3. When 2nd player joins, auto-start game (call `CODE_GAME_STARTING`)
|
||||
4. Remove Chinese comments
|
||||
|
||||
**`ServerEventListener_CODE_CLIENT_EXIT.java`** -- Currently uses `ClientRole.PLAYER`. Update:
|
||||
1. Replace `ClientRole.PLAYER` with check against `BLACK_PLAYER`/`WHITE_PLAYER`
|
||||
2. Remove Chinese comments
|
||||
|
||||
**`RoomClearTask.java`** -- Heavy landlords logic (robot substitution, poker custody). Rewrite:
|
||||
1. Keep timeout-based room cleanup (waitingStatusInterval, liveTime)
|
||||
2. Remove all robot-substitution logic (lines 76-130)
|
||||
3. Remove references to `currentSellClient`, `lastSellClient`, `landlordId`, `setPokers`, `setType`
|
||||
4. On timeout: just close the room and notify players
|
||||
5. Remove `RobotEventListener` import and call
|
||||
|
||||
**`RobotEventListener.java`** (interface) -- Keep but will only resolve `CODE_GAME_MOVE`:
|
||||
1. No structural change needed, reflection-based lookup still works
|
||||
|
||||
### Files to Verify (minor touch-ups)
|
||||
|
||||
- `ServerEventListener_CODE_GAME_WATCH.java` -- likely uses Chinese comments, remove them
|
||||
- `ServerEventListener_CODE_GAME_WATCH_EXIT.java` -- same
|
||||
- `ServerEventListener_CODE_CLIENT_OFFLINE.java` -- verify no poker references
|
||||
- `ServerEventListener_CODE_CLIENT_INFO_SET.java` -- verify clean
|
||||
- `ServerEventListener_CODE_CLIENT_NICKNAME_SET.java` -- verify clean
|
||||
- `ServerEventListener_CODE_GET_ROOMS.java` -- verify clean
|
||||
|
||||
### Architecture: PVE Move Flow
|
||||
|
||||
```
|
||||
Human makes move -> ServerEventListener_CODE_GAME_MOVE
|
||||
-> validate + apply move
|
||||
-> check game over?
|
||||
-> if not over && next turn is AI:
|
||||
-> GomokuAI.getNextMove(board, difficulty)
|
||||
-> apply AI move to board via GomokuHelper
|
||||
-> broadcast AI move as CODE_GAME_MOVE_SUCCESS
|
||||
-> check game over again
|
||||
```
|
||||
|
||||
No separate robot event listener needed for moves -- handle AI inline in CODE_GAME_MOVE handler to avoid complexity. Delete RobotEventListener_CODE_GAME_MOVE if created, or simply don't create it.
|
||||
|
||||
**Revised approach:** Handle AI response inline in `ServerEventListener_CODE_GAME_MOVE.java` rather than via separate RobotEventListener. Simpler, fewer files, same behavior.
|
||||
|
||||
### Risk Assessment
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|------|-----------|--------|------------|
|
||||
| Breaking room join flow (2 vs 3 players) | Medium | High | Unit test: create room, join, verify auto-start |
|
||||
| AI move infinite loop (AI triggers AI) | Low | High | Guard: only trigger AI if current turn belongs to AI player, and game not over |
|
||||
| Race condition on concurrent moves | Low | Medium | Room operations already single-threaded per room via Netty event loop |
|
||||
|
||||
### Success Criteria
|
||||
- [ ] `ServerEventListener_CODE_GAME_MOVE.java` exists and handles move validation + win detection
|
||||
- [ ] PVE mode creates 1 AI robot, not 2
|
||||
- [ ] Room join auto-starts at 2 players
|
||||
- [ ] No references to Poker, PokerSell, PokerHelper, LastCardsUtils in any server file
|
||||
- [ ] No Chinese text in any server file (except LICENSE)
|
||||
- [ ] `mvn compile -pl landlords-server` passes
|
||||
@@ -1,144 +0,0 @@
|
||||
## Phase 4: Rewrite Client Event Handlers
|
||||
|
||||
### Context Links
|
||||
- [Plan overview](./plan.md)
|
||||
- [Phase 2](./phase-02-clean-shared-code.md) (must complete first)
|
||||
- Can run **in parallel** with Phase 3 (no file overlap)
|
||||
|
||||
### Overview
|
||||
- **Priority:** P1
|
||||
- **Status:** Pending
|
||||
- **Effort:** 1.5h
|
||||
- **Blocked by:** Phase 2
|
||||
|
||||
Rewrite client-side event handlers to display Gomoku game state. The client is a CLI app -- it reads server events and prints board/prompts to the console, then sends user input back.
|
||||
|
||||
### Files to Rewrite
|
||||
|
||||
**`ClientEventListener_CODE_GAME_STARTING.java`** -- Currently prints poker cards. Rewrite:
|
||||
1. Parse server data: `{ roomId, blackPlayer: {id, nickname}, whitePlayer: {id, nickname}, boardSize }`
|
||||
2. Print: "Game starting! You are [BLACK/WHITE]"
|
||||
3. Print initial empty board via `GomokuHelper.formatBoardForDisplay()`
|
||||
4. If player is BLACK, prompt for first move
|
||||
5. Remove all Poker imports and references
|
||||
|
||||
**`ClientEventListener_CODE_GAME_OVER.java`** -- Currently shows poker scores. Rewrite:
|
||||
1. Parse: `{ result, winnerNickname, board }`
|
||||
2. Print final board state
|
||||
3. Print result: "Black wins!" / "White wins!" / "Draw!" via `GomokuHelper.getWinnerMessage()`
|
||||
4. Remove score display logic
|
||||
5. Call `ClientEventListener_CODE_GAME_READY.gameReady(channel)` to offer rematch
|
||||
|
||||
**`ClientEventListener_CODE_SHOW_OPTIONS_PVE.java`** -- Currently calls `initLastSellInfo()`. Update:
|
||||
1. Remove `initLastSellInfo()` call (method deleted in Phase 2)
|
||||
2. Keep difficulty selection (Easy/Medium/Hard maps to 1/2/3)
|
||||
3. Rest is clean
|
||||
|
||||
**`ClientEventListener_CODE_SHOW_OPTIONS_SETTING.java`** -- Check for poker display format references:
|
||||
1. Remove any `pokerDisplayFormat` references
|
||||
2. Keep language selection if present
|
||||
|
||||
**New: `ClientEventListener_CODE_GAME_MOVE_SUCCESS.java`** -- Handle successful move broadcast:
|
||||
1. Parse: `{ row, col, piece, playerNickname, nextPlayerId }`
|
||||
2. Print: "[playerNickname] placed [BLACK/WHITE] at (row, col)"
|
||||
3. Print updated board via `GomokuHelper.formatBoardForDisplay()`
|
||||
4. If it's this player's turn next, prompt for move input
|
||||
5. Read input as "row,col", send `CODE_GAME_MOVE` to server with `{ row, col }`
|
||||
|
||||
**New: `ClientEventListener_CODE_GAME_MOVE_INVALID.java`** -- Handle invalid move:
|
||||
1. Print "Invalid move. Please try again."
|
||||
2. Re-prompt for move input
|
||||
|
||||
**New: `ClientEventListener_CODE_GAME_MOVE_OCCUPIED.java`** -- Handle occupied position:
|
||||
1. Print "Position already occupied. Please choose another."
|
||||
2. Re-prompt for move input
|
||||
|
||||
**New: `ClientEventListener_CODE_GAME_MOVE_OUT_OF_BOUNDS.java`** -- Handle out of bounds:
|
||||
1. Print "Move out of bounds. Board is 15x15 (0-14)."
|
||||
2. Re-prompt for move input
|
||||
|
||||
**New: `ClientEventListener_CODE_GAME_MOVE_NOT_YOUR_TURN.java`** -- Handle wrong turn:
|
||||
1. Print "It's not your turn. Please wait."
|
||||
|
||||
**New: `ClientEventListener_CODE_SHOW_BOARD.java`** -- Handle board display request:
|
||||
1. This is a client-only code; may need special handling
|
||||
2. Or simply handle "board" command locally in the move input loop
|
||||
|
||||
### Files Unchanged (already clean)
|
||||
- `ClientEventListener_CODE_CLIENT_CONNECT.java`
|
||||
- `ClientEventListener_CODE_CLIENT_EXIT.java`
|
||||
- `ClientEventListener_CODE_CLIENT_KICK.java`
|
||||
- `ClientEventListener_CODE_CLIENT_NICKNAME_SET.java`
|
||||
- `ClientEventListener_CODE_ROOM_CREATE_SUCCESS.java`
|
||||
- `ClientEventListener_CODE_ROOM_JOIN_SUCCESS.java`
|
||||
- `ClientEventListener_CODE_ROOM_JOIN_FAIL_BY_FULL.java`
|
||||
- `ClientEventListener_CODE_ROOM_JOIN_FAIL_BY_INEXIST.java`
|
||||
- `ClientEventListener_CODE_ROOM_PLAY_FAIL_BY_INEXIST.java`
|
||||
- `ClientEventListener_CODE_SHOW_OPTIONS.java`
|
||||
- `ClientEventListener_CODE_SHOW_OPTIONS_PVP.java`
|
||||
- `ClientEventListener_CODE_SHOW_ROOMS.java`
|
||||
- `ClientEventListener_CODE_PVE_DIFFICULTY_NOT_SUPPORT.java`
|
||||
- `ClientEventListener_CODE_GAME_READY.java`
|
||||
- `ClientEventListener_CODE_GAME_WATCH.java`
|
||||
- `ClientEventListener_CODE_GAME_WATCH_SUCCESSFUL.java`
|
||||
|
||||
### Move Input Pattern
|
||||
|
||||
The client prompts for a move and sends it to server. Pattern used in new handlers:
|
||||
|
||||
```java
|
||||
String input = SimpleWriter.write(nickname, "move");
|
||||
// Parse "row,col" format
|
||||
// Handle special commands: "board"/"b", "history"/"h", "exit"/"e"
|
||||
if (input matches "\\d+,\\d+") {
|
||||
String moveData = MapHelper.newInstance()
|
||||
.put("row", row).put("col", col).json();
|
||||
pushToServer(channel, ServerEventCode.CODE_GAME_MOVE, moveData);
|
||||
} else if (input is "board" or "b") {
|
||||
// print board locally (need to store board state client-side or request from server)
|
||||
} else if (input is "exit" or "e") {
|
||||
pushToServer(channel, ServerEventCode.CODE_CLIENT_EXIT, null);
|
||||
}
|
||||
```
|
||||
|
||||
**Key decision:** Store board state client-side (in a static field on the listener or a shared client state object) so "board" and "history" commands work without server round-trip.
|
||||
|
||||
### Client-Side State
|
||||
|
||||
Add a simple static state holder (or use existing `User.java`):
|
||||
|
||||
```java
|
||||
// In ClientEventListener or a new small class
|
||||
static Board localBoard = null;
|
||||
static PieceType myPiece = null;
|
||||
static String myNickname = null;
|
||||
```
|
||||
|
||||
Set these in `CODE_GAME_STARTING` handler. Update board in `CODE_GAME_MOVE_SUCCESS` handler.
|
||||
|
||||
### Files to Verify (Chinese text removal)
|
||||
- `ClientEventListener_CODE_GAME_WATCH.java` -- grep found Chinese; remove
|
||||
- `ClientEventListener_CODE_GAME_WATCH_SUCCESSFUL.java` -- check
|
||||
|
||||
### SimpleClient.java Changes
|
||||
- Remove `serverAddressSource` array (fetches from upstream ratel repo)
|
||||
- When no `-h` flag provided, print error asking user to specify host instead of fetching server list
|
||||
- Remove `getServerAddressList()` method
|
||||
- Keep language selection logic (only en_US matters now)
|
||||
|
||||
### Risk Assessment
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|------|-----------|--------|------------|
|
||||
| Move input parsing errors (non "row,col" format) | Medium | Low | Validate format, re-prompt on bad input |
|
||||
| Client board state out of sync with server | Low | Medium | Server is authoritative; client board is display-only. Re-sync on each CODE_GAME_MOVE_SUCCESS |
|
||||
| Blocking on SimpleWriter.write() during opponent's turn | Low | Low | Existing pattern -- client blocks on stdin. Move prompt only shown when it's player's turn |
|
||||
|
||||
### Success Criteria
|
||||
- [ ] 5 new client event handler files created for Gomoku move codes
|
||||
- [ ] `CODE_GAME_STARTING` prints board, not poker cards
|
||||
- [ ] `CODE_GAME_OVER` shows winner without scores
|
||||
- [ ] Client can display board and prompt for "row,col" input
|
||||
- [ ] No Poker imports in any client file
|
||||
- [ ] No Chinese text in any client file
|
||||
- [ ] `mvn compile -pl landlords-client` passes
|
||||
@@ -1,116 +0,0 @@
|
||||
## Phase 5: Integration Test & Compile Verify
|
||||
|
||||
### Context Links
|
||||
- [Plan overview](./plan.md)
|
||||
- [Phase 3](./phase-03-rewrite-server-events.md), [Phase 4](./phase-04-rewrite-client-events.md) (must both complete first)
|
||||
|
||||
### Overview
|
||||
- **Priority:** P1
|
||||
- **Status:** Pending
|
||||
- **Effort:** 1h
|
||||
- **Blocked by:** Phase 3, Phase 4
|
||||
|
||||
Full build verification, fix remaining compile errors, clean up README, and validate the game flow works end-to-end.
|
||||
|
||||
### Implementation Steps
|
||||
|
||||
#### 1. Full Maven Build
|
||||
```bash
|
||||
mvn clean compile
|
||||
```
|
||||
Fix any compile errors iteratively. Common expected issues:
|
||||
- Missing imports of deleted classes in files not yet touched
|
||||
- `ClientRole.PLAYER` references in files outside the main event handlers
|
||||
- `ClientType` references anywhere
|
||||
- `getNext()`/`getPre()` calls if any kept file uses them
|
||||
|
||||
#### 2. Run Existing Tests
|
||||
```bash
|
||||
mvn test
|
||||
```
|
||||
- `GomokuHelperTest.java` should pass (tests Gomoku logic, no poker deps)
|
||||
- Deleted tests (PokerHelperTest, MediumRobotDecisionMakersTests) are gone -- no failures from those
|
||||
|
||||
#### 3. Add Basic Gomoku Integration Test
|
||||
|
||||
Create `landlords-common/src/test/java/org/nico/ratel/landlords/helper/tests/GomokuIntegrationTest.java`:
|
||||
- Test full game flow in-memory: create Room, assign players, make moves, detect win
|
||||
- Test draw detection (fill board)
|
||||
- Test invalid move rejection (occupied, out of bounds, wrong turn)
|
||||
- Test board reset
|
||||
|
||||
#### 4. Clean Up README.md
|
||||
- Remove references to original ratel/landlords
|
||||
- Remove badge URLs pointing to ainilili/ratel
|
||||
- Remove serverlist.json references
|
||||
- Remove ecosystem links (go-ratel-client etc.) or mark as incompatible
|
||||
- Remove bilibili video link
|
||||
- Keep installation instructions, update to reflect current project
|
||||
- Update game commands section (already correct for Gomoku)
|
||||
|
||||
#### 5. Grep Sweep -- Verify No Leftovers
|
||||
|
||||
Run these greps to ensure nothing was missed:
|
||||
|
||||
```bash
|
||||
# No poker/landlord references in kept Java files
|
||||
grep -r "Poker\|PokerSell\|PokerHelper\|PokerLevel\|PokerType" --include="*.java" .
|
||||
grep -r "landlord\|LANDLORD\|Landlord" --include="*.java" .
|
||||
grep -r "LastCardsUtils\|lastCards\|lastPokers" --include="*.java" .
|
||||
|
||||
# No Chinese characters in Java files
|
||||
grep -rP "[\x{4e00}-\x{9fff}]" --include="*.java" .
|
||||
|
||||
# No references to deleted event codes
|
||||
grep -r "CODE_GAME_POKER\|CODE_GAME_LANDLORD\|CODE_SHOW_POKERS" --include="*.java" .
|
||||
```
|
||||
|
||||
Allowed exceptions:
|
||||
- `LandlordException.java` class name -- rename to `GameException.java` or leave (low priority)
|
||||
- Package names contain `landlords` -- intentionally kept (see plan.md decision #1)
|
||||
- `LICENSE` file -- keep as-is
|
||||
|
||||
#### 6. Verify Game Flow Manually (if time permits)
|
||||
|
||||
Start server:
|
||||
```bash
|
||||
java -jar landlords-server/target/landlords-server-1.4.0.jar -p 1024
|
||||
```
|
||||
|
||||
Start 2 clients:
|
||||
```bash
|
||||
java -jar landlords-client/target/landlords-client-1.4.0.jar -h 127.0.0.1 -p 1024
|
||||
```
|
||||
|
||||
Test flow:
|
||||
1. Client 1: set nickname, create room
|
||||
2. Client 2: set nickname, join room
|
||||
3. Verify game auto-starts, board displays
|
||||
4. Make alternating moves, verify board updates
|
||||
5. Play to win condition, verify game over message
|
||||
|
||||
### Cleanup Items (Low Priority, Optional)
|
||||
|
||||
| Item | Rationale |
|
||||
|------|-----------|
|
||||
| Rename `LandlordException` to `GameException` | Cosmetic, low value |
|
||||
| Rename modules `landlords-*` to `caro-*` | High churn, defer to separate PR |
|
||||
| Remove `FormatPrinter.java` | Check if used; if not, delete |
|
||||
| Remove `features/Features.java` | Check if only VERSION constant; if so, keep |
|
||||
| Simplify `SimpleClient.java` server list fetching | Already addressed in Phase 4 |
|
||||
|
||||
### Risk Assessment
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|------|-----------|--------|------------|
|
||||
| Hidden compile error in untouched file | Medium | Low | Full `mvn compile` catches all |
|
||||
| GomokuHelperTest relies on deleted code | Low | Low | Test file already reviewed -- uses only Gomoku classes |
|
||||
| Manual test reveals game flow bug | Medium | Medium | Fix in this phase before merging |
|
||||
|
||||
### Success Criteria
|
||||
- [ ] `mvn clean compile` passes with zero errors
|
||||
- [ ] `mvn test` passes -- all tests green
|
||||
- [ ] Grep sweep shows no poker/landlord references in Java code (except package names and LandlordException)
|
||||
- [ ] No Chinese characters in Java files
|
||||
- [ ] README reflects current Gomoku project
|
||||
- [ ] GomokuIntegrationTest covers: valid move, invalid move, win detection, draw detection
|
||||
@@ -1,46 +0,0 @@
|
||||
---
|
||||
title: "Caro/Gomoku Codebase Simplification"
|
||||
description: "Strip landlords card game code, fix Gomoku game flow end-to-end, simplify to working client-server Gomoku"
|
||||
status: pending
|
||||
priority: P1
|
||||
effort: 6h
|
||||
branch: master
|
||||
tags: [cleanup, gomoku, simplification]
|
||||
created: 2026-04-09
|
||||
---
|
||||
|
||||
# Caro/Gomoku Codebase Simplification
|
||||
|
||||
## Overview
|
||||
|
||||
Strip all leftover Chinese Landlords card game code from this Netty-based project, leaving a clean Gomoku (Five-in-a-Row) client-server application. The Gomoku domain classes (Board, GameMove, GomokuHelper, GomokuAI, PieceType, GameResult) already exist and are well-implemented. The main gap is the server/client event handlers still run landlords logic.
|
||||
|
||||
## Phase Summary
|
||||
|
||||
| # | Phase | Status | Effort | Blocked By |
|
||||
|---|-------|--------|--------|------------|
|
||||
| 1 | Delete dead files | Pending | 30m | - |
|
||||
| 2 | Clean shared code (common module) | Pending | 1h | Phase 1 |
|
||||
| 3 | Rewrite server event handlers | Pending | 2h | Phase 2 |
|
||||
| 4 | Rewrite client event handlers | Pending | 1.5h | Phase 2 |
|
||||
| 5 | Integration test & compile verify | Pending | 1h | Phase 3, 4 |
|
||||
|
||||
## Phases
|
||||
|
||||
- [Phase 1: Delete Dead Files](./phase-01-delete-dead-files.md)
|
||||
- [Phase 2: Clean Shared Code](./phase-02-clean-shared-code.md)
|
||||
- [Phase 3: Rewrite Server Event Handlers](./phase-03-rewrite-server-events.md)
|
||||
- [Phase 4: Rewrite Client Event Handlers](./phase-04-rewrite-client-events.md)
|
||||
- [Phase 5: Integration Test & Compile Verify](./phase-05-integration-verify.md)
|
||||
|
||||
## Key Architectural Decisions
|
||||
|
||||
1. **Keep module names as `landlords-*`** -- renaming Maven modules cascades into groupId, package names, imports across every file. High churn, zero functional value. Defer to a separate PR if desired.
|
||||
2. **Keep WebSocket support** -- already wired, removing adds risk, keeping costs nothing.
|
||||
3. **2-player rooms** -- Gomoku is 2-player. Change room full check from `size == 3` to `size == 2`. Auto-start when second player joins.
|
||||
4. **Remove scoring system** -- Gomoku has no points/scoring. Strip `score`, `scoreRate`, `baseScore`, `scoreInc` from Room and ClientSide.
|
||||
5. **PVE uses GomokuAI** -- Replace old robot system with single GomokuAI class. Delete AbstractRobotDecisionMakers, Easy/MediumRobotDecisionMakers, RobotDecisionMakers.
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
Each phase is a separate commit. `git revert` any phase independently. Phase 1 (file deletion) is fully recoverable from git history.
|
||||
@@ -1,128 +0,0 @@
|
||||
# Phase 1: Server — Static File Handler
|
||||
|
||||
## Context Links
|
||||
- [WebsocketProxy.java](../../landlords-server/src/main/java/org/nico/ratel/landlords/server/proxy/WebsocketProxy.java) — pipeline setup
|
||||
- [WebsocketTransferHandler.java](../../landlords-server/src/main/java/org/nico/ratel/landlords/server/handler/WebsocketTransferHandler.java) — WS handler
|
||||
|
||||
## Overview
|
||||
- **Priority:** P1 (blocks all client work for integration testing)
|
||||
- **Status:** Pending
|
||||
- **Effort:** 1.5h
|
||||
|
||||
Add an HTTP static file handler to the existing Netty WebSocket pipeline so `http://localhost:1025/` serves the web client. The WebSocket upgrade at `/ratel` must continue working untouched.
|
||||
|
||||
## Key Insight
|
||||
|
||||
The current pipeline is: `IdleStateHandler -> HttpServerCodec -> ChunkedWriteHandler -> HttpObjectAggregator -> WebSocketServerProtocolHandler("/ratel") -> WebsocketTransferHandler`.
|
||||
|
||||
`WebSocketServerProtocolHandler` only upgrades requests to `/ratel`. For any other URI, it passes through as a regular `FullHttpRequest`. We insert our `StaticFileHandler` **before** the WS protocol handler to intercept non-`/ratel` HTTP requests and serve files. Requests to `/ratel` get passed through to the WS handler as before.
|
||||
|
||||
**Alternative considered:** Adding handler after WS handler. Rejected because `WebSocketServerProtocolHandler` may consume or reject non-upgrade HTTP requests.
|
||||
|
||||
**Chosen approach:** Add handler before WS handler. Check URI: if `/ratel`, pass through via `ctx.fireChannelRead(msg)`. Otherwise, serve static file.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Pipeline order (updated):
|
||||
IdleStateHandler
|
||||
HttpServerCodec
|
||||
ChunkedWriteHandler
|
||||
HttpObjectAggregator(8192)
|
||||
StaticFileHandler <-- NEW: serves files or passes /ratel through
|
||||
WebSocketServerProtocolHandler("/ratel")
|
||||
WebsocketTransferHandler
|
||||
```
|
||||
|
||||
**Data flow:**
|
||||
1. HTTP GET `/` arrives as `FullHttpRequest`
|
||||
2. `StaticFileHandler.channelRead0()` checks URI
|
||||
3. If URI is `/ratel` -> `ctx.fireChannelRead(msg.retain())` (pass to WS handler)
|
||||
4. If URI is `/` -> rewrite to `/index.html`
|
||||
5. Load resource from classpath `static/` prefix
|
||||
6. Set Content-Type from MIME map
|
||||
7. Write `DefaultFullHttpResponse` with file bytes
|
||||
8. Close connection (HTTP/1.1 keep-alive optional, not required)
|
||||
|
||||
## Related Code Files
|
||||
|
||||
### Files to Create
|
||||
- `landlords-server/src/main/java/org/nico/ratel/landlords/server/handler/StaticFileHandler.java`
|
||||
|
||||
### Files to Modify
|
||||
- `landlords-server/src/main/java/org/nico/ratel/landlords/server/proxy/WebsocketProxy.java` — add handler to pipeline
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: Create `StaticFileHandler.java`
|
||||
|
||||
Location: `landlords-server/src/main/java/org/nico/ratel/landlords/server/handler/StaticFileHandler.java`
|
||||
|
||||
```java
|
||||
// Extends SimpleChannelInboundHandler<FullHttpRequest>
|
||||
// MIME types map: .html->text/html, .css->text/css, .js->application/javascript,
|
||||
// .json->application/json, .mp3->audio/mpeg, .jpg->image/jpeg, .png->image/png, .svg->image/svg+xml
|
||||
```
|
||||
|
||||
Logic:
|
||||
1. Check `msg.uri()`. If starts with `/ratel`, call `ctx.fireChannelRead(msg.retain())` and return.
|
||||
2. Sanitize URI: strip query string, decode `%20` etc, reject `..` path traversal.
|
||||
3. Map `/` to `/index.html`.
|
||||
4. Build classpath path: `"static" + sanitizedUri`.
|
||||
5. Load via `getClass().getClassLoader().getResourceAsStream(path)`.
|
||||
6. If null -> 404 response.
|
||||
7. Read all bytes into `ByteBuf`.
|
||||
8. Build `DefaultFullHttpResponse(OK)`, set `Content-Type` and `Content-Length` headers.
|
||||
9. Write and flush, close if not keep-alive.
|
||||
|
||||
**Security:** Reject URIs containing `..` to prevent directory traversal. Only serve from `static/` classpath prefix.
|
||||
|
||||
**Keep file under 200 lines.** The handler is straightforward — MIME map + resource loading + response building.
|
||||
|
||||
### Step 2: Modify `WebsocketProxy.java`
|
||||
|
||||
Add one line to the pipeline, before the WS handler:
|
||||
|
||||
```java
|
||||
.addLast(new StaticFileHandler()) // <-- NEW
|
||||
.addLast("ws", new WebSocketServerProtocolHandler("/ratel"))
|
||||
```
|
||||
|
||||
Import: `org.nico.ratel.landlords.server.handler.StaticFileHandler`
|
||||
|
||||
## Todo List
|
||||
|
||||
- [ ] Create `StaticFileHandler.java` with MIME map and classpath resource serving
|
||||
- [ ] Handle `/ratel` passthrough (retain + fireChannelRead)
|
||||
- [ ] Handle `/` -> `/index.html` redirect
|
||||
- [ ] Handle 404 for missing resources
|
||||
- [ ] Sanitize URI (reject `..`, strip query string)
|
||||
- [ ] Add handler to `WebsocketProxy.java` pipeline
|
||||
- [ ] Create placeholder `static/index.html` for testing
|
||||
- [ ] Run `mvn clean compile` — must pass
|
||||
- [ ] Run `mvn test` — must pass
|
||||
- [ ] Manual test: start server, `curl http://localhost:1025/` returns HTML
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- `http://localhost:1025/` returns `index.html` content with `Content-Type: text/html`
|
||||
- `http://localhost:1025/css/style.css` returns CSS with correct MIME type
|
||||
- `http://localhost:1025/js/game-board.js` returns JS with correct MIME type
|
||||
- WebSocket at `ws://localhost:1025/ratel` still works (existing Java client connects)
|
||||
- `mvn clean compile` and `mvn test` pass
|
||||
- No path traversal vulnerability
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| Handler breaks WS upgrade | Check URI first; `/ratel` requests passed through untouched |
|
||||
| `FullHttpRequest` refcount leak | Call `msg.retain()` before fireChannelRead, use `ReferenceCountUtil` |
|
||||
| Large files OOM | Game assets are tiny (<1MB total); read fully into memory is fine |
|
||||
| Classpath resource not found in packaged JAR | Spring Boot Maven plugin repackages resources correctly; `getResourceAsStream` works in fat JARs |
|
||||
|
||||
## Backwards Compatibility
|
||||
|
||||
- Existing TCP Protobuf clients (port 1024) are unaffected — different port/pipeline
|
||||
- Existing WebSocket clients connecting to `/ratel` are unaffected — passthrough logic
|
||||
- No changes to game logic, event codes, or message format
|
||||
@@ -1,170 +0,0 @@
|
||||
# Phase 2: Client — HTML Shell + CSS
|
||||
|
||||
## Context Links
|
||||
- [Phase 1](phase-01-server-static-file-handler.md) — server serves these files
|
||||
- [Phase 3](phase-03-client-connection-state.md) — JS loaded by this HTML
|
||||
|
||||
## Overview
|
||||
- **Priority:** P1
|
||||
- **Status:** Pending
|
||||
- **Effort:** 2h
|
||||
|
||||
Create the single-page HTML shell and all CSS. The HTML defines all screens (hidden by default, shown via JS class toggling). CSS provides the professional game aesthetic: dark theme, wooden board area, clean typography, transitions.
|
||||
|
||||
## Key Insights
|
||||
|
||||
- All screens live in one HTML file, toggled via `.screen.active` CSS class
|
||||
- Screens: nickname, lobby (main menu), pvp-menu, pve-menu, room-list, waiting-room, game, game-over
|
||||
- Board rendered on `<canvas>` (Phase 4), everything else is DOM
|
||||
- CSS handles transitions between screens (fade or slide)
|
||||
- Responsive: flexbox layout, max-width container, canvas scales
|
||||
|
||||
## Architecture
|
||||
|
||||
### Screen Flow (DOM sections)
|
||||
|
||||
```
|
||||
#screen-nickname --> #screen-lobby
|
||||
#screen-lobby --> #screen-pvp-menu | #screen-pve-menu
|
||||
#screen-pvp-menu --> #screen-room-list | (create room -> #screen-waiting-room)
|
||||
#screen-pve-menu --> #screen-game (auto-starts)
|
||||
#screen-room-list --> #screen-game (join) | #screen-game (watch)
|
||||
#screen-waiting-room --> #screen-game (when opponent joins)
|
||||
#screen-game --> #screen-game-over
|
||||
#screen-game-over --> #screen-game (rematch) | #screen-lobby (exit)
|
||||
```
|
||||
|
||||
### Layout Structure
|
||||
|
||||
```
|
||||
.app-container (centered, max-width: 1200px)
|
||||
header.game-header (logo, connection status, nickname)
|
||||
main#screens-container
|
||||
section.screen#screen-nickname
|
||||
section.screen#screen-lobby
|
||||
section.screen#screen-pvp-menu
|
||||
section.screen#screen-pve-menu
|
||||
section.screen#screen-room-list
|
||||
section.screen#screen-waiting-room
|
||||
section.screen#screen-game
|
||||
.game-layout (flexbox row)
|
||||
.game-sidebar-left (player info, turn indicator)
|
||||
.game-board-container (canvas)
|
||||
.game-sidebar-right (move history, chat)
|
||||
section.screen#screen-game-over
|
||||
footer (version, credits)
|
||||
#toast-container (floating notifications)
|
||||
```
|
||||
|
||||
## Related Code Files
|
||||
|
||||
### Files to Create
|
||||
- `landlords-server/src/main/resources/static/index.html`
|
||||
- `landlords-server/src/main/resources/static/css/style.css`
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: Create `index.html`
|
||||
|
||||
Key elements per screen:
|
||||
|
||||
**#screen-nickname:**
|
||||
- Title "Gomoku"
|
||||
- Input field for nickname
|
||||
- "Play" button
|
||||
- Subtitle text
|
||||
|
||||
**#screen-lobby:**
|
||||
- Welcome message with nickname
|
||||
- Two large buttons: "Player vs Player", "Player vs AI"
|
||||
- Subtitle describing each mode
|
||||
|
||||
**#screen-pvp-menu:**
|
||||
- "Create Room" button
|
||||
- "Join Room" (shows room list) button
|
||||
- "Back" button
|
||||
|
||||
**#screen-pve-menu:**
|
||||
- Three difficulty buttons: Easy / Medium / Hard
|
||||
- Brief description per difficulty
|
||||
- "Back" button
|
||||
|
||||
**#screen-room-list:**
|
||||
- Table: Room ID, Owner, Players, Type, Actions (Join / Watch)
|
||||
- "Refresh" button
|
||||
- "Back" button
|
||||
- Empty state message
|
||||
|
||||
**#screen-waiting-room:**
|
||||
- Room info display
|
||||
- "Waiting for opponent..." message with spinner
|
||||
- "Leave" button
|
||||
|
||||
**#screen-game:**
|
||||
- Left sidebar: player cards (black/white), turn indicator arrow, timer placeholder
|
||||
- Center: `<canvas id="game-canvas">` (responsive)
|
||||
- Right sidebar: move history list (scrollable), coordinates display
|
||||
- Bottom bar: "Exit" button, sound toggle
|
||||
|
||||
**#screen-game-over:**
|
||||
- Result (Win/Lose/Draw) with large text
|
||||
- Winner name
|
||||
- Final board snapshot (reuse canvas)
|
||||
- "Rematch" and "Exit to Lobby" buttons
|
||||
|
||||
**Script tags** at bottom: load JS files in order (game-state.js first, then game-connection.js, game-board.js, game-ui.js, game-audio.js) or use `type="module"`.
|
||||
|
||||
**Decision: Use classic `<script>` tags, not ES modules.** Reason: simpler, no CORS issues with `file://` during dev, Java 8 server doesn't need to set module MIME. Global namespace with namespaced objects (`GameState`, `GameConnection`, etc.).
|
||||
|
||||
### Step 2: Create `style.css`
|
||||
|
||||
**Color palette (dark theme):**
|
||||
- Background: `#1a1a2e` (dark navy)
|
||||
- Surface: `#16213e` (card backgrounds)
|
||||
- Primary: `#e94560` (buttons, accents)
|
||||
- Text: `#eee`
|
||||
- Board: `#dcb35c` (golden wood)
|
||||
- Grid lines: `#8b6914`
|
||||
- Black stone: `#111`
|
||||
- White stone: `#f5f5f5`
|
||||
|
||||
**Key CSS patterns:**
|
||||
- `.screen { display: none; }` / `.screen.active { display: flex; }`
|
||||
- Transition: opacity + transform for screen switches
|
||||
- `.game-layout { display: flex; gap: 20px; }` with sidebars 200px, center flexible
|
||||
- Canvas container: `aspect-ratio: 1` or padding trick for square
|
||||
- Buttons: rounded, hover effects, active press effect
|
||||
- Toast notifications: fixed bottom-right, slide-in animation
|
||||
- Responsive: `@media (max-width: 900px)` stack game layout vertically, hide right sidebar
|
||||
- Stone placement animation: `@keyframes stone-drop` (scale 0->1 with slight bounce)
|
||||
|
||||
**Typography:** System font stack, no external fonts (no network dependency).
|
||||
|
||||
## Todo List
|
||||
|
||||
- [ ] Create `index.html` with all 8 screen sections
|
||||
- [ ] Add semantic IDs for all interactive elements
|
||||
- [ ] Add `<canvas id="game-canvas">` in game screen
|
||||
- [ ] Create `style.css` with dark theme
|
||||
- [ ] Style all screens (nickname, lobby, menus, room list, game, game over)
|
||||
- [ ] Add responsive breakpoints
|
||||
- [ ] Add transition animations for screen switches
|
||||
- [ ] Add toast notification styles
|
||||
- [ ] Add loading/spinner styles for waiting room
|
||||
- [ ] Verify HTML validates (no unclosed tags)
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Opening `index.html` directly shows nickname screen (other screens hidden)
|
||||
- All screens are visually complete when `.active` class is toggled manually in devtools
|
||||
- Game screen layout is correct: sidebar - canvas - sidebar
|
||||
- Responsive: stacks vertically below 900px
|
||||
- No external dependencies (fonts, CDNs)
|
||||
- File sizes: HTML < 200 lines (content only, no inline styles), CSS can be up to 400 lines (styling exempt from 200-line code rule)
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| CSS conflicts between screens | Each screen is a `.screen` section with unique ID; styles scoped via `#screen-name .element` |
|
||||
| Canvas sizing issues | Use `ResizeObserver` in JS (Phase 4) to match CSS container size |
|
||||
@@ -1,243 +0,0 @@
|
||||
# Phase 3: Client — WebSocket Connection + State Machine
|
||||
|
||||
## Context Links
|
||||
- [Phase 2](phase-02-client-html-css.md) — HTML shell this JS attaches to
|
||||
- [ChannelUtils.java](../../landlords-common/src/main/java/org/nico/ratel/landlords/channel/ChannelUtils.java) — server message format
|
||||
- [ServerEventCode.java](../../landlords-common/src/main/java/org/nico/ratel/landlords/enums/ServerEventCode.java) — codes client sends
|
||||
- [ClientEventCode.java](../../landlords-common/src/main/java/org/nico/ratel/landlords/enums/ClientEventCode.java) — codes server sends
|
||||
|
||||
## Overview
|
||||
- **Priority:** P1 (all UI/board logic depends on this)
|
||||
- **Status:** Pending
|
||||
- **Effort:** 2h
|
||||
- **Depends on:** Phase 2
|
||||
|
||||
Two JS files: `game-connection.js` (WebSocket transport) and `game-state.js` (state machine + event dispatch). These form the core communication and state layer.
|
||||
|
||||
## Key Insights
|
||||
|
||||
- Server message format: `{"code": "CODE_...", "data": "json_string_or_plain", "info": ""}`
|
||||
- Client sends same format via `ServerEventCode` enum names
|
||||
- Server sends `ClientEventCode` enum names as `code` field
|
||||
- On WS connect, server waits 2s then sends `CODE_CLIENT_CONNECT` + `CODE_CLIENT_NICKNAME_SET`
|
||||
- Heartbeat: client should send `CODE_CLIENT_HEAD_BEAT` every ~60s (server idle timeout is 30min, but heartbeat keeps connection alive)
|
||||
- PVE auto-starts game immediately after room creation (no waiting room)
|
||||
- PVP with 2 players auto-starts game on join (no ready step needed for initial game)
|
||||
- Ready/rematch uses `CODE_GAME_READY` — toggles ready state, game starts when both ready
|
||||
|
||||
## Architecture
|
||||
|
||||
### `game-state.js` — State Machine
|
||||
|
||||
```
|
||||
States:
|
||||
CONNECTING -> NICKNAME (on CODE_CLIENT_NICKNAME_SET)
|
||||
NICKNAME -> LOBBY (on CODE_SHOW_OPTIONS)
|
||||
LOBBY -> PVP_MENU (on CODE_SHOW_OPTIONS_PVP)
|
||||
LOBBY -> PVE_MENU (on CODE_SHOW_OPTIONS_PVE)
|
||||
PVP_MENU -> ROOM_LIST (on CODE_SHOW_ROOMS)
|
||||
PVP_MENU -> WAITING_ROOM (on CODE_ROOM_CREATE_SUCCESS)
|
||||
ROOM_LIST -> GAME (on CODE_GAME_STARTING via join)
|
||||
ROOM_LIST -> SPECTATING (on CODE_GAME_WATCH_SUCCESSFUL)
|
||||
WAITING_ROOM -> GAME (on CODE_GAME_STARTING)
|
||||
PVE_MENU -> GAME (on CODE_GAME_STARTING)
|
||||
GAME -> GAME_OVER (on CODE_GAME_OVER)
|
||||
GAME_OVER -> GAME (on CODE_GAME_STARTING via rematch)
|
||||
GAME_OVER -> LOBBY (on exit)
|
||||
SPECTATING -> LOBBY (on exit watch)
|
||||
* -> LOBBY (on CODE_CLIENT_EXIT from server)
|
||||
* -> LOBBY (on CODE_CLIENT_KICK)
|
||||
```
|
||||
|
||||
### `game-connection.js` — Transport
|
||||
|
||||
```
|
||||
GameConnection {
|
||||
ws: WebSocket
|
||||
clientId: number
|
||||
heartbeatInterval: timer
|
||||
|
||||
connect(url)
|
||||
send(code, data)
|
||||
onMessage(handler) // parses JSON, calls handler(code, data)
|
||||
disconnect()
|
||||
}
|
||||
```
|
||||
|
||||
### Event Dispatch Pattern
|
||||
|
||||
`game-state.js` exposes a global `GameState` object with:
|
||||
- `state` — current screen/state enum
|
||||
- `clientId`, `nickname`, `roomId`, `isBlack`, `isSpectator`
|
||||
- `gameData` — current game info (players, board size, move history)
|
||||
- `on(eventCode, callback)` — register handler
|
||||
- `emit(eventCode, data)` — internal dispatch
|
||||
- `switchScreen(screenId)` — hides all `.screen`, shows target
|
||||
|
||||
All other modules register handlers: `GameState.on('CODE_GAME_MOVE_SUCCESS', data => { ... })`.
|
||||
|
||||
### Data stored in GameState
|
||||
|
||||
```js
|
||||
{
|
||||
state: 'LOBBY',
|
||||
clientId: null,
|
||||
nickname: '',
|
||||
roomId: null,
|
||||
isBlack: false,
|
||||
isSpectator: false,
|
||||
gameData: {
|
||||
blackPlayerId: null,
|
||||
blackPlayerNickname: '',
|
||||
whitePlayerId: null,
|
||||
whitePlayerNickname: '',
|
||||
boardSize: 15,
|
||||
moves: [], // [{row, col, piece, playerNickname}]
|
||||
currentTurn: 'BLACK'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Related Code Files
|
||||
|
||||
### Files to Create
|
||||
- `landlords-server/src/main/resources/static/js/game-state.js` (~150 lines)
|
||||
- `landlords-server/src/main/resources/static/js/game-connection.js` (~100 lines)
|
||||
|
||||
### Files Referenced (read-only)
|
||||
- `landlords-common/.../enums/ServerEventCode.java` — code strings client sends
|
||||
- `landlords-common/.../enums/ClientEventCode.java` — code strings client receives
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: Create `game-state.js`
|
||||
|
||||
Global object `GameState`:
|
||||
|
||||
1. Define state enum constants (strings matching screen IDs)
|
||||
2. Event bus: `_handlers` map of `code -> [callbacks]`
|
||||
3. `on(code, fn)` — push to handlers
|
||||
4. `emit(code, data)` — call all registered handlers
|
||||
5. `switchScreen(id)` — `document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'))`, then add `.active` to target
|
||||
6. `init()` — set initial state, called on DOMContentLoaded
|
||||
7. State properties: `clientId`, `nickname`, `roomId`, `isBlack`, `isSpectator`, `gameData`
|
||||
8. `resetGameData()` — clear moves, players, turn
|
||||
|
||||
### Step 2: Create `game-connection.js`
|
||||
|
||||
Global object `GameConnection`:
|
||||
|
||||
1. `connect()` — construct WS URL from `window.location` (`ws://${location.host}/ratel`), create `WebSocket`
|
||||
2. `ws.onopen` — log connected, start heartbeat interval (50s)
|
||||
3. `ws.onmessage` — parse JSON, extract `code` and `data` (parse `data` as JSON if possible, fall back to string), call `GameState.emit(code, parsedData)`
|
||||
4. `ws.onclose` — log, clear heartbeat, show reconnect toast
|
||||
5. `ws.onerror` — log error
|
||||
6. `send(code, data)` — `ws.send(JSON.stringify({code, data: typeof data === 'string' ? data : JSON.stringify(data), info: ''}))`
|
||||
7. `startHeartbeat()` — `setInterval(() => send('CODE_CLIENT_HEAD_BEAT', ''), 50000)`
|
||||
8. `disconnect()` — close WS, clear interval
|
||||
|
||||
### Step 3: Wire initial server events in `game-state.js`
|
||||
|
||||
Register core handlers:
|
||||
|
||||
```js
|
||||
GameState.on('CODE_CLIENT_CONNECT', (data) => {
|
||||
GameState.clientId = parseInt(data);
|
||||
});
|
||||
|
||||
GameState.on('CODE_CLIENT_NICKNAME_SET', () => {
|
||||
GameState.switchScreen('screen-nickname');
|
||||
});
|
||||
|
||||
GameState.on('CODE_SHOW_OPTIONS', () => {
|
||||
GameState.switchScreen('screen-lobby');
|
||||
});
|
||||
|
||||
GameState.on('CODE_SHOW_OPTIONS_PVP', () => {
|
||||
GameState.switchScreen('screen-pvp-menu');
|
||||
});
|
||||
|
||||
GameState.on('CODE_SHOW_OPTIONS_PVE', () => {
|
||||
GameState.switchScreen('screen-pve-menu');
|
||||
});
|
||||
|
||||
GameState.on('CODE_GAME_STARTING', (data) => {
|
||||
GameState.roomId = data.roomId;
|
||||
GameState.gameData.blackPlayerId = data.blackPlayerId;
|
||||
GameState.gameData.whitePlayerId = data.whitePlayerId;
|
||||
// ... store all fields
|
||||
GameState.isBlack = (GameState.clientId === data.blackPlayerId);
|
||||
GameState.gameData.currentTurn = 'BLACK';
|
||||
GameState.gameData.moves = [];
|
||||
GameState.switchScreen('screen-game');
|
||||
});
|
||||
|
||||
GameState.on('CODE_GAME_MOVE_SUCCESS', (data) => {
|
||||
GameState.gameData.moves.push(data);
|
||||
GameState.gameData.currentTurn =
|
||||
data.piece === 'BLACK' ? 'WHITE' : 'BLACK';
|
||||
});
|
||||
|
||||
GameState.on('CODE_GAME_OVER', (data) => {
|
||||
GameState.gameData.result = data.result;
|
||||
GameState.gameData.winnerNickname = data.winnerNickname;
|
||||
GameState.switchScreen('screen-game-over');
|
||||
});
|
||||
|
||||
GameState.on('CODE_CLIENT_EXIT', () => {
|
||||
GameState.switchScreen('screen-lobby');
|
||||
});
|
||||
|
||||
GameState.on('CODE_CLIENT_KICK', () => {
|
||||
GameState.switchScreen('screen-lobby');
|
||||
});
|
||||
```
|
||||
|
||||
### Step 4: DOMContentLoaded initialization
|
||||
|
||||
```js
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
GameState.init();
|
||||
GameConnection.connect();
|
||||
});
|
||||
```
|
||||
|
||||
## Todo List
|
||||
|
||||
- [ ] Create `game-state.js` with event bus and screen switching
|
||||
- [ ] Create `game-connection.js` with WS connect/send/heartbeat
|
||||
- [ ] Register all server event handlers in game-state
|
||||
- [ ] Handle `CODE_CLIENT_CONNECT` — store client ID
|
||||
- [ ] Handle `CODE_CLIENT_NICKNAME_SET` — show nickname screen
|
||||
- [ ] Handle `CODE_SHOW_OPTIONS` / `_PVP` / `_PVE` — screen transitions
|
||||
- [ ] Handle `CODE_GAME_STARTING` — populate game data, switch to game
|
||||
- [ ] Handle `CODE_GAME_MOVE_SUCCESS` — update moves + turn
|
||||
- [ ] Handle `CODE_GAME_OVER` — store result, switch to game-over
|
||||
- [ ] Handle `CODE_CLIENT_EXIT` / `CODE_CLIENT_KICK` — return to lobby
|
||||
- [ ] Handle `CODE_ROOM_CREATE_SUCCESS` — switch to waiting room
|
||||
- [ ] Handle `CODE_ROOM_JOIN_SUCCESS` — update waiting room info
|
||||
- [ ] Handle `CODE_SHOW_ROOMS` — store room list
|
||||
- [ ] Handle `CODE_GAME_WATCH_SUCCESSFUL` — set spectator flag, switch to game
|
||||
- [ ] Handle error codes (room full, not found, not your turn, etc.) — show toast
|
||||
- [ ] Test: open in browser, verify WS connects and nickname screen appears
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- WS connects to server automatically on page load
|
||||
- Client ID stored from `CODE_CLIENT_CONNECT`
|
||||
- Nickname screen shown after `CODE_CLIENT_NICKNAME_SET`
|
||||
- All screen transitions work based on server events
|
||||
- Game data populated correctly from `CODE_GAME_STARTING`
|
||||
- Moves tracked in order from `CODE_GAME_MOVE_SUCCESS`
|
||||
- Heartbeat sent every 50s (visible in WS devtools)
|
||||
- Error events show toast notifications
|
||||
- Each JS file under 200 lines
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| WS URL wrong (different host/port) | Derive from `window.location` — same host:port |
|
||||
| Data field is string vs object inconsistency | Try `JSON.parse(data)`, catch and use raw string |
|
||||
| Race condition: events before handlers registered | Load `game-state.js` first; server has 2s delay before first message |
|
||||
| Reconnection after disconnect | Show toast with "Reconnect" button; user clicks to reload page (KISS) |
|
||||
@@ -1,200 +0,0 @@
|
||||
# Phase 4: Client — Canvas Board Rendering
|
||||
|
||||
## Context Links
|
||||
- [Phase 3](phase-03-client-connection-state.md) — state machine this renders from
|
||||
- [ServerEventListener_CODE_GAME_MOVE.java](../../landlords-server/src/main/java/org/nico/ratel/landlords/server/event/ServerEventListener_CODE_GAME_MOVE.java) — move data format
|
||||
|
||||
## Overview
|
||||
- **Priority:** P1
|
||||
- **Status:** Pending
|
||||
- **Effort:** 2h
|
||||
- **Depends on:** Phase 3
|
||||
|
||||
Canvas-based 15x15 Gomoku board with wood texture background, grid lines, coordinate labels, stone rendering with placement animation, and click-to-move input.
|
||||
|
||||
## Key Insights
|
||||
|
||||
- Board is 15x15 with intersections (not cells) — stones placed on line crossings
|
||||
- Canvas needs to be responsive (resize with container)
|
||||
- Only redraw what changed: full board on init, single stone on move
|
||||
- Click detection: map pixel coords to nearest grid intersection
|
||||
- Animation: stone scales from 0 to 1 over ~150ms on placement
|
||||
- Last move indicator: small dot or highlight on most recent stone
|
||||
- Coordinate labels: A-O columns, 1-15 rows along edges
|
||||
|
||||
## Architecture
|
||||
|
||||
### Canvas Layout (conceptual)
|
||||
|
||||
```
|
||||
padding (40px) for labels
|
||||
|
|
||||
v
|
||||
A B C D E ... O
|
||||
1 +--+--+--+--+--...+
|
||||
2 +--+--+--+--+--...+
|
||||
. . . . . . .
|
||||
15 +--+--+--+--+--...+
|
||||
```
|
||||
|
||||
### Coordinate System
|
||||
|
||||
```
|
||||
PADDING = 40 (space for labels)
|
||||
cellSize = (canvasSize - 2 * PADDING) / 14 (14 gaps for 15 lines)
|
||||
gridX(col) = PADDING + col * cellSize
|
||||
gridY(row) = PADDING + row * cellSize
|
||||
```
|
||||
|
||||
### Rendering Layers (draw order)
|
||||
|
||||
1. **Background** — fill with wood color (`#dcb35c`) or CSS gradient
|
||||
2. **Grid lines** — 15 horizontal + 15 vertical lines
|
||||
3. **Star points** — 5 dots at standard positions: (3,3), (3,11), (7,7), (11,3), (11,11)
|
||||
4. **Coordinate labels** — letters top/bottom, numbers left/right
|
||||
5. **Stones** — iterate `GameState.gameData.moves`, draw circles with gradients
|
||||
6. **Last move marker** — small red dot on center of last placed stone
|
||||
7. **Hover indicator** — semi-transparent stone at nearest intersection (if player's turn)
|
||||
|
||||
### Stone Rendering
|
||||
|
||||
- Black: radial gradient from `#444` (top-left highlight) to `#111`
|
||||
- White: radial gradient from `#fff` to `#ddd` with thin `#999` border
|
||||
- Radius: `cellSize * 0.43` (slight gap between adjacent stones)
|
||||
- Shadow: `ctx.shadowBlur = 4; ctx.shadowColor = 'rgba(0,0,0,0.5)'`
|
||||
|
||||
## Related Code Files
|
||||
|
||||
### Files to Create
|
||||
- `landlords-server/src/main/resources/static/js/game-board.js` (~190 lines)
|
||||
|
||||
### Files Referenced
|
||||
- `game-state.js` — reads `GameState.gameData.moves`, `GameState.isBlack`, `GameState.gameData.currentTurn`
|
||||
- `game-connection.js` — calls `GameConnection.send('CODE_GAME_MOVE', {row, col})`
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: Canvas setup and sizing
|
||||
|
||||
```js
|
||||
const GameBoard = {
|
||||
canvas: null,
|
||||
ctx: null,
|
||||
cellSize: 0,
|
||||
PADDING: 40,
|
||||
BOARD_SIZE: 15,
|
||||
animatingStone: null, // {row, col, piece, progress, startTime}
|
||||
hoverPos: null, // {row, col} or null
|
||||
|
||||
init() {
|
||||
this.canvas = document.getElementById('game-canvas');
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.resize();
|
||||
window.addEventListener('resize', () => this.resize());
|
||||
this.canvas.addEventListener('click', (e) => this.handleClick(e));
|
||||
this.canvas.addEventListener('mousemove', (e) => this.handleHover(e));
|
||||
this.canvas.addEventListener('mouseleave', () => { this.hoverPos = null; this.draw(); });
|
||||
},
|
||||
|
||||
resize() {
|
||||
const container = this.canvas.parentElement;
|
||||
const size = Math.min(container.clientWidth, container.clientHeight, 700);
|
||||
this.canvas.width = size;
|
||||
this.canvas.height = size;
|
||||
this.cellSize = (size - 2 * this.PADDING) / (this.BOARD_SIZE - 1);
|
||||
this.draw();
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Step 2: Drawing functions
|
||||
|
||||
- `draw()` — clear canvas, call drawBoard, drawStones, drawHover
|
||||
- `drawBoard()` — fill background, draw grid lines, star points, labels
|
||||
- `drawStone(row, col, piece, alpha)` — draw single stone with gradient, optional alpha for animation/hover
|
||||
- `drawStones()` — iterate moves array, draw each; for last move add red dot marker
|
||||
- `drawHover()` — if hoverPos set and it's player's turn and position empty, draw semi-transparent stone
|
||||
|
||||
### Step 3: Click handling
|
||||
|
||||
```js
|
||||
handleClick(e) {
|
||||
if (GameState.isSpectator) return;
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
const col = Math.round((x - this.PADDING) / this.cellSize);
|
||||
const row = Math.round((y - this.PADDING) / this.cellSize);
|
||||
if (row < 0 || row >= this.BOARD_SIZE || col < 0 || col >= this.BOARD_SIZE) return;
|
||||
|
||||
// Check it's our turn
|
||||
const myPiece = GameState.isBlack ? 'BLACK' : 'WHITE';
|
||||
if (GameState.gameData.currentTurn !== myPiece) return;
|
||||
|
||||
// Check position not occupied (client-side pre-check)
|
||||
const occupied = GameState.gameData.moves.some(m => m.row === row && m.col === col);
|
||||
if (occupied) return;
|
||||
|
||||
GameConnection.send('CODE_GAME_MOVE', JSON.stringify({row, col}));
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Stone placement animation
|
||||
|
||||
On `CODE_GAME_MOVE_SUCCESS`:
|
||||
1. Set `animatingStone = {row, col, piece, startTime: Date.now()}`
|
||||
2. Run `requestAnimationFrame` loop for 150ms
|
||||
3. Draw stone with `scale = easeOutBack(progress)` where progress = elapsed/150
|
||||
4. After animation completes, set `animatingStone = null`, full redraw
|
||||
|
||||
### Step 5: Register event handlers
|
||||
|
||||
```js
|
||||
GameState.on('CODE_GAME_STARTING', () => {
|
||||
GameBoard.init(); // or re-init
|
||||
GameBoard.draw();
|
||||
});
|
||||
|
||||
GameState.on('CODE_GAME_MOVE_SUCCESS', (data) => {
|
||||
GameBoard.animateStone(data.row, data.col, data.piece);
|
||||
});
|
||||
```
|
||||
|
||||
## Todo List
|
||||
|
||||
- [ ] Create `game-board.js` with `GameBoard` global object
|
||||
- [ ] Implement canvas sizing with ResizeObserver or resize event
|
||||
- [ ] Draw wood-colored background
|
||||
- [ ] Draw 15x15 grid lines
|
||||
- [ ] Draw star points (5 standard positions)
|
||||
- [ ] Draw coordinate labels (A-O, 1-15)
|
||||
- [ ] Draw black stones with radial gradient + shadow
|
||||
- [ ] Draw white stones with radial gradient + border
|
||||
- [ ] Draw last-move indicator (red dot)
|
||||
- [ ] Implement click-to-grid-intersection mapping
|
||||
- [ ] Client-side turn + occupied validation before sending
|
||||
- [ ] Stone placement animation (scale easeOutBack, 150ms)
|
||||
- [ ] Hover indicator (semi-transparent stone preview)
|
||||
- [ ] Register for `CODE_GAME_STARTING` and `CODE_GAME_MOVE_SUCCESS` events
|
||||
- [ ] Test: visual check of board rendering in browser
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Board renders centered with grid, labels, star points
|
||||
- Black/white stones render with gradient and shadow
|
||||
- Clicking an intersection sends move to server
|
||||
- Clicking occupied position or out of turn does nothing
|
||||
- New stones animate in with scale effect
|
||||
- Last move has red dot indicator
|
||||
- Hover shows preview stone
|
||||
- Board resizes cleanly when window resizes
|
||||
- File under 200 lines
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| Click position off by one pixel | Use `Math.round()` to snap to nearest intersection |
|
||||
| Canvas blurry on HiDPI | Multiply canvas dimensions by `devicePixelRatio`, scale context |
|
||||
| Animation jank | Use `requestAnimationFrame`, keep draw logic simple |
|
||||
| Hover flicker | Only redraw on position change, debounce moves |
|
||||
@@ -1,242 +0,0 @@
|
||||
# Phase 5: Client — UI Panels + Lobby
|
||||
|
||||
## Context Links
|
||||
- [Phase 2](phase-02-client-html-css.md) — HTML elements this JS manipulates
|
||||
- [Phase 3](phase-03-client-connection-state.md) — state machine and event bus
|
||||
- [ServerEventListener_CODE_GET_ROOMS.java](../../landlords-server/src/main/java/org/nico/ratel/landlords/server/event/ServerEventListener_CODE_GET_ROOMS.java) — room list format
|
||||
|
||||
## Overview
|
||||
- **Priority:** P1
|
||||
- **Status:** Pending
|
||||
- **Effort:** 1.5h
|
||||
- **Depends on:** Phase 3
|
||||
|
||||
Handles all DOM manipulation: button clicks, form submissions, room list rendering, player info panels, move history, toast notifications, game-over screen. Bridges user actions to `GameConnection.send()`.
|
||||
|
||||
## Key Insights
|
||||
|
||||
- Room list data: `[{roomId, roomOwner, roomClientCount, roomType}]`
|
||||
- Join auto-starts game when 2 players present (server sends `CODE_GAME_STARTING`)
|
||||
- Spectator join sends `CODE_GAME_WATCH` with room ID
|
||||
- Move history: append each `CODE_GAME_MOVE_SUCCESS` as a row (e.g., "#1 Black D7")
|
||||
- Toast notifications for errors: room full, room not found, not your turn, etc.
|
||||
- Game-over screen shows result relative to player: "You Win!" / "You Lose!" / "Draw!"
|
||||
|
||||
## Architecture
|
||||
|
||||
### User Action -> Server Message Mapping
|
||||
|
||||
| User Action | Send Code | Data |
|
||||
|------------|-----------|------|
|
||||
| Submit nickname | `CODE_CLIENT_NICKNAME_SET` | nickname string |
|
||||
| Click "PVP" | `CODE_CLIENT_INFO_SET` | `{"version":"web"}` then server shows PVP menu |
|
||||
| Click "Create Room" | `CODE_ROOM_CREATE` | (none) |
|
||||
| Click "Create PVE Easy" | `CODE_ROOM_CREATE_PVE` | `"1"` |
|
||||
| Click "Create PVE Medium" | `CODE_ROOM_CREATE_PVE` | `"2"` |
|
||||
| Click "Create PVE Hard" | `CODE_ROOM_CREATE_PVE` | `"3"` |
|
||||
| Click "Room List" | `CODE_GET_ROOMS` | (none) |
|
||||
| Click "Join" on room row | `CODE_ROOM_JOIN` | room ID string |
|
||||
| Click "Watch" on room row | `CODE_GAME_WATCH` | room ID string |
|
||||
| Click "Rematch" | `CODE_GAME_READY` | (none) |
|
||||
| Click "Exit" (in game/room) | `CODE_CLIENT_EXIT` | (none) |
|
||||
| Click "Exit Watch" | `CODE_GAME_WATCH_EXIT` | (none) |
|
||||
|
||||
**Important discovery from server code:** After nickname set, server sends `CODE_SHOW_OPTIONS`. Client doesn't need to request it. The lobby menu buttons trigger client-side screen switches that correspond to what server would send. Actually, looking at the client event flow:
|
||||
|
||||
1. Client sends `CODE_CLIENT_NICKNAME_SET` with nickname
|
||||
2. Server stores nickname, sends back `CODE_SHOW_OPTIONS`
|
||||
3. From lobby, user picks PVP or PVE — these are client-side navigation decisions
|
||||
4. For PVP: client shows pvp-menu locally, then sends server codes when creating/joining rooms
|
||||
5. For PVE: client shows pve-menu locally, sends `CODE_ROOM_CREATE_PVE` with difficulty
|
||||
|
||||
So lobby/menu navigation is **client-driven**, not server-driven. The `CODE_SHOW_OPTIONS_PVP` / `CODE_SHOW_OPTIONS_PVE` events from server are for the CLI client's menu system. The web client can handle menu navigation purely in DOM.
|
||||
|
||||
### Toast System
|
||||
|
||||
```js
|
||||
GameUI.showToast(message, type) // type: 'error', 'info', 'success'
|
||||
// Creates div in #toast-container, auto-removes after 3s
|
||||
```
|
||||
|
||||
## Related Code Files
|
||||
|
||||
### Files to Create
|
||||
- `landlords-server/src/main/resources/static/js/game-ui.js` (~190 lines)
|
||||
|
||||
### Files Referenced
|
||||
- `game-state.js` — event registration, state reads
|
||||
- `game-connection.js` — `send()` calls
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: Button event listeners (DOMContentLoaded)
|
||||
|
||||
Wire all buttons in `GameUI.init()`:
|
||||
|
||||
```js
|
||||
// Nickname
|
||||
document.getElementById('nickname-submit').addEventListener('click', () => {
|
||||
const name = document.getElementById('nickname-input').value.trim();
|
||||
if (!name) return;
|
||||
GameState.nickname = name;
|
||||
GameConnection.send('CODE_CLIENT_NICKNAME_SET', name);
|
||||
});
|
||||
|
||||
// Lobby
|
||||
document.getElementById('btn-pvp').addEventListener('click', () => {
|
||||
GameState.switchScreen('screen-pvp-menu');
|
||||
});
|
||||
document.getElementById('btn-pve').addEventListener('click', () => {
|
||||
GameState.switchScreen('screen-pve-menu');
|
||||
});
|
||||
|
||||
// PVP menu
|
||||
document.getElementById('btn-create-room').addEventListener('click', () => {
|
||||
GameConnection.send('CODE_ROOM_CREATE', '');
|
||||
});
|
||||
document.getElementById('btn-room-list').addEventListener('click', () => {
|
||||
GameConnection.send('CODE_GET_ROOMS', '');
|
||||
});
|
||||
|
||||
// PVE difficulty buttons
|
||||
document.getElementById('btn-pve-easy').addEventListener('click', () => {
|
||||
GameConnection.send('CODE_ROOM_CREATE_PVE', '1');
|
||||
});
|
||||
// ... medium (2), hard (3)
|
||||
|
||||
// Back buttons -> switchScreen('screen-lobby')
|
||||
// Exit button -> GameConnection.send('CODE_CLIENT_EXIT', '')
|
||||
// Rematch -> GameConnection.send('CODE_GAME_READY', '')
|
||||
```
|
||||
|
||||
### Step 2: Room list rendering
|
||||
|
||||
```js
|
||||
GameState.on('CODE_SHOW_ROOMS', (data) => {
|
||||
const rooms = typeof data === 'string' ? JSON.parse(data) : data;
|
||||
const tbody = document.getElementById('room-list-body');
|
||||
tbody.innerHTML = '';
|
||||
if (rooms.length === 0) {
|
||||
// Show empty state
|
||||
}
|
||||
rooms.forEach(room => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${room.roomId}</td>
|
||||
<td>${room.roomOwner}</td>
|
||||
<td>${room.roomClientCount}/2</td>
|
||||
<td>${room.roomType}</td>
|
||||
<td>
|
||||
<button onclick="GameUI.joinRoom(${room.roomId})">Join</button>
|
||||
<button onclick="GameUI.watchRoom(${room.roomId})">Watch</button>
|
||||
</td>`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
GameState.switchScreen('screen-room-list');
|
||||
});
|
||||
```
|
||||
|
||||
### Step 3: Game screen updates
|
||||
|
||||
- **Player info panels:** On `CODE_GAME_STARTING`, populate black/white player names, highlight current player's panel
|
||||
- **Turn indicator:** On `CODE_GAME_MOVE_SUCCESS`, update which player card is "active" (CSS class)
|
||||
- **Move history:** Append row to move list: `"#N piece col-row"` (e.g., "#1 BLACK H8")
|
||||
- **Spectator badge:** If `GameState.isSpectator`, show "Spectating" label, hide move controls
|
||||
|
||||
### Step 4: Game-over screen
|
||||
|
||||
```js
|
||||
GameState.on('CODE_GAME_OVER', (data) => {
|
||||
const resultEl = document.getElementById('game-result');
|
||||
const winnerEl = document.getElementById('game-winner');
|
||||
|
||||
if (data.result === 'DRAW') {
|
||||
resultEl.textContent = 'Draw!';
|
||||
} else if (data.winnerNickname === GameState.nickname) {
|
||||
resultEl.textContent = 'You Win!';
|
||||
resultEl.className = 'result-win';
|
||||
} else {
|
||||
resultEl.textContent = 'You Lose!';
|
||||
resultEl.className = 'result-lose';
|
||||
}
|
||||
winnerEl.textContent = data.winnerNickname ? `Winner: ${data.winnerNickname}` : '';
|
||||
});
|
||||
```
|
||||
|
||||
### Step 5: Toast notifications
|
||||
|
||||
```js
|
||||
GameUI.showToast = function(message, type) {
|
||||
const container = document.getElementById('toast-container');
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
toast.textContent = message;
|
||||
container.appendChild(toast);
|
||||
setTimeout(() => toast.remove(), 3000);
|
||||
};
|
||||
|
||||
// Register error handlers
|
||||
GameState.on('CODE_ROOM_JOIN_FAIL_BY_FULL', () => GameUI.showToast('Room is full', 'error'));
|
||||
GameState.on('CODE_ROOM_JOIN_FAIL_BY_INEXIST', () => GameUI.showToast('Room not found', 'error'));
|
||||
GameState.on('CODE_GAME_MOVE_NOT_YOUR_TURN', () => GameUI.showToast('Not your turn', 'error'));
|
||||
GameState.on('CODE_GAME_MOVE_OCCUPIED', () => GameUI.showToast('Position occupied', 'error'));
|
||||
GameState.on('CODE_GAME_MOVE_OUT_OF_BOUNDS', () => GameUI.showToast('Out of bounds', 'error'));
|
||||
GameState.on('CODE_CLIENT_KICK', () => GameUI.showToast('Kicked for inactivity', 'error'));
|
||||
```
|
||||
|
||||
### Step 6: Waiting room
|
||||
|
||||
```js
|
||||
GameState.on('CODE_ROOM_CREATE_SUCCESS', (data) => {
|
||||
GameState.roomId = data.id || data.roomId;
|
||||
document.getElementById('waiting-room-id').textContent = GameState.roomId;
|
||||
GameState.switchScreen('screen-waiting-room');
|
||||
});
|
||||
|
||||
GameState.on('CODE_ROOM_JOIN_SUCCESS', (data) => {
|
||||
// If we're in waiting room, update player count
|
||||
// Game auto-starts when 2 players join (server sends CODE_GAME_STARTING)
|
||||
});
|
||||
```
|
||||
|
||||
## Todo List
|
||||
|
||||
- [ ] Create `game-ui.js` with `GameUI` global object
|
||||
- [ ] Wire nickname form submission
|
||||
- [ ] Wire lobby navigation buttons (PVP, PVE, back)
|
||||
- [ ] Wire PVP menu (create room, room list, back)
|
||||
- [ ] Wire PVE difficulty buttons (easy/medium/hard, back)
|
||||
- [ ] Render room list table from `CODE_SHOW_ROOMS`
|
||||
- [ ] Implement join/watch room actions
|
||||
- [ ] Update player info panels on `CODE_GAME_STARTING`
|
||||
- [ ] Update turn indicator on `CODE_GAME_MOVE_SUCCESS`
|
||||
- [ ] Append move history entries
|
||||
- [ ] Render game-over screen with personalized result
|
||||
- [ ] Wire rematch and exit buttons
|
||||
- [ ] Implement toast notification system
|
||||
- [ ] Register all error event toasts
|
||||
- [ ] Waiting room display with room ID
|
||||
- [ ] Spectator mode UI adjustments (hide controls, show badge)
|
||||
- [ ] Enter key submits nickname form
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Nickname submission works, transitions to lobby
|
||||
- PVP flow: create room -> waiting -> game starts when opponent joins
|
||||
- PVE flow: select difficulty -> game starts immediately
|
||||
- Room list shows active rooms with join/watch buttons
|
||||
- Move history updates per move
|
||||
- Turn indicator switches correctly
|
||||
- Game-over shows win/lose/draw relative to player
|
||||
- Rematch works (both players click ready)
|
||||
- Exit returns to lobby
|
||||
- Error toasts appear and auto-dismiss
|
||||
- File under 200 lines
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| Room data format mismatch | Verified from `ServerEventListener_CODE_GET_ROOMS` source |
|
||||
| Nickname with special chars | Server handles storage; client just sends string |
|
||||
| Rapid button clicks send duplicate | Disable button after click, re-enable on response |
|
||||
@@ -1,172 +0,0 @@
|
||||
# Phase 6: Client — Audio
|
||||
|
||||
## Context Links
|
||||
- [Phase 4](phase-04-client-board-rendering.md) — stone placement triggers sound
|
||||
|
||||
## Overview
|
||||
- **Priority:** P2
|
||||
- **Status:** Pending
|
||||
- **Effort:** 0.5h
|
||||
- **Depends on:** Phase 4
|
||||
|
||||
Sound effects for stone placement and game-over events. Handles browser autoplay restrictions gracefully.
|
||||
|
||||
## Key Insights
|
||||
|
||||
- Browsers block `Audio.play()` before user interaction — must unlock on first click
|
||||
- Three sounds: stone click (each move), win jingle, lose/draw sound
|
||||
- Audio files: use small MP3s or generate tones with Web Audio API
|
||||
- Mute toggle button in game UI
|
||||
- KISS approach: preload `Audio` objects, play on events
|
||||
|
||||
## Architecture
|
||||
|
||||
```js
|
||||
GameAudio = {
|
||||
sounds: { stone: Audio, win: Audio, lose: Audio },
|
||||
muted: false,
|
||||
unlocked: false,
|
||||
|
||||
init() // preload sounds, attach unlock listener
|
||||
play(name) // play if not muted and unlocked
|
||||
toggle() // flip muted state
|
||||
unlock() // called on first user click anywhere
|
||||
}
|
||||
```
|
||||
|
||||
### Audio Asset Strategy
|
||||
|
||||
**Option A:** Ship MP3 files in `static/audio/`. Simple but need to source/create files.
|
||||
**Option B:** Generate sounds with Web Audio API. No files needed, but more code.
|
||||
|
||||
**Decision: Option A (MP3 files).** Simpler, better sound quality. Use royalty-free short clips or generate with an audio tool. Fallback: if audio files missing, game works silently (no errors).
|
||||
|
||||
For initial implementation, create placeholder silence detection — if MP3 fails to load, disable audio silently. Audio files can be sourced later; the code handles their absence.
|
||||
|
||||
## Related Code Files
|
||||
|
||||
### Files to Create
|
||||
- `landlords-server/src/main/resources/static/js/game-audio.js` (~60 lines)
|
||||
- `landlords-server/src/main/resources/static/audio/stone-place.mp3`
|
||||
- `landlords-server/src/main/resources/static/audio/game-win.mp3`
|
||||
- `landlords-server/src/main/resources/static/audio/game-lose.mp3`
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: Create `game-audio.js`
|
||||
|
||||
```js
|
||||
const GameAudio = {
|
||||
sounds: {},
|
||||
muted: false,
|
||||
unlocked: false,
|
||||
|
||||
init() {
|
||||
this.sounds.stone = new Audio('audio/stone-place.mp3');
|
||||
this.sounds.win = new Audio('audio/game-win.mp3');
|
||||
this.sounds.lose = new Audio('audio/game-lose.mp3');
|
||||
|
||||
// Preload
|
||||
Object.values(this.sounds).forEach(s => {
|
||||
s.load();
|
||||
s.onerror = () => {}; // Silently ignore missing files
|
||||
});
|
||||
|
||||
// Unlock on first interaction
|
||||
const unlockFn = () => {
|
||||
if (!this.unlocked) {
|
||||
// Play and immediately pause a silent context to unlock
|
||||
Object.values(this.sounds).forEach(s => {
|
||||
s.play().then(() => s.pause()).catch(() => {});
|
||||
});
|
||||
this.unlocked = true;
|
||||
}
|
||||
document.removeEventListener('click', unlockFn);
|
||||
};
|
||||
document.addEventListener('click', unlockFn);
|
||||
|
||||
// Mute toggle
|
||||
const btn = document.getElementById('btn-sound-toggle');
|
||||
if (btn) {
|
||||
btn.addEventListener('click', () => this.toggle());
|
||||
}
|
||||
},
|
||||
|
||||
play(name) {
|
||||
if (this.muted || !this.sounds[name]) return;
|
||||
const s = this.sounds[name];
|
||||
s.currentTime = 0;
|
||||
s.play().catch(() => {});
|
||||
},
|
||||
|
||||
toggle() {
|
||||
this.muted = !this.muted;
|
||||
const btn = document.getElementById('btn-sound-toggle');
|
||||
if (btn) btn.textContent = this.muted ? 'Sound: OFF' : 'Sound: ON';
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Step 2: Register event handlers
|
||||
|
||||
```js
|
||||
GameState.on('CODE_GAME_MOVE_SUCCESS', () => {
|
||||
GameAudio.play('stone');
|
||||
});
|
||||
|
||||
GameState.on('CODE_GAME_OVER', (data) => {
|
||||
if (data.result === 'DRAW') {
|
||||
GameAudio.play('lose');
|
||||
} else if (data.winnerNickname === GameState.nickname) {
|
||||
GameAudio.play('win');
|
||||
} else {
|
||||
GameAudio.play('lose');
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Step 3: Audio assets
|
||||
|
||||
Generate minimal MP3 files. Options:
|
||||
- Use Web Audio API in a helper script to generate and export short tones
|
||||
- Use ffmpeg to create synthetic sounds: `ffmpeg -f lavfi -i "sine=frequency=800:duration=0.1" stone-place.mp3`
|
||||
- Source royalty-free clips
|
||||
|
||||
For MVP: create minimal placeholder files. If ffmpeg available, generate with:
|
||||
```bash
|
||||
# Stone click: short 800Hz blip
|
||||
ffmpeg -f lavfi -i "sine=frequency=800:duration=0.08" -q:a 9 audio/stone-place.mp3
|
||||
# Win: ascending tone
|
||||
ffmpeg -f lavfi -i "sine=frequency=523:duration=0.15" -f lavfi -i "sine=frequency=659:duration=0.15" -f lavfi -i "sine=frequency=784:duration=0.3" -filter_complex "[0][1][2]concat=n=3:v=0:a=1" -q:a 9 audio/game-win.mp3
|
||||
# Lose: descending tone
|
||||
ffmpeg -f lavfi -i "sine=frequency=400:duration=0.2" -f lavfi -i "sine=frequency=300:duration=0.3" -filter_complex "[0][1]concat=n=2:v=0:a=1" -q:a 9 audio/game-lose.mp3
|
||||
```
|
||||
|
||||
If ffmpeg unavailable, create empty placeholder files. Audio is non-critical.
|
||||
|
||||
## Todo List
|
||||
|
||||
- [ ] Create `game-audio.js` with preload, play, toggle, unlock
|
||||
- [ ] Register stone placement sound on `CODE_GAME_MOVE_SUCCESS`
|
||||
- [ ] Register win/lose sound on `CODE_GAME_OVER`
|
||||
- [ ] Add mute toggle button handler
|
||||
- [ ] Generate or source MP3 audio files
|
||||
- [ ] Handle missing audio files gracefully (no errors)
|
||||
- [ ] Test: sound plays on stone placement in browser
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Stone click sound plays on each move
|
||||
- Win/lose sound plays on game over
|
||||
- Mute toggle works
|
||||
- No console errors if audio files missing
|
||||
- No autoplay errors (unlocked on first click)
|
||||
- File under 200 lines (expected ~60 lines)
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| Browser blocks autoplay | Unlock pattern on first user click |
|
||||
| MP3 files missing | `onerror` handler silences failures; game works without sound |
|
||||
| Audio latency on mobile | Short clips (<0.5s); acceptable for board game |
|
||||
@@ -1,103 +0,0 @@
|
||||
# Phase 7: Integration Test + Polish
|
||||
|
||||
## Context Links
|
||||
- All prior phases
|
||||
|
||||
## Overview
|
||||
- **Priority:** P1
|
||||
- **Status:** Pending
|
||||
- **Effort:** 0.5h
|
||||
- **Depends on:** Phases 1-6
|
||||
|
||||
End-to-end validation of the full game flow. Compile check, server startup, browser testing across all game modes.
|
||||
|
||||
## Test Matrix
|
||||
|
||||
### Build Verification
|
||||
- [ ] `mvn clean compile` passes
|
||||
- [ ] `mvn test` passes
|
||||
- [ ] `mvn package` produces runnable JAR
|
||||
|
||||
### Server Startup
|
||||
- [ ] Start server: `java -jar landlords-server/target/landlords-server-1.4.0.jar -p 1024`
|
||||
- [ ] WebSocket port 1025 open
|
||||
- [ ] `curl http://localhost:1025/` returns HTML
|
||||
- [ ] `curl http://localhost:1025/css/style.css` returns CSS with correct Content-Type
|
||||
- [ ] `curl http://localhost:1025/js/game-state.js` returns JS with correct Content-Type
|
||||
- [ ] `curl http://localhost:1025/nonexistent` returns 404
|
||||
|
||||
### Security
|
||||
- [ ] `curl http://localhost:1025/../../../etc/passwd` returns 403 or 404 (no traversal)
|
||||
- [ ] `curl http://localhost:1025/js/../../../pom.xml` returns 403 or 404
|
||||
|
||||
### WebSocket Compatibility
|
||||
- [ ] Existing Java CLI client still connects via TCP port 1024
|
||||
- [ ] Existing Java CLI client still connects via WS port 1025 `/ratel`
|
||||
|
||||
### PVP Flow (2 browser tabs)
|
||||
- [ ] Tab 1: open `http://localhost:1025/`, enter nickname, reach lobby
|
||||
- [ ] Tab 1: PVP -> Create Room -> see waiting room with room ID
|
||||
- [ ] Tab 2: open `http://localhost:1025/`, enter nickname, reach lobby
|
||||
- [ ] Tab 2: PVP -> Room List -> see room from Tab 1 -> Join
|
||||
- [ ] Both tabs: game starts, board renders with player names
|
||||
- [ ] Tab 1 (black): click intersection -> stone appears on both tabs
|
||||
- [ ] Tab 2 (white): click intersection -> stone appears on both tabs
|
||||
- [ ] Alternating turns work correctly
|
||||
- [ ] Move history updates on both tabs
|
||||
- [ ] Turn indicator switches on both tabs
|
||||
- [ ] Sound plays on stone placement
|
||||
- [ ] Play to completion -> game over screen shows on both
|
||||
- [ ] Winner sees "You Win!", loser sees "You Lose!"
|
||||
- [ ] Both click "Rematch" -> new game starts
|
||||
- [ ] One clicks "Exit" -> both return to lobby
|
||||
|
||||
### PVE Flow
|
||||
- [ ] Enter nickname, lobby, PVE -> Easy
|
||||
- [ ] Game starts immediately (no waiting)
|
||||
- [ ] Player places stone, AI responds
|
||||
- [ ] AI moves render with animation
|
||||
- [ ] Game reaches conclusion (play or resign)
|
||||
|
||||
### Spectator Flow
|
||||
- [ ] Tab 1+2: start PVP game
|
||||
- [ ] Tab 3: open game, enter nickname, PVP -> Room List -> Watch
|
||||
- [ ] Tab 3: sees board with existing moves
|
||||
- [ ] Tab 3: new moves appear in real-time
|
||||
- [ ] Tab 3: clicking board does nothing (spectator)
|
||||
- [ ] Tab 3: "Spectating" label visible
|
||||
- [ ] Tab 3: "Exit Watch" returns to lobby
|
||||
|
||||
### Error Handling
|
||||
- [ ] Join nonexistent room -> toast "Room not found"
|
||||
- [ ] Join full room -> toast "Room is full"
|
||||
- [ ] Click out of turn -> no action (client-side block)
|
||||
- [ ] Server stops -> reconnect message shown
|
||||
- [ ] Refresh page -> reconnects, shows nickname screen
|
||||
|
||||
### Responsive
|
||||
- [ ] Desktop (1920x1080): full layout with sidebars
|
||||
- [ ] Tablet (768px): sidebars collapse or stack
|
||||
- [ ] Board remains square and usable at all sizes
|
||||
|
||||
### Performance
|
||||
- [ ] Page load < 1s (all assets local)
|
||||
- [ ] No console errors during full game flow
|
||||
- [ ] No memory leaks (monitor heap during 50-move game)
|
||||
|
||||
## Polish Items (if time permits)
|
||||
- Smooth screen transitions (CSS opacity/transform)
|
||||
- Hover effects on all interactive elements
|
||||
- Keyboard shortcut: Enter submits nickname
|
||||
- Room list auto-refresh every 5s while on that screen
|
||||
- Connection status indicator in header
|
||||
|
||||
## Success Criteria
|
||||
|
||||
All items in the test matrix checked. Build compiles, tests pass, all three game modes (PVP, PVE, Spectator) functional end-to-end.
|
||||
|
||||
## Rollback
|
||||
|
||||
If integration fails:
|
||||
1. Server issue: revert `WebsocketProxy.java` change (remove `StaticFileHandler` from pipeline). One-line revert.
|
||||
2. Client issue: delete `static/` directory. No server code references it.
|
||||
3. Both changes are additive — no existing functionality modified.
|
||||
@@ -1,89 +0,0 @@
|
||||
---
|
||||
title: "Web 2D Gomoku Client"
|
||||
description: "Professional vanilla JS Gomoku web client served from existing Netty server"
|
||||
status: pending
|
||||
priority: P1
|
||||
effort: 10h
|
||||
branch: master
|
||||
tags: [web-client, gomoku, netty, websocket, canvas]
|
||||
created: 2026-04-09
|
||||
---
|
||||
|
||||
# Web 2D Gomoku Client
|
||||
|
||||
## Overview
|
||||
|
||||
Add a professional 2D web-based Gomoku client served as static files from the existing Netty WebSocket server. The server already handles all game logic; the client is display-only. No build tools (vanilla HTML/CSS/JS).
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Browser (port 1025)
|
||||
|
|
||||
|-- GET / (HTTP) --> StaticFileHandler --> static/index.html
|
||||
|-- GET /css/* --> StaticFileHandler --> static/css/*
|
||||
|-- GET /js/* --> StaticFileHandler --> static/js/*
|
||||
|
|
||||
|-- WS /ratel --> WebSocketServerProtocolHandler (existing)
|
||||
WebsocketTransferHandler (existing)
|
||||
```
|
||||
|
||||
**Data flow:** Browser opens WS to `/ratel`. Server sends `CODE_CLIENT_CONNECT` + `CODE_CLIENT_NICKNAME_SET`. Client walks through nickname -> lobby -> room -> game screens, sending JSON `{code, data, info}` messages. All state transitions driven by server events.
|
||||
|
||||
## Phases
|
||||
|
||||
| # | Phase | Status | Effort | Files Modified/Created |
|
||||
|---|-------|--------|--------|----------------------|
|
||||
| 1 | [Server: Static file handler](phase-01-server-static-file-handler.md) | Pending | 1.5h | 2 Java files |
|
||||
| 2 | [Client: HTML shell + CSS](phase-02-client-html-css.md) | Pending | 2h | 2 files |
|
||||
| 3 | [Client: WebSocket + state machine](phase-03-client-connection-state.md) | Pending | 2h | 2 JS files |
|
||||
| 4 | [Client: Canvas board rendering](phase-04-client-board-rendering.md) | Pending | 2h | 1 JS file |
|
||||
| 5 | [Client: UI panels + lobby](phase-05-client-ui-panels.md) | Pending | 1.5h | 1 JS file |
|
||||
| 6 | [Client: Audio](phase-06-client-audio.md) | Pending | 0.5h | 1 JS file + audio assets |
|
||||
| 7 | [Integration test + polish](phase-07-integration-test.md) | Pending | 0.5h | - |
|
||||
|
||||
## Dependency Graph
|
||||
|
||||
```
|
||||
Phase 1 (server) --+
|
||||
+--> Phase 7 (integration)
|
||||
Phase 2 (HTML) --+
|
||||
| |
|
||||
v |
|
||||
Phase 3 (WS+state) --> Phase 5 (UI)
|
||||
| |
|
||||
v v
|
||||
Phase 4 (board) --------> Phase 7
|
||||
|
|
||||
v
|
||||
Phase 6 (audio) ---------> Phase 7
|
||||
```
|
||||
|
||||
Phases 1 and 2 are independent (can run in parallel). Phase 3 depends on 2. Phase 4 depends on 3. Phase 5 depends on 3. Phase 6 depends on 4. Phase 7 depends on all.
|
||||
|
||||
## Key Risks
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|------|-----------|--------|------------|
|
||||
| Netty pipeline ordering breaks WS upgrade | Medium | High | Insert static handler BEFORE `WebSocketServerProtocolHandler`; handler checks URI and passes through `/ratel` |
|
||||
| Canvas rendering performance on large boards | Low | Medium | 15x15 is small; only redraw dirty cells |
|
||||
| MIME type issues for static files | Low | Medium | Explicit MIME map in handler |
|
||||
| Audio autoplay blocked by browser | Medium | Low | Play on first user interaction; graceful fallback |
|
||||
|
||||
## Rollback
|
||||
|
||||
- Server change: Remove `StaticFileHandler` from pipeline. Single file revert.
|
||||
- Client files: Delete `landlords-server/src/main/resources/static/` directory. No server code depends on it.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] `http://localhost:1025/` loads the game UI
|
||||
- [ ] Full game flow: nickname -> lobby -> create room -> play -> game over -> rematch
|
||||
- [ ] PVP mode works (2 browser tabs)
|
||||
- [ ] PVE mode works (all 3 difficulties)
|
||||
- [ ] Spectator mode works
|
||||
- [ ] `mvn clean compile` passes
|
||||
- [ ] `mvn test` passes
|
||||
- [ ] Canvas board renders with wood texture, grid, coordinates
|
||||
- [ ] Stone placement animates
|
||||
- [ ] Sound plays on stone click
|
||||
@@ -1,100 +0,0 @@
|
||||
# Phase 1: Project Scaffold
|
||||
|
||||
## Context Links
|
||||
- [Plan overview](plan.md)
|
||||
- Server WS handler: `landlords-server/.../handler/WebsocketTransferHandler.java`
|
||||
- Server WS proxy: `landlords-server/.../proxy/WebsocketProxy.java`
|
||||
|
||||
## Overview
|
||||
- **Priority:** P1 (blocker for all other phases)
|
||||
- **Status:** Pending
|
||||
- **Description:** Initialize Vite + Phaser 3 project in `web-client/`, verify Phaser boots a blank canvas.
|
||||
|
||||
## Requirements
|
||||
- `npm create vite` with vanilla JS template (or manual init)
|
||||
- Phaser 3 latest stable as dependency
|
||||
- Vite dev server on port 5173 (default)
|
||||
- `index.html` with a `#game-container` div for Phaser canvas + a `#ui-overlay` div for DOM menus
|
||||
- `src/main.js` creates Phaser.Game with config from `src/config/game-config.js`
|
||||
- BootScene placeholder that shows "Loading..." text
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
web-client/
|
||||
package.json
|
||||
vite.config.js
|
||||
index.html <-- #game-container + #ui-overlay
|
||||
src/
|
||||
main.js <-- Phaser.Game instantiation
|
||||
config/
|
||||
game-config.js <-- Phaser config: 800x800, Scale.FIT, scenes list
|
||||
scenes/
|
||||
boot-scene.js <-- placeholder "Loading..." text
|
||||
public/
|
||||
(empty, for future assets)
|
||||
```
|
||||
|
||||
## Related Code Files
|
||||
|
||||
### Files to Create
|
||||
- `web-client/package.json`
|
||||
- `web-client/vite.config.js`
|
||||
- `web-client/index.html`
|
||||
- `web-client/src/main.js`
|
||||
- `web-client/src/config/game-config.js`
|
||||
- `web-client/src/scenes/boot-scene.js`
|
||||
|
||||
### Files to Modify
|
||||
- None
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Create `web-client/` directory at repo root
|
||||
2. Create `package.json` with:
|
||||
- `name: "caro-web-client"`
|
||||
- `type: "module"`
|
||||
- `scripts: { "dev": "vite", "build": "vite build", "preview": "vite preview" }`
|
||||
- `dependencies: { "phaser": "^3.80.0" }`
|
||||
- `devDependencies: { "vite": "^6.0.0" }`
|
||||
3. Create `vite.config.js`:
|
||||
```js
|
||||
import { defineConfig } from 'vite';
|
||||
export default defineConfig({
|
||||
server: { port: 5173 }
|
||||
});
|
||||
```
|
||||
4. Create `index.html`:
|
||||
- Minimal HTML5 boilerplate
|
||||
- `<div id="game-container"></div>` -- Phaser mounts here
|
||||
- `<div id="ui-overlay"></div>` -- DOM menus render here (hidden by default)
|
||||
- `<script type="module" src="/src/main.js"></script>`
|
||||
- Basic CSS: body margin 0, background #1a1a2e, flex-center the container, overlay absolute positioned over canvas
|
||||
5. Create `src/config/game-config.js`:
|
||||
- Export Phaser config object: `type: Phaser.AUTO`, `width: 800`, `height: 800`
|
||||
- `scale: { mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH }`
|
||||
- `parent: 'game-container'`
|
||||
- `backgroundColor: '#2d2d44'`
|
||||
- `scene: [BootScene]` (import from scenes)
|
||||
6. Create `src/scenes/boot-scene.js`:
|
||||
- Extends `Phaser.Scene`, key: `'BootScene'`
|
||||
- `create()`: display centered "Loading..." text
|
||||
- Will be expanded in Phase 3 to transition to MenuScene
|
||||
7. Create `src/main.js`:
|
||||
- Import config from `game-config.js`
|
||||
- `new Phaser.Game(config)`
|
||||
- Export game instance for potential service access
|
||||
8. Run `npm install` and `npm run dev` to verify Phaser boots
|
||||
|
||||
## Success Criteria
|
||||
- [ ] `npm run dev` starts Vite on port 5173
|
||||
- [ ] Browser shows Phaser canvas with "Loading..." text
|
||||
- [ ] No console errors
|
||||
- [ ] `npm run build` produces working static build in `dist/`
|
||||
|
||||
## Risk Assessment
|
||||
- **Node.js not installed:** User must have Node.js. Document in README.
|
||||
- **Phaser version mismatch:** Pin to `^3.80.0` for stability.
|
||||
|
||||
## Next Steps
|
||||
- Phase 2: Services layer (can start immediately after scaffold)
|
||||
@@ -1,190 +0,0 @@
|
||||
# Phase 2: Services Layer
|
||||
|
||||
## Context Links
|
||||
- [Plan overview](plan.md)
|
||||
- [Phase 1: Scaffold](phase-01-project-scaffold.md)
|
||||
- Server `Msg` entity: `landlords-common/.../entity/Msg.java` -- `{code, data, info}`
|
||||
- Server event codes: `landlords-common/.../enums/ServerEventCode.java`, `ClientEventCode.java`
|
||||
- Server WS handler: `landlords-server/.../handler/WebsocketTransferHandler.java`
|
||||
|
||||
## Overview
|
||||
- **Priority:** P1 (all scenes depend on these services)
|
||||
- **Status:** Pending
|
||||
- **Blocked by:** Phase 1
|
||||
- **Description:** Build three decoupled service modules: WebSocket connection, event bus, game state. Plus protocol constants extracted from server source.
|
||||
|
||||
## Key Insights
|
||||
- Server WS message format: `{"code": "CODE_...", "data": "json_string_or_plain_string", "info": ""}`
|
||||
- `data` field is a JSON **string** (not nested object) -- must `JSON.parse(data)` when data is structured
|
||||
- Server sends `CODE_CLIENT_CONNECT` + `CODE_CLIENT_NICKNAME_SET` ~2s after WS handshake (server has `Thread.sleep(2000L)`)
|
||||
- Heartbeat: server reads idle timeout; client must send `CODE_CLIENT_HEAD_BEAT` every ~50s
|
||||
- Server ignores heartbeat messages (no handler called, just keeps connection alive)
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
connection-service.js
|
||||
|-- wraps browser WebSocket
|
||||
|-- auto-reconnect with backoff
|
||||
|-- heartbeat timer (50s interval)
|
||||
|-- on message: parse JSON -> event-bus.emit(code, parsedData)
|
||||
|-- send(code, data): serialize to Msg format -> ws.send()
|
||||
|
||||
event-bus.js
|
||||
|-- on(event, callback): subscribe
|
||||
|-- off(event, callback): unsubscribe
|
||||
|-- emit(event, data): notify all subscribers
|
||||
|
||||
game-state-service.js
|
||||
|-- stores: clientId, nickname, roomId, isBlack, isMyTurn, boardState[][], moves[]
|
||||
|-- reset methods for new game / exit room
|
||||
|-- no logic, pure state container
|
||||
|
||||
protocol-constants.js
|
||||
|-- SERVER_EVENTS: all CODE_CLIENT_* codes (server -> client)
|
||||
|-- CLIENT_EVENTS: all CODE_* codes (client -> server)
|
||||
|-- string constants, no enums needed
|
||||
```
|
||||
|
||||
### Data Flow: Sending a Move
|
||||
```
|
||||
GameScene.onBoardClick(row, col)
|
||||
-> connectionService.send('CODE_GAME_MOVE', JSON.stringify({row, col}))
|
||||
-> ws.send('{"code":"CODE_GAME_MOVE","data":"{\"row\":7,\"col\":7}","info":""}')
|
||||
```
|
||||
|
||||
### Data Flow: Receiving a Move
|
||||
```
|
||||
ws.onmessage(frame)
|
||||
-> JSON.parse(frame.data) => {code: "CODE_GAME_MOVE_SUCCESS", data: "{\"row\":7,...}", info: ""}
|
||||
-> eventBus.emit('CODE_GAME_MOVE_SUCCESS', {row:7, col:7, piece:"BLACK", playerNickname:"p1", playerId:1})
|
||||
(data string auto-parsed to object by connection-service)
|
||||
```
|
||||
|
||||
## Related Code Files
|
||||
|
||||
### Files to Create
|
||||
- `web-client/src/services/connection-service.js`
|
||||
- `web-client/src/services/event-bus.js`
|
||||
- `web-client/src/services/game-state-service.js`
|
||||
- `web-client/src/config/protocol-constants.js`
|
||||
|
||||
### Files to Modify
|
||||
- None
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### 1. `protocol-constants.js`
|
||||
|
||||
Export two frozen objects with all event code strings:
|
||||
|
||||
```js
|
||||
/** @enum {string} Codes the client sends TO the server */
|
||||
export const ServerEventCode = Object.freeze({
|
||||
NICKNAME_SET: 'CODE_CLIENT_NICKNAME_SET',
|
||||
INFO_SET: 'CODE_CLIENT_INFO_SET',
|
||||
ROOM_CREATE: 'CODE_ROOM_CREATE',
|
||||
ROOM_CREATE_PVE: 'CODE_ROOM_CREATE_PVE',
|
||||
ROOM_JOIN: 'CODE_ROOM_JOIN',
|
||||
GET_ROOMS: 'CODE_GET_ROOMS',
|
||||
GAME_MOVE: 'CODE_GAME_MOVE',
|
||||
GAME_READY: 'CODE_GAME_READY',
|
||||
CLIENT_EXIT: 'CODE_CLIENT_EXIT',
|
||||
GAME_WATCH: 'CODE_GAME_WATCH',
|
||||
GAME_WATCH_EXIT: 'CODE_GAME_WATCH_EXIT',
|
||||
HEARTBEAT: 'CODE_CLIENT_HEAD_BEAT',
|
||||
});
|
||||
|
||||
/** @enum {string} Codes the server sends TO the client */
|
||||
export const ClientEventCode = Object.freeze({
|
||||
CLIENT_CONNECT: 'CODE_CLIENT_CONNECT',
|
||||
NICKNAME_SET: 'CODE_CLIENT_NICKNAME_SET',
|
||||
SHOW_OPTIONS: 'CODE_SHOW_OPTIONS',
|
||||
SHOW_ROOMS: 'CODE_SHOW_ROOMS',
|
||||
ROOM_CREATE_SUCCESS: 'CODE_ROOM_CREATE_SUCCESS',
|
||||
ROOM_JOIN_SUCCESS: 'CODE_ROOM_JOIN_SUCCESS',
|
||||
ROOM_JOIN_FAIL_FULL: 'CODE_ROOM_JOIN_FAIL_BY_FULL',
|
||||
ROOM_JOIN_FAIL_INEXIST: 'CODE_ROOM_JOIN_FAIL_BY_INEXIST',
|
||||
GAME_STARTING: 'CODE_GAME_STARTING',
|
||||
GAME_MOVE_SUCCESS: 'CODE_GAME_MOVE_SUCCESS',
|
||||
GAME_MOVE_INVALID: 'CODE_GAME_MOVE_INVALID',
|
||||
GAME_MOVE_OCCUPIED: 'CODE_GAME_MOVE_OCCUPIED',
|
||||
GAME_MOVE_OUT_OF_BOUNDS: 'CODE_GAME_MOVE_OUT_OF_BOUNDS',
|
||||
GAME_MOVE_NOT_YOUR_TURN: 'CODE_GAME_MOVE_NOT_YOUR_TURN',
|
||||
GAME_OVER: 'CODE_GAME_OVER',
|
||||
GAME_READY: 'CODE_GAME_READY',
|
||||
CLIENT_EXIT: 'CODE_CLIENT_EXIT',
|
||||
CLIENT_KICK: 'CODE_CLIENT_KICK',
|
||||
GAME_WATCH: 'CODE_GAME_WATCH',
|
||||
GAME_WATCH_SUCCESSFUL: 'CODE_GAME_WATCH_SUCCESSFUL',
|
||||
PVE_DIFFICULTY_NOT_SUPPORT: 'CODE_PVE_DIFFICULTY_NOT_SUPPORT',
|
||||
});
|
||||
```
|
||||
|
||||
### 2. `event-bus.js`
|
||||
|
||||
Simple pub/sub:
|
||||
- `_listeners` Map of `event -> Set<callback>`
|
||||
- `on(event, cb)` -- add listener
|
||||
- `off(event, cb)` -- remove listener
|
||||
- `emit(event, data)` -- call all listeners for event
|
||||
- Export singleton instance
|
||||
- ~40 lines
|
||||
|
||||
### 3. `connection-service.js`
|
||||
|
||||
WebSocket wrapper:
|
||||
- `connect(url)` -- create WebSocket, attach handlers
|
||||
- `send(code, data)` -- build `{code, data, info:""}`, `ws.send(JSON.stringify(msg))`
|
||||
- `disconnect()` -- close WS, clear heartbeat
|
||||
- Internal: `_onMessage(event)`:
|
||||
1. `JSON.parse(event.data)` to get `{code, data, info}`
|
||||
2. Try `JSON.parse(msg.data)` for structured data; fall back to raw string
|
||||
3. `eventBus.emit(msg.code, parsedData)`
|
||||
- Internal: `_startHeartbeat()` -- `setInterval` every 50000ms, send `CODE_CLIENT_HEAD_BEAT`
|
||||
- Internal: `_stopHeartbeat()` -- `clearInterval`
|
||||
- `onopen`: emit internal `'ws:connected'` event
|
||||
- `onclose`: emit `'ws:disconnected'`, attempt reconnect with exponential backoff (1s, 2s, 4s, max 30s)
|
||||
- `onerror`: log, let `onclose` handle reconnect
|
||||
- Export singleton
|
||||
- ~80 lines
|
||||
|
||||
**Critical detail:** The `data` field in outgoing messages must be a **string**. For structured data like `{row, col}`, use `JSON.stringify({row, col})` as the data value. For simple strings like nickname, pass the string directly.
|
||||
|
||||
### 4. `game-state-service.js`
|
||||
|
||||
Plain state object:
|
||||
- `clientId` -- set on `CODE_CLIENT_CONNECT`
|
||||
- `nickname` -- set after nickname submission
|
||||
- `roomId` -- set on room create/join
|
||||
- `isBlack` -- derived from `CODE_GAME_STARTING` comparing clientId to blackPlayerId
|
||||
- `isMyTurn` -- toggled on each `CODE_GAME_MOVE_SUCCESS`
|
||||
- `board` -- 15x15 2D array, initialized to `null`, set cells on move success
|
||||
- `moves` -- array of `{row, col, piece, playerNickname}` for move history
|
||||
- `isSpectating` -- boolean
|
||||
- `reset()` -- clear room/game state
|
||||
- `resetBoard()` -- clear board/moves for rematch
|
||||
- Export singleton
|
||||
- ~60 lines
|
||||
|
||||
## Todo List
|
||||
- [ ] Create `protocol-constants.js` with all event codes
|
||||
- [ ] Create `event-bus.js` with on/off/emit
|
||||
- [ ] Create `connection-service.js` with connect/send/heartbeat/reconnect
|
||||
- [ ] Create `game-state-service.js` with state fields and reset methods
|
||||
- [ ] Verify WS connection to running server in browser console
|
||||
|
||||
## Success Criteria
|
||||
- [ ] `eventBus.on('CODE_CLIENT_CONNECT', cb)` fires when server sends connect event
|
||||
- [ ] `connectionService.send('CODE_CLIENT_NICKNAME_SET', 'TestUser')` accepted by server
|
||||
- [ ] Heartbeat keeps connection alive beyond 60s
|
||||
- [ ] `game-state-service` stores and resets state correctly
|
||||
- [ ] All files under 100 lines each
|
||||
- [ ] JSDoc on all exported functions
|
||||
|
||||
## Risk Assessment
|
||||
- **`data` field double-encoding:** Server expects `data` as a string. If we pass an object, server's `MapHelper.parser()` will fail. Mitigation: `connection-service.send()` must `JSON.stringify` objects before placing in `data` field.
|
||||
- **Reconnect during active game:** If WS drops mid-game, server has no rejoin mechanism. Mitigation: show "Connection lost" overlay; on reconnect, user starts fresh (server already cleaned up the room via `CODE_CLIENT_OFFLINE`).
|
||||
|
||||
## Next Steps
|
||||
- Phase 3 (Boot + Menu scenes) depends on these services being complete
|
||||
@@ -1,206 +0,0 @@
|
||||
# Phase 3: Boot + Menu Scenes
|
||||
|
||||
## Context Links
|
||||
- [Plan overview](plan.md)
|
||||
- [Phase 2: Services](phase-02-services-layer.md)
|
||||
- Server nickname handler: `landlords-server/.../event/ServerEventListener_CODE_CLIENT_NICKNAME_SET.java` -- max 10 chars, non-empty
|
||||
- Server show options flow: after nickname set, server sends `CODE_SHOW_OPTIONS`
|
||||
- Server room list: `CODE_GET_ROOMS` -> `CODE_SHOW_ROOMS` with `[{roomId, roomOwner, roomClientCount, roomType}]`
|
||||
|
||||
## Overview
|
||||
- **Priority:** P1
|
||||
- **Status:** Pending
|
||||
- **Blocked by:** Phase 1, Phase 2
|
||||
- **Description:** BootScene connects to server, MenuScene manages all pre-game UI via DOM overlays (nickname, lobby, PVP/PVE menus).
|
||||
|
||||
## Key Insights
|
||||
- Server flow after WS connect: waits 2s, sends `CODE_CLIENT_CONNECT` (data=clientId), then `CODE_CLIENT_NICKNAME_SET` (data=null, meaning "please set nickname")
|
||||
- If nickname invalid (empty or >10 chars), server sends `CODE_CLIENT_NICKNAME_SET` again with `{invalidLength: N}` -- client should re-prompt
|
||||
- After valid nickname, server sends `CODE_SHOW_OPTIONS` -- client shows main menu
|
||||
- PVP: `CODE_ROOM_CREATE` -> `CODE_ROOM_CREATE_SUCCESS` (room JSON) -> wait for opponent -> auto-starts on join
|
||||
- PVE: `CODE_ROOM_CREATE_PVE` with data "1"/"2"/"3" -> server auto-starts immediately
|
||||
- Join room: `CODE_ROOM_JOIN` with data=roomId string -> `CODE_ROOM_JOIN_SUCCESS` -> auto-starts if 2 players
|
||||
|
||||
## Architecture
|
||||
|
||||
### Scene Flow
|
||||
```
|
||||
BootScene
|
||||
create(): connect to WS, show "Connecting..." text on canvas
|
||||
on 'CODE_CLIENT_CONNECT': store clientId, show "Connected!"
|
||||
on 'CODE_CLIENT_NICKNAME_SET': transition to MenuScene
|
||||
|
||||
MenuScene
|
||||
create(): show nickname form via menu-ui.js
|
||||
Substates (managed by menu-ui.js DOM swaps):
|
||||
1. NICKNAME -- input + submit button
|
||||
2. LOBBY -- main menu: PVP / PVE / Spectate buttons
|
||||
3. PVP_MENU -- "Create Room" button + room list table + join button
|
||||
4. PVE_MENU -- difficulty picker (Easy/Medium/Hard)
|
||||
5. SPECTATE_MENU -- room list + watch button
|
||||
6. WAITING -- "Waiting for opponent..." (after PVP room create)
|
||||
|
||||
on 'CODE_SHOW_OPTIONS': switch to LOBBY substate
|
||||
on 'CODE_SHOW_ROOMS': populate room list table
|
||||
on 'CODE_ROOM_CREATE_SUCCESS': switch to WAITING, store roomId
|
||||
on 'CODE_ROOM_JOIN_SUCCESS': store room info
|
||||
on 'CODE_GAME_STARTING': hide all overlays, transition to GameScene
|
||||
on 'CODE_ROOM_JOIN_FAIL_*': show error toast
|
||||
on 'CODE_PVE_DIFFICULTY_NOT_SUPPORT': show error toast
|
||||
on 'CODE_GAME_WATCH_SUCCESSFUL': transition to GameScene (spectator mode)
|
||||
```
|
||||
|
||||
### DOM Overlay Strategy
|
||||
- All menus live in `#ui-overlay` div (positioned absolute over Phaser canvas)
|
||||
- `menu-ui.js` manages showing/hiding substate containers
|
||||
- Phaser canvas stays visible as background (dark board aesthetic)
|
||||
- On transition to GameScene, hide `#ui-overlay` entirely
|
||||
|
||||
## Related Code Files
|
||||
|
||||
### Files to Create
|
||||
- `web-client/src/scenes/boot-scene.js` (overwrite Phase 1 placeholder)
|
||||
- `web-client/src/scenes/menu-scene.js`
|
||||
- `web-client/src/ui/menu-ui.js`
|
||||
|
||||
### Files to Modify
|
||||
- `web-client/index.html` -- add DOM overlay structure and CSS
|
||||
- `web-client/src/config/game-config.js` -- add MenuScene to scene list
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### 1. Update `index.html` -- Add DOM overlay structure
|
||||
|
||||
Inside `#ui-overlay`, add containers for each substate:
|
||||
|
||||
```html
|
||||
<div id="ui-overlay" class="hidden">
|
||||
<div id="nickname-screen" class="ui-screen hidden">
|
||||
<h1>Caro (Gomoku)</h1>
|
||||
<input id="nickname-input" maxlength="10" placeholder="Enter nickname..." />
|
||||
<button id="nickname-submit">Play</button>
|
||||
<p id="nickname-error" class="error hidden"></p>
|
||||
</div>
|
||||
|
||||
<div id="lobby-screen" class="ui-screen hidden">
|
||||
<h2>Welcome, <span id="player-name"></span></h2>
|
||||
<button id="btn-pvp">Player vs Player</button>
|
||||
<button id="btn-pve">Player vs AI</button>
|
||||
<button id="btn-spectate">Spectate</button>
|
||||
</div>
|
||||
|
||||
<div id="pvp-screen" class="ui-screen hidden">
|
||||
<h2>PVP Lobby</h2>
|
||||
<button id="btn-create-room">Create Room</button>
|
||||
<button id="btn-refresh-rooms">Refresh</button>
|
||||
<table id="room-table"><thead><tr><th>ID</th><th>Owner</th><th>Players</th><th></th></tr></thead><tbody></tbody></table>
|
||||
<button id="btn-back-pvp">Back</button>
|
||||
</div>
|
||||
|
||||
<div id="pve-screen" class="ui-screen hidden">
|
||||
<h2>Play vs AI</h2>
|
||||
<button data-difficulty="1">Easy</button>
|
||||
<button data-difficulty="2">Medium</button>
|
||||
<button data-difficulty="3">Hard</button>
|
||||
<button id="btn-back-pve">Back</button>
|
||||
</div>
|
||||
|
||||
<div id="spectate-screen" class="ui-screen hidden">
|
||||
<h2>Spectate</h2>
|
||||
<button id="btn-refresh-spectate">Refresh</button>
|
||||
<table id="spectate-table"><thead><tr><th>ID</th><th>Owner</th><th>Players</th><th></th></tr></thead><tbody></tbody></table>
|
||||
<button id="btn-back-spectate">Back</button>
|
||||
</div>
|
||||
|
||||
<div id="waiting-screen" class="ui-screen hidden">
|
||||
<h2>Waiting for opponent...</h2>
|
||||
<p>Room ID: <span id="waiting-room-id"></span></p>
|
||||
<button id="btn-cancel-wait">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
CSS: dark theme, centered cards, simple button styles. Keep inline in `<style>` tag (~50 lines).
|
||||
|
||||
### 2. `boot-scene.js` (~50 lines)
|
||||
|
||||
```
|
||||
- constructor: super({ key: 'BootScene' })
|
||||
- create():
|
||||
- Display "Connecting..." centered text
|
||||
- connectionService.connect(wsUrl)
|
||||
- eventBus.on(CLIENT_CONNECT, (clientId) => {
|
||||
gameState.clientId = clientId
|
||||
this.statusText.setText('Connected!')
|
||||
})
|
||||
- eventBus.on(NICKNAME_SET, () => {
|
||||
this.scene.start('MenuScene')
|
||||
})
|
||||
- wsUrl: derive from window.location or fallback 'ws://localhost:1025/ratel'
|
||||
- Config: check URL param ?ws=... for override, else default
|
||||
```
|
||||
|
||||
### 3. `menu-scene.js` (~80 lines)
|
||||
|
||||
```
|
||||
- constructor: super({ key: 'MenuScene' })
|
||||
- create():
|
||||
- menuUi.show('nickname')
|
||||
- Register event listeners on eventBus for all menu-related server events
|
||||
- Wire up menuUi callbacks for user actions
|
||||
- Event handlers:
|
||||
- CODE_SHOW_OPTIONS: menuUi.show('lobby')
|
||||
- CODE_SHOW_ROOMS: menuUi.populateRooms(data)
|
||||
- CODE_ROOM_CREATE_SUCCESS: gameState.roomId = data.id; menuUi.show('waiting')
|
||||
- CODE_ROOM_JOIN_SUCCESS: store room info in gameState
|
||||
- CODE_GAME_STARTING: store game info, menuUi.hideAll(), this.scene.start('GameScene')
|
||||
- CODE_ROOM_JOIN_FAIL_*: menuUi.showError('Room full' / 'Room not found')
|
||||
- CODE_GAME_WATCH_SUCCESSFUL: gameState.isSpectating = true; menuUi.hideAll(); this.scene.start('GameScene')
|
||||
- shutdown(): unsubscribe all eventBus listeners, menuUi.hideAll()
|
||||
```
|
||||
|
||||
### 4. `menu-ui.js` (~150 lines)
|
||||
|
||||
DOM manipulation module:
|
||||
- `show(screen)` -- hide all `.ui-screen`, show target, show overlay
|
||||
- `hideAll()` -- hide overlay
|
||||
- `populateRooms(roomList)` -- clear + rebuild room table tbody with join/watch buttons
|
||||
- `showError(msg)` -- show toast or error text, auto-hide after 3s
|
||||
- `bindActions(callbacks)` -- attach click handlers to all buttons, pass action callbacks:
|
||||
- `onNicknameSubmit(nickname)`
|
||||
- `onCreateRoom()`
|
||||
- `onJoinRoom(roomId)`
|
||||
- `onCreatePVE(difficulty)`
|
||||
- `onWatchRoom(roomId)`
|
||||
- `onRefreshRooms()`
|
||||
- `onCancelWait()`
|
||||
- `onBack()`
|
||||
|
||||
Each callback in menu-scene.js calls the appropriate `connectionService.send()`.
|
||||
|
||||
## Todo List
|
||||
- [ ] Add DOM overlay HTML structure to `index.html`
|
||||
- [ ] Add CSS styles (dark theme) to `index.html`
|
||||
- [ ] Implement `boot-scene.js` with WS connect + transition
|
||||
- [ ] Implement `menu-ui.js` with show/hide/populate/bind
|
||||
- [ ] Implement `menu-scene.js` wiring eventBus to menuUi
|
||||
- [ ] Update `game-config.js` scene list: [BootScene, MenuScene]
|
||||
- [ ] Test: nickname flow end-to-end with running server
|
||||
- [ ] Test: PVP room create + join flow
|
||||
- [ ] Test: PVE game start flow
|
||||
|
||||
## Success Criteria
|
||||
- [ ] BootScene connects and transitions to MenuScene on server prompt
|
||||
- [ ] Nickname submission accepted by server, lobby appears
|
||||
- [ ] PVP room creation shows waiting screen with room ID
|
||||
- [ ] Room list populates with active rooms, join works
|
||||
- [ ] PVE difficulty selection starts game immediately
|
||||
- [ ] Error toasts show on join failures
|
||||
- [ ] All files under 150 lines, JSDoc on exports
|
||||
|
||||
## Risk Assessment
|
||||
- **DOM events not cleaned up on scene restart:** Each `menu-scene.js` shutdown must unbind eventBus listeners. Use named function refs (not anonymous) so `off()` works.
|
||||
- **Race condition:** `CODE_GAME_STARTING` can arrive very quickly after `CODE_ROOM_JOIN_SUCCESS` in PVP (server auto-starts when 2 players join). MenuScene must handle both events without assuming a delay.
|
||||
|
||||
## Next Steps
|
||||
- Phase 4: GameScene + board rendering (depends on this phase)
|
||||
@@ -1,202 +0,0 @@
|
||||
# Phase 4: Game Scene + Board Rendering
|
||||
|
||||
## Context Links
|
||||
- [Plan overview](plan.md)
|
||||
- [Phase 3: Boot + Menu](phase-03-boot-menu-scenes.md)
|
||||
- Server move handler: `landlords-server/.../event/ServerEventListener_CODE_GAME_MOVE.java`
|
||||
- Server game starting: `landlords-server/.../event/ServerEventListener_CODE_GAME_STARTING.java`
|
||||
- Move success data: `{row, col, piece("BLACK"/"WHITE"), playerNickname, playerId}`
|
||||
- Board size: 15x15 (from `Board.BOARD_SIZE`)
|
||||
|
||||
## Overview
|
||||
- **Priority:** P1
|
||||
- **Status:** Pending
|
||||
- **Blocked by:** Phase 3
|
||||
- **Description:** Render the Gomoku board using Phaser Graphics, handle click-to-place, display player info and turn indicator via DOM panel.
|
||||
|
||||
## Key Insights
|
||||
- `CODE_GAME_STARTING` data: `{roomId, blackPlayerId, blackPlayerNickname, whitePlayerId, whitePlayerNickname, boardSize: 15}`
|
||||
- Player determines their color by comparing `gameState.clientId` to `blackPlayerId`
|
||||
- Turn alternates: black always first. Client tracks turn locally via move count (odd=black, even=white) or by checking `playerId` in move success
|
||||
- `CODE_GAME_MOVE_SUCCESS` is broadcast to BOTH players (and spectators). Client must render the move regardless of who made it.
|
||||
- Move errors (`OCCUPIED`, `OUT_OF_BOUNDS`, `NOT_YOUR_TURN`, `INVALID`) only sent to the player who attempted the invalid move
|
||||
|
||||
## Architecture
|
||||
|
||||
### Board Rendering (Phaser Graphics)
|
||||
```
|
||||
800x800 canvas
|
||||
Margin: 40px each side -> play area: 720x720
|
||||
Grid: 15x15 intersections (14 gaps)
|
||||
Cell size: 720 / 14 = ~51.4px
|
||||
Grid lines: Phaser.Graphics lines, color #8B7355 (wood brown)
|
||||
Star points: 5 dots at standard Gomoku positions (3,3), (3,11), (7,7), (11,3), (11,11)
|
||||
|
||||
Stone: Phaser.Graphics circle, radius ~22px
|
||||
BLACK: radial gradient fill #111 -> #333
|
||||
WHITE: radial gradient fill #fff -> #ddd
|
||||
Placement animation: scale tween 0 -> 1 over 150ms, ease 'Back.easeOut'
|
||||
|
||||
Last move indicator: small colored dot or ring on the most recent stone
|
||||
|
||||
Click detection: pointer event on canvas, snap to nearest intersection
|
||||
- Calculate (row, col) from pointer position
|
||||
- Reject if not player's turn (local check before sending)
|
||||
- Send CODE_GAME_MOVE with {row, col}
|
||||
```
|
||||
|
||||
### Coordinate Mapping
|
||||
```
|
||||
boardX(col) = MARGIN + col * CELL_SIZE
|
||||
boardY(row) = MARGIN + row * CELL_SIZE
|
||||
|
||||
colFromX(x) = Math.round((x - MARGIN) / CELL_SIZE)
|
||||
rowFromY(y) = Math.round((y - MARGIN) / CELL_SIZE)
|
||||
|
||||
Clamp to [0, 14] range
|
||||
```
|
||||
|
||||
### DOM Game Panel (game-ui.js)
|
||||
- Player info bar (top or side): black player name, white player name, highlight active turn
|
||||
- Move history panel (optional, scrollable): list of moves with row,col
|
||||
- Toast area for errors ("Not your turn!", "Position occupied")
|
||||
|
||||
## Related Code Files
|
||||
|
||||
### Files to Create
|
||||
- `web-client/src/scenes/game-scene.js`
|
||||
- `web-client/src/objects/board.js`
|
||||
- `web-client/src/objects/stone.js`
|
||||
- `web-client/src/ui/game-ui.js`
|
||||
|
||||
### Files to Modify
|
||||
- `web-client/src/config/game-config.js` -- add GameScene to scene list
|
||||
- `web-client/src/services/game-state-service.js` -- add board update methods
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### 1. `board.js` (~120 lines) -- Phaser GameObject
|
||||
|
||||
Board rendering class:
|
||||
- `constructor(scene, config)` -- config has margin, cellSize, boardSize
|
||||
- `drawGrid()` -- draw 15 horizontal + 15 vertical lines using `scene.add.graphics()`
|
||||
- `drawStarPoints()` -- 5 filled circles at standard positions
|
||||
- `drawLabels()` -- optional: row numbers (0-14) and column letters along edges
|
||||
- `getIntersection(pointerX, pointerY)` -- returns `{row, col}` snapped to nearest intersection, or null if too far from any intersection (tolerance: cellSize * 0.4)
|
||||
- `getCenterPosition(row, col)` -- returns `{x, y}` canvas coordinates
|
||||
- Constants: `MARGIN = 40`, `CELL_SIZE`, `BOARD_SIZE = 15`
|
||||
- Export class
|
||||
|
||||
### 2. `stone.js` (~60 lines) -- Phaser GameObject
|
||||
|
||||
Stone rendering:
|
||||
- `constructor(scene, x, y, piece)` -- piece is "BLACK" or "WHITE"
|
||||
- Draw filled circle with gradient-like effect:
|
||||
- Main circle with fill color
|
||||
- Smaller inner circle offset for 3D highlight effect
|
||||
- `playPlaceAnimation()` -- scale tween from 0 to 1, 150ms, Back.easeOut
|
||||
- `setLastMoveIndicator(show)` -- add/remove small red dot at center
|
||||
- Export class
|
||||
|
||||
### 3. `game-scene.js` (~150 lines)
|
||||
|
||||
```
|
||||
constructor: super({ key: 'GameScene' })
|
||||
|
||||
create():
|
||||
- Read game info from gameState (set by MenuScene before transition)
|
||||
- Create Board object
|
||||
- Create stones container (empty initially)
|
||||
- Setup game-ui.js DOM panel (player names, turn indicator)
|
||||
- Register eventBus listeners:
|
||||
- CODE_GAME_MOVE_SUCCESS: placeStone(data)
|
||||
- CODE_GAME_MOVE_OCCUPIED: gameUi.showToast('Position occupied')
|
||||
- CODE_GAME_MOVE_OUT_OF_BOUNDS: gameUi.showToast('Out of bounds')
|
||||
- CODE_GAME_MOVE_NOT_YOUR_TURN: gameUi.showToast('Not your turn')
|
||||
- CODE_GAME_MOVE_INVALID: gameUi.showToast('Invalid move')
|
||||
- CODE_GAME_OVER: handled in Phase 5
|
||||
- CODE_CLIENT_EXIT: opponent left, show message, return to menu
|
||||
- CODE_CLIENT_KICK: kicked for idle, return to menu
|
||||
- Setup pointer click handler on canvas
|
||||
|
||||
handleClick(pointer):
|
||||
- If spectating, ignore
|
||||
- If not my turn (local check), ignore (avoid unnecessary server round-trip)
|
||||
- board.getIntersection(pointer.x, pointer.y) -> {row, col}
|
||||
- If null (clicked too far from intersection), ignore
|
||||
- If gameState.board[row][col] != null (local occupied check), ignore
|
||||
- connectionService.send(CODE_GAME_MOVE, JSON.stringify({row, col}))
|
||||
|
||||
placeStone(data):
|
||||
- {row, col, piece, playerNickname, playerId} = data
|
||||
- pos = board.getCenterPosition(row, col)
|
||||
- Create new Stone(scene, pos.x, pos.y, piece)
|
||||
- stone.playPlaceAnimation()
|
||||
- Remove last-move indicator from previous stone, add to this one
|
||||
- gameState.board[row][col] = piece
|
||||
- gameState.moves.push(data)
|
||||
- Update turn: gameState.isMyTurn = (data.playerId !== gameState.clientId)
|
||||
- gameUi.updateTurn(gameState.isMyTurn)
|
||||
- gameUi.addMoveToHistory(data)
|
||||
|
||||
shutdown():
|
||||
- Unsubscribe all eventBus listeners
|
||||
- gameUi.hide()
|
||||
```
|
||||
|
||||
### 4. `game-ui.js` (~100 lines)
|
||||
|
||||
DOM manipulation for in-game panels:
|
||||
- `show(blackName, whiteName, isBlack)` -- create/show player info bar
|
||||
- Two player cards: name + piece color indicator
|
||||
- Highlight current turn
|
||||
- `updateTurn(isMyTurn)` -- toggle highlight between player cards, show "Your turn" / "Opponent's turn"
|
||||
- `addMoveToHistory(moveData)` -- append to scrollable move list
|
||||
- `showToast(msg)` -- temporary error message, auto-fade after 2s
|
||||
- `hide()` -- remove DOM elements
|
||||
|
||||
Add DOM containers to `index.html`:
|
||||
```html
|
||||
<div id="game-panel" class="hidden">
|
||||
<div id="player-info"></div>
|
||||
<div id="move-history"></div>
|
||||
<div id="toast-container"></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 5. Update `game-state-service.js`
|
||||
|
||||
Add methods:
|
||||
- `initGame(startingData)` -- set roomId, player IDs/names, isBlack, isMyTurn (black goes first), init board 15x15 nulls
|
||||
- `updateBoard(row, col, piece)` -- set board[row][col]
|
||||
- `isMyTurn` getter based on current state
|
||||
|
||||
## Todo List
|
||||
- [ ] Create `board.js` with grid, star points, coordinate mapping
|
||||
- [ ] Create `stone.js` with circle rendering and placement tween
|
||||
- [ ] Create `game-ui.js` with player info, turn indicator, toast
|
||||
- [ ] Create `game-scene.js` wiring board + stones + events + clicks
|
||||
- [ ] Add `#game-panel` DOM structure to `index.html`
|
||||
- [ ] Update `game-config.js` scene list
|
||||
- [ ] Update `game-state-service.js` with game init and board methods
|
||||
- [ ] Test: click to place stone, see it appear after server confirms
|
||||
- [ ] Test: opponent's move appears with animation
|
||||
- [ ] Test: error toasts on invalid moves
|
||||
|
||||
## Success Criteria
|
||||
- [ ] 15x15 board renders with grid lines and star points
|
||||
- [ ] Clicking intersection sends move to server
|
||||
- [ ] Confirmed moves appear as stones with placement animation
|
||||
- [ ] Last move indicator visible
|
||||
- [ ] Turn indicator updates correctly
|
||||
- [ ] Error toasts display and auto-dismiss
|
||||
- [ ] Spectator mode: board renders moves but clicks are ignored
|
||||
- [ ] All files under 150 lines, JSDoc on exports
|
||||
|
||||
## Risk Assessment
|
||||
- **Click precision on small screens:** `CELL_SIZE` may be small on mobile. Mitigation: Phaser `Scale.FIT` + generous snap tolerance (40% of cell size).
|
||||
- **Rapid successive moves in PVE:** AI responds instantly, two `CODE_GAME_MOVE_SUCCESS` events arrive back-to-back. Ensure `placeStone` is idempotent and animation queue doesn't break. Each stone is an independent tween -- no sequential dependency needed.
|
||||
- **Canvas vs DOM event conflict:** Phaser pointer events and DOM overlay events can conflict. Mitigation: hide DOM overlays when GameScene is active; game-panel is positioned outside the canvas clickable area (above or beside).
|
||||
|
||||
## Next Steps
|
||||
- Phase 5: Game over + spectator (depends on this phase)
|
||||
@@ -1,227 +0,0 @@
|
||||
# Phase 5: Game Over + Spectator Mode
|
||||
|
||||
## Context Links
|
||||
- [Plan overview](plan.md)
|
||||
- [Phase 4: Game Scene](phase-04-game-scene-board.md)
|
||||
- Server game over: `landlords-server/.../event/ServerEventListener_CODE_GAME_MOVE.java` lines 115-131
|
||||
- Server ready handler: `landlords-server/.../event/ServerEventListener_CODE_GAME_READY.java`
|
||||
- Server watch handler: `landlords-server/.../event/ServerEventListener_CODE_GAME_WATCH.java`
|
||||
- Spectator event wrapper: `ClientEventListener_CODE_GAME_WATCH.java` -- `{code, data}` wrapping
|
||||
|
||||
## Overview
|
||||
- **Priority:** P2
|
||||
- **Status:** Pending
|
||||
- **Blocked by:** Phase 4
|
||||
- **Description:** Handle game end (win/lose/draw), rematch flow, opponent disconnect, and spectator mode viewing.
|
||||
|
||||
## Key Insights
|
||||
|
||||
### Game Over
|
||||
- `CODE_GAME_OVER` data: `{result: "BLACK_WIN"|"WHITE_WIN"|"DRAW", winnerNickname: string, board: string}`
|
||||
- Sent to both players and spectators
|
||||
- After game over, players can send `CODE_GAME_READY` to signal rematch willingness
|
||||
- `CODE_GAME_READY` response: `{clientNickName, status: "READY"|"NO_READY", clientId}`
|
||||
- Ready is a toggle -- sending again toggles back to NO_READY
|
||||
- When both players are READY, server auto-fires `CODE_GAME_STARTING` again (new game, same room)
|
||||
|
||||
### Opponent Disconnect
|
||||
- `CODE_CLIENT_EXIT` data (to player): `{roomId, exitClientId, exitClientNickname}` -- room is destroyed server-side
|
||||
- `CODE_CLIENT_KICK` data: client nickname string -- idle kick, room destroyed
|
||||
- After either, client should return to lobby (server sends `CODE_SHOW_OPTIONS` after cleanup)
|
||||
|
||||
### Spectator Mode
|
||||
- Spectator receives `CODE_GAME_WATCH` events wrapping inner events: `{code: "CODE_...", data: ...}`
|
||||
- Inner codes: `CODE_ROOM_JOIN_SUCCESS`, `CODE_GAME_STARTING`, `CODE_GAME_MOVE_SUCCESS`, `CODE_CLIENT_EXIT`, `CODE_CLIENT_KICK`, `CODE_GAME_OVER`
|
||||
- Spectator also receives `CODE_GAME_WATCH_SUCCESSFUL` directly: `{owner, status}`
|
||||
- Spectator exits by sending `CODE_GAME_WATCH_EXIT`, then navigates back to menu
|
||||
- Spectator CANNOT make moves, ready up, or interact with game
|
||||
|
||||
## Architecture
|
||||
|
||||
### Game Over Flow
|
||||
```
|
||||
CODE_GAME_OVER received
|
||||
-> GameScene: disable click handler
|
||||
-> Show GameOverScene (overlay or scene transition)
|
||||
- Display: "You Win!" / "You Lose!" / "Draw!"
|
||||
- Winner name
|
||||
- Buttons: [Rematch] [Exit to Lobby]
|
||||
|
||||
[Rematch] clicked:
|
||||
-> send CODE_GAME_READY
|
||||
-> show "Waiting for opponent..." or "Both ready!"
|
||||
-> on CODE_GAME_READY from opponent: update UI to show their ready status
|
||||
-> on CODE_GAME_STARTING: clear overlay, reset board, start new game in GameScene
|
||||
|
||||
[Exit] clicked:
|
||||
-> send CODE_CLIENT_EXIT
|
||||
-> gameState.reset()
|
||||
-> scene.start('MenuScene')
|
||||
```
|
||||
|
||||
### Spectator Flow
|
||||
```
|
||||
MenuScene: user clicks Watch on a room
|
||||
-> send CODE_GAME_WATCH with roomId
|
||||
-> receive CODE_GAME_WATCH_SUCCESSFUL: {owner, status}
|
||||
-> transition to GameScene with isSpectating=true
|
||||
|
||||
GameScene (spectator):
|
||||
- Board renders normally
|
||||
- Click handler disabled
|
||||
- Player info shows "Spectating" badge
|
||||
- Move events come wrapped in CODE_GAME_WATCH: {code, data}
|
||||
- Unwrap and handle inner code normally (reuse placeStone, etc.)
|
||||
- On game over: show result, offer [Exit Spectating] button
|
||||
- Exit: send CODE_GAME_WATCH_EXIT, return to MenuScene
|
||||
```
|
||||
|
||||
### Spectator Event Unwrapping (in game-scene.js)
|
||||
```js
|
||||
eventBus.on(ClientEventCode.GAME_WATCH, (wrapData) => {
|
||||
const innerCode = wrapData.code;
|
||||
const innerData = typeof wrapData.data === 'string'
|
||||
? tryParseJson(wrapData.data)
|
||||
: wrapData.data;
|
||||
// Route to existing handlers
|
||||
this.handleServerEvent(innerCode, innerData);
|
||||
});
|
||||
```
|
||||
|
||||
## Related Code Files
|
||||
|
||||
### Files to Create
|
||||
- `web-client/src/scenes/game-over-scene.js`
|
||||
|
||||
### Files to Modify
|
||||
- `web-client/src/scenes/game-scene.js` -- add game over, exit, kick, spectator event handlers
|
||||
- `web-client/src/ui/game-ui.js` -- add game over overlay, spectator badge
|
||||
- `web-client/src/config/game-config.js` -- add GameOverScene to scene list
|
||||
- `web-client/index.html` -- add game-over DOM overlay structure
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### 1. Game Over DOM overlay (in `index.html`)
|
||||
|
||||
```html
|
||||
<div id="game-over-overlay" class="hidden">
|
||||
<div class="game-over-card">
|
||||
<h1 id="game-result-text"></h1>
|
||||
<p id="game-result-detail"></p>
|
||||
<div id="rematch-status" class="hidden">
|
||||
<p>You: <span id="my-ready-status">Not Ready</span></p>
|
||||
<p>Opponent: <span id="opponent-ready-status">Not Ready</span></p>
|
||||
</div>
|
||||
<div class="game-over-buttons">
|
||||
<button id="btn-rematch">Rematch</button>
|
||||
<button id="btn-exit-game">Exit to Lobby</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 2. `game-over-scene.js` (~80 lines)
|
||||
|
||||
Decision: use DOM overlay managed by game-ui.js rather than a separate Phaser scene. The board should remain visible behind the overlay. Rename this to game-over logic inside `game-ui.js` instead.
|
||||
|
||||
Actually -- implement as **methods in `game-ui.js`** rather than a separate scene. This avoids scene transition complexity and keeps the board visible.
|
||||
|
||||
- `showGameOver(result, winnerNickname, isWinner, isDraw)` -- populate and show overlay
|
||||
- `showRematchStatus(myReady, opponentReady)` -- update ready indicators
|
||||
- `hideGameOver()` -- hide overlay for new game
|
||||
- `showSpectatorGameOver(result, winnerNickname)` -- simplified: result + exit button only
|
||||
|
||||
Remove `game-over-scene.js` from plan. Update `game-config.js` -- no extra scene needed.
|
||||
|
||||
### 3. Update `game-scene.js` -- Game Over Handling (~30 lines added)
|
||||
|
||||
```
|
||||
on CODE_GAME_OVER:
|
||||
- Parse result, determine if current player won
|
||||
- Disable click handler
|
||||
- gameUi.showGameOver(result, winnerNickname, isWinner, isDraw)
|
||||
- Wire rematch button: send CODE_GAME_READY, show rematch status
|
||||
|
||||
on CODE_GAME_READY:
|
||||
- Update rematch status display
|
||||
- If both ready, server will send CODE_GAME_STARTING
|
||||
|
||||
on CODE_GAME_STARTING (during rematch):
|
||||
- gameState.resetBoard()
|
||||
- Clear all stone objects from scene
|
||||
- gameUi.hideGameOver()
|
||||
- Re-init board state (new black/white assignment)
|
||||
- Re-enable click handler
|
||||
```
|
||||
|
||||
### 4. Update `game-scene.js` -- Exit/Kick Handling (~15 lines added)
|
||||
|
||||
```
|
||||
on CODE_CLIENT_EXIT:
|
||||
- gameUi.showToast(exitClientNickname + ' left the game')
|
||||
- After 2s delay: gameState.reset(), scene.start('MenuScene')
|
||||
|
||||
on CODE_CLIENT_KICK:
|
||||
- gameUi.showToast('Kicked for being idle')
|
||||
- gameState.reset(), scene.start('MenuScene')
|
||||
```
|
||||
|
||||
### 5. Update `game-scene.js` -- Spectator Support (~25 lines added)
|
||||
|
||||
```
|
||||
create():
|
||||
- if gameState.isSpectating:
|
||||
- Disable click handler
|
||||
- gameUi.showSpectatorBadge()
|
||||
- Register CODE_GAME_WATCH listener (unwrap + route)
|
||||
|
||||
on CODE_GAME_WATCH:
|
||||
- Unwrap {code, data}
|
||||
- Route to existing handlers (placeStone, game over, etc.)
|
||||
|
||||
Spectator exit button:
|
||||
- send CODE_GAME_WATCH_EXIT
|
||||
- gameState.reset()
|
||||
- scene.start('MenuScene')
|
||||
```
|
||||
|
||||
### 6. Update `game-ui.js` -- New Methods (~50 lines added)
|
||||
|
||||
- `showGameOver(...)` -- show overlay with result text
|
||||
- `hideGameOver()` -- hide overlay
|
||||
- `showRematchStatus(myReady, opponentReady)` -- toggle ready indicators
|
||||
- `showSpectatorBadge()` -- add "SPECTATING" label to player info
|
||||
- `showSpectatorExit()` -- add exit button for spectators
|
||||
|
||||
## Todo List
|
||||
- [ ] Add game-over DOM overlay to `index.html`
|
||||
- [ ] Add game over methods to `game-ui.js`
|
||||
- [ ] Add `CODE_GAME_OVER` handler in `game-scene.js`
|
||||
- [ ] Add rematch flow (`CODE_GAME_READY` send + receive) in `game-scene.js`
|
||||
- [ ] Add rematch restart logic (clear board, re-init) in `game-scene.js`
|
||||
- [ ] Add `CODE_CLIENT_EXIT` and `CODE_CLIENT_KICK` handlers in `game-scene.js`
|
||||
- [ ] Add spectator event unwrapping (`CODE_GAME_WATCH`) in `game-scene.js`
|
||||
- [ ] Add spectator UI elements (badge, exit button) to `game-ui.js`
|
||||
- [ ] Test: win/lose/draw displays correctly
|
||||
- [ ] Test: rematch flow -- both ready -> new game starts
|
||||
- [ ] Test: opponent exits mid-game -> return to lobby
|
||||
- [ ] Test: spectator sees moves and game over
|
||||
|
||||
## Success Criteria
|
||||
- [ ] Game over overlay shows correct result (win/lose/draw)
|
||||
- [ ] Rematch button sends ready signal, UI shows both players' ready state
|
||||
- [ ] New game starts with cleared board when both ready
|
||||
- [ ] Exit button sends `CODE_CLIENT_EXIT` and returns to lobby
|
||||
- [ ] Opponent disconnect shows notification and returns to lobby after delay
|
||||
- [ ] Spectator sees all moves in real-time, cannot interact
|
||||
- [ ] Spectator exit sends `CODE_GAME_WATCH_EXIT` and returns to lobby
|
||||
- [ ] `game-scene.js` stays under 200 lines total
|
||||
- [ ] `game-ui.js` stays under 200 lines total
|
||||
|
||||
## Risk Assessment
|
||||
- **Rematch player color swap:** Server re-assigns black/white in `CODE_GAME_STARTING`. Client must NOT assume same colors. Re-read `blackPlayerId` from new starting data.
|
||||
- **Stale event listeners after rematch:** Board reset clears stones but scene is NOT restarted. Event listeners persist, which is correct -- no unbind/rebind needed.
|
||||
- **Spectator joining mid-game:** Server sends `CODE_GAME_WATCH_SUCCESSFUL` with room status but does NOT replay past moves. Spectator sees the board from their join point onward. This is a server limitation -- document it, don't try to work around.
|
||||
|
||||
## Next Steps
|
||||
- Phase 6: Polish + error handling (cross-cutting)
|
||||
@@ -1,170 +0,0 @@
|
||||
# Phase 6: Polish + Error Handling
|
||||
|
||||
## Context Links
|
||||
- [Plan overview](plan.md)
|
||||
- [Phase 2: Services](phase-02-services-layer.md) -- reconnect logic lives here
|
||||
- [Phase 3: Menu](phase-03-boot-menu-scenes.md) -- toast system
|
||||
- [Phase 4: Game](phase-04-game-scene-board.md) -- game-ui.js toast
|
||||
|
||||
## Overview
|
||||
- **Priority:** P2
|
||||
- **Status:** Pending
|
||||
- **Blocked by:** Phase 5
|
||||
- **Description:** Cross-cutting improvements: connection lost overlay, reconnect UX, hover effects, sound placeholders, input validation hardening, manual integration test checklist.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### 1. Connection Lost Overlay (~20 lines in `game-ui.js`)
|
||||
|
||||
Add to `index.html`:
|
||||
```html
|
||||
<div id="connection-lost" class="hidden">
|
||||
<div class="connection-card">
|
||||
<h2>Connection Lost</h2>
|
||||
<p>Reconnecting<span id="reconnect-dots">...</span></p>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
- `eventBus.on('ws:disconnected')` -> show overlay (full-screen semi-transparent)
|
||||
- `eventBus.on('ws:connected')` -> hide overlay
|
||||
- If reconnect succeeds, server treats it as a new client (old room is gone). Redirect to BootScene.
|
||||
|
||||
### 2. Board Hover Effect (~15 lines in `game-scene.js`)
|
||||
|
||||
- On `pointermove`: calculate nearest intersection
|
||||
- If valid + empty + my turn: draw a semi-transparent stone preview at that position
|
||||
- On `pointerout` or click: clear preview
|
||||
- Use a single reusable Graphics object for the preview (no object churn)
|
||||
|
||||
### 3. Input Validation Hardening
|
||||
|
||||
**Nickname input (`menu-ui.js`):**
|
||||
- Trim whitespace
|
||||
- Reject empty or >10 chars client-side before sending (avoid server round-trip)
|
||||
- Disable submit button while waiting for server response
|
||||
|
||||
**Click debounce (`game-scene.js`):**
|
||||
- After sending a move, set `awaitingResponse = true`
|
||||
- On `CODE_GAME_MOVE_SUCCESS` or any move error: set `awaitingResponse = false`
|
||||
- Reject clicks while `awaitingResponse` is true
|
||||
- Prevents double-click sending duplicate moves
|
||||
|
||||
### 4. Toast System Consolidation
|
||||
|
||||
Both `menu-ui.js` and `game-ui.js` need toast capability. Extract shared toast logic:
|
||||
- Add `#toast-container` to `index.html` at top level (outside scene-specific containers)
|
||||
- Create `showToast(msg, type='error', duration=2500)` function in `game-ui.js` (reuse from menu too)
|
||||
- Types: `error` (red), `info` (blue), `success` (green)
|
||||
- Auto-dismiss with CSS fade-out animation
|
||||
|
||||
### 5. Keyboard Shortcuts
|
||||
|
||||
- `Escape` during game: show confirmation "Exit game?" dialog
|
||||
- `Enter` on nickname input: submit (already handled if form has submit event)
|
||||
|
||||
### 6. WS URL Configuration
|
||||
|
||||
- Check `?ws=` URL parameter for custom server address
|
||||
- Fallback: `ws://localhost:1025/ratel`
|
||||
- Display connected server address in BootScene
|
||||
- Example: `http://localhost:5173/?ws=ws://192.168.1.5:1025/ratel`
|
||||
|
||||
### 7. Responsive Layout
|
||||
|
||||
- Phaser `Scale.FIT` handles canvas scaling
|
||||
- DOM overlays: use viewport-relative units (vh/vw) and max-width for panels
|
||||
- Test at 800x800, 1920x1080, 1366x768 browser sizes
|
||||
- Mobile: not a priority but should not break entirely
|
||||
|
||||
## Integration Test Checklist (Manual)
|
||||
|
||||
Run server: `java -jar landlords-server/target/landlords-server-*.jar -p 1024`
|
||||
(WebSocket will be on port 1025)
|
||||
|
||||
Open client: `http://localhost:5173/?ws=ws://localhost:1025/ratel`
|
||||
|
||||
### Connection Flow
|
||||
- [ ] Client connects, shows "Connecting..."
|
||||
- [ ] After ~2s, nickname prompt appears
|
||||
- [ ] Enter valid nickname (1-10 chars), lobby appears
|
||||
- [ ] Enter invalid nickname (empty / >10), error shown, re-prompted
|
||||
|
||||
### PVP Flow
|
||||
- [ ] Create room, waiting screen shows room ID
|
||||
- [ ] Open second browser tab, join room by ID
|
||||
- [ ] Game starts, board renders with player info
|
||||
- [ ] Black player moves first, click places stone
|
||||
- [ ] White player's turn, black player click rejected locally
|
||||
- [ ] Play until 5-in-a-row, game over screen shows winner
|
||||
- [ ] Both click Rematch, new game starts with cleared board
|
||||
- [ ] One player exits, other returns to lobby
|
||||
- [ ] Test room list refresh shows available rooms
|
||||
|
||||
### PVE Flow
|
||||
- [ ] Select PVE, choose difficulty
|
||||
- [ ] Game starts immediately (player is black)
|
||||
- [ ] Place stone, AI responds automatically
|
||||
- [ ] Play until game over
|
||||
- [ ] Exit returns to lobby
|
||||
|
||||
### Spectator Flow
|
||||
- [ ] Start a PVP game in two tabs
|
||||
- [ ] Third tab: spectate the room
|
||||
- [ ] Spectator sees moves in real-time
|
||||
- [ ] Spectator cannot click to place stones
|
||||
- [ ] Game over shown to spectator
|
||||
- [ ] Spectator exits, returns to lobby
|
||||
|
||||
### Error Handling
|
||||
- [ ] Close server: "Connection Lost" overlay appears
|
||||
- [ ] Restart server: client reconnects, returns to nickname prompt
|
||||
- [ ] Join non-existent room: error toast
|
||||
- [ ] Join full room: error toast
|
||||
- [ ] Rapid-click same position: no duplicate requests
|
||||
|
||||
## Todo List
|
||||
- [ ] Add connection-lost overlay to `index.html` and wire in `game-ui.js`
|
||||
- [ ] Add board hover preview in `game-scene.js`
|
||||
- [ ] Add click debounce in `game-scene.js`
|
||||
- [ ] Add client-side nickname validation in `menu-ui.js`
|
||||
- [ ] Consolidate toast system in `game-ui.js`
|
||||
- [ ] Add WS URL parameter support in `boot-scene.js`
|
||||
- [ ] Add Escape key handler in `game-scene.js`
|
||||
- [ ] Test responsive layout at multiple sizes
|
||||
- [ ] Run full integration test checklist
|
||||
|
||||
## Success Criteria
|
||||
- [ ] Connection lost overlay appears/disappears correctly
|
||||
- [ ] Hover preview shows semi-transparent stone at valid positions
|
||||
- [ ] No duplicate move requests on rapid clicks
|
||||
- [ ] All integration test checklist items pass
|
||||
- [ ] No console errors during normal gameplay
|
||||
- [ ] All JS files remain under 200 lines
|
||||
|
||||
## Risk Assessment
|
||||
- **Reconnect creates new identity:** Server has no session resumption. This is a known limitation. After reconnect, user must re-enter nickname and rejoin. Document this, don't over-engineer.
|
||||
- **Hover performance:** Redrawing preview on every pointermove could lag. Mitigation: use a single Graphics object, clear+redraw only when intersection changes (cache last hover position).
|
||||
|
||||
## File Ownership Summary (All Phases)
|
||||
|
||||
| File | Owner Phase | Touched By |
|
||||
|------|------------|------------|
|
||||
| `package.json` | 1 | 1 only |
|
||||
| `vite.config.js` | 1 | 1 only |
|
||||
| `index.html` | 1 | 3, 4, 5, 6 (DOM additions) |
|
||||
| `main.js` | 1 | 1 only |
|
||||
| `game-config.js` | 1 | 3, 4 (scene list) |
|
||||
| `protocol-constants.js` | 2 | 2 only |
|
||||
| `event-bus.js` | 2 | 2 only |
|
||||
| `connection-service.js` | 2 | 2, 6 (reconnect polish) |
|
||||
| `game-state-service.js` | 2 | 2, 4, 5 (game methods) |
|
||||
| `boot-scene.js` | 3 | 3, 6 (WS URL param) |
|
||||
| `menu-scene.js` | 3 | 3 only |
|
||||
| `menu-ui.js` | 3 | 3, 6 (validation) |
|
||||
| `board.js` | 4 | 4 only |
|
||||
| `stone.js` | 4 | 4 only |
|
||||
| `game-scene.js` | 4 | 4, 5, 6 (game over, spectator, hover) |
|
||||
| `game-ui.js` | 4 | 4, 5, 6 (game over overlay, toast, connection) |
|
||||
|
||||
Note: Phases are sequential so file ownership conflicts are not possible.
|
||||
@@ -1,101 +0,0 @@
|
||||
---
|
||||
title: "Phaser 3 Web Client for Gomoku"
|
||||
description: "Standalone Phaser 3 + Vite web client connecting to existing Netty server via WebSocket"
|
||||
status: pending
|
||||
priority: P1
|
||||
effort: 12h
|
||||
branch: master
|
||||
tags: [web-client, phaser3, vite, gomoku, websocket]
|
||||
created: 2026-04-10
|
||||
---
|
||||
|
||||
# Phaser 3 Web Client for Gomoku
|
||||
|
||||
## Overview
|
||||
|
||||
Build a standalone web client using Phaser 3 (game engine) + Vite (build tool) + vanilla JavaScript (with JSDoc). Connects to existing Netty server at `ws://host:port/ratel`. Server owns all game logic; client is display + input only.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Browser
|
||||
+-- Phaser 3 Game (canvas: board rendering, stones, animations)
|
||||
+-- DOM Overlays (HTML/CSS: menus, forms, lobby, toasts)
|
||||
+-- Services (JS modules, not Phaser-coupled)
|
||||
+-- connection-service.js (WebSocket I/O + heartbeat)
|
||||
+-- event-bus.js (pub/sub decoupling)
|
||||
+-- game-state-service.js (clientId, roomId, turn, board state)
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
User click -> GameScene -> connection-service.send(CODE_GAME_MOVE, {row, col})
|
||||
|
|
||||
v
|
||||
WebSocket -> Server
|
||||
|
|
||||
v
|
||||
Server -> WebSocket -> connection-service.onMessage -> event-bus.emit(code, data)
|
||||
|
|
||||
v
|
||||
GameScene listener -> update board, play animation
|
||||
```
|
||||
|
||||
## Phases
|
||||
|
||||
| # | Phase | Status | Effort | Files |
|
||||
|---|-------|--------|--------|-------|
|
||||
| 1 | [Project scaffold](phase-01-project-scaffold.md) | Pending | 1h | package.json, vite.config.js, index.html, main.js, game-config.js |
|
||||
| 2 | [Services layer](phase-02-services-layer.md) | Pending | 2h | connection-service.js, event-bus.js, game-state-service.js, protocol-constants.js |
|
||||
| 3 | [Boot + Menu scenes](phase-03-boot-menu-scenes.md) | Pending | 2.5h | boot-scene.js, menu-scene.js, menu-ui.js, styles in index.html |
|
||||
| 4 | [Game scene + board](phase-04-game-scene-board.md) | Pending | 3h | game-scene.js, board.js, stone.js, game-ui.js |
|
||||
| 5 | [Game over + spectator](phase-05-gameover-spectator.md) | Pending | 2h | DOM overlay in game-ui.js, updates to game-scene.js |
|
||||
| 6 | [Polish + error handling](phase-06-polish-errors.md) | Pending | 1.5h | cross-cutting updates, reconnect logic, toast system |
|
||||
|
||||
## Dependency Graph
|
||||
|
||||
```
|
||||
Phase 1 (scaffold)
|
||||
+-> Phase 2 (services) -- no Phaser dependency, can start after scaffold
|
||||
+-> Phase 3 (boot+menu) -- needs scaffold + services
|
||||
+-> Phase 4 (game scene) -- needs menu to navigate + services for WS
|
||||
+-> Phase 5 (game over + spectator) -- needs game scene
|
||||
+-> Phase 6 (polish) -- cross-cutting, touches all
|
||||
```
|
||||
|
||||
## Key Decisions
|
||||
|
||||
1. **DOM overlays for menus** -- Phaser text/buttons too limited for forms and tables. Standard Phaser practice.
|
||||
2. **Plain WebSocket API** -- No socket.io. Server uses raw WS frames. `connection-service.js` wraps reconnect + heartbeat.
|
||||
3. **Event bus decoupling** -- Scenes subscribe to game events via event-bus, not direct WS references. Testable, replaceable.
|
||||
4. **No TypeScript** -- User preference. JSDoc `@typedef` for type documentation.
|
||||
5. **15x15 board** -- Hardcoded from server `Board.BOARD_SIZE = 15`. Client reads `boardSize` from `CODE_GAME_STARTING` anyway.
|
||||
6. **3 Phaser scenes only** -- BootScene, MenuScene, GameScene. Game over is a DOM overlay on GameScene (keeps board visible behind result card). No GameOverScene needed.
|
||||
|
||||
## Backwards Compatibility
|
||||
|
||||
- **Server: zero changes.** Client connects via existing WS endpoint `/ratel`.
|
||||
- **Existing Java client: unaffected.** Web client is additive.
|
||||
- **Protocol verified** from server source: `Msg{code, data, info}` JSON format.
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
- `web-client/` is a standalone directory. `rm -rf web-client/` to revert.
|
||||
- No server code modified. No migration needed.
|
||||
|
||||
## Test Strategy
|
||||
|
||||
- **Manual integration test** with running server (Phase 6)
|
||||
- **Browser dev tools** for WS frame inspection
|
||||
- No unit test framework initially (YAGNI -- thin UI client with server-owned logic)
|
||||
- If needed later: Vitest for service modules
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|------|-----------|--------|------------|
|
||||
| WS message format mismatch | Low | High | Protocol constants extracted from server source; verify with live server in Phase 2 |
|
||||
| Phaser canvas sizing on different screens | Medium | Medium | Use Phaser `Scale.FIT` + responsive CSS container |
|
||||
| Server heartbeat timeout (<60s idle) | Low | High | Client sends `CODE_CLIENT_HEAD_BEAT` every 50s via setInterval |
|
||||
| DOM overlay z-index conflicts with Phaser | Medium | Low | Explicit z-index layering; hide overlays during game scene |
|
||||
@@ -1,115 +0,0 @@
|
||||
# Phase 1 — Deletions (CLI client, static web UI, i18n)
|
||||
|
||||
## Context Links
|
||||
- Overview: [plan.md](plan.md)
|
||||
- Next: [phase-02-consolidate-java-sources.md](phase-02-consolidate-java-sources.md)
|
||||
- Docs touched later: `docs/codebase-summary.md`, `docs/system-architecture.md`
|
||||
|
||||
## Overview
|
||||
- **Priority:** P2
|
||||
- **Status:** pending
|
||||
- **Description:** Remove dead code: the Java CLI client module, the i18n helper only it used, the legacy static web UI served from `landlords-server/src/main/resources/static/` and its handler. Leaves repo structurally identical to today minus dead weight, still buildable via parent POM.
|
||||
|
||||
## Key Insights
|
||||
- `SimplePrinter.printTranslate()` is grep-verified unused after CLI client deletion — safe removal.
|
||||
- `I18nHelper.enable()` only called from `SimpleClient.java` (CLI) — safe removal.
|
||||
- `StaticFileHandler` is only registered in `WebsocketProxy` pipeline; removing it means non-WS HTTP requests return default Netty WS error, which is acceptable (locked decision #2).
|
||||
- Phase 1 does NOT restructure — parent `pom.xml` still exists, module list shrinks by one.
|
||||
|
||||
## Requirements
|
||||
**Functional**
|
||||
- Repo must still `mvn -pl landlords-server -am clean verify` after phase.
|
||||
- Server must still boot, accept WebSocket connections, play a full game against the current web-client.
|
||||
|
||||
**Non-functional**
|
||||
- No behavior change for WS clients.
|
||||
- Single commit (or small atomic series) so rollback is `git reset --hard HEAD~1`.
|
||||
|
||||
## Architecture
|
||||
- Current: `parent pom → {landlords-common, landlords-server, landlords-client}`
|
||||
- After phase: `parent pom → {landlords-common, landlords-server}`
|
||||
- WebsocketProxy pipeline loses `StaticFileHandler`; keeps `HttpServerCodec`, `HttpObjectAggregator`, `WebSocketServerProtocolHandler`, `MessageToMessageCodec`, `ChannelInputHandler`.
|
||||
|
||||
## Related Code Files
|
||||
|
||||
**Delete (directories):**
|
||||
- `landlords-client/` (entire module)
|
||||
- `landlords-server/src/main/resources/static/` (css, js, index.html)
|
||||
|
||||
**Delete (files):**
|
||||
- `landlords-common/src/main/java/org/nico/ratel/landlords/helper/I18nHelper.java`
|
||||
- `landlords-common/src/main/resources/messages_en_US.properties`
|
||||
- `landlords-server/src/main/java/org/nico/ratel/landlords/server/handler/StaticFileHandler.java`
|
||||
|
||||
**Modify:**
|
||||
- `landlords-common/src/main/java/org/nico/ratel/landlords/print/SimplePrinter.java` — delete the `printTranslate(...)` method(s); keep `printNotice` + `serverLog`
|
||||
- `landlords-server/src/main/java/org/nico/ratel/landlords/server/proxy/WebsocketProxy.java` — remove `StaticFileHandler` import + `.addLast(new StaticFileHandler())` line (~line 48)
|
||||
- `pom.xml` (root) — remove `<module>landlords-client</module>`
|
||||
- `landlords-server/Dockerfile` — remove `COPY landlords-client/...` lines (if any)
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Create branch checkpoint: ensure clean tree (`git status`), note HEAD sha.
|
||||
2. `git rm -r landlords-client`
|
||||
3. `git rm landlords-common/src/main/java/org/nico/ratel/landlords/helper/I18nHelper.java`
|
||||
4. `git rm landlords-common/src/main/resources/messages_en_US.properties`
|
||||
5. Open `landlords-common/.../print/SimplePrinter.java`, delete `printTranslate` method(s). Verify no other references remain: `grep -rn "printTranslate" landlords-common landlords-server`.
|
||||
6. `git rm landlords-server/src/main/java/org/nico/ratel/landlords/server/handler/StaticFileHandler.java`
|
||||
7. `git rm -r landlords-server/src/main/resources/static`
|
||||
8. Open `landlords-server/.../proxy/WebsocketProxy.java`:
|
||||
- Remove `import ...server.handler.StaticFileHandler;`
|
||||
- Remove `.addLast(new StaticFileHandler())` line
|
||||
9. Open root `pom.xml`, delete `<module>landlords-client</module>` line.
|
||||
10. Open `landlords-server/Dockerfile`, remove any `COPY landlords-client ...` lines.
|
||||
11. `grep -rn "I18nHelper\|printTranslate\|StaticFileHandler\|messages_en_US" .` — must return zero (or only matches inside already-deleted paths).
|
||||
12. Run validation commands.
|
||||
13. Commit: `refactor: delete CLI client, i18n helper, and legacy static web UI`
|
||||
|
||||
## Todo List
|
||||
- [ ] Delete `landlords-client/` directory
|
||||
- [ ] Delete `I18nHelper.java`
|
||||
- [ ] Delete `messages_en_US.properties`
|
||||
- [ ] Remove `printTranslate()` method from `SimplePrinter.java`
|
||||
- [ ] Delete `StaticFileHandler.java`
|
||||
- [ ] Delete `landlords-server/src/main/resources/static/`
|
||||
- [ ] Remove `StaticFileHandler` from `WebsocketProxy.java` pipeline + import
|
||||
- [ ] Remove `landlords-client` module from root `pom.xml`
|
||||
- [ ] Remove `COPY landlords-client` from Dockerfile (if present)
|
||||
- [ ] Grep-verify no dangling references
|
||||
- [ ] `mvn -pl landlords-server -am clean verify` passes
|
||||
- [ ] Manual WS smoke test passes
|
||||
- [ ] Commit
|
||||
|
||||
## Success Criteria
|
||||
- `mvn -pl landlords-server -am clean verify` exits 0.
|
||||
- `java -jar landlords-server/target/*.jar` (or existing run command) starts server without error.
|
||||
- Current web-client can connect via WS, create room, and play one move.
|
||||
- `curl http://localhost:1025/` no longer returns HTML (returns WS-expected 400 or connection-close — acceptable).
|
||||
- `grep -rn "landlords-client\|StaticFileHandler\|I18nHelper\|printTranslate\|messages_en_US" .` returns only historical matches in `docs/` (those cleaned in Phase 6).
|
||||
|
||||
## Validation Commands
|
||||
```bash
|
||||
mvn -pl landlords-server -am clean verify
|
||||
# In one shell:
|
||||
java -jar landlords-server/target/landlords-server-*.jar
|
||||
# In another: open client, play a move
|
||||
```
|
||||
|
||||
## Risk Assessment
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|------|------------|--------|------------|
|
||||
| Unseen dependency on `I18nHelper` outside `SimpleClient` | Low | Med | Grep before delete; fix compile errors if any before commit |
|
||||
| `printTranslate` called via reflection | Very Low | Low | Grep for `"printTranslate"` string literal too |
|
||||
| Web-client expects to load a static asset from server | Low | Low | Verified Phase 3 docs — client is self-hosted by Vite/nginx |
|
||||
| Dockerfile build breaks from stale COPY | Low | Low | `docker compose build server` before commit |
|
||||
|
||||
## Security Considerations
|
||||
- Removing static file serving reduces attack surface (no path traversal, no stale HTML/JS).
|
||||
- No auth changes.
|
||||
|
||||
## Rollback
|
||||
`git reset --hard <pre-phase1-sha>` — single phase, single commit, no external state.
|
||||
|
||||
## Next Steps
|
||||
- Proceed to Phase 2 only after validation is green.
|
||||
- Phase 2 depends on knowing the common/server sub-package sets are disjoint (verified in locked decisions).
|
||||
@@ -1,125 +0,0 @@
|
||||
# Phase 2 — Consolidate Java sources into landlords-server
|
||||
|
||||
## Context Links
|
||||
- Prev: [phase-01-deletions.md](phase-01-deletions.md)
|
||||
- Next: [phase-03-standalone-maven-and-rename-server.md](phase-03-standalone-maven-and-rename-server.md)
|
||||
- Overview: [plan.md](plan.md)
|
||||
|
||||
## Overview
|
||||
- **Priority:** P2
|
||||
- **Status:** pending
|
||||
- **Description:** Physically move `landlords-common/src/**` into `landlords-server/src/**`, delete the `landlords-common` module, delete `protoc-resource/` (after moving `*.proto` + `generate.sh` to `landlords-server/src/main/resources/proto/`). Parent POM still exists after this phase but only contains a single child module; it will be removed in Phase 3.
|
||||
|
||||
## Key Insights
|
||||
- `landlords-common` sub-packages (`channel, entity, enums, exception, features, handler, helper, print, robot, transfer, utils`) are **disjoint** from `landlords-server`'s sub-packages (`event, handler, proxy, timer` + `SimpleServer`, `ServerContains`) — merge can be a plain directory-level `git mv` with no filename collisions.
|
||||
- **Potential collision:** both modules have a `handler/` sub-package. Verify file names don't clash before moving; if they do, move individually.
|
||||
- Proto files are moved now (not Phase 3) so Phase 3 can focus on POM rewrite without also juggling file moves.
|
||||
|
||||
## Requirements
|
||||
**Functional**
|
||||
- `mvn -pl landlords-server -am clean verify` (which after this phase equals `mvn verify` on the single remaining module) passes.
|
||||
- All existing tests still run and pass.
|
||||
|
||||
**Non-functional**
|
||||
- Zero Java source edits (no package renames yet, no noson→gson yet).
|
||||
- File history preserved via `git mv`.
|
||||
|
||||
## Architecture
|
||||
- Before: two Maven modules, common as dep of server.
|
||||
- After: single module `landlords-server` containing all Java code + proto resources. Root `pom.xml` now has `<modules><module>landlords-server</module></modules>`.
|
||||
|
||||
## Related Code Files
|
||||
|
||||
**Move (directories, preserving structure):**
|
||||
- `landlords-common/src/main/java/org/nico/ratel/landlords/channel/` → `landlords-server/src/main/java/org/nico/ratel/landlords/channel/`
|
||||
- Same for: `entity`, `enums`, `exception`, `features`, `helper`, `print`, `robot`, `transfer`, `utils`
|
||||
- `landlords-common/src/main/java/org/nico/ratel/landlords/handler/` → merge into `landlords-server/src/main/java/org/nico/ratel/landlords/handler/` (verify no filename clashes first)
|
||||
- `landlords-common/src/test/java/**` → `landlords-server/src/test/java/**` (create target dir if missing)
|
||||
- `protoc-resource/*.proto` + `protoc-resource/generate.sh` → `landlords-server/src/main/resources/proto/`
|
||||
|
||||
**Delete (after moves):**
|
||||
- `landlords-common/` entirely
|
||||
- `protoc-resource/` entirely
|
||||
|
||||
**Modify:**
|
||||
- `pom.xml` (root) — remove `<module>landlords-common</module>`
|
||||
- `landlords-server/pom.xml` — remove `<dependency>` block referencing `landlords-common`
|
||||
- `landlords-server/Dockerfile` — remove `COPY landlords-common ...` + `COPY protoc-resource protoc-resource` lines (if present)
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Verify clean tree; note HEAD sha.
|
||||
2. **Collision check:** `ls landlords-common/src/main/java/org/nico/ratel/landlords/handler/ landlords-server/src/main/java/org/nico/ratel/landlords/handler/` — list both directories. If any filename appears in both, stop and report (should not happen per codebase-summary).
|
||||
3. Create target test dir: `mkdir -p landlords-server/src/test/java/org/nico/ratel/landlords`
|
||||
4. Create target proto dir: `mkdir -p landlords-server/src/main/resources/proto`
|
||||
5. Move Java main sources, one sub-package at a time, using `git mv`:
|
||||
```bash
|
||||
for pkg in channel entity enums exception features helper print robot transfer utils; do
|
||||
git mv "landlords-common/src/main/java/org/nico/ratel/landlords/$pkg" \
|
||||
"landlords-server/src/main/java/org/nico/ratel/landlords/$pkg"
|
||||
done
|
||||
```
|
||||
6. Move `handler/` sub-package file-by-file (since server already has a `handler/` dir):
|
||||
```bash
|
||||
git mv landlords-common/src/main/java/org/nico/ratel/landlords/handler/*.java \
|
||||
landlords-server/src/main/java/org/nico/ratel/landlords/handler/
|
||||
rmdir landlords-common/src/main/java/org/nico/ratel/landlords/handler
|
||||
```
|
||||
7. Move tests: `git mv landlords-common/src/test/java/org/nico/ratel/landlords/* landlords-server/src/test/java/org/nico/ratel/landlords/` (create any missing intermediate dirs first).
|
||||
8. Move proto: `git mv protoc-resource/*.proto protoc-resource/generate.sh landlords-server/src/main/resources/proto/`
|
||||
9. Delete emptied dirs:
|
||||
```bash
|
||||
git rm -r landlords-common
|
||||
git rm -r protoc-resource
|
||||
```
|
||||
10. Edit root `pom.xml` — remove `<module>landlords-common</module>`.
|
||||
11. Edit `landlords-server/pom.xml` — remove the `<dependency>…landlords-common…</dependency>` block.
|
||||
12. Edit `landlords-server/Dockerfile` — delete `COPY landlords-common ...` and `COPY protoc-resource protoc-resource` lines.
|
||||
13. Run validation.
|
||||
14. Commit: `refactor: consolidate landlords-common into landlords-server; move protos into server resources`
|
||||
|
||||
## Todo List
|
||||
- [ ] Collision check between `common/handler` and `server/handler`
|
||||
- [ ] Move 10 disjoint sub-packages via `git mv`
|
||||
- [ ] Merge `handler/` sub-package file-by-file
|
||||
- [ ] Move test sources
|
||||
- [ ] Move proto files + generate.sh
|
||||
- [ ] Delete `landlords-common/` and `protoc-resource/`
|
||||
- [ ] Update root `pom.xml` modules
|
||||
- [ ] Remove `landlords-common` dependency from `landlords-server/pom.xml`
|
||||
- [ ] Update `Dockerfile` COPY paths
|
||||
- [ ] `mvn -pl landlords-server -am clean verify` passes
|
||||
- [ ] Tests run + pass (count matches pre-phase)
|
||||
- [ ] Commit
|
||||
|
||||
## Success Criteria
|
||||
- `mvn -pl landlords-server -am clean verify` exits 0.
|
||||
- Test count in Surefire report ≥ pre-phase count (no tests silently lost).
|
||||
- Server boots; WS smoke test passes.
|
||||
- `find landlords-common protoc-resource 2>/dev/null` returns nothing.
|
||||
- `git log --follow` on a moved file shows history preserved.
|
||||
|
||||
## Validation Commands
|
||||
```bash
|
||||
mvn -pl landlords-server -am clean verify
|
||||
java -jar landlords-server/target/landlords-server-*.jar &
|
||||
# WS smoke test via web-client
|
||||
```
|
||||
|
||||
## Risk Assessment
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|------|------------|--------|------------|
|
||||
| Filename collision in `handler/` sub-package | Low | Med | Step 2 explicit check; abort phase if found |
|
||||
| Lost test sources (not moved) | Low | High | Compare test count before/after |
|
||||
| Git loses file history | Low | Low | Use `git mv` not raw `mv`+add |
|
||||
| Dockerfile build breaks from stale `COPY` | Med | Low | `docker compose build server` before commit |
|
||||
| Maven reactor caches stale jar | Low | Low | `mvn clean` before verify |
|
||||
|
||||
## Security Considerations
|
||||
- None (pure file move).
|
||||
|
||||
## Rollback
|
||||
`git reset --hard <pre-phase2-sha>` — single phase, atomic commit.
|
||||
|
||||
## Next Steps
|
||||
- Phase 3: standalone POM rewrite, Java 25, shade, deps modernization, dir rename `landlords-server/` → `server/`.
|
||||
-225
@@ -1,225 +0,0 @@
|
||||
# Phase 3 — Standalone Maven + Java 25 + shade + noson→gson + JUnit 5 + rename to server/
|
||||
|
||||
## Context Links
|
||||
- Prev: [phase-02-consolidate-java-sources.md](phase-02-consolidate-java-sources.md)
|
||||
- Next: [phase-04-package-rename-and-java25-modernization.md](phase-04-package-rename-and-java25-modernization.md)
|
||||
- Overview: [plan.md](plan.md)
|
||||
- Relevant docs to update later: `docs/code-standards.md`, `docs/deployment-guide.md`, `docs/system-architecture.md`
|
||||
|
||||
## Overview
|
||||
- **Priority:** P2
|
||||
- **Status:** pending
|
||||
- **Description:** Rewrite `landlords-server/pom.xml` as a standalone project (no parent), upgrade to Java 25, replace Spring Boot parent with `maven-shade-plugin`, swap `com.smallnico:noson` for `com.google.code.gson:gson`, migrate JUnit 4 tests to JUnit 5, delete root `pom.xml`, then rename directory `landlords-server/` → `server/`. Update Dockerfile base images and docker-compose + CI path references. Package names remain `org.nico.ratel.landlords.*` in this phase — they will be renamed in Phase 4.
|
||||
|
||||
## Key Insights
|
||||
- This is the largest risk phase: build system + deps + dir rename + test framework change.
|
||||
- Keep phases disjoint: **no** package rename here — doing both at once explodes diff size and makes bisect impossible.
|
||||
- noson → gson call sites are grep-verified (7 total). Use a shared `Gson` instance where possible for clarity, but correctness-first.
|
||||
- JUnit 4 → 5: only two test files (`GomokuHelperTest.java`, `GomokuAITest.java`) per docs; mechanical edit.
|
||||
- Java 25 modernization (records, `var`, switch expressions) is deferred to Phase 4 — this phase changes ONLY build config + imports + dep swaps.
|
||||
- Version pins:
|
||||
- `netty-all` 4.1.x latest stable
|
||||
- `protobuf-java` 3.25.5 (pinned per locked decision)
|
||||
- `gson` latest stable
|
||||
- `junit-jupiter` 5.x latest stable
|
||||
- `maven-shade-plugin` 3.6.0 (or latest)
|
||||
- Java 25 base images: `maven:3.9-eclipse-temurin-25` (build), `eclipse-temurin:25-jre-alpine` (runtime) — verify tag availability at execution time; fall back to `25-jre` if alpine variant unavailable.
|
||||
|
||||
## Requirements
|
||||
**Functional**
|
||||
- `mvn -f server/pom.xml clean verify` exits 0.
|
||||
- Shade produces a runnable fat jar at `server/target/caro-server-0.0.1-beta.jar`.
|
||||
- `java -jar server/target/caro-server-0.0.1-beta.jar` starts the server on port 1025.
|
||||
- `docker compose build server && docker compose up server` boots successfully on Java 25.
|
||||
- Full WS game flow works end-to-end with current web-client.
|
||||
- All tests (now JUnit 5) pass.
|
||||
|
||||
**Non-functional**
|
||||
- Single coherent commit per logical step (POM rewrite, dep swap, test migration, dir rename, Dockerfile, CI) — or one well-organized squash.
|
||||
- No code behavior change outside mechanical migrations.
|
||||
|
||||
## Architecture
|
||||
- Before: parent POM → landlords-server; Spring Boot parent provides dep mgmt + packaging.
|
||||
- After: `server/pom.xml` standalone, explicit deps, `maven-shade-plugin` packaging, `<mainClass>org.nico.ratel.landlords.server.SimpleServer</mainClass>` (package still old until Phase 4).
|
||||
- Dir: `landlords-server/` → `server/`.
|
||||
- Dockerfile multi-stage: Java 25 build → Java 25 runtime.
|
||||
|
||||
## Related Code Files
|
||||
|
||||
**Delete:**
|
||||
- `pom.xml` (root)
|
||||
|
||||
**Rewrite:**
|
||||
- `landlords-server/pom.xml` → standalone (then moved to `server/pom.xml`)
|
||||
|
||||
**Modify (noson → gson call sites):**
|
||||
- `landlords-server/src/main/java/org/nico/ratel/landlords/helper/MapHelper.java` (2 calls, now under `landlords-server/` after Phase 2)
|
||||
- `landlords-server/src/main/java/org/nico/ratel/landlords/transfer/TransferProtocolUtils.java` (2 calls)
|
||||
- `landlords-server/src/main/java/org/nico/ratel/landlords/server/event/ServerEventListener_CODE_GAME_WATCH.java` (1 call)
|
||||
- `landlords-server/src/main/java/org/nico/ratel/landlords/server/event/ServerEventListener_CODE_GET_ROOMS.java` (1 call)
|
||||
- `landlords-server/src/main/java/org/nico/ratel/landlords/server/event/ServerEventListener_CODE_ROOM_CREATE.java` (1 call)
|
||||
- Any additional noson imports in tests — grep to confirm.
|
||||
|
||||
**Modify (JUnit 4 → 5):**
|
||||
- `landlords-server/src/test/java/org/nico/ratel/landlords/.../GomokuHelperTest.java`
|
||||
- `landlords-server/src/test/java/org/nico/ratel/landlords/.../GomokuAITest.java`
|
||||
|
||||
**Rename (directory):**
|
||||
- `landlords-server/` → `server/`
|
||||
|
||||
**Modify after rename:**
|
||||
- `server/Dockerfile` — Java 25 base images, updated `COPY` paths (no more `landlords-server/` prefix)
|
||||
- `docker-compose.yml` — `dockerfile: server/Dockerfile` (context stays `.`)
|
||||
- `.github/workflows/build.yml` — Maven step: `mvn -f server/pom.xml verify`; adjust working-directory if used
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Part A — POM rewrite + dep swap (still in `landlords-server/`)
|
||||
|
||||
1. Grep all noson call sites to lock list: `grep -rn "Noson\." landlords-server/src`. Confirm matches the 7 listed above (plus any test hits).
|
||||
2. Grep JUnit 4 imports: `grep -rn "org.junit.Test\|org.junit.Assert\|org.junit.Before" landlords-server/src/test`.
|
||||
3. Delete root `pom.xml`.
|
||||
4. Rewrite `landlords-server/pom.xml` as standalone:
|
||||
```xml
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" ...>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.miti99.caro</groupId>
|
||||
<artifactId>caro-server</artifactId>
|
||||
<version>0.0.1-beta</version>
|
||||
<packaging>jar</packaging>
|
||||
<properties>
|
||||
<maven.compiler.source>25</maven.compiler.source>
|
||||
<maven.compiler.target>25</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency><groupId>io.netty</groupId><artifactId>netty-all</artifactId><version>4.1.x</version></dependency>
|
||||
<dependency><groupId>com.google.protobuf</groupId><artifactId>protobuf-java</artifactId><version>3.25.5</version></dependency>
|
||||
<dependency><groupId>com.google.code.gson</groupId><artifactId>gson</artifactId><version>latest</version></dependency>
|
||||
<dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId><version>5.x</version><scope>test</scope></dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<finalName>caro-server-0.0.1-beta</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId><artifactId>maven-shade-plugin</artifactId><version>3.6.0</version>
|
||||
<executions><execution><phase>package</phase><goals><goal>shade</goal></goals>
|
||||
<configuration>
|
||||
<transformers>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>org.nico.ratel.landlords.server.SimpleServer</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
</configuration>
|
||||
</execution></executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-surefire-plugin</artifactId><version>3.2.x</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
```
|
||||
Pin exact versions at execution time to the latest stable releases.
|
||||
5. `mvn -f landlords-server/pom.xml clean verify` — expect compile errors from noson call sites + JUnit 4 imports. This is the checkpoint before migration.
|
||||
|
||||
### Part B — noson → gson migration
|
||||
|
||||
6. For each of the 7 call sites, replace:
|
||||
- `import com.smallnico.noson.Noson;` → `import com.google.gson.Gson;`
|
||||
- Class-level `private static final Gson GSON = new Gson();` where multiple calls exist (DRY)
|
||||
- `Noson.reversal(obj)` → `GSON.toJson(obj)`
|
||||
- `Noson.convert(json, Clazz.class)` → `GSON.fromJson(json, Clazz.class)`
|
||||
7. Grep-verify zero `Noson` references remain: `grep -rn "Noson\|noson\|smallnico" landlords-server`.
|
||||
|
||||
### Part C — JUnit 4 → 5 migration
|
||||
|
||||
8. In both test files:
|
||||
- `import org.junit.Test;` → `import org.junit.jupiter.api.Test;`
|
||||
- `import org.junit.Before;` → `import org.junit.jupiter.api.BeforeEach;` (and rename annotations)
|
||||
- `import org.junit.Assert;` (or static imports) → `import org.junit.jupiter.api.Assertions;` (adjust static imports)
|
||||
- `@Before` → `@BeforeEach`
|
||||
- `Assert.assertEquals(...)` → `Assertions.assertEquals(...)` (or static import)
|
||||
9. `mvn -f landlords-server/pom.xml clean verify` — expect green.
|
||||
|
||||
### Part D — Directory rename
|
||||
|
||||
10. `git mv landlords-server server`
|
||||
11. Run `mvn -f server/pom.xml clean verify` — expect green (POM has no hardcoded dir paths).
|
||||
|
||||
### Part E — Dockerfile + docker-compose + CI
|
||||
|
||||
12. Rewrite `server/Dockerfile`:
|
||||
- Build stage: `FROM maven:3.9-eclipse-temurin-25 AS build`, `WORKDIR /build`, `COPY pom.xml .`, `COPY src src`, `RUN mvn -B -DskipTests package`
|
||||
- Runtime stage: `FROM eclipse-temurin:25-jre-alpine` (fallback `25-jre`), `WORKDIR /app`, `COPY --from=build /build/target/caro-server-0.0.1-beta.jar app.jar`, `EXPOSE 1025`, `ENTRYPOINT ["java","-jar","app.jar"]`
|
||||
- Note: build-stage context is the `server/` dir because docker-compose will set `context: ./server` OR root with `dockerfile: server/Dockerfile` — choose based on current compose shape. Locked decision: context stays `.`, dockerfile path `server/Dockerfile`; COPY paths in build stage must be `server/pom.xml` and `server/src`.
|
||||
13. Edit `docker-compose.yml`:
|
||||
- `services.server.build.context: .`
|
||||
- `services.server.build.dockerfile: server/Dockerfile`
|
||||
- Container name and other fields untouched.
|
||||
14. Edit `.github/workflows/build.yml`:
|
||||
- Maven step: `run: mvn -f server/pom.xml -B verify`
|
||||
- Cache key / path for `~/.m2` unchanged.
|
||||
15. Run validation.
|
||||
16. Commit (or split into logical commits): `refactor: standalone maven + java 25 + shade + gson + junit5; rename landlords-server to server`
|
||||
|
||||
## Todo List
|
||||
- [ ] Grep-lock noson call sites (expect 7)
|
||||
- [ ] Grep-lock JUnit 4 test files (expect 2)
|
||||
- [ ] Delete root `pom.xml`
|
||||
- [ ] Rewrite `landlords-server/pom.xml` standalone with Java 25 + shade
|
||||
- [ ] Pin exact latest versions (netty, gson, junit-jupiter, shade-plugin)
|
||||
- [ ] Migrate 7 noson → gson call sites
|
||||
- [ ] Migrate 2 test files JUnit 4 → 5
|
||||
- [ ] `mvn verify` green before directory rename
|
||||
- [ ] `git mv landlords-server server`
|
||||
- [ ] Rewrite `server/Dockerfile` with Java 25 base images + new COPY paths
|
||||
- [ ] Update `docker-compose.yml` dockerfile path
|
||||
- [ ] Update `.github/workflows/build.yml` Maven step
|
||||
- [ ] `mvn -f server/pom.xml clean verify` green
|
||||
- [ ] `docker compose build server` succeeds
|
||||
- [ ] `docker compose up server` + WS smoke test passes
|
||||
- [ ] Commit
|
||||
|
||||
## Success Criteria
|
||||
- Build: `mvn -f server/pom.xml clean verify` exits 0.
|
||||
- Artifact: `server/target/caro-server-0.0.1-beta.jar` exists and is runnable.
|
||||
- Runtime: `java -jar` boots, WS port listens, full game flow passes.
|
||||
- Docker: `docker compose build server` succeeds; container runs on Java 25 (`docker compose exec server java -version` shows 25).
|
||||
- CI: manually simulate `mvn -f server/pom.xml -B verify` from repo root — exits 0.
|
||||
- Grep: zero `Noson`, `smallnico`, `junit.Test`, `junit.Assert` references in `server/`.
|
||||
|
||||
## Validation Commands
|
||||
```bash
|
||||
mvn -f server/pom.xml clean verify
|
||||
java -jar server/target/caro-server-0.0.1-beta.jar &
|
||||
# WS smoke test
|
||||
docker compose build server
|
||||
docker compose up -d server
|
||||
docker compose exec server java -version
|
||||
# WS smoke test against containerized server
|
||||
docker compose down
|
||||
```
|
||||
|
||||
## Risk Assessment
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|------|------------|--------|------------|
|
||||
| Java 25 not available in Spring/Netty deps | Low | High | Dropping Spring parent removes risk; Netty 4.1 is pure-Java, compatible with 25 |
|
||||
| `eclipse-temurin:25-jre-alpine` tag missing | Med | Low | Fall back to `25-jre`; note in commit |
|
||||
| Shade plugin misses a transformer (e.g., META-INF services) | Med | Med | Add `ServicesResourceTransformer` if Netty uses SPI (it does for some providers); test jar directly |
|
||||
| gson behaves differently than noson (whitespace, field naming) | Low | Med | Smoke test covers actual WS payloads; inspect JSON on wire with browser devtools |
|
||||
| JUnit 5 needs `junit-jupiter` (aggregator) not just `junit-jupiter-api` | Low | Low | Use aggregator artifact per locked decision |
|
||||
| CI fails because cache key stale | Low | Low | `actions/cache` is keyed by POM hash — will self-invalidate |
|
||||
| `git mv` on Windows case-insensitive FS misses | Low | Low | Directory rename is a distinct name change, no case issue |
|
||||
| Dockerfile context mismatch between compose and Dockerfile `COPY` | Med | Med | Explicit step 12 note; test `docker compose build` before commit |
|
||||
|
||||
## Security Considerations
|
||||
- Dropping Spring Boot parent removes transitive CVE exposure from Spring ecosystem.
|
||||
- gson has a larger install base + active security maintenance vs noson (unmaintained).
|
||||
- Pin all versions explicitly; no version ranges.
|
||||
|
||||
## Rollback
|
||||
`git reset --hard <pre-phase3-sha>` — single-phase rollback. Because this phase also deletes root `pom.xml`, rollback restores it automatically.
|
||||
|
||||
## Next Steps
|
||||
- Phase 4: rename all packages `org.nico.ratel.landlords.*` → `com.miti99.caro.{common,server}.*` + opportunistic Java 25 modernization (records, `var`, switch expressions).
|
||||
-187
@@ -1,187 +0,0 @@
|
||||
# Phase 4 — Package rename + Java 25 opportunistic modernization
|
||||
|
||||
## Context Links
|
||||
- Prev: [phase-03-standalone-maven-and-rename-server.md](phase-03-standalone-maven-and-rename-server.md)
|
||||
- Next: [phase-05-rename-web-client-to-client.md](phase-05-rename-web-client-to-client.md)
|
||||
- Overview: [plan.md](plan.md)
|
||||
|
||||
## Overview
|
||||
- **Priority:** P2
|
||||
- **Status:** pending
|
||||
- **Description:** Rename all Java packages from `org.nico.ratel.landlords.*` to `com.miti99.caro.{common,server}.*`, updating `package` + `import` + `<mainClass>` + any string references. Then apply opportunistic Java 25 language modernization (records, `var`, switch expressions, text blocks) while every file is already being touched. Scope limited to low-risk, obvious rewrites; Netty handler threading untouched.
|
||||
|
||||
## Key Insights
|
||||
- Server's sub-packages (`event, handler, proxy, timer, SimpleServer, ServerContains`) map to `com.miti99.caro.server.*`.
|
||||
- All other sub-packages (`channel, entity, enums, exception, features, helper, print, robot, transfer, utils`) map to `com.miti99.caro.common.*`.
|
||||
- The existing `handler/` sub-package sits in BOTH: common-origin files (from `landlords-common/handler/` pre-phase-2) → `com.miti99.caro.common.handler`; server-origin files (pre-existing server handlers) → `com.miti99.caro.server.handler`. Planner must split them at rename time. Use git log to identify origin if ambiguous.
|
||||
- Java 25 modernization is strictly opportunistic — if a file needs 10 lines changed to become a record, skip it; if it needs 2 lines, do it.
|
||||
- Record candidates: inspect `common/entity/` for pure-data classes (fields + getters only, no mutation). `Room` was called out as a candidate — verify mutability before converting; if any setter is actively used, keep as class.
|
||||
|
||||
## Requirements
|
||||
**Functional**
|
||||
- `mvn -f server/pom.xml clean verify` exits 0.
|
||||
- All tests pass (same count as end of Phase 3).
|
||||
- Full WS game flow passes.
|
||||
- `java -jar` still runs; `<mainClass>` updated in shade config.
|
||||
|
||||
**Non-functional**
|
||||
- One commit for the mechanical package rename; separate commit(s) for Java 25 modernization so bisect stays useful.
|
||||
- No behavior change beyond what language features imply.
|
||||
|
||||
## Architecture
|
||||
- Before: `org.nico.ratel.landlords.{channel,entity,...,server.event,...}`
|
||||
- After: `com.miti99.caro.common.{channel,entity,...}` + `com.miti99.caro.server.{event,handler,proxy,timer}` + `com.miti99.caro.server.SimpleServer` + `com.miti99.caro.server.ServerContains`
|
||||
|
||||
## Related Code Files
|
||||
|
||||
**Rename (directories):**
|
||||
- `server/src/main/java/org/nico/ratel/landlords/channel/` → `server/src/main/java/com/miti99/caro/common/channel/`
|
||||
- Repeat for: `entity, enums, exception, features, helper, print, robot, transfer, utils`
|
||||
- `server/src/main/java/org/nico/ratel/landlords/handler/` → split into:
|
||||
- common-origin files → `server/src/main/java/com/miti99/caro/common/handler/`
|
||||
- server-origin files → `server/src/main/java/com/miti99/caro/server/handler/` (merge with existing content)
|
||||
- `server/src/main/java/org/nico/ratel/landlords/server/event/` → `server/src/main/java/com/miti99/caro/server/event/`
|
||||
- Same for `handler, proxy, timer`
|
||||
- `server/src/main/java/org/nico/ratel/landlords/server/SimpleServer.java` → `server/src/main/java/com/miti99/caro/server/SimpleServer.java`
|
||||
- `server/src/main/java/org/nico/ratel/landlords/server/ServerContains.java` → `server/src/main/java/com/miti99/caro/server/ServerContains.java`
|
||||
- `server/src/test/java/org/nico/ratel/landlords/*` → `server/src/test/java/com/miti99/caro/common/*` (tests cover common-package code)
|
||||
|
||||
**Modify (content):**
|
||||
- Every `.java` file under `server/src/{main,test}/java/` — update `package` declaration + every `import org.nico.ratel.landlords.*`
|
||||
- `server/pom.xml` — `<mainClass>com.miti99.caro.server.SimpleServer</mainClass>`
|
||||
|
||||
**Grep verify:**
|
||||
- `grep -rn "org.nico.ratel" server/` — zero hits expected
|
||||
- `grep -rn "landlords" server/src/main/java` — zero hits (catches string literals too)
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Part A — Mechanical package rename
|
||||
|
||||
1. Verify clean tree; note HEAD sha.
|
||||
2. Create target dirs:
|
||||
```bash
|
||||
mkdir -p server/src/main/java/com/miti99/caro/common
|
||||
mkdir -p server/src/main/java/com/miti99/caro/server
|
||||
mkdir -p server/src/test/java/com/miti99/caro/common
|
||||
```
|
||||
3. Move common sub-packages:
|
||||
```bash
|
||||
for pkg in channel entity enums exception features helper print robot transfer utils; do
|
||||
git mv "server/src/main/java/org/nico/ratel/landlords/$pkg" \
|
||||
"server/src/main/java/com/miti99/caro/common/$pkg"
|
||||
done
|
||||
```
|
||||
4. **Split the ambiguous `handler/` dir:** list files in `server/src/main/java/org/nico/ratel/landlords/handler/` and classify each as common vs server using `git log --follow` origin. Move common-origin to `com.miti99.caro.common.handler/`, server-origin to `com.miti99.caro.server.handler/`. If classification uncertain for a file, default to **common** (safer — it's referenced from server, not vice versa) and adjust if compile fails.
|
||||
5. Move server sub-packages:
|
||||
```bash
|
||||
for pkg in event handler proxy timer; do
|
||||
git mv "server/src/main/java/org/nico/ratel/landlords/server/$pkg" \
|
||||
"server/src/main/java/com/miti99/caro/server/$pkg"
|
||||
done
|
||||
```
|
||||
If `handler` target dir was created in step 4, merge contents file-by-file instead.
|
||||
6. Move server root classes:
|
||||
```bash
|
||||
git mv server/src/main/java/org/nico/ratel/landlords/server/SimpleServer.java \
|
||||
server/src/main/java/com/miti99/caro/server/SimpleServer.java
|
||||
git mv server/src/main/java/org/nico/ratel/landlords/server/ServerContains.java \
|
||||
server/src/main/java/com/miti99/caro/server/ServerContains.java
|
||||
```
|
||||
7. Delete the now-empty old tree:
|
||||
```bash
|
||||
rm -rf server/src/main/java/org/nico/ratel/landlords/server
|
||||
rm -rf server/src/main/java/org/nico/ratel
|
||||
rm -rf server/src/main/java/org/nico
|
||||
rm -rf server/src/main/java/org
|
||||
```
|
||||
8. Move test sources analogously into `server/src/test/java/com/miti99/caro/common/...`.
|
||||
9. Rewrite `package` and `import` statements across all moved files. Use a scripted sed/IDE refactor with careful preview. Two regex rules cover everything:
|
||||
- `package org\.nico\.ratel\.landlords\.server(\.\w+)?;` → `package com.miti99.caro.server\1;`
|
||||
- `package org\.nico\.ratel\.landlords(\.\w+)?;` → `package com.miti99.caro.common\1;`
|
||||
- `import org\.nico\.ratel\.landlords\.server(\.[\w\.]+);` → `import com.miti99.caro.server\1;`
|
||||
- `import org\.nico\.ratel\.landlords(\.[\w\.]+);` → `import com.miti99.caro.common\1;`
|
||||
- Apply in the order: server rules BEFORE common rules (so `landlords.server.X` isn't eaten by the common rule first).
|
||||
10. Update `server/pom.xml` — `<mainClass>org.nico.ratel.landlords.server.SimpleServer</mainClass>` → `<mainClass>com.miti99.caro.server.SimpleServer</mainClass>`
|
||||
11. Grep for stragglers: `grep -rn "org\.nico\.ratel\|landlords" server/ .github/ docker-compose.yml`. Investigate any hits. Docs hits are fine (Phase 6 handles them).
|
||||
12. `mvn -f server/pom.xml clean verify` — green expected. Fix any misclassified `handler/` files if compile fails.
|
||||
13. Commit: `refactor: rename packages org.nico.ratel.landlords -> com.miti99.caro.{common,server}`
|
||||
|
||||
### Part B — Java 25 opportunistic modernization
|
||||
|
||||
14. **Record candidates:** list files in `com.miti99.caro.common.entity/`. For each, inspect:
|
||||
- All fields `final` (or convertible without breakage)?
|
||||
- Only getters, no setters or mutating methods?
|
||||
- No inheritance hierarchy depended on?
|
||||
- If yes → rewrite as `record`. If doubt → skip.
|
||||
15. **`var` for local variables:** within method bodies only, where RHS type is trivially obvious (e.g., `Map<String, List<Foo>> x = new HashMap<>();` → `var x = new HashMap<String, List<Foo>>();`). Do not use `var` where readers can't infer the type at a glance.
|
||||
16. **Switch expressions:** convert `switch` statements that are pure `case X: return Y;` or assignment-only to arrow-form switch expressions. Leave fall-through switches alone.
|
||||
17. **Text blocks:** find multi-line string concatenation (e.g., help text, ASCII art) and convert to `"""..."""`.
|
||||
18. **Explicit non-goals (do NOT do):**
|
||||
- Rewrite Netty handlers
|
||||
- Change threading model
|
||||
- Introduce sealed types or pattern matching in switch (keeps diff small)
|
||||
- Inline/extract methods beyond what modernization requires
|
||||
19. Run validation after each small batch; commit in logical groups (e.g., `refactor(java25): convert common/entity classes to records`, `refactor(java25): var + switch expressions`).
|
||||
|
||||
## Todo List
|
||||
- [ ] Create target package directories
|
||||
- [ ] Move 10 common sub-packages via `git mv`
|
||||
- [ ] Split ambiguous `handler/` (classify common vs server per file)
|
||||
- [ ] Move server sub-packages (event, handler, proxy, timer)
|
||||
- [ ] Move `SimpleServer`, `ServerContains`
|
||||
- [ ] Move test sources
|
||||
- [ ] Rewrite `package` + `import` lines (server rules before common rules)
|
||||
- [ ] Update `<mainClass>` in `server/pom.xml`
|
||||
- [ ] Grep-verify zero `org.nico.ratel` / `landlords` in `server/`
|
||||
- [ ] `mvn -f server/pom.xml clean verify` green
|
||||
- [ ] WS smoke test passes
|
||||
- [ ] Commit package rename
|
||||
- [ ] List record candidates in `common/entity/`
|
||||
- [ ] Apply record conversions (skip if risky)
|
||||
- [ ] Apply `var` for obvious local types
|
||||
- [ ] Convert trivial `switch` statements to switch expressions
|
||||
- [ ] Apply text blocks to multi-line strings
|
||||
- [ ] Re-run `mvn verify` after each modernization batch
|
||||
- [ ] Commit modernization in logical groups
|
||||
|
||||
## Success Criteria
|
||||
- `mvn -f server/pom.xml clean verify` exits 0.
|
||||
- Shade jar runs; WS game flow passes.
|
||||
- Zero hits for `grep -rn "org\.nico\.ratel" server/`.
|
||||
- Tests still pass with same count.
|
||||
- No new compiler warnings about raw types or deprecated APIs introduced.
|
||||
|
||||
## Validation Commands
|
||||
```bash
|
||||
# After package rename
|
||||
mvn -f server/pom.xml clean verify
|
||||
grep -rn "org\.nico\.ratel\|landlords" server/src
|
||||
# After each Java 25 batch
|
||||
mvn -f server/pom.xml clean verify
|
||||
java -jar server/target/caro-server-0.0.1-beta.jar &
|
||||
# WS smoke test
|
||||
```
|
||||
|
||||
## Risk Assessment
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|------|------------|--------|------------|
|
||||
| `handler/` misclassification (common vs server) | Med | Med | Compile catches mistakes; step 4 default to common; document classifications in commit |
|
||||
| Regex package rewrite misses edge case (e.g., fully-qualified names in code) | Med | Med | Grep-verify with `org\.nico\.ratel` hunts any survivors |
|
||||
| String literal references to old package (reflection, logging) | Low | Med | Grep for `"org.nico.ratel"` specifically |
|
||||
| Record conversion breaks serialization (gson field naming) | Med | Med | Test actual WS payload with web-client after each record conversion; revert if payload changes |
|
||||
| `var` overuse hurts readability | Low | Low | Restrict to cases where RHS is obvious |
|
||||
| Switch expression semantic change (fall-through) | Low | High | Only convert switches without fall-through; code review each |
|
||||
| Java 25 modernization balloons scope | High | Low | Strict time-box; skip doubtful cases |
|
||||
|
||||
## Security Considerations
|
||||
- None (pure rename + language feature adoption).
|
||||
- gson records may encode differently; verify JSON stability on the wire to avoid breaking web-client.
|
||||
|
||||
## Rollback
|
||||
- Package rename: `git revert <rename-commit>` — mechanical, safe.
|
||||
- Java 25 batches: each is a separate commit, revert individually.
|
||||
- Full phase: `git reset --hard <pre-phase4-sha>`.
|
||||
|
||||
## Next Steps
|
||||
- Phase 5: rename `web-client/` → `client/` and update docker-compose + CI path references.
|
||||
@@ -1,124 +0,0 @@
|
||||
# Phase 5 — Rename web-client/ → client/
|
||||
|
||||
## Context Links
|
||||
- Prev: [phase-04-package-rename-and-java25-modernization.md](phase-04-package-rename-and-java25-modernization.md)
|
||||
- Next: [phase-06-docs-and-readme-sweep.md](phase-06-docs-and-readme-sweep.md)
|
||||
- Overview: [plan.md](plan.md)
|
||||
|
||||
## Overview
|
||||
- **Priority:** P2
|
||||
- **Status:** pending
|
||||
- **Description:** Rename top-level `web-client/` directory to `client/`, update docker-compose service/container names + build context, and fix every `web-client/**` path reference in both GitHub Actions workflows. No changes inside `client/` source tree.
|
||||
|
||||
## Key Insights
|
||||
- Internal files of the frontend (src/, package.json, Vite config) are untouched — pure directory rename.
|
||||
- Service name change (`web-client` → `client`) and container name change (`caro-web-client` → `caro-client`) are distinct from the directory rename; any external docs/scripts referencing the old container name must be updated in Phase 6.
|
||||
- `deploy-pages.yml` has multiple `web-client/**` path references (path filter, working-directory, npm cache path, upload-pages-artifact path) — enumerate all before editing.
|
||||
|
||||
## Requirements
|
||||
**Functional**
|
||||
- `cd client && npm ci && npm run build` succeeds (proves nothing internal broke).
|
||||
- `docker compose build client` succeeds.
|
||||
- `docker compose up client` serves the UI; manual browser check loads the game.
|
||||
- WS connection from new `client` container to `server` container still works end-to-end.
|
||||
- CI workflows remain syntactically valid (YAML parse) — verify via `gh workflow view` or `actionlint` if available; otherwise manual dispatch after push.
|
||||
|
||||
**Non-functional**
|
||||
- Single commit for the directory rename + config updates.
|
||||
- File history preserved via `git mv`.
|
||||
|
||||
## Architecture
|
||||
- Before: `web-client/` (Vite + Phaser); compose service `web-client`, container `caro-web-client`.
|
||||
- After: `client/` (identical contents); compose service `client`, container `caro-client`.
|
||||
|
||||
## Related Code Files
|
||||
|
||||
**Rename (directory):**
|
||||
- `web-client/` → `client/`
|
||||
|
||||
**Modify:**
|
||||
- `docker-compose.yml`:
|
||||
- `services.web-client:` → `services.client:`
|
||||
- `services.client.container_name: caro-web-client` → `caro-client`
|
||||
- `services.client.build.context: ./web-client` → `./client`
|
||||
- Any `depends_on: [web-client]` or similar cross-references
|
||||
- `.github/workflows/build.yml`:
|
||||
- Job working directory: `web-client` → `client`
|
||||
- `paths:` filter: `web-client/**` → `client/**`
|
||||
- `cache-dependency-path: web-client/package-lock.json` → `client/package-lock.json`
|
||||
- Any step-level `working-directory: web-client` → `client`
|
||||
- `.github/workflows/deploy-pages.yml`:
|
||||
- `paths:` filter: `web-client/**` → `client/**`
|
||||
- `working-directory: web-client` → `client` (all occurrences)
|
||||
- `cache-dependency-path: web-client/package-lock.json` → `client/package-lock.json`
|
||||
- `actions/upload-pages-artifact` `path: web-client/dist` → `client/dist`
|
||||
- Any `${{ github.workspace }}/web-client` → `${{ github.workspace }}/client`
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Verify clean tree; note HEAD sha.
|
||||
2. Enumerate all `web-client` references for final grep target: `grep -rn "web-client" .github docker-compose.yml`. Print to scratch and verify each will be covered by edits below.
|
||||
3. `git mv web-client client`
|
||||
4. Edit `docker-compose.yml`:
|
||||
- Rename `web-client` service key to `client`
|
||||
- Update `container_name` to `caro-client`
|
||||
- Update `build.context` to `./client`
|
||||
- Update any `depends_on` entries
|
||||
5. Edit `.github/workflows/build.yml`:
|
||||
- Replace all `web-client` occurrences with `client` (use scoped replace to avoid touching unrelated words)
|
||||
- Confirm `paths:` filter, `working-directory`, `cache-dependency-path`
|
||||
6. Edit `.github/workflows/deploy-pages.yml`:
|
||||
- Same style of replacement; verify `upload-pages-artifact` `path` is now `client/dist`
|
||||
7. Grep-verify: `grep -rn "web-client\|caro-web-client" .github docker-compose.yml` — expect zero hits.
|
||||
8. Local build check: `cd client && npm ci && npm run build`
|
||||
9. Docker check: `docker compose build client && docker compose up -d client`
|
||||
10. Browser smoke test: open UI, confirm it loads and connects to server (if server is also up).
|
||||
11. `docker compose down`
|
||||
12. Optional: `actionlint .github/workflows/*.yml` if installed.
|
||||
13. Commit: `refactor: rename web-client/ to client/ and update compose + CI references`
|
||||
|
||||
## Todo List
|
||||
- [ ] Enumerate all `web-client` references for coverage check
|
||||
- [ ] `git mv web-client client`
|
||||
- [ ] Update `docker-compose.yml` service key, container_name, build.context
|
||||
- [ ] Update `.github/workflows/build.yml` paths + working-directory + cache paths
|
||||
- [ ] Update `.github/workflows/deploy-pages.yml` paths + working-directory + cache + artifact path
|
||||
- [ ] Grep-verify zero `web-client` references outside `docs/`
|
||||
- [ ] `npm ci && npm run build` in `client/` passes
|
||||
- [ ] `docker compose build client` succeeds
|
||||
- [ ] Browser smoke test passes
|
||||
- [ ] Commit
|
||||
|
||||
## Success Criteria
|
||||
- Zero `web-client` / `caro-web-client` hits in `docker-compose.yml` and `.github/`.
|
||||
- `client/` builds locally and via Docker.
|
||||
- Browser loads the game and connects to server.
|
||||
- Workflows parse without errors.
|
||||
|
||||
## Validation Commands
|
||||
```bash
|
||||
grep -rn "web-client" .github docker-compose.yml
|
||||
cd client && npm ci && npm run build && cd ..
|
||||
docker compose build client
|
||||
docker compose up -d client
|
||||
# Manual browser check at the exposed port
|
||||
docker compose down
|
||||
```
|
||||
|
||||
## Risk Assessment
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|------|------------|--------|------------|
|
||||
| Missed `web-client/**` path in `deploy-pages.yml` | Med | Med | Step 2 enumeration + step 7 grep-verify |
|
||||
| GitHub Pages deploy breaks until next successful run | Med | Low | Trigger manual dispatch after merge to validate |
|
||||
| `paths:` filter skips all pushes because no matching change | Low | Low | First push after rename will definitely match |
|
||||
| docker-compose service rename breaks downstream scripts | Low | Low | Search repo for `web-client` usage outside listed files |
|
||||
| Cached npm artifacts keyed by old path | Low | Low | `actions/cache` key includes `package-lock.json` hash — self-invalidates |
|
||||
|
||||
## Security Considerations
|
||||
- None. Directory rename does not change any security posture.
|
||||
|
||||
## Rollback
|
||||
`git reset --hard <pre-phase5-sha>` — single-phase rollback; GitHub Pages will resume from previous config on next push.
|
||||
|
||||
## Next Steps
|
||||
- Phase 6: sweep `docs/` and `README.md` for stale references (`landlords-`, `web-client`, `org.nico.ratel`, `noson`, CLI client, built-in web UI).
|
||||
@@ -1,164 +0,0 @@
|
||||
# Phase 6 — Docs + README sweep
|
||||
|
||||
## Context Links
|
||||
- Prev: [phase-05-rename-web-client-to-client.md](phase-05-rename-web-client-to-client.md)
|
||||
- Overview: [plan.md](plan.md)
|
||||
- Files touched: all 6 in `docs/` + root `README.md`
|
||||
|
||||
## Overview
|
||||
- **Priority:** P2
|
||||
- **Status:** pending
|
||||
- **Description:** Update every documentation file in `docs/` and the root `README.md` to reflect the post-refactor reality: new module/directory names, new package names, new jar path, dropped dependencies, Java 25, and removal of the CLI client + built-in static web UI. Preserve historical Ratel/ainilili credits in the README Credits section.
|
||||
|
||||
## Key Insights
|
||||
- This is the final cleanup phase; it does not touch any code.
|
||||
- Docs can safely mention the old names only in a "history" context — the grep-verify rule at the end tolerates zero hits outside a permitted Credits block.
|
||||
- Architecture diagrams in `system-architecture.md` likely reference `StaticFileHandler` and the three-module Maven structure — both are obsolete.
|
||||
- `codebase-summary.md` has the fastest staleness decay; prioritize it.
|
||||
|
||||
## Requirements
|
||||
**Functional**
|
||||
- Every doc accurately describes the current repo state.
|
||||
- No references to deleted code, deleted modules, or old directory names outside permitted historical credits.
|
||||
- Build/run/test commands in `deployment-guide.md` work when copy-pasted.
|
||||
|
||||
**Non-functional**
|
||||
- Single commit (or one per doc file if reviewers prefer granularity).
|
||||
- Preserve the tone and structure of existing docs — do not rewrite them wholesale.
|
||||
|
||||
## Architecture
|
||||
- Documentation tree unchanged in shape; contents updated.
|
||||
|
||||
## Related Code Files
|
||||
|
||||
**Modify:**
|
||||
- `README.md`
|
||||
- `docs/codebase-summary.md`
|
||||
- `docs/code-standards.md`
|
||||
- `docs/deployment-guide.md`
|
||||
- `docs/project-overview-pdr.md`
|
||||
- `docs/project-roadmap.md`
|
||||
- `docs/system-architecture.md`
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Enumerate stale references up-front:
|
||||
```bash
|
||||
grep -rn "landlords-\|web-client\|org\.nico\.ratel\|noson\|StaticFileHandler\|I18nHelper\|printTranslate\|SimpleClient\|landlords-client\|landlords-common\|messages_en_US" README.md docs/
|
||||
```
|
||||
Save to scratch. Every hit must be resolved below.
|
||||
|
||||
2. **`docs/codebase-summary.md`:**
|
||||
- Replace module list `landlords-common / landlords-server / landlords-client / web-client` with `server/ + client/`
|
||||
- Update package namespace mentions: `org.nico.ratel.landlords.*` → `com.miti99.caro.{common,server}.*`
|
||||
- Update directory trees (common sub-packages moved under `server/src/main/java/com/miti99/caro/common/`)
|
||||
- Remove CLI client section entirely
|
||||
- Remove built-in static web UI section entirely
|
||||
- Update test file paths (e.g. `GomokuHelperTest.java`, `GomokuAITest.java`) to reflect new location + JUnit 5
|
||||
- Mention Java 25 + shade jar path `server/target/caro-server-0.0.1-beta.jar`
|
||||
- Update dep table: drop `noson`, drop JUnit 4, drop Spring Boot parent; add `gson`, `junit-jupiter 5.x`, `maven-shade-plugin 3.6.0`
|
||||
|
||||
3. **`docs/code-standards.md`:**
|
||||
- Java version: update to 25
|
||||
- Package prefix: update to `com.miti99.caro`
|
||||
- Mention record usage guideline (records for immutable DTOs), `var` usage guideline (when RHS is obvious)
|
||||
- Test framework: JUnit 5 (`org.junit.jupiter.api`)
|
||||
|
||||
4. **`docs/deployment-guide.md`:**
|
||||
- Build command: `mvn -f server/pom.xml clean verify` (drop multi-module `-pl` syntax)
|
||||
- Run command: `java -jar server/target/caro-server-0.0.1-beta.jar`
|
||||
- Docker: `docker compose build server` + `docker compose up -d` — note service names `server` and `client`
|
||||
- Docker base images: Java 25
|
||||
- Remove any mention of `landlords-client` jar or CLI run command
|
||||
- Remove any mention of static UI at `http://localhost:1025/`
|
||||
- Update env vars / ports table if present (1025 WS unchanged)
|
||||
|
||||
5. **`docs/project-overview-pdr.md`:**
|
||||
- Update project structure diagram
|
||||
- Drop CLI client feature description
|
||||
- Drop built-in web UI feature description
|
||||
- Update tech stack section (Java 25, gson, JUnit 5)
|
||||
|
||||
6. **`docs/project-roadmap.md`:**
|
||||
- Add completed entry for this refactor (date: 2026-04-10)
|
||||
- Remove any roadmap items that targeted the deleted modules
|
||||
- Preserve any forward-looking items (e.g. "proto-over-websocket") — note protos are now under `server/src/main/resources/proto/`
|
||||
|
||||
7. **`docs/system-architecture.md`:**
|
||||
- Update component diagram: no `StaticFileHandler`; pipeline is `HttpServerCodec → HttpObjectAggregator → WebSocketServerProtocolHandler → MessageToMessageCodec → ChannelInputHandler`
|
||||
- Update module diagram: single `server/` module (no parent, no common, no CLI)
|
||||
- Update package tree: `com.miti99.caro.{common,server}.*`
|
||||
- Remove the static-UI serving flow
|
||||
- Drop references to `I18nHelper` / i18n properties file
|
||||
|
||||
8. **`README.md`:**
|
||||
- Quick start section: new build/run commands
|
||||
- Architecture overview: `server/` + `client/` with short descriptions
|
||||
- Drop CLI client getting-started section
|
||||
- Drop built-in web UI section (replaced by `client/`)
|
||||
- Update screenshots / asset paths if any were served from the deleted static dir
|
||||
- **Credits section:** preserve Ratel / ainilili attribution verbatim (historical, not a live reference)
|
||||
- Add note: "Originally forked from ainilili/ratel (Landlords card game). Rewritten as Caro (Gomoku)." if not already present
|
||||
|
||||
9. Re-run the enumeration grep from step 1. Expected remaining hits:
|
||||
- Zero in `docs/`.
|
||||
- In `README.md`: only the Credits section's historical mention of `ratel` / `ainilili`.
|
||||
- If any other hit remains, resolve it.
|
||||
|
||||
10. Commit: `docs: sweep all docs + README for post-refactor state (server/ + client/, java 25, gson, junit5)`
|
||||
|
||||
## Todo List
|
||||
- [ ] Enumerate stale references across `README.md` + `docs/`
|
||||
- [ ] Update `docs/codebase-summary.md`
|
||||
- [ ] Update `docs/code-standards.md`
|
||||
- [ ] Update `docs/deployment-guide.md`
|
||||
- [ ] Update `docs/project-overview-pdr.md`
|
||||
- [ ] Update `docs/project-roadmap.md`
|
||||
- [ ] Update `docs/system-architecture.md`
|
||||
- [ ] Update root `README.md` (preserve Credits)
|
||||
- [ ] Re-run grep — verify zero hits outside Credits
|
||||
- [ ] Copy-paste verify deployment-guide commands actually work
|
||||
- [ ] Commit
|
||||
|
||||
## Success Criteria
|
||||
- `grep -rn "landlords-\|org\.nico\.ratel\|noson\|StaticFileHandler\|I18nHelper\|printTranslate\|SimpleClient\|messages_en_US" docs/ README.md` returns only permitted Credits matches (if any).
|
||||
- `grep -rn "web-client" docs/ README.md` returns zero hits.
|
||||
- Commands in `deployment-guide.md` execute successfully when copy-pasted.
|
||||
- Architecture diagrams match actual runtime pipeline.
|
||||
|
||||
## Validation Commands
|
||||
```bash
|
||||
grep -rn "landlords-\|web-client\|org\.nico\.ratel\|noson\|StaticFileHandler\|I18nHelper\|printTranslate\|SimpleClient\|messages_en_US\|1.4.0" docs/ README.md
|
||||
# Copy-paste build/run commands from deployment-guide.md:
|
||||
mvn -f server/pom.xml clean verify
|
||||
java -jar server/target/caro-server-0.0.1-beta.jar
|
||||
docker compose build
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Risk Assessment
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|------|------------|--------|------------|
|
||||
| Stale command in deployment-guide copied by user and fails | Med | Med | Explicit copy-paste validation in step 10 |
|
||||
| Architecture diagram drawn incorrectly | Low | Low | Read `WebsocketProxy.java` to confirm current pipeline before drawing |
|
||||
| Credits section accidentally stripped | Low | Low | Explicit "preserve Credits" step; diff review |
|
||||
| Roadmap entry timestamped incorrectly | Low | Low | Use today's date from context: 2026-04-10 |
|
||||
| Missed grep keyword (e.g., `1.4.0` old jar version) | Low | Low | Enumeration step includes version string |
|
||||
|
||||
## Security Considerations
|
||||
- None (docs-only).
|
||||
|
||||
## Rollback
|
||||
`git reset --hard <pre-phase6-sha>` — single-phase, docs-only, safe.
|
||||
|
||||
## Next Steps
|
||||
- Refactor complete. Post-merge tasks for a follow-up session:
|
||||
- Verify CI `build.yml` run is green
|
||||
- Verify `deploy-pages.yml` publishes `client/dist` to GH Pages
|
||||
- Monitor first week of prod for any regression in the WS protocol (gson JSON stability)
|
||||
|
||||
## Unresolved Questions
|
||||
- Exact latest stable versions to pin (netty-all, gson, junit-jupiter, maven-shade-plugin) — defer to implementer at execution time.
|
||||
- Whether `eclipse-temurin:25-jre-alpine` is published; fallback is `25-jre`.
|
||||
- Whether `docs/project-roadmap.md` has other in-flight items that must be reconciled with this refactor's completion.
|
||||
- Whether any CI secret or GH Pages repo setting references the old `web-client` path externally (outside `.github/workflows/`).
|
||||
@@ -1,40 +0,0 @@
|
||||
---
|
||||
title: "Monorepo Refactor: Consolidate to server/ + client/"
|
||||
description: "Collapse multi-module Maven into standalone server/, rename web-client/ to client/, modernize to Java 25, migrate noson→gson + JUnit 4→5"
|
||||
status: pending
|
||||
priority: P2
|
||||
effort: 12h
|
||||
branch: master
|
||||
tags: [refactor, maven, java25, docker, monorepo]
|
||||
created: 2026-04-10
|
||||
---
|
||||
|
||||
# Refactor Project Structure
|
||||
|
||||
## Goal
|
||||
Collapse 3-module Maven build (`landlords-common`, `landlords-server`, `landlords-client`) into a single standalone `server/` Maven project; rename `web-client/` → `client/`; migrate to Java 25 + gson + JUnit 5; repackage to `com.miti99.caro.*`; delete dead CLI-client + static-UI code.
|
||||
|
||||
## Phases (strictly serial — each leaves repo buildable)
|
||||
|
||||
| # | Phase | Status | File | Effort |
|
||||
|---|-------|--------|------|--------|
|
||||
| 1 | Deletions (CLI client, static UI, i18n) | pending | [phase-01-deletions.md](phase-01-deletions.md) | 1h |
|
||||
| 2 | Consolidate Java sources into landlords-server | pending | [phase-02-consolidate-java-sources.md](phase-02-consolidate-java-sources.md) | 1.5h |
|
||||
| 3 | Standalone Maven + Java 25 + shade + noson→gson + JUnit 5 + rename to server/ | pending | [phase-03-standalone-maven-and-rename-server.md](phase-03-standalone-maven-and-rename-server.md) | 3.5h |
|
||||
| 4 | Package rename org.nico.ratel → com.miti99.caro + Java 25 modernization | pending | [phase-04-package-rename-and-java25-modernization.md](phase-04-package-rename-and-java25-modernization.md) | 3h |
|
||||
| 5 | Rename web-client/ → client/ | pending | [phase-05-rename-web-client-to-client.md](phase-05-rename-web-client-to-client.md) | 1h |
|
||||
| 6 | Docs + README sweep | pending | [phase-06-docs-and-readme-sweep.md](phase-06-docs-and-readme-sweep.md) | 2h |
|
||||
|
||||
## Dependencies
|
||||
- Phase N depends on Phase N-1 (hard serial). No parallelism.
|
||||
- Each phase ends with a **working build + working WS game flow**.
|
||||
- Phase 3 is the largest risk (build system rewrite); Phase 4 is the largest diff (every Java file touched).
|
||||
|
||||
## Rollback
|
||||
Each phase = one or more commits on `master`. Rollback = `git reset --hard <prev-commit>`; no schema/data migration.
|
||||
|
||||
## Out of scope
|
||||
- Netty threading / virtual threads
|
||||
- Proto-over-websocket protocol change (proto files moved for future use only)
|
||||
- web-client internal refactor (dir rename only)
|
||||
- Removing Ratel/ainilili historical credits from README
|
||||
@@ -1,50 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""One-shot package rewriter: org.nico.ratel.landlords.* -> com.miti99.caro.{common,server}.*.
|
||||
|
||||
Rules (order matters — server rules first so they don't get absorbed by common):
|
||||
- org.nico.ratel.landlords.server -> com.miti99.caro.server
|
||||
- org.nico.ratel.landlords -> com.miti99.caro.common
|
||||
|
||||
Touches: package declarations, imports, and any fully-qualified references in code/strings.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[2] / "server" / "src"
|
||||
|
||||
# Server rule must precede common rule (otherwise landlords.server.X becomes common.server.X).
|
||||
REPLACEMENTS = [
|
||||
(re.compile(r"org\.nico\.ratel\.landlords\.server"), "com.miti99.caro.server"),
|
||||
(re.compile(r"org\.nico\.ratel\.landlords"), "com.miti99.caro.common"),
|
||||
]
|
||||
|
||||
|
||||
def rewrite(path: pathlib.Path) -> bool:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
new = text
|
||||
for pattern, repl in REPLACEMENTS:
|
||||
new = pattern.sub(repl, new)
|
||||
if new != text:
|
||||
path.write_text(new, encoding="utf-8")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def main() -> int:
|
||||
files = sorted(ROOT.rglob("*.java"))
|
||||
if not files:
|
||||
print(f"no .java files under {ROOT}", file=sys.stderr)
|
||||
return 1
|
||||
changed = 0
|
||||
for f in files:
|
||||
if rewrite(f):
|
||||
changed += 1
|
||||
print(f"rewrote {changed}/{len(files)} files")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,241 +0,0 @@
|
||||
# Phase 01 — Proto Schemas & Gradle Build Plumbing
|
||||
|
||||
## Context Links
|
||||
- [plan.md](plan.md)
|
||||
- `server/build.gradle.kts`
|
||||
- `server/src/main/resources/proto/ClientTransferDataProtoc.proto` (to delete)
|
||||
- `server/src/main/resources/proto/ServerTransferDataProtoc.proto` (to delete)
|
||||
- `server/src/main/resources/proto/generate.sh` (to delete)
|
||||
- `server/src/main/java/com/miti99/caro/common/entity/ClientTransferData.java` (to delete)
|
||||
- `server/src/main/java/com/miti99/caro/common/entity/ServerTransferData.java` (to delete)
|
||||
- `server/src/main/java/com/miti99/caro/common/enums/ServerEventCode.java` (reference for request schema audit)
|
||||
- `server/src/main/java/com/miti99/caro/common/enums/ClientEventCode.java` (reference for response schema audit)
|
||||
|
||||
## Overview
|
||||
- **Priority:** P2 (blocker for all later phases)
|
||||
- **Status:** pending
|
||||
- Author the typed `request.proto` and `response.proto` schemas based on the audit in `plan.md`. Stand up the Gradle `com.google.protobuf` plugin so Java classes are generated at build time. Delete the hand-committed generated files, the old envelope-only `.proto` files, and the manual `generate.sh`.
|
||||
|
||||
## Key Insights
|
||||
- `common.proto` is NOT created. Audit found no field genuinely shared across both request and response directions (only `GameMove` row/col, which is 2 `int32` fields — cheaper inline than a shared type + import). YAGNI.
|
||||
- Plugin v0.9.6 default source path: `src/main/proto/`. No sourceSet override needed.
|
||||
- Generated package lives in `com.miti99.caro.protocol` (new fresh package — do NOT reuse `common.entity` which was the old envelope home). Call sites that used `ClientTransferData.ClientTransferDataProtoc` disappear entirely in phase 02a/02b — those classes are deleted in this phase.
|
||||
- `protoc` artifact `com.google.protobuf:protoc:3.25.5` is CI-safe (no host protoc).
|
||||
- `java_package` in each `.proto` anchors the FQCN; `java_multiple_files = true` means each top-level message becomes its own `.java` file, nicer imports.
|
||||
|
||||
## Requirements
|
||||
**Functional**
|
||||
- `./gradlew -p server compileJava` generates typed classes under `build/generated/sources/proto/main/java/com/miti99/caro/protocol/`.
|
||||
- Stubbed `WebsocketTransferHandler` (unchanged from today) still compiles — we keep the old handler + old `ChannelUtils` + old listeners compiling via the DELETED envelope types replaced by a temporary no-op? NO — instead, this phase does ONLY proto authoring + plugin wiring. The old generated envelope Java files stay deleted at the end of phase, but the old string-based handler code is still present and must compile. To bridge: **keep this phase isolated to proto+plugin+deletion of the two old generated Java files, but also replace those two imports in the minimum call sites with a 2-line throwaway stub type** — rejected: too hacky. Instead: **this phase DOES NOT compile on its own** — compile verification happens at phase 02a. Phase 01 is a commit that intentionally leaves the build broken and is followed immediately by 02a. That violates "each phase leaves build green". Resolved: **merge phase 01 + 02a into one commit**. See Phase 02a for the unified commit; phase 01 is the planning sub-stage (proto authoring + plugin wiring), phase 02a is the compiling sub-stage (dispatcher scaffolding).
|
||||
|
||||
→ **Action:** keep phase 01 and 02a as separate plan files for clarity, but both are committed together as a single commit at end of 02a. The commit message covers both. Phase 01's success criteria drops "build green" and adds "proto files generate when build attempted at end of 02a".
|
||||
|
||||
**Non-functional**
|
||||
- No new committed generated Java.
|
||||
- Proto files authored in a style consistent with google's style guide (snake_case fields, `CamelCase` messages).
|
||||
- Each message has an obvious 1:1 mapping to an audit row in `plan.md`.
|
||||
|
||||
## Architecture
|
||||
```
|
||||
server/src/main/proto/
|
||||
request.proto // oneof payload: HeartbeatRequest | SetNicknameRequest | ... | ClientExitRequest
|
||||
response.proto // oneof payload: ClientConnectResponse | NicknameSetResponse | ... | ClientExitResponse
|
||||
build.gradle.kts
|
||||
plugins { id("com.google.protobuf") version "0.9.6" }
|
||||
protobuf { protoc { artifact = "com.google.protobuf:protoc:3.25.5" } }
|
||||
-> generates build/generated/sources/proto/main/java/com/miti99/caro/protocol/{Request,Response,HeartbeatRequest,...}.java
|
||||
```
|
||||
|
||||
`request.proto` skeleton:
|
||||
```proto
|
||||
syntax = "proto3";
|
||||
package miti99.caro.protocol;
|
||||
option java_package = "com.miti99.caro.protocol";
|
||||
option java_multiple_files = true;
|
||||
|
||||
message Request {
|
||||
oneof payload {
|
||||
HeartbeatRequest heartbeat = 1;
|
||||
SetNicknameRequest set_nickname = 2;
|
||||
SetClientInfoRequest set_client_info = 3;
|
||||
CreateRoomRequest create_room = 4;
|
||||
CreatePveRoomRequest create_pve_room = 5;
|
||||
GetRoomsRequest get_rooms = 6;
|
||||
JoinRoomRequest join_room = 7;
|
||||
GameStartingRequest game_starting = 8;
|
||||
GameReadyRequest game_ready = 9;
|
||||
GameMoveRequest game_move = 10;
|
||||
GameResetRequest game_reset = 11;
|
||||
WatchGameRequest watch_game = 12;
|
||||
WatchGameExitRequest watch_game_exit = 13;
|
||||
ClientExitRequest client_exit = 14;
|
||||
}
|
||||
}
|
||||
|
||||
message HeartbeatRequest {}
|
||||
message SetNicknameRequest { string nickname = 1; }
|
||||
message SetClientInfoRequest { string version = 1; }
|
||||
message CreateRoomRequest {}
|
||||
message CreatePveRoomRequest { int32 difficulty = 1; }
|
||||
message GetRoomsRequest {}
|
||||
message JoinRoomRequest { int32 room_id = 1; }
|
||||
message GameStartingRequest {}
|
||||
message GameReadyRequest {}
|
||||
message GameMoveRequest { int32 row = 1; int32 col = 2; }
|
||||
message GameResetRequest {}
|
||||
message WatchGameRequest { int32 room_id = 1; }
|
||||
message WatchGameExitRequest {}
|
||||
message ClientExitRequest {}
|
||||
```
|
||||
|
||||
`response.proto` skeleton:
|
||||
```proto
|
||||
syntax = "proto3";
|
||||
package miti99.caro.protocol;
|
||||
option java_package = "com.miti99.caro.protocol";
|
||||
option java_multiple_files = true;
|
||||
|
||||
message Response {
|
||||
oneof payload {
|
||||
ClientConnectResponse client_connect = 1;
|
||||
NicknameSetResponse nickname_set = 2;
|
||||
ShowOptionsResponse show_options = 3;
|
||||
ShowRoomsResponse show_rooms = 4;
|
||||
RoomCreateSuccessResponse room_create_success = 5;
|
||||
RoomJoinSuccessResponse room_join_success = 6;
|
||||
RoomJoinFailFullResponse room_join_fail_full = 7;
|
||||
RoomJoinFailNotFoundResponse room_join_fail_not_found = 8;
|
||||
RoomPlayFailNotFoundResponse room_play_fail_not_found = 9;
|
||||
GameStartingResponse game_starting = 10;
|
||||
GameReadyResponse game_ready = 11;
|
||||
GameMoveSuccessResponse game_move_success = 12;
|
||||
GameMoveInvalidResponse game_move_invalid = 13;
|
||||
GameMoveOccupiedResponse game_move_occupied = 14;
|
||||
GameMoveOutOfBoundsResponse game_move_out_of_bounds = 15;
|
||||
GameMoveNotYourTurnResponse game_move_not_your_turn = 16;
|
||||
GameOverResponse game_over = 17;
|
||||
PveDifficultyNotSupportResponse pve_difficulty_not_support = 18;
|
||||
WatchGameSuccessResponse watch_game_success = 19;
|
||||
ClientExitResponse client_exit = 20;
|
||||
}
|
||||
}
|
||||
|
||||
message ClientConnectResponse { int32 client_id = 1; }
|
||||
message NicknameSetResponse { int32 invalid_length = 1; } // 0 = prompt-only
|
||||
message ShowOptionsResponse {}
|
||||
message RoomSummary {
|
||||
int32 room_id = 1;
|
||||
string room_owner = 2;
|
||||
int32 room_client_count = 3;
|
||||
string room_type = 4;
|
||||
}
|
||||
message ShowRoomsResponse { repeated RoomSummary rooms = 1; }
|
||||
message RoomCreateSuccessResponse { int32 id = 1; string room_owner = 2; string room_type = 3; }
|
||||
message RoomJoinSuccessResponse {
|
||||
int32 client_id = 1;
|
||||
string client_nickname = 2;
|
||||
int32 room_id = 3;
|
||||
string room_owner = 4;
|
||||
int32 room_client_count = 5;
|
||||
}
|
||||
message RoomJoinFailFullResponse { int32 room_id = 1; string room_owner = 2; }
|
||||
message RoomJoinFailNotFoundResponse { int32 room_id = 1; }
|
||||
message RoomPlayFailNotFoundResponse {}
|
||||
message GameStartingResponse {
|
||||
int32 room_id = 1;
|
||||
int32 black_player_id = 2;
|
||||
string black_player_nickname = 3;
|
||||
int32 white_player_id = 4;
|
||||
string white_player_nickname = 5;
|
||||
int32 board_size = 6;
|
||||
}
|
||||
message GameReadyResponse {
|
||||
string client_nickname = 1;
|
||||
string status = 2;
|
||||
int32 client_id = 3;
|
||||
}
|
||||
message GameMoveSuccessResponse {
|
||||
int32 row = 1;
|
||||
int32 col = 2;
|
||||
string piece = 3;
|
||||
string player_nickname = 4;
|
||||
int32 player_id = 5;
|
||||
}
|
||||
message GameMoveInvalidResponse {}
|
||||
message GameMoveOccupiedResponse {}
|
||||
message GameMoveOutOfBoundsResponse {}
|
||||
message GameMoveNotYourTurnResponse {}
|
||||
message GameOverResponse { string result = 1; string winner_nickname = 2; }
|
||||
message PveDifficultyNotSupportResponse {}
|
||||
message WatchGameSuccessResponse { string owner = 1; string status = 2; }
|
||||
message ClientExitResponse {
|
||||
int32 room_id = 1;
|
||||
int32 exit_client_id = 2;
|
||||
string exit_client_nickname = 3;
|
||||
}
|
||||
```
|
||||
|
||||
## Related Code Files
|
||||
**Modify**
|
||||
- `server/build.gradle.kts` — add protobuf plugin + `protobuf{}` block.
|
||||
|
||||
**Create**
|
||||
- `server/src/main/proto/request.proto`
|
||||
- `server/src/main/proto/response.proto`
|
||||
|
||||
**Delete**
|
||||
- `server/src/main/resources/proto/ClientTransferDataProtoc.proto`
|
||||
- `server/src/main/resources/proto/ServerTransferDataProtoc.proto`
|
||||
- `server/src/main/resources/proto/generate.sh`
|
||||
- `server/src/main/java/com/miti99/caro/common/entity/ClientTransferData.java`
|
||||
- `server/src/main/java/com/miti99/caro/common/entity/ServerTransferData.java`
|
||||
|
||||
## Implementation Steps
|
||||
1. Create `server/src/main/proto/request.proto` per skeleton above.
|
||||
2. Create `server/src/main/proto/response.proto` per skeleton above.
|
||||
3. Edit `server/build.gradle.kts`:
|
||||
- Add `id("com.google.protobuf") version "0.9.6"` to `plugins {}`.
|
||||
- Bump `com.gradleup.shadow` plugin **8.3.5 → 8.3.8** (maintenance release).
|
||||
- Add top-level block:
|
||||
```kotlin
|
||||
protobuf {
|
||||
protoc { artifact = "com.google.protobuf:protoc:3.25.5" }
|
||||
}
|
||||
```
|
||||
- Keep `implementation("com.google.protobuf:protobuf-java:3.25.5")`.
|
||||
- Bump `io.netty:netty-all` **4.1.115.Final → 4.1.128.Final** (+13 releases, security patches; low risk).
|
||||
- Bump `org.junit:junit-bom` platform **5.11.3 → 5.11.4** (patch release).
|
||||
- Keep gradle wrapper at 9.2.1 (already ≥ pre-2026 latest).
|
||||
4. `git rm` the five to-delete files listed above.
|
||||
5. Do NOT attempt `./gradlew -p server compileJava` yet — Phase 02a provides the call-site migration that makes the build green again. This phase's work is continued into 02a as one unified commit.
|
||||
|
||||
## Todo List
|
||||
- [ ] Author `request.proto` with full oneof + typed messages
|
||||
- [ ] Author `response.proto` with full oneof + typed messages
|
||||
- [ ] Add protobuf plugin + `protobuf {}` block to `build.gradle.kts`
|
||||
- [ ] Bump `shadow` plugin 8.3.5 → 8.3.8
|
||||
- [ ] Bump `netty-all` 4.1.115.Final → 4.1.128.Final
|
||||
- [ ] Bump `junit-bom` 5.11.3 → 5.11.4
|
||||
- [ ] `git rm` old `.proto` files + `generate.sh`
|
||||
- [ ] `git rm` hand-committed `ClientTransferData.java` + `ServerTransferData.java`
|
||||
- [ ] (Build verification happens at end of Phase 02a — shared commit)
|
||||
|
||||
## Success Criteria
|
||||
- `ls server/src/main/proto/` → `request.proto`, `response.proto` only.
|
||||
- `ls server/src/main/resources/proto/` → directory empty or gone.
|
||||
- `find server/src/main/java/com/miti99/caro/common/entity -name "ClientTransferData.java" -o -name "ServerTransferData.java"` returns nothing.
|
||||
- `build.gradle.kts` contains `com.google.protobuf` plugin id.
|
||||
|
||||
## Risk Assessment
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|---|---|---|---|
|
||||
| Proto field name collision with generated Java accessor (e.g. `class`) | Low | Medium | Audit names: nothing reserved used |
|
||||
| `java_multiple_files = true` output path differs from expectation | Low | Low | Verified at end of 02a when build runs |
|
||||
| IntelliJ doesn't pick up generated sources until reimport | Medium | Low (DX) | `./gradlew idea` or gradle reimport |
|
||||
| Package rename `common.entity` → `protocol` leaves dangling imports | High | High in isolation; mitigated by unified commit with 02a | ALL migrations handled in 02a |
|
||||
|
||||
## Security Considerations
|
||||
- None. Pure schema authoring.
|
||||
|
||||
## Next Steps
|
||||
- Phase 02a uses the generated classes immediately: creates the sealed `ClientRequest` hierarchy + `RequestDispatcher` + rewrites the WS pipeline. **Phases 01 + 02a ship as ONE git commit** because Phase 01 alone leaves the build broken.
|
||||
@@ -1,308 +0,0 @@
|
||||
# Phase 02a — Dispatcher Scaffolding + Binary WS Pipeline
|
||||
|
||||
## Context Links
|
||||
- [plan.md](plan.md)
|
||||
- [phase-01-proto-schemas-and-build.md](phase-01-proto-schemas-and-build.md)
|
||||
- `server/src/main/java/com/miti99/caro/server/SimpleServer.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/ServerContains.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/proxy/WebsocketProxy.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/proxy/ProtobufProxy.java` (to delete)
|
||||
- `server/src/main/java/com/miti99/caro/server/proxy/Proxy.java` (to delete)
|
||||
- `server/src/main/java/com/miti99/caro/server/handler/WebsocketTransferHandler.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/handler/ProtobufTransferHandler.java` (to delete)
|
||||
- `server/src/main/java/com/miti99/caro/server/handler/SecondProtobufCodec.java` (to delete)
|
||||
- `server/src/main/java/com/miti99/caro/common/channel/ChannelUtils.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/ServerEventListener.java`
|
||||
|
||||
## Overview
|
||||
- **Priority:** P2 (core of the refactor)
|
||||
- **Status:** pending (blocked by Phase 01)
|
||||
- Build the sealed `ClientRequest` interface + record hierarchy + `RequestDispatcher`. Rewrite the Netty pipeline for `BinaryWebSocketFrame`. Delete TCP classes. Flip port to 1999. Port ONE handler (heartbeat) as a smoke-test through the new pipeline; leave the other 13 handlers as `// TODO phase 02b` stubs that throw `UnsupportedOperationException`.
|
||||
- **This phase is committed together with Phase 01** as one atomic commit because Phase 01 alone leaves the build broken (deleted envelope types). The commit message is `refactor(server): typed protobuf wire + sealed record dispatcher scaffolding`.
|
||||
- Phase 02b then migrates the 13 stubbed handlers.
|
||||
|
||||
## Key Insights
|
||||
- Sealed interface + records give exhaustive pattern matching in `RequestDispatcher`. The compiler enforces that every oneof case has a record, which eliminates the class of bugs where a new event code is added and some dispatch site forgets to update.
|
||||
- Old `ServerEventListener` reflection lookup (`Class.forName(LISTENER_PREFIX + code.name())`) is replaced by a plain `switch` on the sealed hierarchy. No reflection, no map cache.
|
||||
- Keeping handler business logic: each old `ServerEventListener_CODE_*` is renamed to `<Verb>Handler` (e.g. `GameMoveHandler`, `CreatePveRoomHandler`). The body stays the same — only the signature changes from `(ClientSide, String)` to `(ClientSide, XxxRequest)`. In 02a we create the dispatcher + the `HeartbeatHandler` (no-op today) and stub-dispatch the rest with `// TODO 02b`.
|
||||
- `RoomClearTask` scheduling currently lives in `ProtobufProxy.start()`. Must move to `WebsocketProxy.start()` — explicit todo.
|
||||
- `ChannelUtils` in 02a gets a `push(Channel, Response)` method that takes a fully-built `Response` proto. Existing callers (which pass `ClientEventCode, String`) are NOT migrated yet — instead, `ChannelUtils.pushToClient(Channel, ClientEventCode, String)` is temporarily kept as a throwing stub (`throw new UnsupportedOperationException("phase 02b")`) so callers still compile. Phase 02b deletes the stub as each handler migrates to the typed `push(Channel, Response)`.
|
||||
- Wait — if old call sites throw at runtime on every outbound message, the server crashes immediately. Alternative: keep `pushToClient` logging at WARN + no-op return. Accept: the build compiles and boots, but NO outbound traffic works between 02a and 02b commit. Since 02a and 02b are both code changes and we do not deploy 02a alone to prod (personal project), this is acceptable. **Rule: 02a + 02b MUST NOT be split across a deployment boundary.** Documented in Risk Assessment.
|
||||
- Alternatively: DO NOT stub — migrate ALL handlers in one big commit. Rejected: ~14 handlers + full pipeline rewrite in one commit is the "too big to review" anti-pattern and breaks the phase-at-a-time rollback.
|
||||
- Final decision: **02a temporarily leaves old `pushToClient(ch, code, data)` stub as `throw UnsupportedOperationException`** — but NONE of the 13 stubbed handlers actually reach it because the dispatcher throws BEFORE calling them. So 02a compiles, boots, accepts a connection, and responds to heartbeat. Any other inbound request throws in the dispatcher. This is the 02a acceptance shape.
|
||||
|
||||
## Requirements
|
||||
**Functional**
|
||||
- Server binds single port (default 1999), serves WebSocket on `/ratel`.
|
||||
- Incoming `BinaryWebSocketFrame` → `Request.parseFrom(bytes)` → `switch` on payloadCase → record conversion → `RequestDispatcher.dispatch(client, request)`.
|
||||
- `HeartbeatRequest` flows end-to-end (no-op).
|
||||
- Every other request throws `UnsupportedOperationException("TODO phase 02b: <case>")` from the dispatcher — loud, unmissable.
|
||||
- Pipeline retains: `HttpServerCodec` → `HttpObjectAggregator(8192)` → `ChunkedWriteHandler` → `IdleStateHandler(30min)` → `WebSocketServerProtocolHandler("/ratel")` → `WebsocketTransferHandler`.
|
||||
- `HandshakeComplete` init logic preserved.
|
||||
- `RoomClearTask` scheduled from `WebsocketProxy.start()`.
|
||||
- `-p 1999` arg still honored, default `1999`.
|
||||
|
||||
**Non-functional**
|
||||
- Build green: `./gradlew -p server clean build` passes (all 37 unit tests still pass; they test pure game logic, unaffected).
|
||||
- Zero references to `TextWebSocketFrame`, `ProtobufProxy`, `SecondProtobufCodec`, `implements Proxy`.
|
||||
|
||||
## Architecture
|
||||
```
|
||||
client ---ws://host:1999/ratel---> Netty
|
||||
HttpServerCodec
|
||||
-> HttpObjectAggregator(8192)
|
||||
-> ChunkedWriteHandler
|
||||
-> IdleStateHandler(30min read)
|
||||
-> WebSocketServerProtocolHandler("/ratel")
|
||||
-> WebsocketTransferHandler : SimpleChannelInboundHandler<BinaryWebSocketFrame>
|
||||
channelRead0(ctx, frame):
|
||||
byte[] bytes = ByteBufUtil.getBytes(frame.content());
|
||||
Request req = Request.parseFrom(bytes);
|
||||
ClientRequest converted = RequestConverter.convert(req); // sealed record
|
||||
if (!(converted instanceof HeartbeatRequestRecord)) log dispatch;
|
||||
RequestDispatcher.dispatch(client, converted);
|
||||
```
|
||||
|
||||
Sealed hierarchy (package `com.miti99.caro.server.event.request`):
|
||||
```java
|
||||
public sealed interface ClientRequest permits
|
||||
HeartbeatRequestRecord,
|
||||
SetNicknameRequestRecord,
|
||||
SetClientInfoRequestRecord,
|
||||
CreateRoomRequestRecord,
|
||||
CreatePveRoomRequestRecord,
|
||||
GetRoomsRequestRecord,
|
||||
JoinRoomRequestRecord,
|
||||
GameStartingRequestRecord,
|
||||
GameReadyRequestRecord,
|
||||
GameMoveRequestRecord,
|
||||
GameResetRequestRecord,
|
||||
WatchGameRequestRecord,
|
||||
WatchGameExitRequestRecord,
|
||||
ClientExitRequestRecord { }
|
||||
|
||||
public record HeartbeatRequestRecord() implements ClientRequest { }
|
||||
public record SetNicknameRequestRecord(String nickname) implements ClientRequest { }
|
||||
public record SetClientInfoRequestRecord(String version) implements ClientRequest { }
|
||||
public record CreateRoomRequestRecord() implements ClientRequest { }
|
||||
public record CreatePveRoomRequestRecord(int difficulty) implements ClientRequest { }
|
||||
public record GetRoomsRequestRecord() implements ClientRequest { }
|
||||
public record JoinRoomRequestRecord(int roomId) implements ClientRequest { }
|
||||
public record GameStartingRequestRecord() implements ClientRequest { }
|
||||
public record GameReadyRequestRecord() implements ClientRequest { }
|
||||
public record GameMoveRequestRecord(int row, int col) implements ClientRequest { }
|
||||
public record GameResetRequestRecord() implements ClientRequest { }
|
||||
public record WatchGameRequestRecord(int roomId) implements ClientRequest { }
|
||||
public record WatchGameExitRequestRecord() implements ClientRequest { }
|
||||
public record ClientExitRequestRecord() implements ClientRequest { }
|
||||
```
|
||||
|
||||
Converter (single place, keeps dispatcher clean):
|
||||
```java
|
||||
public final class RequestConverter {
|
||||
public static ClientRequest convert(Request req) {
|
||||
return switch (req.getPayloadCase()) {
|
||||
case HEARTBEAT -> new HeartbeatRequestRecord();
|
||||
case SET_NICKNAME -> new SetNicknameRequestRecord(req.getSetNickname().getNickname());
|
||||
case SET_CLIENT_INFO -> new SetClientInfoRequestRecord(req.getSetClientInfo().getVersion());
|
||||
case CREATE_ROOM -> new CreateRoomRequestRecord();
|
||||
case CREATE_PVE_ROOM -> new CreatePveRoomRequestRecord(req.getCreatePveRoom().getDifficulty());
|
||||
case GET_ROOMS -> new GetRoomsRequestRecord();
|
||||
case JOIN_ROOM -> new JoinRoomRequestRecord(req.getJoinRoom().getRoomId());
|
||||
case GAME_STARTING -> new GameStartingRequestRecord();
|
||||
case GAME_READY -> new GameReadyRequestRecord();
|
||||
case GAME_MOVE -> new GameMoveRequestRecord(req.getGameMove().getRow(), req.getGameMove().getCol());
|
||||
case GAME_RESET -> new GameResetRequestRecord();
|
||||
case WATCH_GAME -> new WatchGameRequestRecord(req.getWatchGame().getRoomId());
|
||||
case WATCH_GAME_EXIT -> new WatchGameExitRequestRecord();
|
||||
case CLIENT_EXIT -> new ClientExitRequestRecord();
|
||||
case PAYLOAD_NOT_SET -> throw new IllegalArgumentException("Request payload not set");
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Dispatcher (02a: heartbeat only; 02b fills in):
|
||||
```java
|
||||
public final class RequestDispatcher {
|
||||
public static void dispatch(ClientSide client, ClientRequest req) {
|
||||
switch (req) {
|
||||
case HeartbeatRequestRecord r -> { /* no-op */ }
|
||||
case SetNicknameRequestRecord r -> throw todo("set_nickname");
|
||||
case SetClientInfoRequestRecord r -> throw todo("set_client_info");
|
||||
case CreateRoomRequestRecord r -> throw todo("create_room");
|
||||
case CreatePveRoomRequestRecord r -> throw todo("create_pve_room");
|
||||
case GetRoomsRequestRecord r -> throw todo("get_rooms");
|
||||
case JoinRoomRequestRecord r -> throw todo("join_room");
|
||||
case GameStartingRequestRecord r -> throw todo("game_starting");
|
||||
case GameReadyRequestRecord r -> throw todo("game_ready");
|
||||
case GameMoveRequestRecord r -> throw todo("game_move");
|
||||
case GameResetRequestRecord r -> throw todo("game_reset");
|
||||
case WatchGameRequestRecord r -> throw todo("watch_game");
|
||||
case WatchGameExitRequestRecord r -> throw todo("watch_game_exit");
|
||||
case ClientExitRequestRecord r -> throw todo("client_exit");
|
||||
}
|
||||
}
|
||||
private static UnsupportedOperationException todo(String name) {
|
||||
return new UnsupportedOperationException("TODO phase 02b: " + name);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`ChannelUtils` in 02a:
|
||||
```java
|
||||
public final class ChannelUtils {
|
||||
public static ChannelFuture push(Channel channel, Response response) {
|
||||
byte[] bytes = response.toByteArray();
|
||||
return channel.writeAndFlush(new BinaryWebSocketFrame(Unpooled.wrappedBuffer(bytes)));
|
||||
}
|
||||
}
|
||||
```
|
||||
Old `pushToClient` / `pushToServer` methods are DELETED in 02a — but the old `ServerEventListener_CODE_*` classes call them. Instead, in 02a, **delete all old `ServerEventListener_CODE_*.java` files** and the `ServerEventListener` interface entirely. The dispatcher throws for all non-heartbeat cases, so no handler needs to exist yet. Phase 02b creates new `<Verb>Handler` classes from scratch, using the old files as reference (git history).
|
||||
|
||||
Wait — if old handlers are deleted in 02a, where does the business logic come from in 02b? Answer: **re-implemented in 02b by copying from git history of each file**, adapted to take a record and call `ChannelUtils.push(ch, Response)`. This is cleaner than a halfway state.
|
||||
|
||||
Revised 02a deletion scope:
|
||||
- Delete `server/src/main/java/com/miti99/caro/server/event/ServerEventListener.java`
|
||||
- Delete ALL `server/src/main/java/com/miti99/caro/server/event/ServerEventListener_CODE_*.java` (14 files)
|
||||
- The business logic lives in git history; phase 02b re-creates it file-by-file as `<Verb>Handler` classes.
|
||||
|
||||
## Related Code Files
|
||||
**Modify**
|
||||
- `server/src/main/java/com/miti99/caro/server/ServerContains.java` — `public static int port = 1999;`
|
||||
- `server/src/main/java/com/miti99/caro/server/SimpleServer.java` — remove `ProtobufProxy` thread, keep only `WebsocketProxy`.
|
||||
- `server/src/main/java/com/miti99/caro/server/proxy/WebsocketProxy.java` — schedule `RoomClearTask` here; remove `implements Proxy`.
|
||||
- `server/src/main/java/com/miti99/caro/server/handler/WebsocketTransferHandler.java` — extends `SimpleChannelInboundHandler<BinaryWebSocketFrame>`; parse `Request`; dispatch.
|
||||
- `server/src/main/java/com/miti99/caro/common/channel/ChannelUtils.java` — replaced with single `push(Channel, Response)` method.
|
||||
- `server/build.gradle.kts` — (already modified in phase 01) confirm protobuf plugin wires `compileJava`.
|
||||
|
||||
**Create**
|
||||
- `server/src/main/java/com/miti99/caro/server/event/request/ClientRequest.java` (sealed interface)
|
||||
- `server/src/main/java/com/miti99/caro/server/event/request/HeartbeatRequestRecord.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/request/SetNicknameRequestRecord.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/request/SetClientInfoRequestRecord.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/request/CreateRoomRequestRecord.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/request/CreatePveRoomRequestRecord.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/request/GetRoomsRequestRecord.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/request/JoinRoomRequestRecord.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/request/GameStartingRequestRecord.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/request/GameReadyRequestRecord.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/request/GameMoveRequestRecord.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/request/GameResetRequestRecord.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/request/WatchGameRequestRecord.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/request/WatchGameExitRequestRecord.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/request/ClientExitRequestRecord.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/RequestConverter.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/RequestDispatcher.java`
|
||||
|
||||
**Delete**
|
||||
- `server/src/main/java/com/miti99/caro/server/proxy/ProtobufProxy.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/proxy/Proxy.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/handler/ProtobufTransferHandler.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/handler/SecondProtobufCodec.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/ServerEventListener.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/ServerEventListener_CODE_CLIENT_EXIT.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/ServerEventListener_CODE_CLIENT_INFO_SET.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/ServerEventListener_CODE_CLIENT_NICKNAME_SET.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/ServerEventListener_CODE_CLIENT_OFFLINE.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/ServerEventListener_CODE_GAME_MOVE.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/ServerEventListener_CODE_GAME_READY.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/ServerEventListener_CODE_GAME_STARTING.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/ServerEventListener_CODE_GAME_WATCH.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/ServerEventListener_CODE_GAME_WATCH_EXIT.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/ServerEventListener_CODE_GET_ROOMS.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/ServerEventListener_CODE_ROOM_CREATE.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/ServerEventListener_CODE_ROOM_CREATE_PVE.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/ServerEventListener_CODE_ROOM_JOIN.java`
|
||||
|
||||
**Keep for now** (used by HandshakeComplete / ClientSide offline path which phase 02a DOES need to emit):
|
||||
- `server/src/main/java/com/miti99/caro/common/enums/ClientEventCode.java` — kept. Used only in JS event bus mapping; Java side may drop references but enum stays.
|
||||
|
||||
`WebsocketTransferHandler` in 02a still needs to send `CLIENT_CONNECT` + `NICKNAME_SET` prompt on handshake. Those are 2 outbound messages — `ChannelUtils.push(ch, Response.newBuilder().setClientConnect(ClientConnectResponse.newBuilder().setClientId(id)).build())` and the analogous `setNicknameSet(...)`. So `ChannelUtils.push` is exercised in 02a. Good.
|
||||
|
||||
`CLIENT_OFFLINE` is a server-internal event triggered on channel close — NOT a client request. In the old design it was piggy-backed on `ServerEventCode` + `ServerEventListener.get(CLIENT_OFFLINE)`. In the new design, `WebsocketTransferHandler.clientOfflineEvent()` calls a dedicated `ClientOfflineHandler.handle(client)` method directly — NOT routed through the request dispatcher. Phase 02b creates `ClientOfflineHandler`. For 02a, the offline path is a TODO-noop (log only) — no business impact during 02a smoke test.
|
||||
|
||||
## Implementation Steps
|
||||
1. **Port flip.** `ServerContains.port = 1999;`
|
||||
2. **Move `RoomClearTask`** from `ProtobufProxy.start()` into `WebsocketProxy.start()`, inside the `bootstrap.bind().sync()` block before `closeFuture().sync()`.
|
||||
3. **Delete TCP stack:** `ProtobufProxy.java`, `Proxy.java`, `ProtobufTransferHandler.java`, `SecondProtobufCodec.java`. Remove `implements Proxy` from `WebsocketProxy`. `SimpleServer.main` drops the TCP proxy thread and starts only `new WebsocketProxy().start(ServerContains.port)` (no `+1`).
|
||||
4. **Delete old listener package contents:** `ServerEventListener.java` + all 14 `ServerEventListener_CODE_*.java` files. Phase 02b will re-create handlers in the new style.
|
||||
5. **Create records package.** Under `com.miti99.caro.server.event.request`, create the sealed interface `ClientRequest` and all 14 record classes per design above.
|
||||
6. **Create `RequestConverter.java`** in `com.miti99.caro.server.event` with the exhaustive switch.
|
||||
7. **Create `RequestDispatcher.java`** in `com.miti99.caro.server.event` with heartbeat no-op + `UnsupportedOperationException` stubs for all other cases.
|
||||
8. **Rewrite `ChannelUtils.java`:** single static method `push(Channel channel, Response response)` that writes `new BinaryWebSocketFrame(Unpooled.wrappedBuffer(response.toByteArray()))`.
|
||||
9. **Rewrite `WebsocketTransferHandler.java`:**
|
||||
- Extend `SimpleChannelInboundHandler<BinaryWebSocketFrame>`.
|
||||
- `channelRead0`:
|
||||
```java
|
||||
byte[] bytes = ByteBufUtil.getBytes(frame.content());
|
||||
Request raw;
|
||||
try { raw = Request.parseFrom(bytes); }
|
||||
catch (InvalidProtocolBufferException e) {
|
||||
SimplePrinter.serverLog("WARN malformed request: " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
ClientRequest req = RequestConverter.convert(raw);
|
||||
ClientSide client = ServerContains.CLIENT_SIDE_MAP.get(getId(ctx.channel()));
|
||||
if (!(req instanceof HeartbeatRequestRecord)) {
|
||||
SimplePrinter.serverLog(client.getId() + " | " + client.getNickname() + " do: " + req.getClass().getSimpleName());
|
||||
}
|
||||
RequestDispatcher.dispatch(client, req);
|
||||
```
|
||||
- `userEventTriggered` `HandshakeComplete` block: unchanged state setup; the 2s-delayed thread now emits two typed `Response` messages:
|
||||
```java
|
||||
ChannelUtils.push(ch, Response.newBuilder()
|
||||
.setClientConnect(ClientConnectResponse.newBuilder().setClientId(clientSide.getId()))
|
||||
.build());
|
||||
ChannelUtils.push(ch, Response.newBuilder()
|
||||
.setNicknameSet(NicknameSetResponse.newBuilder().setInvalidLength(0))
|
||||
.build());
|
||||
```
|
||||
(`invalid_length=0` is the "prompt" sentinel — documented in Phase 04 client decode.)
|
||||
- `clientOfflineEvent` becomes a log-only TODO for 02a; wired up to `ClientOfflineHandler` in 02b.
|
||||
10. **Compile + test:** `./gradlew -p server clean build`. Must be green.
|
||||
11. **Boot smoke:** `./gradlew -p server run` (or execute jar). Server logs "websocket server was successfully started on port 1999".
|
||||
12. **Single commit** with Phase 01 + Phase 02a changes: `refactor(server): typed protobuf wire + sealed record dispatcher scaffolding`.
|
||||
|
||||
## Todo List
|
||||
- [ ] `ServerContains.port = 1999`
|
||||
- [ ] `SimpleServer` starts only `WebsocketProxy`
|
||||
- [ ] `RoomClearTask` migrated into `WebsocketProxy.start()`
|
||||
- [ ] Delete `ProtobufProxy`, `Proxy`, `ProtobufTransferHandler`, `SecondProtobufCodec`
|
||||
- [ ] Delete `ServerEventListener` + all 14 `ServerEventListener_CODE_*.java`
|
||||
- [ ] Create sealed `ClientRequest` + 14 record classes
|
||||
- [ ] Create `RequestConverter`
|
||||
- [ ] Create `RequestDispatcher` (heartbeat no-op + others throw)
|
||||
- [ ] Rewrite `ChannelUtils.push(Channel, Response)`
|
||||
- [ ] Rewrite `WebsocketTransferHandler` for BinaryWebSocketFrame
|
||||
- [ ] HandshakeComplete emits typed `CLIENT_CONNECT` + `NICKNAME_SET` prompt
|
||||
- [ ] `./gradlew -p server clean build` green (37/37 unit tests)
|
||||
- [ ] Commit (jointly with Phase 01): `refactor(server): typed protobuf wire + sealed record dispatcher scaffolding`
|
||||
|
||||
## Success Criteria
|
||||
- Zero references in `server/src` to `TextWebSocketFrame`, `ProtobufProxy`, `ProtobufTransferHandler`, `SecondProtobufCodec`, `implements Proxy`, `ServerEventListener`.
|
||||
- `grep -r "com.miti99.caro.common.entity.Client\|ServerTransferData" server/src` returns nothing.
|
||||
- `grep -r "Class.forName" server/src` returns nothing (reflection lookup dead).
|
||||
- `./gradlew -p server clean build` green.
|
||||
- Server boots on port 1999 and logs startup message.
|
||||
- Any non-heartbeat inbound message is logged and causes `UnsupportedOperationException` (intentional — sanity check).
|
||||
|
||||
## Risk Assessment
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|---|---|---|---|
|
||||
| `RoomClearTask` scheduling lost during move | Medium | High (stale rooms pile up) | Explicit todo + verify log on boot |
|
||||
| 02a + 02b split across a deployment | Low (personal project) | High (outbound traffic broken between commits) | Documented: DO NOT deploy 02a without 02b |
|
||||
| `ByteBufUtil.getBytes` vs manual copy: leaks ref count | Low | Medium | Use `ByteBufUtil.getBytes(buf)` which copies without retain |
|
||||
| Sealed switch missing `PAYLOAD_NOT_SET` makes compile fail | Low | Low | Explicitly handle it in `RequestConverter` |
|
||||
| Protobuf plugin caches stale output after package rename | Low | Low | `clean` before `build` |
|
||||
| IntelliJ red highlights despite gradle build green | Medium | Low (DX) | Gradle reimport |
|
||||
| Heartbeat record is `HeartbeatRequestRecord()` (empty) — Java record with zero components is legal from JDK 14+, confirm target | Low | Low | Project already uses records (`GameMove` might not — check build.gradle `sourceCompatibility`). Caro server uses Java 17+ based on sealed interface support. |
|
||||
| Missing `exceptionCaught` handling for non-IO errors now that dispatcher throws | High | Medium | Keep existing try/catch in `channelRead0` logging exceptions; do NOT close channel on dispatcher throw (the server operator wants loud errors but channel stays open for heartbeat) |
|
||||
|
||||
## Security Considerations
|
||||
- Binary frame parse: `Request.parseFrom(bytes)` on untrusted input can throw `InvalidProtocolBufferException` — caught, logged, discarded. Cannot execute code.
|
||||
- `HttpObjectAggregator(8192)` limit unchanged; typed proto is smaller than JSON so no bump needed.
|
||||
- Dispatcher throw on unhandled cases is fine (caught by `channelRead0` try/catch).
|
||||
|
||||
## Next Steps
|
||||
- **Phase 02b:** replace each `UnsupportedOperationException` in the dispatcher with a real handler call. Re-implement the 14 business-logic classes as `<Verb>Handler` using git history of the deleted files as reference.
|
||||
@@ -1,192 +0,0 @@
|
||||
# Phase 02b — Migrate Event Handlers to Typed Records
|
||||
|
||||
## Context Links
|
||||
- [plan.md](plan.md)
|
||||
- [phase-02a-dispatcher-scaffolding.md](phase-02a-dispatcher-scaffolding.md)
|
||||
- `server/src/main/java/com/miti99/caro/server/event/RequestDispatcher.java`
|
||||
- Git history of deleted `ServerEventListener_CODE_*.java` files (source of business logic)
|
||||
- Audit tables in `plan.md` (inbound/outbound schemas)
|
||||
|
||||
## Overview
|
||||
- **Priority:** P2 (game logic is unusable until this phase ships)
|
||||
- **Status:** pending (blocked by Phase 02a)
|
||||
- Replace every `UnsupportedOperationException` in `RequestDispatcher` with a real handler call. Re-implement each of the 14 old `ServerEventListener_CODE_*` business-logic classes as `<Verb>Handler` classes that take a typed record and emit a typed `Response` proto via `ChannelUtils.push(Channel, Response)`.
|
||||
- Single commit: `refactor(server): migrate event handlers to typed records`.
|
||||
|
||||
## Key Insights
|
||||
- Business logic is unchanged. Only the signatures and the outbound serialization shape change.
|
||||
- Handler naming convention: one class per record, placed in `com.miti99.caro.server.event.handler`. Class name = record prefix (e.g. `GameMoveRequestRecord` → `GameMoveHandler`).
|
||||
- Each handler has a `static void handle(ClientSide client, XxxRequestRecord req)` entry point. Stateless utility classes — no need for DI or instances (old code used reflection-instantiated singletons; records + static methods are simpler and thread-safe by default).
|
||||
- `ClientOfflineHandler` is the ONE handler that is NOT routed via the dispatcher. It's called directly from `WebsocketTransferHandler.clientOfflineEvent()` on channel close. Not a `ClientRequest` — no record, no dispatcher entry.
|
||||
- `GameStartingHandler` is called from two places: internally from `GameReadyHandler` when both players are ready, and from `CreatePveRoomHandler` for auto-start. NOT reachable via the dispatcher in normal flow (client doesn't send `game_starting` directly) — but kept in the oneof for symmetry. The dispatcher's `GameStartingRequestRecord` case can simply call `GameStartingHandler.handle(client, req)` — safe if client ever sends it.
|
||||
- Outbound messages are built inline: `Response.newBuilder().setGameMoveSuccess(GameMoveSuccessResponse.newBuilder().setRow(r).setCol(c)...).build()`. Slightly verbose vs helper methods; keep inline for transparency + YAGNI.
|
||||
- **Extract** a single private static helper per handler if the same `Response` is broadcast to multiple channels (e.g. `GameMoveHandler.broadcastMoveSuccess(room, ...)`). Don't over-extract.
|
||||
|
||||
## Requirements
|
||||
**Functional**
|
||||
- Every `ServerEventCode` path that existed before has equivalent typed-record handler logic.
|
||||
- Every `ClientEventCode` that was ever emitted (per audit) is emitted with the matching typed `Response`.
|
||||
- End-to-end game flow works: nickname → lobby → PVE room → move → win.
|
||||
- `ClientOfflineHandler` runs on channel close (room cleanup, notify peers via `ClientExitResponse`).
|
||||
- `GameReadyHandler` auto-triggers `GameStartingHandler` when both players are ready (replicating old `ServerEventListener.get(GAME_STARTING).call(...)` call).
|
||||
- `CreatePveRoomHandler` auto-starts the game via `GameStartingHandler.handle(client, new GameStartingRequestRecord())`.
|
||||
|
||||
**Non-functional**
|
||||
- `./gradlew -p server clean build` green (all 37 unit tests pass).
|
||||
- No reflection, no `Class.forName`, no `HashMap<EventCode, Listener>`.
|
||||
- All 14 handler classes ≤ 200 LOC (most will be ≤ 80).
|
||||
|
||||
## Architecture
|
||||
```
|
||||
com.miti99.caro.server.event
|
||||
RequestConverter (phase 02a)
|
||||
RequestDispatcher (phase 02a; fills in real calls this phase)
|
||||
request/
|
||||
ClientRequest.java (sealed) (phase 02a)
|
||||
*RequestRecord.java (14 records) (phase 02a)
|
||||
handler/
|
||||
HeartbeatHandler.java (noop — or inline in dispatcher)
|
||||
SetNicknameHandler.java
|
||||
SetClientInfoHandler.java
|
||||
CreateRoomHandler.java
|
||||
CreatePveRoomHandler.java
|
||||
GetRoomsHandler.java
|
||||
JoinRoomHandler.java
|
||||
GameStartingHandler.java
|
||||
GameReadyHandler.java
|
||||
GameMoveHandler.java
|
||||
GameResetHandler.java
|
||||
WatchGameHandler.java
|
||||
WatchGameExitHandler.java
|
||||
ClientExitHandler.java
|
||||
ClientOfflineHandler.java (not dispatcher-routed)
|
||||
```
|
||||
|
||||
`RequestDispatcher` after this phase:
|
||||
```java
|
||||
public static void dispatch(ClientSide client, ClientRequest req) {
|
||||
switch (req) {
|
||||
case HeartbeatRequestRecord r -> { /* noop */ }
|
||||
case SetNicknameRequestRecord r -> SetNicknameHandler.handle(client, r);
|
||||
case SetClientInfoRequestRecord r -> SetClientInfoHandler.handle(client, r);
|
||||
case CreateRoomRequestRecord r -> CreateRoomHandler.handle(client, r);
|
||||
case CreatePveRoomRequestRecord r -> CreatePveRoomHandler.handle(client, r);
|
||||
case GetRoomsRequestRecord r -> GetRoomsHandler.handle(client, r);
|
||||
case JoinRoomRequestRecord r -> JoinRoomHandler.handle(client, r);
|
||||
case GameStartingRequestRecord r -> GameStartingHandler.handle(client, r);
|
||||
case GameReadyRequestRecord r -> GameReadyHandler.handle(client, r);
|
||||
case GameMoveRequestRecord r -> GameMoveHandler.handle(client, r);
|
||||
case GameResetRequestRecord r -> GameResetHandler.handle(client, r);
|
||||
case WatchGameRequestRecord r -> WatchGameHandler.handle(client, r);
|
||||
case WatchGameExitRequestRecord r -> WatchGameExitHandler.handle(client, r);
|
||||
case ClientExitRequestRecord r -> ClientExitHandler.handle(client, r);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Related Code Files
|
||||
**Modify**
|
||||
- `server/src/main/java/com/miti99/caro/server/event/RequestDispatcher.java` — replace stubs with handler calls.
|
||||
- `server/src/main/java/com/miti99/caro/server/handler/WebsocketTransferHandler.java` — `clientOfflineEvent()` calls `ClientOfflineHandler.handle(client)`.
|
||||
|
||||
**Create** (handler classes; each is a port of the old `ServerEventListener_CODE_*` business logic with typed signatures)
|
||||
- `server/src/main/java/com/miti99/caro/server/event/handler/SetNicknameHandler.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/handler/SetClientInfoHandler.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/handler/CreateRoomHandler.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/handler/CreatePveRoomHandler.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/handler/GetRoomsHandler.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/handler/JoinRoomHandler.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/handler/GameStartingHandler.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/handler/GameReadyHandler.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/handler/GameMoveHandler.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/handler/GameResetHandler.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/handler/WatchGameHandler.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/handler/WatchGameExitHandler.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/handler/ClientExitHandler.java`
|
||||
- `server/src/main/java/com/miti99/caro/server/event/handler/ClientOfflineHandler.java`
|
||||
|
||||
**Delete**
|
||||
- None (already done in 02a).
|
||||
|
||||
## Implementation Steps
|
||||
Migrate handlers in dependency order — `GameStartingHandler` before `GameReadyHandler` and `CreatePveRoomHandler` because they both call it internally.
|
||||
|
||||
For EACH handler, the recipe is:
|
||||
1. `git show <old-file-path>` to read the deleted logic.
|
||||
2. Create a new file with `public final class <Verb>Handler` + `public static void handle(ClientSide client, <Record> req)`.
|
||||
3. Port the logic verbatim; replace:
|
||||
- `String data` → typed fields from `req`.
|
||||
- `MapHelper.newInstance().put(k,v)...json()` → `Response.newBuilder().setXxx(XxxResponse.newBuilder().setField(v))...build()`.
|
||||
- `ChannelUtils.pushToClient(ch, ClientEventCode.CODE_XYZ, result)` → `ChannelUtils.push(ch, Response.newBuilder().setXyz(XyzResponse.newBuilder()...).build())`.
|
||||
- `JsonUtils.fromJson(data, Map.class)` → direct field access on the record.
|
||||
- `JsonUtils.toJson(room)` → build `RoomCreateSuccessResponse` with only `id / room_owner / room_type` (the three fields the client reads; see audit).
|
||||
4. Update `RequestDispatcher` to call the new handler instead of `throw todo(...)`.
|
||||
5. Compile check after each 2-3 handlers: `./gradlew -p server compileJava` — catch mistakes early.
|
||||
|
||||
### Migration Order (14 handlers + 1 offline)
|
||||
1. **HeartbeatHandler** — noop; inline in dispatcher, no separate class needed. (Already done in 02a.)
|
||||
2. **SetClientInfoHandler** — trivial: `client.setVersion(req.version())`. No outbound message.
|
||||
3. **SetNicknameHandler** — validate length, set nickname, emit `SHOW_OPTIONS` OR `NICKNAME_SET{invalid_length}`.
|
||||
4. **CreateRoomHandler** — build PVP room, emit `ROOM_CREATE_SUCCESS{id, room_owner, room_type}`.
|
||||
5. **GetRoomsHandler** — enumerate rooms, emit `SHOW_ROOMS{rooms: [RoomSummary]}`.
|
||||
6. **GameStartingHandler** — (called internally) assign roles, emit `GAME_STARTING{...}` to players + watchers.
|
||||
7. **CreatePveRoomHandler** — validate difficulty, build PVE room, add AI robot, call `GameStartingHandler.handle(client, new GameStartingRequestRecord())`.
|
||||
8. **GameReadyHandler** — toggle ready, emit `GAME_READY{...}`, auto-trigger `GameStartingHandler.handle(client, new GameStartingRequestRecord())` when both ready.
|
||||
9. **JoinRoomHandler** — find room, append client, emit `ROOM_JOIN_SUCCESS` to all members, trigger `GameStartingHandler` when full. On fail emit `ROOM_JOIN_FAIL_FULL` or `ROOM_JOIN_FAIL_NOT_FOUND`.
|
||||
10. **GameMoveHandler** — bounds + turn check, make move, emit `GAME_MOVE_SUCCESS` broadcast, check game over → `GAME_OVER`, trigger AI move if PVE. Error cases: `ROOM_PLAY_FAIL_NOT_FOUND`, `GAME_MOVE_NOT_YOUR_TURN`, `GAME_MOVE_OUT_OF_BOUNDS`, `GAME_MOVE_OCCUPIED`.
|
||||
11. **GameResetHandler** — no logic in old code (listener file did not exist → `GAME_RESET` was never implemented). Confirmed by `ls` in audit. Handler body is `// TODO: not implemented — noop for now`.
|
||||
12. **WatchGameHandler** — add client to `watcherList`, emit `WATCH_GAME_SUCCESS` or `ROOM_JOIN_FAIL_NOT_FOUND`.
|
||||
13. **WatchGameExitHandler** — remove client from `watcherList`. No outbound.
|
||||
14. **ClientExitHandler** — leave room, notify peers via `CLIENT_EXIT{room_id, exit_client_id, exit_client_nickname}` + notify watchers. Clean up room.
|
||||
15. **ClientOfflineHandler** (NOT dispatcher-routed) — called from `WebsocketTransferHandler.clientOfflineEvent()`. Same cleanup as ClientExit but triggered by socket close.
|
||||
16. Update `RequestDispatcher.dispatch` to route every record to its handler. Remove all `todo(...)` calls.
|
||||
17. Wire `clientOfflineEvent()` in `WebsocketTransferHandler` to call `ClientOfflineHandler.handle(client)`.
|
||||
18. `./gradlew -p server clean build` — all 37 unit tests pass.
|
||||
19. Commit: `refactor(server): migrate event handlers to typed records`.
|
||||
|
||||
## Todo List
|
||||
- [ ] Confirm 02a commit ships before starting
|
||||
- [ ] `SetClientInfoHandler`
|
||||
- [ ] `SetNicknameHandler`
|
||||
- [ ] `CreateRoomHandler`
|
||||
- [ ] `GetRoomsHandler`
|
||||
- [ ] `GameStartingHandler`
|
||||
- [ ] `CreatePveRoomHandler`
|
||||
- [ ] `GameReadyHandler`
|
||||
- [ ] `JoinRoomHandler`
|
||||
- [ ] `GameMoveHandler`
|
||||
- [ ] `GameResetHandler` (noop)
|
||||
- [ ] `WatchGameHandler`
|
||||
- [ ] `WatchGameExitHandler`
|
||||
- [ ] `ClientExitHandler`
|
||||
- [ ] `ClientOfflineHandler` (not dispatcher-routed)
|
||||
- [ ] `RequestDispatcher` every case wired
|
||||
- [ ] `WebsocketTransferHandler.clientOfflineEvent` wires `ClientOfflineHandler`
|
||||
- [ ] `./gradlew -p server clean build` green (37/37 tests)
|
||||
- [ ] Commit: `refactor(server): migrate event handlers to typed records`
|
||||
|
||||
## Success Criteria
|
||||
- `grep -r "UnsupportedOperationException" server/src/main/java/com/miti99/caro/server/event` returns nothing.
|
||||
- `grep -r "JsonUtils\|MapHelper" server/src/main/java/com/miti99/caro/server/event` returns nothing.
|
||||
- `RequestDispatcher` has 14 cases, all calling real handlers.
|
||||
- 37 unit tests pass.
|
||||
- Server + a hand-crafted protobuf client script can complete a PVE move round-trip (manual smoke, optional).
|
||||
|
||||
## Risk Assessment
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|---|---|---|---|
|
||||
| Handler ports mis-copy fields (e.g. swap row/col) | Medium | Medium | Unit tests for game logic catch board-level bugs; E2E Phase 06 catches wire-level bugs |
|
||||
| Broadcasting to peers misses watchers | Medium | Low | Mirror old broadcast helper verbatim; each handler has a private `broadcastToRoom` like the old `GAME_MOVE` |
|
||||
| `GameReadyHandler` triggering `GameStartingHandler` mid-dispatch causes re-entrant lock issues | Low | Medium | Old code also did this (synchronously) — no new risk; no locks involved |
|
||||
| `ClientOfflineHandler` wired twice (idle + channel close) | Medium | Medium | Old code had same risk; guard with `client.getChannel() != null` check or idempotent cleanup |
|
||||
| `RoomCreateSuccessResponse` strips fields the client silently reads | Medium | Medium | Audit confirms ONLY `data.id` is read client-side; remaining proto fields `room_owner`/`room_type` are defensive additions |
|
||||
| Gson-stringified enums become `""` when proto expects a string (`RoomType.PVE.toString()` vs hypothetical `null`) | Low | Low | Explicit `.name()` or `.toString()` on enum references, matching old code |
|
||||
| Compile runs out of memory during massive rewrite | Very Low | Low | Incremental compile per 2-3 handlers |
|
||||
|
||||
## Security Considerations
|
||||
- Same surface as old code. Each handler still validates the request (bounds check, difficulty range, room existence).
|
||||
- `SetNicknameHandler` still enforces `<= 10` chars limit.
|
||||
- No new reflection paths introduced.
|
||||
|
||||
## Next Steps
|
||||
- Phase 03: delete `Msg.java`, `JsonUtils.java`, `MapHelper.java`, the transfer package, gson dep, and the now-unused `ServerEventCode` enum (the enum is dead because dispatch no longer looks it up). `ClientEventCode` enum can also shrink — but is orthogonal, defer.
|
||||
@@ -1,110 +0,0 @@
|
||||
# Phase 03 — Server Cleanup (Dead Code + Gson Removal)
|
||||
|
||||
## Context Links
|
||||
- [plan.md](plan.md)
|
||||
- [phase-02b-migrate-handlers.md](phase-02b-migrate-handlers.md)
|
||||
- `server/src/main/java/com/miti99/caro/common/entity/Msg.java`
|
||||
- `server/src/main/java/com/miti99/caro/common/utils/JsonUtils.java`
|
||||
- `server/src/main/java/com/miti99/caro/common/helper/MapHelper.java`
|
||||
- `server/src/main/java/com/miti99/caro/common/transfer/ByteKit.java`
|
||||
- `server/src/main/java/com/miti99/caro/common/transfer/ByteLink.java`
|
||||
- `server/src/main/java/com/miti99/caro/common/transfer/TransferProtocolUtils.java`
|
||||
- `server/src/main/java/com/miti99/caro/common/handler/DefaultDecoder.java`
|
||||
- `server/src/main/java/com/miti99/caro/common/enums/ServerEventCode.java`
|
||||
- `server/build.gradle.kts` (gson dep removal)
|
||||
|
||||
## Overview
|
||||
- **Priority:** P3 (cleanup; doesn't block client)
|
||||
- **Status:** pending (blocked by Phase 02b)
|
||||
- Remove dead code now that typed records replace JSON envelopes AND inner-JSON payloads. `JsonUtils`, `MapHelper`, `Msg`, and gson dep all die together.
|
||||
|
||||
## Key Insights
|
||||
- Post-02b usage audit (must re-grep before deletion — findings below are projected):
|
||||
- `Msg.java` — orphaned after 02a. DELETE.
|
||||
- `JsonUtils.java` — was used by 4 listeners + ChannelUtils envelope path. All 02b handlers port inline, so zero remaining callers. DELETE.
|
||||
- `MapHelper.java` — was used by many listeners for `MapHelper.newInstance().put(...).json()`. All replaced by proto builders in 02b. DELETE.
|
||||
- `TransferProtocolUtils.java`, `ByteKit.java`, `ByteLink.java` — only reachable via `DefaultDecoder` which was only used by TCP path. DELETE.
|
||||
- `DefaultDecoder.java` — only referenced from dead transfer package. DELETE.
|
||||
- `gson` dependency in `build.gradle.kts` — was required transitively by JsonUtils + MapHelper. DELETE.
|
||||
- `ServerEventCode.java` — was the string key for reflection dispatch. After 02b, nothing references it on the server (the sealed record hierarchy IS the taxonomy). DELETE.
|
||||
- `ClientEventCode.java` — server no longer references any code value (outbound messages are typed proto Response). Client JS still uses it as a local event bus constant. **KEEP** in server source for now: client mirror via `enums-overview` doc — OR delete from server since it's never imported. Grep confirms it has zero Java imports after 02b → DELETE.
|
||||
- After deletion the `common/transfer` and `common/handler` packages are empty and git auto-cleans them.
|
||||
|
||||
## Requirements
|
||||
**Functional**
|
||||
- No behavior change. Pure deletion.
|
||||
- 37 tests still pass.
|
||||
|
||||
**Non-functional**
|
||||
- Server LOC drops noticeably (~500 LOC est).
|
||||
- `build.gradle.kts` loses gson dep.
|
||||
- Shadow jar shrinks.
|
||||
|
||||
## Architecture
|
||||
No architectural change. Removes dead helpers and a dead dep.
|
||||
|
||||
## Related Code Files
|
||||
**Modify**
|
||||
- `server/build.gradle.kts` — remove `implementation("com.google.code.gson:gson")` line (verify exact coordinate during implementation).
|
||||
|
||||
**Create**
|
||||
- None.
|
||||
|
||||
**Delete**
|
||||
- `server/src/main/java/com/miti99/caro/common/entity/Msg.java`
|
||||
- `server/src/main/java/com/miti99/caro/common/utils/JsonUtils.java`
|
||||
- `server/src/main/java/com/miti99/caro/common/helper/MapHelper.java`
|
||||
- `server/src/main/java/com/miti99/caro/common/transfer/ByteKit.java`
|
||||
- `server/src/main/java/com/miti99/caro/common/transfer/ByteLink.java`
|
||||
- `server/src/main/java/com/miti99/caro/common/transfer/TransferProtocolUtils.java`
|
||||
- `server/src/main/java/com/miti99/caro/common/handler/DefaultDecoder.java`
|
||||
- `server/src/main/java/com/miti99/caro/common/enums/ServerEventCode.java`
|
||||
- `server/src/main/java/com/miti99/caro/common/enums/ClientEventCode.java` (pending re-grep confirmation)
|
||||
- Empty directories `common/transfer/`, `common/handler/`, `common/utils/` if only `JsonUtils` lived there.
|
||||
|
||||
## Implementation Steps
|
||||
1. **Pre-delete grep audit** — confirm zero references:
|
||||
- `grep -rn "import com.miti99.caro.common.entity.Msg" server/src` → empty.
|
||||
- `grep -rn "import com.miti99.caro.common.utils.JsonUtils" server/src` → empty.
|
||||
- `grep -rn "import com.miti99.caro.common.helper.MapHelper" server/src` → empty.
|
||||
- `grep -rn "TransferProtocolUtils\|ByteKit\|ByteLink\|DefaultDecoder" server/src` → only the files themselves.
|
||||
- `grep -rn "import com.miti99.caro.common.enums.ServerEventCode" server/src` → empty.
|
||||
- `grep -rn "import com.miti99.caro.common.enums.ClientEventCode" server/src` → empty. If non-empty: keep `ClientEventCode` in this phase, flag to a follow-up cleanup task.
|
||||
2. `git rm` each confirmed-dead file.
|
||||
3. Edit `server/build.gradle.kts`: remove gson dep line. Confirm no other subproject imports gson via transitive.
|
||||
4. `./gradlew -p server clean build` — all 37 tests must pass.
|
||||
5. Optional: `./gradlew -p server shadowJar && ls -lh server/build/libs/*.jar` — log the before/after size for the commit body.
|
||||
6. Commit: `refactor(server): drop gson and dead json/map/tcp helpers`.
|
||||
|
||||
## Todo List
|
||||
- [ ] Grep audit: `Msg`, `JsonUtils`, `MapHelper`, `TransferProtocolUtils`, `ByteKit`, `ByteLink`, `DefaultDecoder`, `ServerEventCode`, `ClientEventCode`
|
||||
- [ ] `git rm` confirmed-dead files
|
||||
- [ ] Remove gson dep from `build.gradle.kts`
|
||||
- [ ] `./gradlew -p server clean build` green (37/37)
|
||||
- [ ] Commit: `refactor(server): drop gson and dead json/map/tcp helpers`
|
||||
|
||||
## Success Criteria
|
||||
- `find server/src/main/java/com/miti99/caro/common/transfer -type f` → empty.
|
||||
- `find server/src/main/java/com/miti99/caro/common/handler -type f` → empty.
|
||||
- `find server/src/main/java/com/miti99/caro/common/entity/Msg.java` → nothing.
|
||||
- `find server/src/main/java/com/miti99/caro/common/utils/JsonUtils.java` → nothing.
|
||||
- `find server/src/main/java/com/miti99/caro/common/helper/MapHelper.java` → nothing.
|
||||
- `grep -n "gson" server/build.gradle.kts` → nothing.
|
||||
- 37 unit tests pass.
|
||||
- Shadow jar size decrease documented in commit body.
|
||||
|
||||
## Risk Assessment
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|---|---|---|---|
|
||||
| Hidden reflective reference to deleted class | Very Low | Medium | No reflection used after 02a |
|
||||
| Build caches reference stale class files | Low | Low | `clean` before `build` |
|
||||
| `ClientEventCode` still referenced somewhere unexpected | Medium | Low | Grep confirms before delete; if non-empty keep + defer |
|
||||
| Gson transitively required by another dep (e.g. protobuf-java-util) | Low | Low | Verify with `./gradlew -p server dependencies` after remove; if break, restore line |
|
||||
| `common/entity/GameMove.java` — orphaned? | Medium | Low | GameMove is used by `GomokuAI` for return type; KEEP |
|
||||
| `common/entity/Board.java`, `ClientSide.java`, `Room.java` orphaned? | Very Low | N/A | All still load-bearing for game logic; KEEP |
|
||||
|
||||
## Security Considerations
|
||||
- None. Deletion only.
|
||||
|
||||
## Next Steps
|
||||
- Server work done. Phase 04 migrates the client.
|
||||
@@ -1,236 +0,0 @@
|
||||
# Phase 04 — Client Typed Protobuf Integration
|
||||
|
||||
## Context Links
|
||||
- [plan.md](plan.md)
|
||||
- [phase-01-proto-schemas-and-build.md](phase-01-proto-schemas-and-build.md)
|
||||
- [phase-02b-migrate-handlers.md](phase-02b-migrate-handlers.md)
|
||||
- `client/package.json`
|
||||
- `client/src/services/connection-service.js`
|
||||
- `client/src/config/protocol-constants.js`
|
||||
- `client/src/services/game-state-service.js`
|
||||
- `client/src/ui/menu-ui.js`
|
||||
- `client/src/ui/game-ui.js`
|
||||
- `client/src/scenes/game-scene.js`
|
||||
- `client/src/scenes/menu-scene.js`
|
||||
- `client/src/scenes/boot-scene.js`
|
||||
- `server/src/main/proto/request.proto`
|
||||
- `server/src/main/proto/response.proto`
|
||||
|
||||
## Overview
|
||||
- **Priority:** P2 (client can't talk to new server until this lands)
|
||||
- **Status:** pending (blocked by Phase 02b)
|
||||
- Add `protobufjs-cli` dev dependency, generate static ESM JS + `.d.ts` from the typed `request.proto` + `response.proto`, commit under `client/src/generated/`, rewrite `connection-service.js` to build typed `Request` oneof frames and parse typed `Response` oneof frames. Map each `Response.payloadCase` to the existing `ClientEventCode.*` constant so all UI listeners keep working.
|
||||
|
||||
## Key Insights
|
||||
- Browser `WebSocket` supports binary: `ws.binaryType = 'arraybuffer'` + `ws.send(Uint8Array)`.
|
||||
- `pbjs -t static-module -w es6` produces a tree-shakeable ESM module covering both `.proto` files in a single output.
|
||||
- `protobufjs/minimal` is the only runtime dep (~10KB). `protobufjs-cli` is dev-only.
|
||||
- The client's `ClientEventCode` constants in `protocol-constants.js` stay — they're the event-bus keys. A new mapping table translates `Response.payload` field name (e.g. `gameMoveSuccess`) → `ClientEventCode.GAME_MOVE_SUCCESS`. Protobufjs JS naming converts snake_case fields to camelCase automatically.
|
||||
- The client's `ServerEventCode` constants become dead — they were the string codes sent on the wire. Replaced by builder methods that set a oneof field. **Delete them from `protocol-constants.js`.**
|
||||
- `connection-service.js` `send` signature changes. Old: `send(code, data)`. New: one builder method per request type, e.g. `sendNickname(nickname)`, `sendGameMove(row, col)`, `sendHeartbeat()`. This is clearer but requires editing every `connectionService.send(...)` call site in menu-ui.js, game-ui.js, game-scene.js.
|
||||
- Call site count from audit: 14 `connectionService.send(...)` calls across 3 files — small enough to migrate individually.
|
||||
- Inner `data.xxx` access on received events: audit showed client reads `data.row`, `data.col`, `data.piece`, `data.result`, `data.winnerNickname`, `data.roomId`, `data.blackPlayerId`, `data.blackPlayerNickname`, `data.whitePlayerId`, `data.whitePlayerNickname`, `data.boardSize`, `data.id` (room create), `data.invalidLength`. All field names match the proto camelCase derivation — protobufjs `.toObject()` + camelCase default, no extra transform needed.
|
||||
|
||||
## Requirements
|
||||
**Functional**
|
||||
- Client opens binary WebSocket to `ws://host:1999/ratel`.
|
||||
- Each outbound action calls a typed helper on `connectionService` that constructs a `Request.create({<oneof>: {...}})` and sends `Uint8Array`.
|
||||
- `onmessage` decodes `ArrayBuffer` → `Response.decode(u8)` → `response.payload` (oneof accessor) → switch on `payloadCase` → emit `eventBus.emit(ClientEventCode.XXX, payloadObject)`.
|
||||
- Heartbeat loop (50s) sends `sendHeartbeat()`.
|
||||
- Reconnect/backoff unchanged.
|
||||
- Event bus consumers (menu-ui, game-ui, game-scene, game-state-service) keep working with NO changes because the payload field names match.
|
||||
|
||||
**Non-functional**
|
||||
- Bundle size increase <100 KB minified.
|
||||
- No runtime `.proto` parsing — static codegen only.
|
||||
- Generated files committed (`client/src/generated/` NOT gitignored).
|
||||
- `npm --prefix client run build` green.
|
||||
|
||||
## Architecture
|
||||
```
|
||||
client/
|
||||
package.json
|
||||
deps: + protobufjs
|
||||
devDeps: + protobufjs-cli
|
||||
scripts:
|
||||
"proto:gen": "pbjs -t static-module -w es6 -o src/generated/protocol.js ../server/src/main/proto/request.proto ../server/src/main/proto/response.proto && pbts -o src/generated/protocol.d.ts src/generated/protocol.js"
|
||||
src/
|
||||
generated/
|
||||
protocol.js (committed)
|
||||
protocol.d.ts (committed)
|
||||
services/
|
||||
connection-service.js (rewritten: typed send methods + typed onmessage)
|
||||
config/
|
||||
protocol-constants.js (ClientEventCode kept; ServerEventCode removed)
|
||||
```
|
||||
|
||||
Encode path example:
|
||||
```js
|
||||
import protoRoot from '../generated/protocol.js';
|
||||
const { Request } = protoRoot.miti99.caro.protocol;
|
||||
|
||||
sendGameMove(row, col) {
|
||||
const req = Request.create({ gameMove: { row, col } });
|
||||
const bytes = Request.encode(req).finish();
|
||||
this._ws.send(bytes);
|
||||
}
|
||||
```
|
||||
|
||||
Decode path:
|
||||
```js
|
||||
const { Response } = protoRoot.miti99.caro.protocol;
|
||||
|
||||
_onMessage(event) {
|
||||
const u8 = new Uint8Array(event.data);
|
||||
const res = Response.decode(u8);
|
||||
const caseName = res.payload; // protobufjs exposes oneof via `payload` property
|
||||
// caseName is the field name of the set oneof (e.g. 'gameMoveSuccess')
|
||||
const payloadObj = res[caseName];
|
||||
const eventCode = RESPONSE_CASE_TO_CLIENT_CODE[caseName];
|
||||
if (eventCode) {
|
||||
eventBus.emit(eventCode, payloadObj);
|
||||
}
|
||||
}
|
||||
|
||||
const RESPONSE_CASE_TO_CLIENT_CODE = {
|
||||
clientConnect: ClientEventCode.CLIENT_CONNECT,
|
||||
nicknameSet: ClientEventCode.NICKNAME_SET,
|
||||
showOptions: ClientEventCode.SHOW_OPTIONS,
|
||||
showRooms: ClientEventCode.SHOW_ROOMS,
|
||||
roomCreateSuccess: ClientEventCode.ROOM_CREATE_SUCCESS,
|
||||
roomJoinSuccess: ClientEventCode.ROOM_JOIN_SUCCESS,
|
||||
roomJoinFailFull: ClientEventCode.ROOM_JOIN_FAIL_FULL,
|
||||
roomJoinFailNotFound: ClientEventCode.ROOM_JOIN_FAIL_INEXIST,
|
||||
roomPlayFailNotFound: ClientEventCode.ROOM_PLAY_FAIL_INEXIST,
|
||||
gameStarting: ClientEventCode.GAME_STARTING,
|
||||
gameReady: ClientEventCode.GAME_READY,
|
||||
gameMoveSuccess: ClientEventCode.GAME_MOVE_SUCCESS,
|
||||
gameMoveInvalid: ClientEventCode.GAME_MOVE_INVALID,
|
||||
gameMoveOccupied: ClientEventCode.GAME_MOVE_OCCUPIED,
|
||||
gameMoveOutOfBounds: ClientEventCode.GAME_MOVE_OUT_OF_BOUNDS,
|
||||
gameMoveNotYourTurn: ClientEventCode.GAME_MOVE_NOT_YOUR_TURN,
|
||||
gameOver: ClientEventCode.GAME_OVER,
|
||||
pveDifficultyNotSupport: ClientEventCode.PVE_DIFFICULTY_NOT_SUPPORT,
|
||||
watchGameSuccess: ClientEventCode.GAME_WATCH_SUCCESSFUL,
|
||||
clientExit: ClientEventCode.CLIENT_EXIT,
|
||||
};
|
||||
```
|
||||
|
||||
Special cases:
|
||||
- `clientConnect`: old code read a raw string `clientId`, new path gets `{clientId: number}`. `game-state-service.js` line 86 (`CLIENT_CONNECT` handler) reads `data` directly → update to `data.clientId`. **This is a call-site change** — note it.
|
||||
- `nicknameSet`: old payload was `{invalidLength: n}` OR `null` (prompt case). New proto: always an object with `invalidLength` field (defaults to 0). `boot-scene.js` handler just fires once on prompt regardless of field — unchanged. `menu-ui.js` line 186 reads `data.invalidLength` — still works; defaulting to 0 means "no error" but the handler only runs when length was invalid. Check: if `invalidLength === 0` the UI shouldn't show the error toast. Fix: client UI guards `if (data.invalidLength > 0) showToast(...)`.
|
||||
- `roomCreateSuccess`: old read `data.id`, new proto uses `id` field → unchanged.
|
||||
|
||||
## Related Code Files
|
||||
**Modify**
|
||||
- `client/package.json` — add deps + `proto:gen` script.
|
||||
- `client/src/services/connection-service.js` — binary WS, typed send helpers, typed decode+dispatch.
|
||||
- `client/src/config/protocol-constants.js` — delete `ServerEventCode`; keep `ClientEventCode`.
|
||||
- `client/src/ui/menu-ui.js` — replace `connectionService.send(ServerEventCode.XYZ, payload)` with new typed methods.
|
||||
- `client/src/ui/game-ui.js` — same (`sendGameReady()`, `sendClientExit()`).
|
||||
- `client/src/scenes/game-scene.js` — `connectionService.sendGameMove(row, col)`.
|
||||
- `client/src/services/game-state-service.js` — `data.clientId` instead of raw `data` for CLIENT_CONNECT handler.
|
||||
- `client/src/ui/menu-ui.js` — guard `NICKNAME_SET` error toast on `invalidLength > 0`.
|
||||
- Root `.gitignore` — ensure `client/src/generated/` NOT ignored.
|
||||
|
||||
**Create**
|
||||
- `client/src/generated/protocol.js` (pbjs output, committed)
|
||||
- `client/src/generated/protocol.d.ts` (pbts output, committed)
|
||||
|
||||
**Delete**
|
||||
- None.
|
||||
|
||||
## Implementation Steps
|
||||
1. **Install deps (pin to latest stable pre-2026):**
|
||||
```
|
||||
npm --prefix client install protobufjs@7.5.4
|
||||
npm --prefix client install -D protobufjs-cli@1.1.3
|
||||
```
|
||||
Keep `phaser` at `3.87.0` and `vite` at `6.3.1` — both already ≥ pre-2026 latest.
|
||||
2. **Add npm script** in `client/package.json`:
|
||||
```json
|
||||
"proto:gen": "pbjs -t static-module -w es6 -o src/generated/protocol.js ../server/src/main/proto/request.proto ../server/src/main/proto/response.proto && pbts -o src/generated/protocol.d.ts src/generated/protocol.js"
|
||||
```
|
||||
3. **Run codegen:** `npm --prefix client run proto:gen`. Verify `client/src/generated/protocol.js` and `.d.ts` exist. Open `protocol.js` and confirm namespace path (likely `miti99.caro.protocol` matching `java_package` + proto `package`).
|
||||
4. **Rewrite `connection-service.js`:**
|
||||
- Import: `import protoRoot from '../generated/protocol.js';` then `const { Request, Response } = protoRoot.miti99.caro.protocol;`.
|
||||
- `connect(url)`: set `this._ws.binaryType = 'arraybuffer';`
|
||||
- `_resolveUrl()`: port `1025` → `1999`. `DEFAULT_WS_URL` → `ws://localhost:1999/ratel`.
|
||||
- Replace `send(code, data)` with typed methods:
|
||||
```js
|
||||
sendHeartbeat() { this._sendReq({ heartbeat: {} }); }
|
||||
sendNickname(nickname) { this._sendReq({ setNickname: { nickname } }); }
|
||||
sendClientInfo(version) { this._sendReq({ setClientInfo: { version } }); }
|
||||
sendCreateRoom() { this._sendReq({ createRoom: {} }); }
|
||||
sendCreatePveRoom(difficulty) { this._sendReq({ createPveRoom: { difficulty } }); }
|
||||
sendGetRooms() { this._sendReq({ getRooms: {} }); }
|
||||
sendJoinRoom(roomId) { this._sendReq({ joinRoom: { roomId } }); }
|
||||
sendGameReady() { this._sendReq({ gameReady: {} }); }
|
||||
sendGameMove(row, col) { this._sendReq({ gameMove: { row, col } }); }
|
||||
sendGameReset() { this._sendReq({ gameReset: {} }); }
|
||||
sendWatchGame(roomId) { this._sendReq({ watchGame: { roomId } }); }
|
||||
sendWatchGameExit() { this._sendReq({ watchGameExit: {} }); }
|
||||
sendClientExit() { this._sendReq({ clientExit: {} }); }
|
||||
|
||||
_sendReq(oneof) {
|
||||
if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;
|
||||
const req = Request.create(oneof);
|
||||
const bytes = Request.encode(req).finish();
|
||||
this._ws.send(bytes);
|
||||
}
|
||||
```
|
||||
- Rewrite `_onMessage(event)` per the decode path above, using the `RESPONSE_CASE_TO_CLIENT_CODE` map (kept as a module-level `const`).
|
||||
- Heartbeat interval calls `sendHeartbeat()`.
|
||||
5. **Edit call sites:**
|
||||
- `menu-ui.js`: `sendNickname(name)`, `sendCreateRoom()`, `sendGetRooms()`, `sendCreatePveRoom(1|2|3)`, `sendJoinRoom(id)`, `sendWatchGame(id)`, `sendClientExit()`.
|
||||
- `game-ui.js`: `sendGameReady()`, `sendClientExit()`.
|
||||
- `game-scene.js`: `sendGameMove(row, col)`.
|
||||
- `game-state-service.js` `CLIENT_CONNECT` handler: `this.clientId = data.clientId` (was `this.clientId = Number(data)`).
|
||||
- `menu-ui.js` `NICKNAME_SET` handler: `if (data.invalidLength > 0) showToast(...)`.
|
||||
6. **Delete `ServerEventCode` export** from `protocol-constants.js`. Delete any lingering imports in the three UI files.
|
||||
7. **Build check:** `npm --prefix client run build` — green.
|
||||
8. **Manual (optional):** `npm --prefix client run dev`, open devtools → Network → WS → confirm frames are binary.
|
||||
|
||||
## Todo List
|
||||
- [ ] `npm install protobufjs@7.5.4 protobufjs-cli@1.1.3` (pinned versions)
|
||||
- [ ] Add `proto:gen` script
|
||||
- [ ] Run `proto:gen`, commit `src/generated/protocol.js` + `.d.ts`
|
||||
- [ ] `binaryType = 'arraybuffer'` in connect
|
||||
- [ ] Typed send helpers on `connectionService`
|
||||
- [ ] Typed decode + response-case mapping in `_onMessage`
|
||||
- [ ] Update 14 call sites across menu-ui / game-ui / game-scene
|
||||
- [ ] `game-state-service` `data.clientId` fix
|
||||
- [ ] `menu-ui` `invalidLength > 0` guard
|
||||
- [ ] Delete `ServerEventCode` from `protocol-constants.js`
|
||||
- [ ] Default URL port 1999
|
||||
- [ ] `npm --prefix client run build` green
|
||||
- [ ] Commit: `refactor(client): typed protobuf binary websocket on port 1999`
|
||||
|
||||
## Success Criteria
|
||||
- `client/src/generated/protocol.js` exports `Request` and `Response` via the `miti99.caro.protocol` namespace.
|
||||
- `grep -n "JSON.stringify({" client/src/services/connection-service.js` returns nothing (envelope path).
|
||||
- `grep -rn "ServerEventCode" client/src` returns nothing.
|
||||
- `grep -rn "connectionService.send(" client/src` returns nothing (only the typed helpers).
|
||||
- `npm --prefix client run build` green.
|
||||
- Devtools: WS frames binary (hex view).
|
||||
- Full end-to-end flow: nickname → lobby → PVE → move → win.
|
||||
|
||||
## Risk Assessment
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|---|---|---|---|
|
||||
| Generated namespace path differs from expectation | Medium | Low | Inspect `protocol.js` after first codegen; adjust import alias |
|
||||
| Protobufjs field name camelCase mismatch with existing UI consumers | Low | Medium | Audit confirms consumer field names already match; spot-check during build |
|
||||
| `Response.payload` oneof access returns undefined for PAYLOAD_NOT_SET | Low | Low | Guard `if (!caseName) return;` in `_onMessage` |
|
||||
| Heartbeat frame rejected by server parser | Low | High | `HeartbeatRequest {}` is a valid wrapper variant — tested in phase 02a smoke |
|
||||
| Vite dev HMR chokes on protobufjs CJS | Low | Medium | Static module is ESM; if issue arises add to `optimizeDeps.include` |
|
||||
| Missed call site still calls `send(code, data)` | Medium | High (runtime error) | Delete `ServerEventCode` export so missed call sites fail at build time |
|
||||
| Pbjs output non-deterministic across machines | Low | Low | Pin `protobufjs-cli` version in devDeps |
|
||||
| Client's `CLIENT_CONNECT` previously read raw string; new object shape breaks anything else I missed | Medium | Low | Grep all `CLIENT_CONNECT` listeners; only `game-state-service.js` registers one |
|
||||
|
||||
## Security Considerations
|
||||
- `Response.decode` on untrusted server data can throw; wrap `_onMessage` in try/catch (existing code already has this).
|
||||
- No CSP change. Same-origin WS.
|
||||
- Oversized frames still capped by server `HttpObjectAggregator`.
|
||||
|
||||
## Next Steps
|
||||
- Phase 05: docker + docs sweep. No more client code changes.
|
||||
@@ -1,100 +0,0 @@
|
||||
# Phase 05 — Infra & Docs Sync
|
||||
|
||||
## Context Links
|
||||
- [plan.md](plan.md)
|
||||
- `docker-compose.yml`
|
||||
- `server/Dockerfile`
|
||||
- `client/Dockerfile`
|
||||
- `README.md`
|
||||
- `docs/project-overview.md`
|
||||
- `docs/system-architecture.md`
|
||||
- `docs/codebase-summary.md`
|
||||
- `docs/deployment-guide.md`
|
||||
- `docs/code-standards.md`
|
||||
|
||||
## Overview
|
||||
- **Priority:** P2 (blocks Phase 06 E2E via docker)
|
||||
- **Status:** pending (blocked by Phase 02b, 04)
|
||||
- Update infra (docker) and all user-facing docs to reflect single-port 1999 WebSocket carrying TYPED protobuf (no JSON anywhere). TCP port 1024 is silently gone — no migration notice needed (personal project, no external consumers).
|
||||
|
||||
## Key Insights
|
||||
- `server/Dockerfile` currently `EXPOSE 1024 1025` + `ENTRYPOINT [..., "-p", "1024"]`. Flip to `EXPOSE 1999` + `-p 1999`.
|
||||
- `docker-compose.yml` has two port mappings 1024 & 1025. Reduce to one: `1999:1999`.
|
||||
- `client/Dockerfile` is static nginx — doesn't hard-code server URL. Client reads URL from `window.location.hostname` at runtime. NO change needed.
|
||||
- Docs contain detailed ASCII diagrams with TCP/WS dual port — rewrite those sections to show single WS/protobuf path.
|
||||
- README line 30 says "ports 1024 (TCP) and 1025 (WebSocket)" — rewrite.
|
||||
- `-p` flag documentation in README line 115 references "TCP port (default: 1024, WebSocket = TCP + 1)" — simplify to "WebSocket port (default: 1999)".
|
||||
|
||||
## Requirements
|
||||
**Functional**
|
||||
- `docker compose up --build` starts server on 1999 and client on 8080; client connects.
|
||||
- All docs accurately describe: single port, WS protocol, protobuf binary frames.
|
||||
|
||||
**Non-functional**
|
||||
- No stale TCP/JSON references anywhere in README or docs/.
|
||||
|
||||
## Architecture
|
||||
```
|
||||
host:1999 -> caro-server (websocket /ratel, protobuf binary)
|
||||
host:8080 -> caro-client (nginx static)
|
||||
```
|
||||
|
||||
## Related Code Files
|
||||
**Modify**
|
||||
- `docker-compose.yml` — ports: `["1999:1999"]`.
|
||||
- `server/Dockerfile` — `EXPOSE 1999`, `ENTRYPOINT ["java","-jar","app.jar","-p","1999"]`.
|
||||
- `README.md` — all mentions of 1024/1025/TCP/JSON envelope.
|
||||
- `docs/project-overview.md` — transport description.
|
||||
- `docs/system-architecture.md` — ASCII diagrams, pipeline descriptions.
|
||||
- `docs/codebase-summary.md` — file inventory (remove deleted files, remove mention of TCP proxy/handler).
|
||||
- `docs/deployment-guide.md` — port mappings, docker run examples.
|
||||
- `docs/code-standards.md` — add notes: `.proto` files in `server/src/main/proto/` are the source of truth; server dispatches via sealed `ClientRequest` record hierarchy; client regenerates JS bindings via `npm --prefix client run proto:gen`.
|
||||
|
||||
**Create**
|
||||
- None.
|
||||
|
||||
**Delete**
|
||||
- None.
|
||||
|
||||
## Implementation Steps
|
||||
1. `docker-compose.yml`: delete `"1024:1024"` entry; change `"1025:1025"` -> `"1999:1999"`.
|
||||
2. `server/Dockerfile`: `EXPOSE 1024 1025` -> `EXPOSE 1999`; `-p 1024` -> `-p 1999`.
|
||||
3. `README.md`: sweep for `1024`, `1025`, `TCP`, `Protobuf (TCP)`, `JSON` (envelope), "WebSocket = TCP + 1". Replace with single-port/protobuf-binary narrative.
|
||||
4. `docs/project-overview.md`: update transport section.
|
||||
5. `docs/system-architecture.md`: rewrite pipeline diagram to show only WebSocket path with typed protobuf binary frames; note gradle protobuf plugin generating from `request.proto` + `response.proto`; note sealed `ClientRequest` record dispatch (no reflection); note client `pbjs` static codegen.
|
||||
6. `docs/codebase-summary.md`: remove entries for `ProtobufProxy`, `ProtobufTransferHandler`, `SecondProtobufCodec`, `Msg`, `JsonUtils`, `MapHelper`, `common/transfer/*`, `common/handler/DefaultDecoder`, `ServerEventListener`, `ServerEventListener_CODE_*`; add entries for `server/event/request/*` (sealed records), `server/event/handler/*` (business-logic handlers), `RequestConverter`, `RequestDispatcher`; note generated sources under `build/generated/sources/proto/main/java/com/miti99/caro/protocol/`; mention `client/src/generated/protocol.js`.
|
||||
7. `docs/deployment-guide.md`: update `-p` flag docs, port mappings, any firewall rules narrative.
|
||||
8. `docs/code-standards.md`: add note: "Protobuf `.proto` files are the source of truth under `server/src/main/proto/` (`request.proto` for client→server, `response.proto` for server→client). Java classes are generated by the `com.google.protobuf` Gradle plugin. Server dispatches requests via a sealed `ClientRequest` interface + records — no reflection, no string-keyed lookups. JS bindings are generated by `npm --prefix client run proto:gen` and committed under `client/src/generated/`."
|
||||
9. `docker compose up --build -d` then `curl -v --include --no-buffer --header "Connection: Upgrade" --header "Upgrade: websocket" --header "Sec-WebSocket-Version: 13" --header "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" http://localhost:1999/ratel` — expect 101 Switching Protocols. `docker compose down`.
|
||||
|
||||
## Todo List
|
||||
- [ ] `docker-compose.yml` single-port
|
||||
- [ ] `server/Dockerfile` port + entrypoint
|
||||
- [ ] README.md sweep 1024/1025/TCP/JSON references
|
||||
- [ ] `docs/project-overview.md` updated
|
||||
- [ ] `docs/system-architecture.md` diagrams updated
|
||||
- [ ] `docs/codebase-summary.md` file inventory updated
|
||||
- [ ] `docs/deployment-guide.md` port docs updated
|
||||
- [ ] `docs/code-standards.md` proto source-of-truth note
|
||||
- [ ] `docker compose up --build` smoke green
|
||||
- [ ] Commit: `docs,chore: single-port 1999 websocket protobuf`
|
||||
|
||||
## Success Criteria
|
||||
- `grep -rn "1024\|1025" docker-compose.yml server/Dockerfile README.md docs/` returns nothing.
|
||||
- `grep -rin "TCP port\|TCP (Protobuf)\|JSON envelope\|TextWebSocketFrame" README.md docs/` returns nothing.
|
||||
- `docker compose up --build` brings up `caro-server` listening on 1999 and `caro-client` on 8080.
|
||||
- `docker compose logs caro-server` shows "websocket server was successfully started on port 1999".
|
||||
|
||||
## Risk Assessment
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|---|---|---|---|
|
||||
| Missed port reference in deeply-nested doc | Medium | Low | Final grep for `1024`/`1025` across all docs |
|
||||
| Dockerfile health check (if any) references old port | Low | Low | Current Dockerfile has no HEALTHCHECK — confirmed |
|
||||
| README code samples still show old CLI flag | Medium | Low | Explicit grep for `-p 1024` |
|
||||
|
||||
## Security Considerations
|
||||
- One fewer open port = smaller attack surface. Net positive.
|
||||
- Confirm production docker host firewall rules (if any) open 1999 before deploy.
|
||||
|
||||
## Next Steps
|
||||
- Phase 06: live end-to-end smoke test via docker compose.
|
||||
@@ -1,90 +0,0 @@
|
||||
# Phase 06 — End-to-End Smoke Test
|
||||
|
||||
## Context Links
|
||||
- [plan.md](plan.md)
|
||||
- [phase-02a-dispatcher-scaffolding.md](phase-02a-dispatcher-scaffolding.md)
|
||||
- [phase-02b-migrate-handlers.md](phase-02b-migrate-handlers.md)
|
||||
- [phase-04-client-protobuf.md](phase-04-client-protobuf.md)
|
||||
- [phase-05-infra-and-docs.md](phase-05-infra-and-docs.md)
|
||||
|
||||
## Overview
|
||||
- **Priority:** P1 (gate on merge)
|
||||
- **Status:** pending (blocked by Phase 05)
|
||||
- Live manual validation that the full stack — server + client — works over the new binary protobuf WebSocket on port 1999. Catches integration bugs that unit tests don't cover (wire format, pipeline ordering, heartbeat across real network).
|
||||
|
||||
## Key Insights
|
||||
- Unit tests are pure logic (GomokuHelperTest, GomokuAITest) — they cannot catch wire-format regressions.
|
||||
- The server+client integration has no automated test harness today. Manual smoke is the realistic gate.
|
||||
- Must exercise every oneof variant that carries non-trivial fields: `setNickname`, `createPveRoom`, `joinRoom`, `gameMove`, and the outbound `gameMoveSuccess`, `gameStarting`, `gameOver`, `showRooms`, `roomCreateSuccess`. Empty-oneof variants (heartbeat, getRooms, createRoom, gameReady, clientExit) are exercised as side effects.
|
||||
- Wire-format assertions now check that decoded `Response.payloadCase` matches expectations — not JSON text.
|
||||
|
||||
## Requirements
|
||||
**Functional**
|
||||
- All below test cases pass on a freshly built stack.
|
||||
|
||||
**Non-functional**
|
||||
- All WS frames in devtools are binary (not text).
|
||||
- Server logs show event dispatch without errors.
|
||||
|
||||
## Test Matrix
|
||||
| # | Scenario | Expected |
|
||||
|---|---|---|
|
||||
| 1 | `docker compose up --build`, open `http://localhost:8080` | Client loads, Phaser boots |
|
||||
| 2 | Enter nickname, click confirm | Server log: `client do: CODE_CLIENT_NICKNAME_SET`; client advances to lobby |
|
||||
| 3 | Click "Create PVE room" | Room created, game board renders |
|
||||
| 4 | Place a move on board | Server log: `GAME_MOVE`; move appears, AI responds |
|
||||
| 5 | Continue playing until win/loss | `GAME_OVER` event received; UI shows result |
|
||||
| 6 | Leave tab open 1 min | Heartbeat frames visible in devtools every 50s; no disconnect |
|
||||
| 7 | Kill server container (`docker compose stop server`) | Client detects disconnect, shows reconnect state |
|
||||
| 8 | Restart server (`docker compose start server`) | Client reconnects with backoff |
|
||||
| 9 | DevTools -> Network -> WS tab | All frames show as binary (hex), no text; inspector shows typed proto fields (row/col/piece) not JSON strings |
|
||||
| 10 | Open second browser tab, create PVP room + join from first | Both clients see room list, join works, GAME_STARTING fires |
|
||||
|
||||
## Architecture
|
||||
N/A — test phase. Uses docker compose stack from Phase 05.
|
||||
|
||||
## Related Code Files
|
||||
None modified in this phase.
|
||||
|
||||
## Implementation Steps
|
||||
1. `docker compose down -v` (clean slate).
|
||||
2. `docker compose up --build -d`.
|
||||
3. `docker compose logs -f caro-server` in a second shell.
|
||||
4. Open `http://localhost:8080` in Chrome. DevTools -> Network -> WS.
|
||||
5. Walk through test matrix 1-10.
|
||||
6. Record pass/fail per row. On fail: capture server log + devtools frame + reproduce steps.
|
||||
7. `docker compose down`.
|
||||
|
||||
## Todo List
|
||||
- [ ] Test 1: stack boots
|
||||
- [ ] Test 2: nickname set
|
||||
- [ ] Test 3: PVE room create
|
||||
- [ ] Test 4: game move dispatched
|
||||
- [ ] Test 5: game over delivered
|
||||
- [ ] Test 6: heartbeat keeps channel alive
|
||||
- [ ] Test 7: disconnect detected
|
||||
- [ ] Test 8: reconnect succeeds
|
||||
- [ ] Test 9: WS frames verified binary
|
||||
- [ ] Test 10: PVP two-tab flow
|
||||
- [ ] Tear down, commit nothing (or ship any last-minute fix commit)
|
||||
|
||||
## Success Criteria
|
||||
- All 10 test rows pass.
|
||||
- No uncaught exceptions in server logs during the walkthrough.
|
||||
- No JS console errors client-side.
|
||||
- DevTools confirms binary WS framing.
|
||||
|
||||
## Risk Assessment
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|---|---|---|---|
|
||||
| Integration bug only visible with two clients (race in room join) | Medium | Medium | Test 10 explicitly covers two-tab PVP |
|
||||
| Pbjs field name vs server expected JSON inner string mismatch | Low | Medium | Inner data round-trips as opaque string; low risk |
|
||||
| Heartbeat timing drift causes idle disconnect | Low | Medium | Test 6 waits long enough to prove 50s beat keeps 30min idle handler happy (well within margin) |
|
||||
| Chrome caches old JS bundle | Low | Low | Hard-refresh; Vite build produces hashed filenames in docker image |
|
||||
|
||||
## Security Considerations
|
||||
- Only localhost testing. Production hardening (TLS/wss) is out of scope for this refactor.
|
||||
|
||||
## Next Steps
|
||||
- On all-green: merge branch, tag release, announce single-port 1999 to any API consumers.
|
||||
- On failure: fix in appropriate phase, rerun Phase 06 from step 1.
|
||||
@@ -1,141 +0,0 @@
|
||||
---
|
||||
title: "WebSocket typed-protobuf migration + TCP removal"
|
||||
description: "Drop TCP. Rewrite wire protocol as typed protobuf (request.proto / response.proto) on single port 1999. Dispatch via Java sealed records."
|
||||
status: implementation-complete
|
||||
priority: P2
|
||||
effort: 12h
|
||||
branch: master
|
||||
tags: [refactor, netty, websocket, protobuf, client, breaking-change]
|
||||
created: 2026-04-10
|
||||
shipped-commits: [945a249, b75733f, 3ad9a7b, ecc6177, cbad690]
|
||||
---
|
||||
|
||||
## Goal
|
||||
1. WebSocket frames become typed protobuf binary — one `Request` oneof client→server, one `Response` oneof server→client.
|
||||
2. Delete TCP transport entirely. Server listens on ONE port only (default `1999`).
|
||||
3. Server dispatches requests internally via a **sealed `ClientRequest` interface + Java records**, replacing today's string-keyed `ServerEventListener.get(code)` reflection lookup.
|
||||
4. Inner JSON `data` strings are GONE — every event code has a concrete proto message.
|
||||
|
||||
## Key Decisions (locked)
|
||||
- **Proto file rename:** `request.proto` (client→server, wraps `oneof`), `response.proto` (server→client, wraps `oneof`). `common.proto` is NOT created — no fields are genuinely shared across both directions after audit (see Audit Notes). YAGNI.
|
||||
- **Wire format:** wrapper `Request { oneof payload { HeartbeatRequest heartbeat = 1; GameMoveRequest game_move = 2; ... } }`. No more string `code` field on the wire — the `oneof` case IS the code. Symmetric `Response` wrapper.
|
||||
- **Internal dispatch (server):** `Request.parseFrom(bytes)` → switch on `payloadCase` → convert to a `ClientRequest` record (sealed interface hierarchy in `com.miti99.caro.server.event.request.*`) → `RequestDispatcher.dispatch(client, req)` → pattern-matching switch invokes the existing business-logic handler. No reflection, no string lookup.
|
||||
- **Responses:** server constructs `Response` proto directly in handlers (no response-side record layer — one-way traffic, no dispatch, adding records is ceremony for zero benefit). `ChannelUtils.push(Channel, Response)` serializes and writes a `BinaryWebSocketFrame`.
|
||||
- **Event listeners keep their business logic.** Only the signature/shape changes: instead of `void call(ClientSide, String data)` they become handler methods taking a concrete record (e.g. `GameMoveHandler.handle(ClientSide, GameMoveRequest)`). The classes are renamed from `ServerEventListener_CODE_*` to `<Event>Handler` to drop the reflection-registry prefix.
|
||||
- **Gradle `com.google.protobuf` plugin v0.9.6** generates Java from `server/src/main/proto/*.proto`. Committed generated Java files are deleted. `.proto` files live at `server/src/main/proto/` (plugin convention).
|
||||
- **Client:** `protobufjs-cli` static codegen committed under `client/src/generated/protocol.js` + `.d.ts`. Client event bus keys on a string event name derived from `Response.payloadCase` (mapping table).
|
||||
- **Port:** single port `1999` default (`-p 1999` arg honored). TCP port 1024 silently dropped — no external consumers, no migration notice.
|
||||
- **Gson dep dies.** With no inner JSON strings left, `JsonUtils` and `MapHelper` become dead code and are deleted. `implementation("com.google.code.gson:gson")` removed from `build.gradle.kts`.
|
||||
|
||||
## Dependency Graph
|
||||
```
|
||||
01 (proto schemas + plugin) -> 02a (dispatcher scaffolding) -> 02b (migrate handlers) -> 03 (cleanup) -> 04 (client) -> 05 (infra+docs) -> 06 (E2E)
|
||||
```
|
||||
Strict serial. Each phase must leave server compiling (`./gradlew -p server compileJava`) and tests green. Phase 02 is split 02a/02b — see below.
|
||||
|
||||
## Phases
|
||||
| # | File | Focus | Status |
|
||||
|---|------|-------|--------|
|
||||
| 01 | [phase-01-proto-schemas-and-build.md](phase-01-proto-schemas-and-build.md) | Author `request.proto` / `response.proto`, Gradle protobuf plugin, delete hand-committed generated Java | done (945a249) |
|
||||
| 02a | [phase-02a-dispatcher-scaffolding.md](phase-02a-dispatcher-scaffolding.md) | Sealed `ClientRequest` interface + records, `RequestDispatcher`, binary WS pipeline, stub handlers | done (945a249) |
|
||||
| 02b | [phase-02b-migrate-handlers.md](phase-02b-migrate-handlers.md) | Port every `ServerEventListener_CODE_*` to typed handler; rewrite `ChannelUtils` for `Response` | done (b75733f) |
|
||||
| 03 | [phase-03-server-cleanup.md](phase-03-server-cleanup.md) | Delete `Msg`, `JsonUtils`, `MapHelper`, TCP framing, gson dep; verify 37 tests pass | done (3ad9a7b) |
|
||||
| 04 | [phase-04-client-protobuf.md](phase-04-client-protobuf.md) | `protobufjs-cli` codegen, rewrite `connection-service.js` for typed Request/Response oneof, port 1999 | done (ecc6177) |
|
||||
| 05 | [phase-05-infra-and-docs.md](phase-05-infra-and-docs.md) | docker-compose, Dockerfile, README, docs/* | done (cbad690) |
|
||||
| 06 | [phase-06-e2e-smoke-test.md](phase-06-e2e-smoke-test.md) | Manual E2E checklist via docker compose | deferred (user-run) |
|
||||
|
||||
## Audit Notes (inbound/outbound schemas, from codebase read)
|
||||
Condensed from `server/src/main/java/com/miti99/caro/server/event/*` and `client/src/**/*.js`:
|
||||
|
||||
**Inbound (ServerEventCode → Request oneof field):**
|
||||
| Code | Inner data shape | Request message |
|
||||
|---|---|---|
|
||||
| CLIENT_HEAD_BEAT | (none) | `HeartbeatRequest {}` |
|
||||
| CLIENT_NICKNAME_SET | raw string (nickname) | `SetNicknameRequest { string nickname = 1; }` |
|
||||
| CLIENT_INFO_SET | `{version}` | `SetClientInfoRequest { string version = 1; }` |
|
||||
| ROOM_CREATE | (none) | `CreateRoomRequest {}` |
|
||||
| ROOM_CREATE_PVE | raw string "1"/"2"/"3" | `CreatePveRoomRequest { int32 difficulty = 1; }` |
|
||||
| GET_ROOMS | (none) | `GetRoomsRequest {}` |
|
||||
| ROOM_JOIN | raw string (room id) | `JoinRoomRequest { int32 room_id = 1; }` |
|
||||
| GAME_STARTING | (none, reused internally) | `GameStartingRequest {}` (keep for sealed-hierarchy completeness; never directly sent by client today) |
|
||||
| GAME_READY | (none) | `GameReadyRequest {}` |
|
||||
| GAME_MOVE | `{row, col}` | `GameMoveRequest { int32 row = 1; int32 col = 2; }` |
|
||||
| GAME_RESET | (none, unused) | `GameResetRequest {}` |
|
||||
| GAME_WATCH | raw string (room id) | `WatchGameRequest { int32 room_id = 1; }` |
|
||||
| GAME_WATCH_EXIT | (none) | `WatchGameExitRequest {}` |
|
||||
| CLIENT_EXIT | (none) | `ClientExitRequest {}` |
|
||||
|
||||
**Outbound (ClientEventCode → Response oneof field):**
|
||||
| Code | Inner data shape | Response message |
|
||||
|---|---|---|
|
||||
| CLIENT_CONNECT | string clientId | `ClientConnectResponse { int32 client_id = 1; }` |
|
||||
| CLIENT_NICKNAME_SET | `{invalidLength}` OR null (prompt) | `NicknameSetResponse { int32 invalid_length = 1; }` (0 = prompt-only) |
|
||||
| SHOW_OPTIONS | null | `ShowOptionsResponse {}` |
|
||||
| SHOW_OPTIONS_PVE/PVP/SETTING | null (not emitted today) | skipped — enum values are present but the server never sends them. Keep `ClientEventCode` enum JS-side for compatibility but no proto fields needed. |
|
||||
| SHOW_ROOMS | `List<{roomId, roomOwner, roomClientCount, roomType}>` | `ShowRoomsResponse { repeated RoomSummary rooms = 1; } RoomSummary { int32 room_id; string room_owner; int32 room_client_count; string room_type; }` |
|
||||
| SHOW_BOARD | (never sent) | skipped |
|
||||
| ROOM_CREATE_SUCCESS | full `Room` JSON, client reads `data.id` | `RoomCreateSuccessResponse { int32 id = 1; string room_owner = 2; string room_type = 3; }` (lean — only the three fields the client reads) |
|
||||
| ROOM_JOIN_SUCCESS | `{clientId, clientNickname, roomId, roomOwner, roomClientCount}` OR raw nickname for watchers | Split into `RoomJoinSuccessResponse { ... }` + `WatcherJoinNoticeResponse { string nickname = 1; }`. **Different oneof fields.** Alternative (simpler): one `RoomJoinSuccessResponse` with all fields; watcher path sets only `nickname`. Go with simpler single-message approach. |
|
||||
| ROOM_JOIN_FAIL_BY_FULL | `{roomId, roomOwner}` | `RoomJoinFailFullResponse { int32 room_id = 1; string room_owner = 2; }` |
|
||||
| ROOM_JOIN_FAIL_BY_INEXIST | `{roomId}` | `RoomJoinFailNotFoundResponse { int32 room_id = 1; }` |
|
||||
| ROOM_PLAY_FAIL_BY_INEXIST | null | `RoomPlayFailNotFoundResponse {}` |
|
||||
| GAME_STARTING | `{roomId, blackPlayerId, blackPlayerNickname, whitePlayerId, whitePlayerNickname, boardSize}` | `GameStartingResponse { int32 room_id = 1; int32 black_player_id = 2; string black_player_nickname = 3; int32 white_player_id = 4; string white_player_nickname = 5; int32 board_size = 6; }` |
|
||||
| GAME_READY | `{clientNickName, status, clientId}` | `GameReadyResponse { string client_nickname = 1; string status = 2; int32 client_id = 3; }` |
|
||||
| GAME_MOVE_SUCCESS | `{row, col, piece, playerNickname, playerId}` | `GameMoveSuccessResponse { int32 row = 1; int32 col = 2; string piece = 3; string player_nickname = 4; int32 player_id = 5; }` |
|
||||
| GAME_MOVE_INVALID/OCCUPIED/OUT_OF_BOUNDS/NOT_YOUR_TURN | null | `GameMoveInvalidResponse {}` `GameMoveOccupiedResponse {}` `GameMoveOutOfBoundsResponse {}` `GameMoveNotYourTurnResponse {}` |
|
||||
| GAME_OVER | `{result, winnerNickname}` | `GameOverResponse { string result = 1; string winner_nickname = 2; }` |
|
||||
| GAME_WIN / LOSE / DRAW | (never sent directly — GAME_OVER carries result) | skipped |
|
||||
| PVE_DIFFICULTY_NOT_SUPPORT | null | `PveDifficultyNotSupportResponse {}` |
|
||||
| GAME_WATCH | (never sent directly) | skipped |
|
||||
| GAME_WATCH_SUCCESSFUL | `{owner, status}` | `WatchGameSuccessResponse { string owner = 1; string status = 2; }` |
|
||||
| CLIENT_EXIT | `{roomId, exitClientId, exitClientNickname}` OR raw nickname (watchers) | `ClientExitResponse { int32 room_id = 1; int32 exit_client_id = 2; string exit_client_nickname = 3; }` (watcher path populates only nickname; roomId=0 means "watcher-style notice") |
|
||||
| CLIENT_KICK | (never sent) | skipped |
|
||||
|
||||
Obsolete `ClientEventCode` enum values that the server NEVER sends are excluded from `response.proto`. They remain in the Java enum only as long as needed; ideally deleted in Phase 03. Client JS keeps them until UI code stops referencing (none does — only `CLIENT_KICK` has a listener in game-scene but it's never fired).
|
||||
|
||||
## Global Success Criteria
|
||||
- Server exposes only TCP port 1999 (WebSocket handshake at `/ratel`).
|
||||
- Wireshark/devtools show binary WS frames on the wire — no JSON strings anywhere.
|
||||
- `./gradlew -p server clean build` green (all existing unit tests pass — 37).
|
||||
- `npm --prefix client run build` green.
|
||||
- End-to-end: nickname → lobby → PVE move → win works.
|
||||
- Zero references to `TextWebSocketFrame`, `Msg`, `JsonUtils`, `MapHelper`, `ProtobufProxy`, `SecondProtobufCodec`, `ByteKit/ByteLink/TransferProtocolUtils/DefaultDecoder`, `implements Proxy`, gson, ports 1024/1025.
|
||||
- Zero reflection-based listener lookup (`ServerEventListener.get(code)` gone).
|
||||
|
||||
## Rollback
|
||||
Each phase is one commit (02 is two: 02a + 02b). `git revert <sha>` restores prior state. Phase 02b is the riskiest (touches 14 handlers) — keep 02a isolated so a bad handler port can be reverted without losing the dispatcher scaffolding.
|
||||
|
||||
## Resolved Questions
|
||||
1. **Generated JS location:** commit under `client/src/generated/` (no gitignore, no prebuild step). — User confirmed.
|
||||
2. **TCP 1024 external consumers:** none. Drop silently, no migration notice. — User confirmed.
|
||||
3. **Inner payload migration:** full typed-proto migration NOW. Inner JSON strings gone. Java sealed interface + record dispatch. — User confirmed.
|
||||
|
||||
## Target Dependency Versions (latest stable pre-2026)
|
||||
Pin these exact versions across phases 01 and 04. Source: `plans/reports/researcher-260410-2132-pre-2026-versions.md`.
|
||||
|
||||
**Server (`server/build.gradle.kts`) — Phase 01 updates these alongside the protobuf plugin:**
|
||||
| Dep | Current | Target | Action |
|
||||
|---|---|---|---|
|
||||
| `com.google.protobuf` Gradle plugin | (new) | **0.9.6** | ADD |
|
||||
| `com.google.protobuf:protoc` artifact | (new) | **3.25.5** | ADD |
|
||||
| `com.google.protobuf:protobuf-java` | 3.25.5 | 3.25.5 | keep |
|
||||
| `io.netty:netty-all` | 4.1.115.Final | **4.1.128.Final** | BUMP (+13 releases, security patches) |
|
||||
| `org.junit:junit-bom` | 5.11.3 | **5.11.4** | BUMP (patch) |
|
||||
| `com.gradleup.shadow` plugin | 8.3.5 | **8.3.8** | BUMP (maintenance) |
|
||||
| Gradle wrapper | 9.2.1 | 9.2.1 | keep (repo already ≥ pre-2026 latest) |
|
||||
|
||||
**Client (`client/package.json`) — Phase 04 updates these:**
|
||||
| Dep | Current | Target | Action |
|
||||
|---|---|---|---|
|
||||
| `protobufjs` | (new) | **7.5.4** | ADD (runtime dep, uses `protobufjs/minimal`) |
|
||||
| `protobufjs-cli` | (new) | **1.1.3** | ADD (devDep, for `pbjs`/`pbts` codegen) |
|
||||
| `phaser` | 3.87.0 | 3.87.0 | keep |
|
||||
| `vite` | 6.3.1 | 6.3.1 | keep (repo already ≥ pre-2026 latest) |
|
||||
|
||||
All version bumps are low-risk patch/maintenance releases — no breaking changes expected. Verified in researcher report.
|
||||
|
||||
## New Unresolved Questions
|
||||
- **`WinDetectResult` / `PieceType` enum:** `PieceType` lives in `common.enums` today and is used only in server-internal board logic + as a stringified field on `GameMoveSuccessResponse.piece`. Proto `piece` field is kept as `string` ("BLACK"/"WHITE") so no proto enum needed. Confirm OK (alternative: proto enum).
|
||||
- **`RoomStatus` / `RoomType` strings on the wire:** `GAME_WATCH_SUCCESSFUL.status` and `SHOW_ROOMS.roomType` are stringified enum names today. Kept as proto `string` for the same reason. Confirm OK.
|
||||
- **Removing dead `ClientEventCode` entries** (`CLIENT_KICK`, `SHOW_BOARD`, `SHOW_OPTIONS_PVP/PVE/SETTING`, `GAME_WIN/LOSE/DRAW`, `GAME_WATCH` outbound): keep enum in Java for now (orthogonal cleanup) or delete in Phase 03? Plan assumes **keep enum, do not add to `response.proto`** — YAGNI on cleanup scope.
|
||||
- **Heartbeat frame shape:** `HeartbeatRequest {}` (empty message). Alternative: drop heartbeat from the oneof entirely and use a sentinel frame. Empty message is simpler and keeps the oneof exhaustive for pattern matching. Confirm.
|
||||
Reference in new issue
Block a user