Stage 2: engine package over scrabble-solver (registry, bag, Game, replay)
backend/internal/engine wraps the sibling scrabble-solver library in-process: - Registry: versioned DAWG load via dafsa.Load, keyed by (variant, dict_version), latest-per-variant; English / Russian / Эрудит handled uniformly. - Bag: own deterministic, seeded tile bag with Draw + Return (for exchanges), since the solver's self-play bag cannot return tiles. - Game: pure rules engine — deal, play/pass/exchange/resign, refill, per-move scoring, turn order, and end-condition detection (empty bag + empty rack, six scoreless turns, resignation) with end-game rack adjustment. - decode/ReplayBoard: dictionary-independent MoveRecords and board replay via scrabble.Apply (no internal/encoding), realising ARCHITECTURE §9.1. Wiring: go.work gains "replace scrabble-solver => ../scrabble-solver"; backend requires scrabble-solver (placeholder) and github.com/iliadenisov/dafsa directly. Both Go CI workflows clone the public solver sibling (master HEAD, no token) and set BACKEND_DICT_DIR. Docs: ARCHITECTURE §5/§14, TESTING engine layer, backend README, and PLAN refinements + deferred TODOs (publish/version solver; split engine vs dictionary generator).
This commit is contained in:
@@ -173,3 +173,59 @@ Open details: deployment target/host; dashboards; load expectations.
|
||||
at boot** (migrations at startup) — a deliberate contract change from
|
||||
Stage 0, documented in both READMEs. All code stays in the existing
|
||||
`backend` module under `internal/` (+ `cmd/jetgen`); `go.work` untouched.
|
||||
- **Stage 2** (interview + implementation):
|
||||
- Scope: `internal/engine` is a self-contained **library** (registry, bag,
|
||||
`Game` state machine, decode/replay). No `config`/`main`/`server` wiring this
|
||||
stage — there is no consumer yet; wiring lands in **Stage 3**, mirroring
|
||||
Stage 1's deferred handlers.
|
||||
- **Pure rules engine** (interview): the engine owns the in-memory `Game`,
|
||||
pure transitions (play/pass/exchange/resign + draw) **and end-condition
|
||||
detection**, including the standard **end-game rack-adjustment scoring** — a
|
||||
deliberate slice of Stage 3's "scoring/end-conditions" that the pure-engine
|
||||
boundary implies. Stage 3 keeps scheduling, the 24h timeout, persistence and
|
||||
GCG.
|
||||
- **Solver wiring**: `replace scrabble-solver => ../scrabble-solver` in
|
||||
`go.work`; `backend/go.mod` requires `scrabble-solver` (placeholder version,
|
||||
redirected by the replace) and `github.com/iliadenisov/dafsa` directly (for
|
||||
`dawg.Load`). CI clones the **public** solver repo at **master HEAD**
|
||||
anonymously into `../scrabble-solver` (no token); both Go workflows gained
|
||||
the step (the engine's untagged tests run under the integration workflow too)
|
||||
and set `BACKEND_DICT_DIR`.
|
||||
- **Dictionaries**: registry loads the committed DAWGs from a directory
|
||||
parameter; `dict_version` is an explicit string label; the latest version
|
||||
per variant is tracked. Smoke tests validate a known word per variant
|
||||
(English/Russian/Эрудит). **Эрудит is handled uniformly** — every real
|
||||
difference is already in `rules.Erudit()`; the move.go "single orientation
|
||||
per turn" note needs no special code (any single play is one-directional).
|
||||
- **Bag/blanks/exchange**: own deterministic `Bag` (Draw + Return) because
|
||||
`selfplay.Bag` cannot return tiles; exchange is legal only when the bag holds
|
||||
at least a rack and draws replacements before returning the swapped tiles. A
|
||||
blank is `Placement{Blank:true}` carrying its designated letter; the history
|
||||
keeps the concrete letter plus a blank flag (decoded via `Alphabet.Character`
|
||||
/ `Decode`). `ReplayBoard` reuses `scrabble.Apply`, so no `internal/encoding`
|
||||
dependency.
|
||||
- **Deviation from the approved plan**: `docs/FUNCTIONAL.md` (+`_ru`) was left
|
||||
unchanged. Stage 2 adds no user-visible behaviour; the variant, per-game
|
||||
dictionary and dictionary-independent-history user stories already live in
|
||||
Stages 3–4, so a "light touch" here would have duplicated or pre-empted them.
|
||||
|
||||
## Deferred TODOs (cross-stage)
|
||||
|
||||
- **TODO-1 — publish & version the solver.** Once `scrabble-solver` is stable,
|
||||
give it a real module URL and switch `backend` to a versioned dependency,
|
||||
dropping the `go.work` replace and the CI clone. Removes the floating
|
||||
`master` dependency accepted for now (Stage 2 interview).
|
||||
- **TODO-2 — split the solver into engine vs dictionary generator + versioned
|
||||
dictionary artifacts.** Owner's idea, with the caveats agreed at the Stage 2
|
||||
interview: the split is sound (build-time wordlist→DAWG vs runtime load have
|
||||
different lifecycles and shrink the runtime dependency surface), **but** the
|
||||
generator must pin the **same** `dafsa`/`alphabet` versions and alphabet
|
||||
definitions as the runtime engine or the on-disk format / letter indexing
|
||||
drifts and silently corrupts validation. For delivery prefer **Git LFS or an
|
||||
artifact store** (Gitea releases / OCI artifact / object storage) over a raw
|
||||
git submodule (the ~0.5–0.7 MB DAWGs are regenerated wholesale and bloat git
|
||||
history); pin by tag/hash for a reproducible startup set. A submodule/LFS pull
|
||||
is a **deploy-time** way to populate the directory, **not** the runtime
|
||||
dynamic-reload mechanism (Stage 9) — keep the `BACKEND_DICT_DIR` directory as
|
||||
the runtime contract: a new `.dawg` appears in it and is loaded with
|
||||
`dawg.Load`.
|
||||
|
||||
Reference in New Issue
Block a user