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:
co-authored by
Claude Opus 5
parent
b90c0413d2
commit
517238a727
@@ -798,6 +798,44 @@ describe('Freight Agent operations (§6.3)', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('returns a cleared car to the yard EMPTY, not still loaded', () => {
|
||||
/**
|
||||
* REGRESSION, v0.8.1.0. `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 `origin` stamp and keeps `loaded` DELIBERATELY, because a train can
|
||||
* retire at a Division Point with freight still aboard (`advance.ts`). So every car the Freight
|
||||
* Agent cleared arrived in the Classification Yard still carrying a load that had already been
|
||||
* delivered and already paid for.
|
||||
*
|
||||
* It bites hardest on coaches: `passengersDetrained` takes `type === 'coach' && !loaded` out of
|
||||
* the Division Yard, and §2.2 refills the Division Yard from Classification — so a cleared
|
||||
* coach came back as stock that could not unload a passenger.
|
||||
*/
|
||||
const s = game();
|
||||
const coord = withFacility(s);
|
||||
const f = areaOf(s, 0).grid.get(coordKey(coord))!.facility!;
|
||||
f.allows = { outbound: false, inbound: true };
|
||||
f.inboundBox = [{ type: 'coach', loaded: true }];
|
||||
|
||||
const before = s.yards.classificationYard.length;
|
||||
assert.ok(applyIntent(s, 0, { type: 'localOps.choose', option: 'freightAgent' }).ok);
|
||||
assert.ok(
|
||||
applyIntent(s, 0, { type: 'freightAgent.clearInbound', at: coord, index: 0 }).ok,
|
||||
'the Freight Agent could not clear the red box',
|
||||
);
|
||||
|
||||
assert.equal(f.inboundBox.length, 0, 'the red box was not cleared');
|
||||
assert.equal(s.yards.classificationYard.length, before + 1, 'the car did not reach the yard');
|
||||
const arrived = s.yards.classificationYard[s.yards.classificationYard.length - 1]!;
|
||||
assert.equal(arrived.type, 'coach', 'the wrong car reached the yard');
|
||||
assert.equal(arrived.loaded, false, 'the cleared car arrived in the yard still loaded');
|
||||
// And a coach that arrives empty is stock a Porter can actually use again.
|
||||
assert.ok(
|
||||
s.yards.classificationYard.some((c) => c.type === 'coach' && !c.loaded),
|
||||
'no empty coach reached the common supply',
|
||||
);
|
||||
});
|
||||
|
||||
it('makes the Freight Agent option unavailable with nothing to operate (§6)', () => {
|
||||
// A player with no Facility cannot choose an option that has no possible follow-up.
|
||||
const s = game();
|
||||
|
||||
@@ -241,3 +241,45 @@ describe('a Freight Agent turn says what it did, to what, and where', () => {
|
||||
assert.match(plain.text, /\(4,-2\)/, `the fallback coordinate is not in X,Y order: ${plain.text}`);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* WHICH MAINLINE CARD, AND WHAT THE CARD IS CALLED.
|
||||
*
|
||||
* Reported 2026-09-22, playing ABS Signals: "In the history, it referred to it as Mainline card 7,
|
||||
* but didn't give the actual card type, which was plains. It should specify both. Note there were
|
||||
* two plains cards dealt in this hand." Both halves matter for that reason — the name informs, the
|
||||
* slot is the only thing telling two Plains apart.
|
||||
*/
|
||||
describe('the log names the Mainline card an Enhancement was built on', () => {
|
||||
const ctx = {
|
||||
mainlineAt: (node: number) => (node === 7 ? 'Plains' : node === 3 ? 'Heavy Grade' : null),
|
||||
enhancementName: (key: string) => (key === 'absSignals' ? 'ABS Signals' : null),
|
||||
};
|
||||
|
||||
it('gives the slot AND the card type', () => {
|
||||
const line = narrate({ type: 'enhancementPlaced', player: 0, key: 'absSignals', node: 7 } as never, ctx);
|
||||
assert.match(line.text, /Mainline card 7/, `the slot is gone: ${line.text}`);
|
||||
assert.match(line.text, /Plains/, `the card type is missing: ${line.text}`);
|
||||
});
|
||||
|
||||
it('calls the card by its printed name, not a split key', () => {
|
||||
const line = narrate({ type: 'enhancementPlaced', player: 0, key: 'absSignals', node: 7 } as never, ctx);
|
||||
assert.match(line.text, /ABS Signals/, `not the printed name: ${line.text}`);
|
||||
assert.ok(!/abs Signals/.test(line.text), `still splitting the key: ${line.text}`);
|
||||
});
|
||||
|
||||
it('still says something useful when the card cannot be resolved', () => {
|
||||
// No resolver at all — an engine test narrating events has no division to ask.
|
||||
const bare = narrate({ type: 'enhancementPlaced', player: 0, key: 'absSignals', node: 2 } as never, {});
|
||||
assert.match(bare.text, /Mainline card 2/, `the slot must survive with no resolver: ${bare.text}`);
|
||||
});
|
||||
|
||||
it('leaves a grid-square Enhancement naming its coordinate', () => {
|
||||
const line = narrate(
|
||||
{ type: 'enhancementPlaced', player: 0, key: 'smallYard', at: { row: -1, col: 2 } } as never,
|
||||
{ enhancementName: (k: string) => (k === 'smallYard' ? 'Small Yard' : null) },
|
||||
);
|
||||
assert.match(line.text, /Small Yard/, `not the printed name: ${line.text}`);
|
||||
assert.match(line.text, /\(2,-1\)/, `the square is gone or in the wrong order: ${line.text}`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5726,6 +5726,39 @@ describe('the Quickstart guide reaches the site', () => {
|
||||
assert.match(splash, /href="\.\/quickstart\.md"/, 'the splash page does not link the guide');
|
||||
});
|
||||
|
||||
it('brings the lobby doors back every time the lobby is shown', () => {
|
||||
/**
|
||||
* REPORTED 2026-09-22: "After leaving a game, I go back to the lobby. I see the games that I'm
|
||||
* in, and I have the ability to rejoin or forget. But what if I want to join a different game
|
||||
* or start a new one? How do I get back there?"
|
||||
*
|
||||
* `enterSeating` hides `#lb-choice-section`, and until v0.8.1.0 only the lobby's OWN leave
|
||||
* paths put it back. Leaving a RUNNING game is a third route — `main.ts` calls `runLobby` again
|
||||
* — so the section stayed hidden from the previous seating and the screen came back holding
|
||||
* nothing but "Games you are in", with the two doors on the page at `display:none` and no
|
||||
* control that could reveal them.
|
||||
*
|
||||
* Read out of the source, like the MIME table above: the fix is an ordering property of one
|
||||
* function, and there is no DOM harness here that drives `runLobby` end to end. What this pins
|
||||
* is that the reset exists inside `runLobby` and happens BEFORE the resume path re-enters
|
||||
* seating — reversing those two would hand a resuming browser the wrong screen.
|
||||
*/
|
||||
const src = readFileSync(join(root, 'src/web/lobby.ts'), 'utf8');
|
||||
const body = src.slice(src.indexOf('export function runLobby'));
|
||||
assert.ok(body.length > 0, 'runLobby is gone');
|
||||
|
||||
const reset = body.indexOf("$('lb-choice-section').hidden = false");
|
||||
assert.ok(reset > 0, 'runLobby never reveals the choice section, so leaving a game is a dead end');
|
||||
|
||||
const resume = body.indexOf('if (resume) enterSeating');
|
||||
assert.ok(resume > 0, 'the resume path is gone');
|
||||
assert.ok(reset < resume, 'the reset runs after the resume, which would show a seated browser the doors');
|
||||
|
||||
// And the seating section is put away with it, or the two would be on screen together.
|
||||
const seating = body.indexOf("$('lb-seating-section').hidden = true");
|
||||
assert.ok(seating > 0 && seating < resume, 'the seating section is not reset before resume');
|
||||
});
|
||||
|
||||
it('publishes everything the guide links, so "Where to read more" is not five dead links', () => {
|
||||
/**
|
||||
* v0.8.0.16 published the Quickstart alone. Its §8 links five further documents by relative
|
||||
|
||||
Reference in New Issue
Block a user