The offline vs_ai game engine — a faithful TS port of backend/internal/engine that
drives a whole local game with no backend. Composes with the move generator (#188) and
robot strategy (#189) from Phase A; not yet wired into the UI (Phase B2/B3).
- ui/src/lib/localgame/ruleset.ts: static per-variant tile values / bag counts / blank
count, mirrored from rules.go (offline scoring is self-contained; online uses the
server alphabet). Pinned by ruleset.parity.test.ts against a Go fixture.
- bag.ts: the tile bag (fill from counts/blanks, draw-from-end, return+reshuffle) on a
deterministic in-house PRNG — a game replays from its seed, not bit-identical to a
server game (per plan).
- board.ts: the mutable board, satisfying the validator/generator read view + set().
- engine.ts: LocalGame — deal / play (reusing validate.ts) / pass / exchange / resign,
scoreless(6) & out-of-tiles end detection, end-of-game rack penalties, winner; mirrors
game.go. The end-game math is exported as pure functions, pinned against the Go engine
(engine.parity.test.ts, 9 constructed positions).
- engine.test.ts: a full-loop smoke — two robots play a whole vs_ai game to completion
via generateMoves + decide, and it is reproducible from the seed.
- backend: movegen now dumps the per-variant rulesets; a new in-package engine emitter
(endfixture_test.go, env-gated) produces the end-game golden.
Pure additive library code; no runtime behavior change (unused at runtime, bundle unchanged).
Phase A (A4): prove the ported move generator (#188) against the FULL shipped
dictionaries, not just the tiny samples — the deep graphs and complete 26/33-letter
alphabets the samples cannot reach.
- backend/cmd/movegen: add a -dawg-dir mode that emits per-variant golden move-gen
vectors from the real dawgs (a bounded first-move + a blank case + a deep 7-tile
mid-game position). Regenerated in CI to /tmp, never committed (like the
dictgen/validategen vectors), so no dictionary version is pinned into the repo.
- ui/src/lib/dict/generate.realparity.test.ts: env-gated (DICT_DAWG_DIR +
DICT_MOVEGEN_DIR) parity against that golden — 9 positions across scrabble_en /
scrabble_ru / erudit_ru match the Go solver exactly. Skips cleanly when unset.
- .gitea/workflows/ci.yaml: the conformance job now generates the movegen golden
and points the gated vitest at it (DICT_MOVEGEN_DIR).
First engine-first step of PWA offline mode (Phase A): the client-side move
generator — the "robot brain" a local vs_ai game will run on-device — with no
runtime wiring yet (Phase B).
- dawg.ts: add the step-by-step cursor (root/final/next/arcs), a faithful port
of dafsa traverse.go over the reader's existing bitstream.
- generate.ts: the Appel-Jacobson generator (leftPart/extendRight + cross-sets +
counts-rack + board transpose + moveKey ranking), reusing the cursor and
validate.ts evaluate/connected. A cross-set LetterSet is a Uint8Array, so the
33-letter Russian alphabet (index 32) is exact under JS bit ops.
- validate.ts: export connected for the generator's connectivity filter.
- backend/cmd/movegen: dev tool building small sample dictionaries and emitting
golden move-generation fixtures from the real Go solver (EN + RU).
- tests: dawg.cursor.test.ts (enumeration bijection vs indexOf) and
generate.parity.test.ts (7/7 vs the Go solver: empty board, mid-game, blank,
single-word rule, Russian index-32 cross-set). The committed EN sample also
unblocks the existing skipped dawg.parity.test.ts once wired with DICT_* in CI.
Pure additive library code; no runtime behavior change.