v0.8.0.17 — four things the game knew and the screen did not say

All four reported from a table on Day 1 of v0.8.0.16, and all the same shape.

ABS SIGNALS COULD ONLY BE PLAYED ON ONE MAINLINE CARD, while its tooltip said
"any Mainline card". The engine was never wrong: check accepts any node whose
kind is mainline and legalActions filters by check, so all of them were legal.
The failure was the LABEL — describeIntent named i.placement and never i.node,
so every placement described itself as plain "play ABS Signals", and the action
list drops duplicate labels. All but the lowest-index node were discarded before
the menu saw them. This is the THIRD time that trap has fired and the file
documents the other two three lines apart: a turnout's two rotations, and three
Department discards. Same fix — name what distinguishes them.

The card is also called what the card face calls it. prettyKey rendered
absSignals as "Abs Signals" beside a tooltip saying ABS, an acronym no
key-splitter can recover, so the authored names now win. Three of those names
were transcribed in sentence case and were CORRECTED rather than adopted: the
repository says "Yard Office" 36 times against "Yard office" twice. A lookup
that imports its own source's typos is the drift it exists to prevent.

NOTHING ON A MAINLINE CARD SHOWED WHAT WAS STANDING ON IT. Played, ABS left no
mark and you found out by hovering — the same complaint the Heavy Grade wedge
answered, and it matters more here because ABS decides whether a second train on
that card is safe. It draws a signal mast with a lit lamp now; a signal is the
literal object and needs no room for words, which is what lets it sit clear of a
name as long as "Uncontrolled Siding" on a 152px cell. The Mainline modifiers
draw as BRK, AIR and HLP. Realignment is deliberately not among them: reduce
takes the `became` branch and changes node.card, so a realigned Trestle IS an
Uncontrolled Siding afterwards. Asserted, so the absence reads as a finding.

A FREIGHT AGENT TURN SAID A CAR MOVED WHEN NONE HAD. Three faults behind one
line. It asserted an outcome, where §6.3 requires no action and the bot declines
deliberately — unjamming a healthy box destroys a load that cost a whole action
to stock. An idle Agent was then silent, which read as a dropped turn; a new
freightAgentIdled event says so and why, reducing to nothing exactly like
switchingEnded. And the work named a coordinate rather than the industry, though
a `place` helper has existed for precisely that since the switching lines moved
to it. "Loaded a loaded boxcar INTO the green Outbound box at the Freight House",
with the direction in capitals because to-or-from was the question asked.

THE LOG AND THE ACTION MENU SPELLED THE SAME SQUARE DIFFERENTLY. view.ts wrote
(col,row) — X,Y, east/west then north/south — with a comment saying why;
narrate.ts wrote the internal storage order with no comment at all. So the menu
offered a move to "(1,-1)" and the log reported it at "(-1,1)", side by side.
Pinned by a test that renders one square through BOTH describers and compares
them to each other: a test written against either file alone would have passed.

THE DOCUMENTATION IS REACHABLE FROM A RUNNING GAME, AND ALL OF IT IS PUBLISHED.
v0.8.0.16 published the Quickstart and nothing it points at — its §8 links five
documents by relative path and every one 404'd on the package, verified against
the running container. The build publishes the full set, and the test reads the
links OUT OF the guide rather than listing them. They are linked from the This
Game card, where reference already lives, rather than the header that must not
wrap; no mode awareness is needed, because solitaire and multiplayer are the
same page on the same origin.

THE REFERENCES DROPPED THE VERSION FROM THEIR NAMES. Four described v0.8.0.16
and had since the v0.8.0.15 audit; the v0.4.5 was the prototype edition they
were first written against, kept only because 36 citations pointed at it — and
it read as documentation five minor versions stale. They are quickstart.md,
rules.md, home-deck.md, mainline-deck.md and components.md now, kept current
with each release rather than published as editions. Two errors surfaced while
checking them against this release, which is the argument for doing it:
home-deck.md filed ABS Signals under Enhancements "played into your district"
that "change what a square does" — it does neither, this release's bug written
down — and mainline-deck.md, which lists everything playable onto a Mainline
card, never mentioned it at all.

1010 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-21 05:53:52 -04:00
co-authored by Claude Opus 5
parent dc31555625
commit b90c0413d2
27 changed files with 819 additions and 94 deletions
+38 -5
View File
@@ -74,7 +74,16 @@ export function carsLabel(cars: RollingStock[]): string {
return cars.map(carLabel).join(', ');
}
const at = (c: GridCoord): string => `(${c.row},${c.col})`;
/**
* X,Y — EAST/WEST THEN NORTH/SOUTH, exactly as `view.ts` writes it, and NOT the internal row/col
* storage order.
*
* These two disagreed until 2026-09-21: the action menu said "(1,-1)" and the log said "(-1,1)" for
* the same square, side by side on the same screen. `view.ts` carried the comment explaining why
* the display order is X,Y; this one had no comment at all and was simply the storage order
* reaching the page. Jesse's call — the log and the action menu spell a square the same way.
*/
const at = (c: GridCoord): string => `(${c.col},${c.row})`;
const BOX_NAMES = ['MEN', 'AT', 'WORK'] as const;
const boxName = (i: number): string => BOX_NAMES[i] ?? `box ${i}`;
@@ -188,7 +197,15 @@ export function narrate(e: GameEvent, ctx: NarrateContext = {}): Narration {
? 'Chose to SWITCH — six Moves to shunt cars around the yard. Watch the crew chip on the grid: it carries its consist with it, and cars it passes over are coupled automatically.'
: e.option === 'draw'
? 'Chose to DRAW a card'
: 'Chose FREIGHT AGENT work — one car moved to or from a facility',
/**
* SAYS WHAT MAY BE DONE, NOT WHAT WAS. This read "one car moved to or from a
* facility" — an assertion — and §6.3 requires no action at all, so when the Freight
* Agent went idle the log claimed a car had moved and then fell silent about which.
* Reported from a table on Day 1 Stage 3 of v0.8.0.16. The work itself is narrated by
* `stockToOutbound`, `inboundCleared` and `facilityUnjammed`, each naming the car and
* the industry; an idle Agent is narrated by `freightAgentIdled`.
*/
: 'Chose FREIGHT AGENT work — may stock a green Outbound box, clear a red Inbound one, or free a jam',
};
case 'trayMoved':
// `via` rides on the event only when there was another legal route to the same square
@@ -466,23 +483,39 @@ export function narrate(e: GameEvent, ctx: NarrateContext = {}): Narration {
};
// -- freight agent
/**
* THE INDUSTRY, NOT THE COORDINATE — `place` over `at`, for the reason its own comment gives:
* "(-1,1)" is the grid's notation and means nothing at a table where people are looking at
* cards. The switching lines were moved to it and these three were missed, so a Freight Agent
* turn was the one place the log still spoke in coordinates. Asked directly from a table on
* Day 1 Stage 3 of v0.8.0.16: "can we tell what car, what facility and whether it was to or
* from." The car and the direction were already here; the facility was not.
*/
case 'stockToOutbound':
return {
tone: 'plain',
where: e.at,
text: `Freight Agent put a ${carLabel(e.stock)} into the green Outbound box at ${at(e.at)}`,
text: `Freight Agent loaded a ${carLabel(e.stock)} INTO the green Outbound box at ${place(e.player, e.at)}`,
};
case 'inboundCleared':
return {
tone: 'plain',
where: e.at,
text: `Freight Agent cleared a ${carLabel(e.stock)} from the red Inbound box at ${at(e.at)}`,
text: `Freight Agent cleared a ${carLabel(e.stock)} OUT of the red Inbound box at ${place(e.player, e.at)}`,
};
case 'facilityUnjammed':
return {
tone: 'bad',
text: `UNJAMMED ${place(e.player, e.at)} — pulled a ${carLabel(e.stock)} out of ${e.from} to free the facility`,
where: e.at,
text: `UNJAMMED ${at(e.at)} — pulled a ${carLabel(e.stock)} out of ${e.from} to free the facility`,
};
case 'freightAgentIdled':
return {
tone: 'quiet',
text:
'Freight Agent found nothing worth doing — no green box could be stocked, no red box needed ' +
'clearing, and no load was jammed. §6.3 requires no action, and unjamming a healthy box ' +
'would destroy a load that cost a whole action to stock.',
};
// -- trains