mirror of
https://github.com/tiennm99/noitu.git
synced 2026-10-13 08:15:50 +00:00
docs(plans): record the in-game feedback implementation report
This commit is contained in:
1 parent
e52b0ef2c3
commit
2adab1318b
1 file changed
+122
@@ -0,0 +1,122 @@
|
||||
# In-game feedback: score breakdown, dead-end claim, near-miss suggestion, word report
|
||||
|
||||
Branch: `worktree-agent-a582df720cc6e3dea`
|
||||
Worktree: `/workspace/tiennm99/noitu/.claude/worktrees/agent-a582df720cc6e3dea`
|
||||
Base: rebased onto `dev` at `865222d` (proto/generated code and cross-language
|
||||
fixtures for all four features were already committed there; this work is the
|
||||
implementation against that schema).
|
||||
|
||||
Commits: `16fd379` (game/dictionary/wsapi), `0003a45` (web), `e52b0ef` (docs).
|
||||
|
||||
## Scope delivered
|
||||
|
||||
**A2 — score breakdown.** `game.Move.Parts []PointPart{Kind, Value}`, computed
|
||||
by `pointsFor`, one entry per non-zero term (base, chain, syllables, speed,
|
||||
rarity), trimmed from the end (rarity, speed, syllables — never reaches chain
|
||||
or base) when `maxPointsPerWord` bites, always summing exactly to `Points`.
|
||||
`wsapi.PointKind`/`PointParts` map onto the wire, `PlayedWord.Parts` set from
|
||||
them. Client: `ChainEntry.parts`, rendered as `+10 nền · +15 nhanh` chips in
|
||||
`ChainHistory`; labels in `vi.js` keyed by `PointKind`. Bot rooms get it for
|
||||
free — same `Submit` path.
|
||||
|
||||
**A3 — dead-end claim.** `ClaimDeadEnd` dispatched like `Resign` (seat via
|
||||
`occupies`, rate-limited on `submitLimiter`). Room: no game → `game_not_started`;
|
||||
not the claimant's turn → `not_your_turn`; `HasLegalMove()` true → `not_a_dead_end`,
|
||||
nothing else changes; false → `NoMove` + `applyEliminations`, same as the
|
||||
clock. `metrics.deadEndClaims` keyed `"true"`/`"false"`. Client: "Bí từ" button
|
||||
next to the input, visible only on the player's own turn, armed like resign;
|
||||
a false claim answers inline near the input (`state.claimError`) rather than
|
||||
the general error banner.
|
||||
|
||||
**C3 — near-miss suggestion.** `dictionary.Store.stripped` index built at
|
||||
`Open` (NFD, drop `Mn`, lowercase, đ/Đ→d/D — typing differences only).
|
||||
`NearMiss(normalized) (string, bool)` returns the one candidate when exactly
|
||||
one exists and is not the input itself. Added to `wsapi.Dictionary` and the
|
||||
test doubles. Room fills `MoveRejected.suggestion` on
|
||||
`ReasonNotInDictionary`. Client shows "Ý bạn là "…"?" as a button that fills
|
||||
the (uncontrolled) field and focuses it — a user click, never a write during
|
||||
composition.
|
||||
|
||||
**E3 — word report.** `ReportWord` rate-limited by `chatLimiter`, validated
|
||||
entirely on the session (own rate budget, own per-session distinct-word set,
|
||||
capped at 20) before being routed to the room only for the log line's
|
||||
room-owned context (syllable in play, mode, code) — `handleReportWord` never
|
||||
touches player identity. `word_reported` slog line documented next to
|
||||
`word_rejected` in `docs/deployment.md`; `metrics.wordsReported` counter.
|
||||
Client offers "Báo từ này là từ thật" after a `NOT_IN_DICTIONARY` rejection
|
||||
and shows the confirmation on `WordReported`.
|
||||
|
||||
**Rules/README.** `/rules` dead-end and scoring copy mention the claim button
|
||||
and the visible breakdown; README's "The frontend" and rules paragraphs each
|
||||
gained one sentence. `errorMessages` gained `not_a_dead_end`,
|
||||
`word_report_refused`, `word_report_limit`; `not_your_turn`'s copy was
|
||||
generalized since resign and the claim now share the code.
|
||||
|
||||
## Files changed
|
||||
|
||||
Server: `server/internal/game/{state.go,engine.go,engine_test.go}`,
|
||||
`server/internal/dictionary/{store.go,store_test.go}`,
|
||||
`server/internal/wsapi/{codec.go,convert.go,convert_test.go,metrics.go,room.go,session.go,wsapi_test.go}`.
|
||||
|
||||
Web: `web/src/lib/{i18n/vi.js,stores/game.svelte.js,ws/messages.js}`,
|
||||
`web/src/lib/components/{ChainHistory.svelte,GameBoard.svelte,WordInput.svelte}`,
|
||||
`web/src/routes/{online,play}/+page.svelte`, `web/src/routes/rules/+page.svelte`,
|
||||
`web/tests/{game-store.test.js,game-wire.test.js,i18n.test.js}`.
|
||||
|
||||
Docs: `README.md`, `docs/deployment.md`.
|
||||
|
||||
Not touched: `proto/`, `server/gen/`, `web/src/lib/proto/`, quick-match
|
||||
(`QuickMatch`/`CancelQuickMatch`/`QuickMatchStatus`) — the sibling agent's arms.
|
||||
|
||||
## Verification
|
||||
|
||||
```
|
||||
cd server && gofmt -l . && go vet ./... && golangci-lint run ./... # clean, 0 issues
|
||||
cd server && go test ./... -race -count=1 # all packages ok
|
||||
cd web && npm run lint # 0 errors (33 pre-existing `any` warnings)
|
||||
cd web && npm run check # 0 errors, 0 warnings
|
||||
cd web && npm test # 214 tests passed, build+bundle check included
|
||||
```
|
||||
|
||||
Go test tail:
|
||||
```
|
||||
ok github.com/tiennm99dev/noitu/server/cmd/build-dictionary
|
||||
ok github.com/tiennm99dev/noitu/server/internal/bot
|
||||
ok github.com/tiennm99dev/noitu/server/internal/dictionary
|
||||
ok github.com/tiennm99dev/noitu/server/internal/game
|
||||
ok github.com/tiennm99dev/noitu/server/internal/vietnamese
|
||||
ok github.com/tiennm99dev/noitu/server/internal/wsapi
|
||||
```
|
||||
|
||||
Web test tail: `Test Files 12 passed (12)`, `Tests 214 passed (214)`.
|
||||
|
||||
## Tests added
|
||||
|
||||
- `game`: `TestSubmitScoringPartsMatchTheTotal`, `TestSubmitScoringPartsAreTrimmedAtTheCap`, `TestPointKindStrings`.
|
||||
- `wsapi`: `TestPointKindMappingIsExhaustive`, `TestPlayedWordCarriesItsScoreBreakdown`,
|
||||
`TestClaimDeadEndEliminatesImmediately`, `TestClaimDeadEndRefusedWhenAMoveExists`,
|
||||
`TestClaimDeadEndOutOfTurnRefused`, `TestNearMissSuggestionOnWire`,
|
||||
`TestReportWordAcceptsAndEchoes`, `TestReportWordRefusesOneSyllable`,
|
||||
`TestReportWordEnforcesPerSessionCap`.
|
||||
- `dictionary`: `TestStripDiacriticsFoldsDBreve`, `TestNearMissFindsAUniqueDiacriticTypo`,
|
||||
`TestNearMissRefusesAnAmbiguousStem`, `TestNearMissNeverSuggestsTheWordItself`,
|
||||
`TestNearMissRefusesAnUnrelatedWord`.
|
||||
- `web`: score-breakdown/word-report/dead-end-claim `describe` blocks in
|
||||
`game-store.test.js`; near-miss/parts/wordReported assertions in
|
||||
`game-wire.test.js`; a `point kind labels` block in `i18n.test.js`.
|
||||
|
||||
## Deferred / not in scope
|
||||
|
||||
- Playwright/e2e coverage for the new buttons — this environment cannot run a
|
||||
browser (project constraint); left to the existing `test:e2e` suite to pick
|
||||
up when run elsewhere.
|
||||
- Quick-match (owned by the sibling agent).
|
||||
|
||||
## Unresolved questions
|
||||
|
||||
- None blocking. One judgment call worth flagging: `not_your_turn`'s
|
||||
Vietnamese copy was generalized from "Chỉ đầu hàng được trong lượt của bạn."
|
||||
to "Chưa đến lượt bạn." because the code is now shared between `Resign` and
|
||||
`ClaimDeadEnd`. No test pinned the old string; if the merge with the
|
||||
sibling's changes wants a different wording, it's a one-line change in
|
||||
`vi.js`.
|
||||
Reference in new issue
Block a user