v0.1.0 — undo, the crew's reach on the board, workers on the card, and four engine bugs

Fifteen items from two playtest sessions. Three that read as drawing faults were engine
bugs: cars could be added to a train that was not being made up (50 offers in 8 games),
the make-up panel merged two trains and could couple a car to the wrong one, and an
Office upgrade silently deleted what a Modifier had added. A fourth was a sentinel
inside a coordinate's own value range — a Mainline placement travelling as row -1, which
is an ordinary district row.

Trains are now drawn the way they stand: west on the left, nose toward the way the engine
faces, on both the district card and the Division chip. Undo steps back through the game
by replaying the save without its last intent. The switching walk keeps its rejections, so
the board can say why a square is not offered. Laborers and Porters are on the card, and
the rule that a district only grows outwards is finally written down.

Versions start here: third digit for fixes, second for a feature set, 1.0 for a release.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GgtkX8JnvKa8y2tuJ8aQf4
This commit is contained in:
Jesse
2026-08-08 05:23:17 -04:00
co-authored by Claude Opus 5
parent f4c0f49604
commit 5d825b97d2
20 changed files with 1604 additions and 191 deletions
+41 -7
View File
@@ -17,7 +17,7 @@
import { adTrackCount, coordKey } from '../engine/state.ts';
import type { GameState, GridCoord, RollingStock, TrayId } from '../engine/state.ts';
import { canAdvanceLoad, canStartLoad, facilityCarType, laborersLeft, portersLeft } from '../engine/apply.ts';
import { canAdvanceLoad, canStartLoad, facilityCarType, laborersLeft, movesFor, portersLeft } from '../engine/apply.ts';
import type { GameEvent } from '../engine/events.ts';
// ---------------------------------------------------------------------------
@@ -212,12 +212,15 @@ export function narrate(e: GameEvent, ctx: NarrateContext = {}): Narration {
tone: 'good',
text: `Extra X${e.trainNumber} played — it is NOT scheduled; it runs once as soon as a Crew Tray frees up, then its card is gone`,
};
case 'enhancementPlaced':
return {
tone: 'good',
where: e.at,
text: `ENHANCEMENT built: ${e.key.replace(/([A-Z])/g, ' $1')} at ${at(e.at)}`,
};
case 'enhancementPlaced': {
const name = e.key.replace(/([A-Z])/g, ' $1');
// Out on the Mainline is not a square in anyone's district, so it is named rather than
// given a coordinate the Office Area does not have.
if (e.at === undefined) {
return { tone: 'good', text: `ENHANCEMENT built: ${name} on Mainline card ${e.node}` };
}
return { tone: 'good', where: e.at, text: `ENHANCEMENT built: ${name} at ${at(e.at)}` };
}
case 'secondSectionOrdered':
return {
tone: 'bad',
@@ -491,6 +494,37 @@ export function impediments(s: GameState, player = 0): Impediment[] {
}
}
/**
* WHY THE CREW CANNOT GET THERE.
*
* The panel is called "why nothing is moving" and covered everything except movement: jammed
* facilities, held trains, a full Office. A crew standing one card short of an industry it cannot
* enter had nothing here at all — reported as trains being blocked "in certain conditions", with
* no way to find out which.
*
* Only while switching, and only the cards actually in the crew's way: `movesFor` reports the
* squares the movement walk reached and refused, not every square on the board.
*/
if (s.clock.phase === 'localOps' && s.turn.option === 'switch' && s.turn.movesRemaining > 0) {
for (const [id, tray] of s.trays) {
if (tray.position.at !== 'grid' || tray.position.owner !== player) continue;
const { blocked } = movesFor(s, player, id);
for (const b of blocked) {
// A turnout is not an obstruction — a train runs through one all day and simply may not
// STOP on it. Listing every one would bury the four reasons that are genuinely in the way;
// the board says it on the card instead.
if (b.kind === 'noStopping') continue;
out.push({
where: `${tray.trainNumber === null ? 'crew' : `Train ${tray.trainNumber}`} → (${b.coord.row},${b.coord.col})`,
why: b.why,
// Not "stuck": these are the shape of the district and of the other trains in it, which is
// the puzzle rather than a fault. Amber, not red.
severity: 'waiting',
});
}
}
}
// Trains held for want of a Crew Tray (§7) — the scarcity mechanic, made visible.
const due = s.timetable[s.clock.stage - 1];
if (due !== null && due !== undefined && s.freeTrays.length === 0) {