v0.8.0 — the board replays what everyone else did, instead of arriving rearranged

TODO #13, #15 and #18 — Gitea#20 steps 2-4 pointed at a seated player's own screen.
Every accepted intent, and every automatic phase that does anything, becomes an
ordered presentation step. A bot's whole switching turn used to land in one push;
now it arrives as a run of steps, the district panel follows whoever is acting,
and a [N behind] … [Skip] row says how far the board is from the game.

Solitaire runs the same path — one collector inside submit(), which both session
kinds already funnel through — which is where its automatic phases finally get a
visible beat.

Dwell is assigned by kind: switching holds the screen, turn bookkeeping costs
nothing, and the clock turning over earns the beat. Tunable per viewer without a
rebuild, and off entirely at pace 0.

Also: switching was the one class of action logging unattributed, and now names
its train. Reasoning, measurements and the three things that turned out wrong are
in CHANGELOG.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X6cF1iYvJ1kNmzYBzu4QX6
This commit is contained in:
Jesse.Markowitz
2026-09-09 15:31:46 -04:00
co-authored by Claude Opus 5
parent 312e0301e0
commit 02289e94b8
25 changed files with 2669 additions and 145 deletions
+4 -3
View File
@@ -37,9 +37,10 @@ export type GameEvent =
* more than one legal route to `to`, so the history can say which one ran rather than leaving a
* choice the player made invisible in their own log.
*/
| { type: 'trayMoved'; trayId: TrayId; from: GridCoord; to: GridCoord; movesRemaining: number; facing?: 'n' | 's' | 'e' | 'w'; via?: GridCoord }
| { type: 'trayMoved'; player: PlayerIndex; trayId: TrayId; from: GridCoord; to: GridCoord; movesRemaining: number; facing?: 'n' | 's' | 'e' | 'w'; via?: GridCoord }
| {
type: 'carsCoupled';
player: PlayerIndex;
trayId: TrayId;
at: GridCoord;
stock: RollingStock[];
@@ -71,8 +72,8 @@ export type GameEvent =
*/
recoupled?: { at: GridCoord; stock: RollingStock[] };
}
| { type: 'carsDropped'; trayId: TrayId; at: GridCoord; stock: RollingStock[]; fromNose?: boolean }
| { type: 'consistSorted'; trayId: TrayId; at: GridCoord; before: RollingStock[]; after: RollingStock[] }
| { type: 'carsDropped'; player: PlayerIndex; trayId: TrayId; at: GridCoord; stock: RollingStock[]; fromNose?: boolean }
| { type: 'consistSorted'; player: PlayerIndex; trayId: TrayId; at: GridCoord; before: RollingStock[]; after: RollingStock[] }
| { type: 'cardDrawn'; player: PlayerIndex; source: 'homeOffice' | 'department'; slot?: number; cardId: CardId }
/**
* §6.2 — the Home Office deck ran out, so the Salvage Yard and all three Department decks were