Stage 13: alphabet on the wire (UI alphabet-agnostic, TODO-4)
Tests · Go / test (push) Successful in 10s
Tests · Integration / integration (push) Successful in 12s
Tests · UI / test (push) Successful in 19s
Tests · Go / test (pull_request) Successful in 9s
Tests · Integration / integration (pull_request) Successful in 12s
Tests · UI / test (pull_request) Successful in 19s
Tests · Go / test (push) Successful in 10s
Tests · Integration / integration (push) Successful in 12s
Tests · UI / test (push) Successful in 19s
Tests · Go / test (pull_request) Successful in 9s
Tests · Integration / integration (pull_request) Successful in 12s
Tests · UI / test (pull_request) Successful in 19s
Live play now exchanges per-variant alphabet indices instead of concrete letters (rack out; submit-play, evaluate, exchange, word-check in). The client caches each variant's (index, letter, value) table behind StateRequest.include_alphabet and renders the rack and blank chooser from it, dropping the hardcoded value/alphabet tables. History, the durable journal and GCG stay decoded concrete characters (ARCHITECTURE §9.1, unchanged). - pkg/fbs: new AlphabetEntry + PlayTile; StateView.rack -> [ubyte] + alphabet; StateRequest.include_alphabet; SubmitPlay/Eval tiles -> [PlayTile]; Exchange tiles + CheckWord word -> [ubyte] (committed Go + TS regenerated). - engine: AlphabetTable + a cached per-variant codec (LetterForIndex/EncodeRack/ DecodeTiles/DecodeWord) + BlankIndex sentinel; Go parity test. - backend server edge maps index<->letter (new thin game.Service.GameVariant); game.Service domain methods, engine.Game and the robot keep one letter-based play path. The gateway forwards indices verbatim (no alphabet table). - ui: lib/alphabet.ts in-memory cache; codec encodes/decodes indices; premiums.ts is geometry-only; the mock seeds a fixture table; the UI normalises display to upper case (codec + cache), leaving placement/board/checkword unchanged. Parity moved to the Go engine.AlphabetTable test; premiums.ts loses its value tables. Discharges TODO-4.
This commit is contained in:
@@ -46,7 +46,7 @@ independent (see ARCHITECTURE §9.1).
|
||||
| 10 | Admin & dictionary ops (complaint review, version reload) | **done** |
|
||||
| 11 | Account linking & merge | **done** |
|
||||
| 12 | Observability & performance (telemetry, metrics, guest GC) | **done** |
|
||||
| 13 | Alphabet on the wire (UI alphabet-agnostic) | todo |
|
||||
| 13 | Alphabet on the wire (UI alphabet-agnostic) | **done** |
|
||||
| 14 | CI & deploy (multi-service, dictionary artifacts) | todo |
|
||||
|
||||
Scaffolding is incremental: `go.work` lists only existing modules; each stage
|
||||
@@ -893,6 +893,51 @@ dashboard stack; solver-publish vs clone-in-build; load expectations.
|
||||
`./backend/... ./gateway/... ./pkg/... ./platform/telegram/...`, integration stays
|
||||
`./backend/...`, and the default `none` exporter keeps CI collector-free.
|
||||
|
||||
- **Stage 13** (interview + implementation, discharges TODO-4):
|
||||
- **Scope = live play only** (interview): indices ride the wire for `StateView.rack`
|
||||
(out) and `SubmitPlay`/`Evaluate`/`Exchange`/`CheckWord` (in). The **board path is
|
||||
untouched** — `MoveRecord` (history, move results, hint), formed `words`,
|
||||
`ComplaintRequest.word` (durable, admin-reviewed) and `WordCheckResult.word` (echo) stay
|
||||
decoded concrete characters, so the durable journal / GCG and the §9.1 invariant are
|
||||
unchanged. **Hard cutover**, no dual letter/index fields (single client; the fbs Go + TS
|
||||
regenerate in one PR; no external wire consumer). Exchange moved fully to indices, a
|
||||
blank = the shared sentinel index **255** (`engine.BlankIndex`).
|
||||
- **Edge-mapping layering** (engineering): the engine gained a cached per-variant codec —
|
||||
`AlphabetTable` (the `(index, letter, value)` table from the solver ruleset),
|
||||
`LetterForIndex`, `EncodeRack`, `DecodeTiles`, `DecodeWord` — and the backend **server
|
||||
edge** owns the index↔letter mapping. `game.Service`'s domain methods, `engine.Game` and
|
||||
the **robot** keep a single **letter-based** play path (untouched); a new thin
|
||||
`game.Service.GameVariant` (a single-column `SELECT variant`, cheaper than `GetGame`)
|
||||
lets the inbound handlers resolve the variant without doubling the play-path read. The
|
||||
**gateway carries no alphabet table** — it passes indices through verbatim; `check_word`
|
||||
rides as repeated `?idx=` query params.
|
||||
- **`include_alphabet` flag** (interview): `StateRequest.include_alphabet` gates the table
|
||||
so it is not resent on every poll; the client sets it only on a **per-variant cache
|
||||
miss** (first open of a variant), and the table then arrives with the index rack so the
|
||||
rack is always decodable. The client caches the table in memory by variant
|
||||
(`ui/src/lib/alphabet.ts`).
|
||||
- **Letter case** (discovered): the solver emits **lower-case** letters and the rest of
|
||||
the UI works in **upper case**. The wire and the journal stay lower case; the **UI
|
||||
normalises display to upper case** (the codec upper-cases decoded board tiles and words,
|
||||
and the alphabet cache upper-cases on ingest), so `placement.ts` / `board.ts` /
|
||||
`checkword.ts` are unchanged and the latent real-backend lower-case display is fixed.
|
||||
- **Parity rework** (interview): the real value/alphabet parity moved to a **Go engine
|
||||
test** (`engine.AlphabetTable`: EN/RU/Эрудит sizes, EN a=1/q=10, **Эрудит ё=index 6,
|
||||
value 0**); `ui/src/lib/premiums.ts` is now **geometry only** (its value tables,
|
||||
`tileValue` and `alphabet` were removed, its parity test trimmed to the premium grid);
|
||||
the codec test round-trips the index tiles + the alphabet table; the **mock keeps a
|
||||
fixture table** (relocated from `premiums.ts`) seeded into the client cache, so the
|
||||
mock-driven UI is alphabet-agnostic too.
|
||||
- **Wire/codegen/CI**: new fbs `AlphabetEntry` + `PlayTile`; `StateView.rack`→`[ubyte]` +
|
||||
`alphabet`; `StateRequest.include_alphabet`; `SubmitPlay`/`Eval` tiles→`[PlayTile]`;
|
||||
`Exchange` tiles→`[ubyte]`; `CheckWord.word`→`[ubyte]` (committed Go + TS regenerated).
|
||||
UI ~90 KB gzip JS (budget 100 KB). **No CI workflow change** — the Go workflows already
|
||||
span `./backend/... ./gateway/... ./pkg/...` and the UI workflow runs check/unit/build +
|
||||
a chromium/webkit e2e. `docs/FUNCTIONAL.md` is **untouched** (no user-visible behaviour
|
||||
change — the UI looks and plays the same; like Stage 2). The index-drift caveat is
|
||||
handled by construction (the running backend produces the table, so client↔server cannot
|
||||
drift); the DAWG/solver build-time agreement remains **Stage 14 / TODO-2**.
|
||||
|
||||
## Deferred TODOs (cross-stage)
|
||||
|
||||
- **TODO-1 — publish & version the solver.** Once `scrabble-solver` is stable,
|
||||
@@ -931,17 +976,16 @@ dashboard stack; solver-publish vs clone-in-build; load expectations.
|
||||
`last_seen_at`, so a lingering session never expires and **account age** is the
|
||||
abandonment trigger, not "last session gone". The reaped guest's `sessions`/`identities`/
|
||||
`account_stats` fall away via their own `ON DELETE CASCADE`.
|
||||
- **TODO-4 — put the per-game alphabet on the wire (owner's idea, Stage 7).** Today the
|
||||
client hardcodes each variant's letters/values (ported into `ui/src/lib/premiums.ts`
|
||||
from `scrabble-solver/rules/rules.go`) and the edge exchanges plays/hints by concrete
|
||||
letters. Consider extending `game.state` to carry the variant's `(letter, index,
|
||||
value)` table so the UI stops duplicating it, and optionally moving tile exchange to
|
||||
letter **indices** end-to-end. Caveat (as for the dictionaries, TODO-2): the wire table
|
||||
must stay pinned to the same `rules.Alphabet` the engine uses, or indices drift.
|
||||
**Planned for Stage 13**, expanded (owner) to a fully **alphabet-agnostic UI**: the
|
||||
client caches the per-variant table (display only) behind an `include_alphabet` request
|
||||
flag and exchanges indices both ways, word-check included; the durable journal stays
|
||||
concrete characters (§9.1). See Stage 13.
|
||||
- ~~**TODO-4 — put the per-game alphabet on the wire (owner's idea, Stage 7).**~~ **Done in
|
||||
Stage 13.** The client is now alphabet-agnostic: it caches each variant's `(index, letter,
|
||||
value)` table — sent by the backend behind `StateRequest.include_alphabet` on a per-variant
|
||||
cache miss — and live play exchanges **letter indices** both ways (rack, submit-play,
|
||||
evaluate, exchange, word-check; a blank rides as the sentinel index 255). The table is
|
||||
produced from the solver ruleset (`engine.AlphabetTable`), so it is pinned by the solver
|
||||
version and cannot drift from the running backend, and `ui/src/lib/premiums.ts` is now
|
||||
geometry only. The durable journal / history / GCG stay decoded concrete characters (§9.1,
|
||||
unchanged). The DAWG/solver build-time agreement (the original caveat, shared with TODO-2)
|
||||
remains Stage 14.
|
||||
- **TODO-5 — QR friend codes (owner's idea, Stage 8).** *Partially done in Stage 9:*
|
||||
the deep-link scheme now exists (`f<code>`, shared Go ↔ TS), the bot redeems it on
|
||||
launch, and the UI shows a **share-to-Telegram** link for an issued code when
|
||||
|
||||
Reference in New Issue
Block a user