feat: on-device move preview (local eval) with network fallback
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 58s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m17s

Score and validate a tentative move on-device instead of a per-arrangement network
round trip. The dawg reader and the validate/score/direction slice of the
scrabble-solver engine are ported to TypeScript (ui/src/lib/dict), pinned
byte-for-byte to the Go engine by a `conformance` CI job (full-dictionary reader
parity plus a battery of plays across every variant and both cross-word rules,
including the inferred orientation). The server stays authoritative — submit_play
re-validates — so the local result is an advisory accelerator only.

- backend: Registry.DictBytes + an authed GET /api/v1/user/dict/{variant}/{version}
  (immutable) streaming the pinned per-game dawg; caddy routes /dict to the gateway.
- gateway: a session-gated /dict edge route proxying it; fetchDict on the transport.
- client: the dictionary loads on game open (low priority so it never starves the
  game on a slow link; aborted at a 5s cap or when leaving the game), is cached in
  IndexedDB (best-effort, self-healing on a rejected blob) and reused across
  sessions; a warm-up overlay covers a cold load, then the network preview is the
  fallback; a bad-connection breaker stops warming after repeated misses; the move
  preview cancels its in-flight request when the tiles change.
- parity generators backend/cmd/{dictgen,validategen} + gated Vitest suites, run in
  CI against the release dictionaries. A hidden debug readout lists the cached
  dictionaries + breaker state, and its reset clears the cache.
- docs: ARCHITECTURE §5, TESTING, UI_DESIGN, FUNCTIONAL (+ru).
This commit is contained in:
Ilia Denisov
2026-07-01 22:58:40 +02:00
parent f0399e1bbc
commit 5689f7f6a3
38 changed files with 2612 additions and 28 deletions
+8 -1
View File
@@ -91,12 +91,19 @@ export interface GatewayClient {
exchange(gameId: string, tiles: string[], variant: Variant): Promise<MoveResult>;
resign(gameId: string): Promise<MoveResult>;
hint(gameId: string): Promise<HintResult>;
evaluate(gameId: string, tiles: PlacedTile[], variant: Variant): Promise<EvalResult>;
evaluate(gameId: string, tiles: PlacedTile[], variant: Variant, signal?: AbortSignal): Promise<EvalResult>;
checkWord(gameId: string, word: string, variant: Variant): Promise<WordCheckResult>;
complaint(gameId: string, word: string, note: string): Promise<void>;
/** Hide a finished game from the caller's own lobby list; per-account, irreversible. */
hideGame(gameId: string): Promise<void>;
// --- dictionary (local move preview) ---
/** Fetch the raw serialized dictionary blob for a (variant, version) pair. Session-gated;
* lets the client validate and score a move locally instead of a network round trip.
* opts.signal aborts a stalled download; opts.reload bypasses the browser HTTP cache (the
* debug reset, for testing a cold load). */
fetchDict(variant: Variant, version: string, opts?: { signal?: AbortSignal; reload?: boolean }): Promise<ArrayBuffer>;
// --- draft ---
/** The player's server-persisted client-side composition (rack order + board tiles), so a
* reload or a second device resumes the same arrangement. The JSON is opaque to the