the Roster Pass: every train at the Office visible

Builds "The Roster Pass" / "Two Trains, One Card" (station display for
multiple trains at one Office). The Office is the one square where
more than one train may legally stand at once (one per A/D track), and
CellView.train had room for exactly one — a second train at a busy
Station was counted in the old A/D pips and never drawn.

CellView.train -> CellView.trains: TrainView[], seat-filtered and
collecting every match rather than the first (fixes a latent
cross-district leak in the process: trainOnCard never checked seat).
The A/D pips are replaced with one roster chip per A/D track, always,
free or occupied; clicking a chip sets selectedCrew, wiring the board
and the action panel to the same value.

standingWest moves from CrewTray to TrackCard: two trays sharing one
Office card need one shared split, not one each, and there is no such
thing as "west of one particular A/D track". No save migration — Save
replays through the engine — and a stale value on an emptied card is
inert because nothing reads a split with no train standing there.

The Division map's Office cell is now sized by A/D capacity rather
than occupancy, so it holds still as trains arrive and leave; chips
lay into fixed slots instead of centre-spreading onto the Limits cards
either side.

584 tests, 0 failures. Verified end-to-end against the built app and a
direct render of a 4-train Terminal (screenshotted).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAt2YCXgd5qCjBcF2x34aK
This commit is contained in:
Jesse
2026-08-19 17:32:17 -04:00
co-authored by Claude Sonnet 5
parent fbaa3d4147
commit 37b1e5b969
18 changed files with 506 additions and 187 deletions
+44 -3
View File
@@ -21,9 +21,10 @@ page as `v0.1.0 · <sha> · <date>`, so what is deployed can always be identifie
## 0.4.8 — 2026-08-19
Three reports from the same game, all about squares: which ones a card may go on, which one a button
in the action list means, and — when there is more than one legal road to the same square — which
one a train actually takes.
Four reports from the same game, all about squares: which ones a card may go on, which one a button
in the action list means, when there is more than one legal road to the same square which one a
train actually takes, and — when more than one train is standing at the same Office — which is
which.
### The Limits bound the district, not just the Running Track
@@ -168,6 +169,46 @@ built a district where the discarded route would have mattered. 400 full games (
ablation flag to run at all and this is not a bot heuristic, so paired A/B was not applicable here;
the enumeration-based argument above is the actual evidence.
### The Roster Pass — every train at the Office visible
**The Office is the one square where more than one train may legally stand at once** — it has
several A/D tracks, each holding one — and the board drew it as if only one ever could. Two trays
at a Station couple onto the identical `officeCoord`; `trainOnCard()` returned on the first match
and `CellView.train` had room for exactly one, so a second train was counted in the old A/D pips
and never drawn at all. Reported directly, and analysed in "Two Trains, One Card": the pips and the
picture were reading different fields.
**`CellView.train` is now `CellView.trains: TrainView[]`** — every train standing on the card, each
carrying its own `trayId`. The A/D pips are gone; in their place is one roster chip per A/D track,
always (`adTracks` of them, not one per train), free tracks reading a dashed, dimmed "free" rather
than vanishing. The selected train's chip is lit the same amber the crew strip and action buttons
use, and its consist is the one drawn at the rail — clicking any occupied chip sets `selectedCrew`,
the same value the "Which train are you switching?" picker already wrote, so the board and the
action panel drive one value in both directions.
**`standingWest` moved from `CrewTray` to `TrackCard`.** The split still means what it always meant
— west/east of the row of standing cars — and it is still only meaningful while an engine stands on
the card (an unattended cut has no near or far side; that reasoning did not change). What forced the
move: two trays sharing one Office card could otherwise hold two different splits of the identical
row, and there is no such thing as "west of one particular A/D track" — a cut lies west or east of
the *whole block* of A/D tracks, so there has to be exactly one number for the card to carry. No
save migration: `Save = {seed, history, rules?}` replays through the engine rather than being
loaded, and a stale value on an emptied card is inert because nothing reads a split with no train
standing there.
**The Division map stopped drawing trains on their neighbours.** The Office's Running Track cell was
a fixed 78px regardless of how many A/D tracks it had, so two chips centre-spread wider than the
cell and spilled onto the Limits cards either side. The cell is now sized by **capacity**
(`max(78, adTracks·54+12)`), not by how many tracks are occupied — sized by occupancy instead, the
board would have shifted the East Division Point sideways every time a train arrived or left — and
chips are laid into fixed slots, one per A/D track, so none can ever overhang the cell.
**Verified:** two trays at a Station are both drawn, each with its own chip; a Terminal's four
tracks all fit; a bare cut with a stale `standingWest` still draws left-aligned; every roster chip's
x-extent lies inside its Office cell at full occupancy; and a packed replay row from before this
feature — a lone train object, not an array — rehydrates into a one-train roster rather than
throwing. 584 tests, 0 failures.
### Also
- The three published replays were re-recorded. A save is a seed and its intents, so tightening a