v0.7.1 — a caboose is not a load, a Day that says it ended, and a train you may throw away

Four issues off the Gitea tracker, all of them things a player saw at the board. Reasoning for
every item, and what was verified how: CHANGELOG.md.

- Gitea#8: X22 Pee-Dee refused every caboose, including the one it was made up with, so setting it
  out stranded the train. All six cabooses are minted loaded because §2.2's "coloured is loaded,
  white is empty" doubles as a piece count in the supply table; one read of the flag took that
  literally. A caboose carries the crew, not freight, so it is never a load.
- Gitea#10: a Day turns over inside the phases that run themselves, so it passes between one click
  and the next — and both transient signals fade before a player reading the board notices. A modal
  stops and waits, carrying the standings, the Days left and the combined target. Suppressed on the
  first frame, on Undo stepping back across a rollover, and on the Day the game ends.
- Gitea#9, which SUPERSEDES Gitea#6 from three days ago: a Timetabled train may be tossed face-up
  to a Department slot, where a rival may pick it up — the second half of the ruling needed no code,
  since that is where every discard already goes. An Extra still may not. A New Game setting on this
  line (discardTimetabled, on by default), the plain rule on the 0.4.9 line.
- Gitea#2 is not an engine bug: the rules are implemented exactly, and running the coach pool dry is
  Jesse's ruling to keep — "part of the strategy". What was wrong is that the game said nothing. A
  blocked platform now gives its reason, from the engine's own predicate, including how many coaches
  are stranded in Classification and what brings them back.

The same four ship as v0.4.9g on the playtest line.

Closes #2
Closes #8
Closes #9
Closes #10

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FLnYR4XtXQNamYJXGYT8oC
This commit is contained in:
Jesse.Markowitz
2026-08-25 10:46:22 -04:00
co-authored by Claude Opus 5
parent 603d38602c
commit 441447648d
17 changed files with 977 additions and 115 deletions
+7 -2
View File
@@ -1,7 +1,7 @@
/**
* THE RULES BLOCK, driven identically on both screens.
*
* The lobby (`lobby.ts`) and the solitaire New Game dialog (`main.ts`) ask the same eleven questions.
* The lobby (`lobby.ts`) and the solitaire New Game dialog (`main.ts`) ask the same twelve questions.
* They used to ask them in two hand-written copies and had already drifted — the dialog had "where
* an Extra may start" and no optional rules, the lobby the reverse — so this module owns reading,
* writing, comparing and annotating the block, and each screen supplies only the id prefix its
@@ -47,10 +47,11 @@ export const FIELDS: readonly Field[] = [
{ key: 'reducedVisibility', kind: 'checkbox', id: 'visibility' },
{ key: 'employeeRotation', kind: 'checkbox', id: 'rotation' },
{ key: 'emergencyToolbox', kind: 'checkbox', id: 'toolbox' },
{ key: 'discardTimetabled', kind: 'checkbox', id: 'tossloco' },
];
/**
* What `play.html` must contain for a screen to be able to ask all eleven questions — the exact
* What `play.html` must contain for a screen to be able to ask all twelve questions — the exact
* attribute text, so `test/web.test.ts` can assert it against the built page.
*
* THIS IS THE DRIFT GUARD. The two blocks are generated from one template today; this is what says
@@ -71,6 +72,7 @@ export function fieldSelectors(prefix: string): string[] {
const FIELD_LABELS: Record<keyof Settings, string> = {
startingHand: 'Starting hand',
extraStart: 'An Extra may start at',
discardTimetabled: 'A Timetabled train may be discarded',
passengerPerCoach: 'Passenger per coach',
freightPerLoad: 'Freight per load',
trainPerTransit: 'Train per transit',
@@ -115,6 +117,7 @@ export function rulesListHtml(config: GameConfig, players: number, days: number)
return (
head +
`<h4>Opening</h4><dl>${rows(['startingHand', 'extraStart'])}</dl>` +
`<h4>Train cards</h4><dl>${rows(['discardTimetabled'])}</dl>` +
`<h4>Revenue</h4><dl>${rows(['passengerPerCoach', 'freightPerLoad', 'trainPerTransit'])}</dl>` +
`<h4>Victory conditions</h4><dl>${rows(['minCombinedRevenue', 'maxCollisionsPerDay', 'maxCollisionsTotal'])}</dl>` +
`<h4>Optional rules</h4><dl>${rows(['reducedVisibility', 'employeeRotation', 'emergencyToolbox'])}</dl>`
@@ -205,6 +208,7 @@ export function settingsForm(prefix: string): SettingsForm {
reducedVisibility: el<HTMLInputElement>('visibility')?.checked === true,
employeeRotation: el<HTMLInputElement>('rotation')?.checked === true,
emergencyToolbox: el<HTMLInputElement>('toolbox')?.checked === true,
discardTimetabled: el<HTMLInputElement>('tossloco')?.checked === true,
};
}
@@ -220,6 +224,7 @@ export function settingsForm(prefix: string): SettingsForm {
setChecked('visibility', values.reducedVisibility);
setChecked('rotation', values.employeeRotation);
setChecked('toolbox', values.emergencyToolbox);
setChecked('tossloco', values.discardTimetabled);
}
function setNumber(id: string, value: number): void {