v0.8.1.0 — the car comes back empty, and the lobby lets you leave

A second-digit bump, deliberately. 0.8.1 had been reserved for the seatless
display table; that work is getting more thought, and this table pass over
v0.8.0.17 earned the number on its own. Six reports: one was a rules question,
one a wording complaint with a real bug underneath, four straightforward.

A CAR CLEARED FROM A RED INBOUND BOX CAME BACK STILL LOADED. Reported as
wording — "technically accurate but doesn't make any sense" — and the wording
was the visible half. `inboundCleared` pushed `pooled(e.stock)` under a comment
reading "a car back in a yard is back in the common supply, carrying nothing",
and `pooled` does not do that: it strips the load's origin stamp and keeps
`loaded` ON PURPOSE, because a train can retire at a Division Point with freight
aboard. So the comment described an intention the call never carried out, and
every car the Freight Agent cleared reached the Classification Yard carrying a
load already delivered and already paid for.

It bites hardest on coaches: `passengersDetrained` takes `coach && !loaded` out
of the Division Yard and §2.2 refills that yard from Classification, so a
cleared coach came back as stock that could never unload another passenger.
Measured over five three-Day solitaire games: 18 loaded coaches in
Classification against 6 empty. The red box is where a journey ENDS; clearing it
sends the passengers out of the station, or the delivered load into the
industry, and returns the CAR, empty. The option says that now instead of
describing the counter that moves.

SCOPED TO THE RED BOX ON PURPOSE. `retireTrain` also returns loaded cars and is
left alone: that is what `pooled`'s own documentation describes, and a loaded
car in a yard is pre-loaded cargo rather than dead stock — it can be made up and
delivered, and a loaded coach can still detrain. Only the red box's contents had
already finished their journey.

GAMES IN PROGRESS DO RESUME, MEASURED RATHER THAN ARGUED. Yard contents change,
so the worry was real. All twelve saves on the test server were pulled and
replayed through `tryResumeSession` — the server's own boot check — against this
build. Six resume, six refuse, and the six refusals are the SAME six, at the
same moves, with the same codes, that 0.8.0.17 already logged. Nothing new was
stranded. That pre-install replay is a better check than reading the next boot
log, because it answers before the install rather than after.

THE HISTORY SAID "Mainline card 7" and left the reader to remember what card 7
was — with two Plains dealt, which is why the slot is kept beside the name
rather than replaced by it. A second fault sat one word to its left and nobody
reported it: the name was built as `e.key.replace(/([A-Z])/g, ' $1')`, so
`absSignals` printed as "abs Signals" while the action list directly above said
"ABS Signals". `narrate` takes `mainlineAt` and `enhancementName` beside the
`facilityAt` it already had, and `simpleCardName` is exported so the log reads
the same table the buttons do. `mainlineModified` had both faults and is fixed
with it.

LEAVING A RUNNING GAME WAS A DEAD END. `enterSeating` hides the choice section
and only the lobby's own two leave paths put it back; leaving a running game is
a third route, so the lobby came back holding nothing but "Games you are in"
with both doors on the page at display:none and no control able to reveal them.
Reset in `runLobby`, which is the one function every route onto that screen goes
through — which is exactly why the two paths that did it themselves missed a
third.

THE LOBBY'S ACTION BUTTONS CARRY THE BOARD'S AMBER. A list of the actions rather
than `#lobby button`: the settings form under Create is a field of inputs, and
amber on all of it would say everything is a move and so say nothing. A disabled
Start game drops back to chrome.

THE DEPARTMENT REFILL IS A RULE AND IS NOW WRITTEN DOWN. §6.2 — "if any of the
Department decks is empty, draw a Home Office card and place it in the empty
spot" — firing only when the draw actually empties the pile. Kept as implemented
(Jesse's ruling) and stated in rules.md and home-deck.md, neither of which had
ever mentioned it. A rule implemented from the prototype and never written down
is a rule that surprises the table.

1016 fast tests and 35 sim tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUizFYCMHRWhbWwXhp7WPR
This commit is contained in:
Jesse.Markowitz
2026-09-22 21:20:52 -04:00
co-authored by Claude Opus 5
parent b90c0413d2
commit 517238a727
17 changed files with 385 additions and 25 deletions
+24 -1
View File
@@ -1150,8 +1150,20 @@ export function describeIntent(s: GameState, i: Intent): string {
? 'the Revenue was paid when the passengers detrained'
: 'the Revenue was paid when the load reached the box';
const frees = f?.kind === 'passenger' ? 'more passengers can detrain here' : 'another car can be unloaded here';
/**
* SAY WHAT HAPPENS, NOT WHAT MOVES (Jesse, 2026-09-22). This read "send the loaded coach in
* the red Inbound box to the Classification Yard", which is true of the counters and
* nonsense as a sentence — it sounds like shipping a coachful of people off to a siding.
* What the red box holds is a journey that has ENDED: the passengers are at the station, or
* the load is at the industry, and both were paid for on arrival. Clearing it sends them on
* their way and returns the CAR to the common supply, empty.
*/
const what =
f?.kind === 'passenger'
? `let the passengers out of the station — the ${car ? carLabel(car) : 'coach'} in the red Inbound box at ${at(i.at)} goes back to the Classification Yard empty`
: `send the delivered load into the industry — the ${car ? carLabel(car) : 'car'} in the red Inbound box at ${at(i.at)} goes back to the Classification Yard empty`;
return (
`send the ${car ? carLabel(car) : 'car'} in the red Inbound box at ${at(i.at)} to the Classification Yard` +
`${what}` +
` — pays nothing (${paid}); it frees${full ? ' the last' : ' a'} slot so ${frees}`
);
}
@@ -1935,6 +1947,17 @@ export function cardName(s: GameState, id: string): string {
* Built from the content tables rather than restated, so a card renamed there is renamed here and
* the two cannot drift — which is the whole argument of TODO #15a, applied to names.
*/
/**
* The printed name of a card that has one, by its content key, or null.
*
* Exported so the LOG can reach the same table the buttons do. `narrate.ts` rendered its own name
* from the key (`absSignals` → "abs Signals") and so disagreed with the action list one line above
* it; it takes this through its `enhancementName` resolver now.
*/
export function simpleCardName(key: string): string | null {
return SIMPLE_CARD_NAMES.get(key) ?? null;
}
const SIMPLE_CARD_NAMES: ReadonlyMap<string, string> = new Map(
[
...SPACE_USE_CARDS,