Stage 3: game domain (lifecycle, journal+cache, hint, word-check, GCG, stats)
internal/game drives the engine over a single match and owns everything the
engine does not: event-sourced persistence (a games row + an append-only decoded
move journal, with the live engine.Game kept warm in a cache and rebuilt by
replay on a miss), the play/pass/exchange/resign transitions with
validate-at-submit scoring, an unlimited score/legality preview, the hint
(per-game allowance + profile wallet), the word-check tool with complaint
capture, per-player game state, history and GCG export (Poslfit dialect), and a
background turn-timeout sweeper that auto-resigns overdue turns honouring each
player's daily away window. Like Stages 1-2 it is a service/store layer with no
HTTP; the gateway surface lands in Stage 6.
Engine: additive decoded domain API (Direction, SubmitPlay/SubmitExchange/
EvaluatePlay/HintView/Hand, MoveRecord.{Dir,MainRow,MainCol}, Registry.Lookup,
ParseVariant) so internal/game never imports scrabble-solver; and a Resign fix
so the resigner keeps their score yet never wins (the other player wins a
two-player game). Timeout reuses Resign.
Persistence: migration 00002 adds games, game_players, game_moves, complaints,
account_stats and extends accounts with away_start/away_end/hint_balance; go-jet
regenerated. account gained SpendHint. Config adds BACKEND_DICT_DIR (required),
BACKEND_DICT_VERSION, BACKEND_GAME_TIMEOUT_SWEEP_INTERVAL, BACKEND_GAME_CACHE_TTL;
main loads the registry at boot (hard dependency) and starts the sweeper.
Tests: engine resign + decoded-API tests; game unit tests (GCG, away-window
boundaries, hint budget, cache, keyed mutex, payload); inttest integration
(lifecycle, replay equivalence, timeout sweep with away grace, resign stats,
hint policy, word-check/complaint, per-game-lock). Docs (PLAN, ARCHITECTURE,
FUNCTIONAL +_ru, TESTING, README) updated.
This commit is contained in:
@@ -36,7 +36,7 @@ independent (see ARCHITECTURE §9.1).
|
||||
| 0 | Scaffolding (go.work, backend skeleton, docs, CI) | **done** |
|
||||
| 1 | Backend foundation (config, server, Postgres+goose, sessions, accounts) | **done** |
|
||||
| 2 | Engine package over scrabble-solver | **done** |
|
||||
| 3 | Game domain (lifecycle, rules, hint, word-check, history+GCG, stats) | todo |
|
||||
| 3 | Game domain (lifecycle, rules, hint, word-check, history+GCG, stats) | **done** |
|
||||
| 4 | Lobby & social (matchmaking, friends, block, chat, profile, nudge) | todo |
|
||||
| 5 | Robot opponent | todo |
|
||||
| 6 | Gateway edge (Connect/FB, platform auth, sessions, push bridge, admin) | todo |
|
||||
@@ -208,6 +208,58 @@ Open details: deployment target/host; dashboards; load expectations.
|
||||
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.
|
||||
- **Stage 3** (interview + implementation):
|
||||
- Scope, as in Stages 1–2: **domain service/store layer + engine wiring, no
|
||||
HTTP** (`internal/game`). The gateway↔backend REST surface lands in Stage 6;
|
||||
the only active driver this stage is a background turn-timeout sweeper started
|
||||
from `main`. The robot (Stage 5) will consume the same service API.
|
||||
- **Persistence = event-sourcing + warm cache** (interview): durable state is
|
||||
the `games` row plus an append-only decoded move journal (`game_moves`); the
|
||||
live position is an `engine.Game` kept in an in-memory cache with a ~24h idle
|
||||
TTL and rebuilt by replaying the journal on a miss (the seeded bag makes
|
||||
replay exact). Each game is serialised by a per-game mutex; a persistence
|
||||
failure evicts the live game so the next access rebuilds. §9 reworded from
|
||||
"stored structurally" to this model.
|
||||
- **Resign/timeout split** (interview): 2-player resign/timeout only this stage
|
||||
(the other player wins); multiplayer drop-out-and-continue + resigned-tiles
|
||||
disposition deferred to Stage 4. Per-game **turn-timeout duration** setting
|
||||
(5/10/15/30 min, 1/2/3/6/12/24 h; default 24 h) and a per-user **away window**
|
||||
(`accounts.away_start/away_end`, default 00:00–07:00 local, honoured by the
|
||||
sweeper with midnight-cross handling) added now; profile editing of the away
|
||||
window is Stage 4 and the robot's sleep (Stage 5) reuses it.
|
||||
- **Engine `Resign` fix** (interview, in `internal/engine`): the resigner keeps
|
||||
their accumulated score (no end-game rack adjustment) and never wins; `winner`
|
||||
excludes the resigner, so a two-player resign/timeout gives the win to the
|
||||
other player regardless of score. Timeout reuses `Resign`, so the game domain
|
||||
needs no winner override.
|
||||
- **Additive engine domain API**: `Direction`, `Game.SubmitPlay/SubmitExchange/
|
||||
EvaluatePlay/HintView/Hand`, `MoveRecord.{Dir,MainRow,MainCol}`,
|
||||
`Registry.Lookup`, `ParseVariant` — so `internal/game` never imports
|
||||
`scrabble-solver` (keeps the §5 single-importer invariant).
|
||||
- **Create = atomic with seats** (interview): `Create` seats all accounts and
|
||||
starts; lobby seat-filling is Stage 4. **Sweeper = periodic goroutine**
|
||||
(interview; default 60 s, `BACKEND_GAME_TIMEOUT_SWEEP_INTERVAL`).
|
||||
- **Hint = settings + wallet** (interview): per-game `hints_allowed` +
|
||||
`hints_per_player`, plus a profile wallet `accounts.hint_balance` (spent after
|
||||
the allowance; purchases later). Category defaults (random 1 / tournament 0 /
|
||||
friendly 1-or-0) are the caller's job (lobby/tournaments).
|
||||
- **Stats** (interview): `account_stats` with **`draws`** added beyond §9's
|
||||
wins/losses; `max_word_points` = best single **move** score; ties draw,
|
||||
resign/timeout is a loss, guests get no stats.
|
||||
- **Complaint** (interview): full payload with `game_id`; word-check is scoped
|
||||
to the game's pinned `(variant, dict_version)`. Stage 9 owns the resolution
|
||||
lifecycle, so the `status` column carries no value CHECK yet.
|
||||
- **GCG** (interview): standard Poslfit dialect (UTF-8, `#player`/`#lexicon`
|
||||
pragmas, `8G`/`H8` coordinates, lower-case blanks, `.` pass-throughs, `-TILES`
|
||||
exchange) plus `#note` lines for resign/timeout; derived from the journal, so
|
||||
dictionary-independent.
|
||||
- **Engine wiring + config**: `main` loads the registry (`engine.Open`, a hard
|
||||
boot dependency like migrations) and starts the sweeper. New config:
|
||||
`BACKEND_DICT_DIR` (required), `BACKEND_DICT_VERSION` (default `v1`),
|
||||
`BACKEND_GAME_TIMEOUT_SWEEP_INTERVAL` (60 s), `BACKEND_GAME_CACHE_TTL` (24 h).
|
||||
No CI change — both Go workflows already clone the solver sibling and export
|
||||
`BACKEND_DICT_DIR`. `accounts` gained `away_start`/`away_end`/`hint_balance`
|
||||
and the `account` package gained `SpendHint` (it owns its table).
|
||||
|
||||
## Deferred TODOs (cross-stage)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user