feat(game): official first-move tile draw + admin step-by-step replay
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 17s
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m19s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 17s
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m19s
Decide who moves first by the official rule: each seated player draws one tile, the one closest to "A" leads (a blank beats every letter), ties re-drawing until a single leader remains. Each draw uses honest per-draw crypto/rand entropy (not the deterministic bag seed), so the recorded draw — not a seed — is the only account of the outcome. The leader takes seat 0, so the engine and journal replay are unchanged. The draw is recorded with the game (game_setup_draws, migration 00013) for future tournaments, designed as a discrete "player N draws a tile" step. Friend/AI games draw at creation. Auto-match draws when the game opens, against a synthetic uuid.Nil opponent whose draw rows (NULL account_id) are back-filled to the real opponent on join — so the opener's seat is fixed up front and the existing open-game pre-move is preserved with no reseating. Admin /_gm/games/:id gains the recorded draw list and a simple step-by-step board replay (game.ReplayTimeline + a vanilla-JS stepper): a board with A-O/1-15 headers and highlighted premium squares, placed letters with their tile value as a subscript, rack panels around the board (seat 0 top, 1 bottom, 2 left, 3 right) with the current player highlighted, and a per-move log with the tiles drawn and the bag remainder. Docs: ARCHITECTURE §6/§9, FUNCTIONAL (+_ru), PRERELEASE (FM row), design spec.
This commit is contained in:
+22
-2
@@ -327,6 +327,21 @@ Key points:
|
||||
timed out while asleep. A game whose journal can no longer be replayed — a
|
||||
committed move made illegal by a later rule change — is instead closed as a
|
||||
**draw** (`aborted`) on the next open, never left unopenable (§9.1).
|
||||
- **First move (who goes first)**: decided by the official draw — each seated player
|
||||
draws one tile and the tile closest to "A" leads (a **blank supersedes all letters**);
|
||||
players tied for the best tile re-draw until a single leader remains. Each draw uses
|
||||
**fresh entropy** (`crypto/rand`), **not** the deterministic bag `seed`, so the draw is
|
||||
genuinely random and its **record** — not a seed — is the only account of the outcome. The
|
||||
leader takes **seat 0** (which moves first), the rest keeping their seating order, so the
|
||||
engine and journal replay are unchanged (seat 0 always leads). A directly-seated game
|
||||
(friend/AI) draws at creation. **Auto-match** draws when the game **opens**, with the
|
||||
not-yet-arrived opponent as a synthetic placeholder (`uuid.Nil`, whose draw rows are
|
||||
back-filled to the real opponent on join), so the opener's seat is fixed up front and they
|
||||
may make their opening move while waiting with no later reseating. The draw is **recorded**
|
||||
with the game (`game_setup_draws`, §9) and surfaced only in the admin console's
|
||||
step-by-step game replay — **not shown to players** today; it is kept for future
|
||||
tournaments, where it becomes a manual, per-tile external call. It is modelled as a discrete
|
||||
"player N draws a tile" step so that API is a thin driver over the same component.
|
||||
- **Players**: auto-match is always 2 players; friend games are 2–4 players.
|
||||
`backend` owns turn order and the bag for any player count. A resignation or
|
||||
timeout in a two-player game ends it with the other player winning. In a game
|
||||
@@ -615,7 +630,8 @@ in either direction (the enqueue excludes the caller's `BlockedWith` set);
|
||||
the `kind` admitting `robot`),
|
||||
`sessions` (revoke-only opaque-token hashes), the game tables
|
||||
`games` (carrying the `dropout_tiles` disposition column), `game_players`,
|
||||
`game_moves` (the move journal), `complaints`, `account_stats` and
|
||||
`game_moves` (the move journal), `game_setup_draws` (the first-move draw record, §6),
|
||||
`complaints`, `account_stats` and
|
||||
`account_best_move`, and the
|
||||
social/lobby tables `friendships` (the request/accept graph, its status admitting
|
||||
`declined`), `blocks`
|
||||
@@ -630,7 +646,11 @@ in either direction (the enqueue excludes the caller's `BlockedWith` set);
|
||||
Auto-match has no separate store: a game **awaiting an opponent** is an ordinary
|
||||
`games` row with status `open` and a single seated `game_players` row (the empty
|
||||
opponent seat is a null `account_id`, filled when a human or robot joins), plus an
|
||||
`open_deadline_at` stamp the reaper scans for robot substitution.
|
||||
`open_deadline_at` stamp the reaper scans for robot substitution. The **first-move draw**
|
||||
(§6) is recorded in `game_setup_draws` when the game opens; the synthetic opponent's rows
|
||||
carry a NULL `account_id` until a real opponent joins and back-fills them. The record is
|
||||
dictionary-independent — a decoded letter, a blank flag and the numeric draw rank — like
|
||||
the journal (§9.1), so it never depends on a dictionary or the solver's encoding.
|
||||
- **Active games are event-sourced.** A game is a `games` row (pinned
|
||||
`variant`/`dict_version`, bag `seed`, the per-game settings, and a denormalised
|
||||
turn cursor) plus an append-only, decoded move journal (`game_moves`); the live
|
||||
|
||||
Reference in New Issue
Block a user