v0.4.9 — X,Y coordinates, no-switching can still clear the mainline, Q3 corrected, a clearance bug, three sounds, and the box art

A playtest review of seed 58228926 (day 6), plus one long-standing display complaint and the
first real audio beyond a placeholder.

- Coordinate labels read X,Y everywhere shown to a player, not the internal Y,X storage order.
  Display-only.
- "No switching" now means may not add or drop cars, not "never touch it" — these trains can
  still be moved onto Secondary Track to clear the mainline.
- Q3 corrected: Expedite governs WHERE a train may be left standing, not WHEN it leaves. The
  forced same-Stage departure is gone; a new fault costs 1 Revenue if an expedited train is left
  off the station when a Mainline Phase begins. Resolves "3/4 Express prints a rule it can never
  use" as a side effect.
- evaluateClearance now checks every occupant on a Mainline card before offering a judgment
  call, instead of returning on whichever it found first — found while explaining a playtest
  report, fixed with a regression test.
- Three new synthesised sounds: arrive, depart, crash.
- The splash page shows the box art.

Full detail, measurements and reasoning in CHANGELOG.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FvU99NEakShRMg3nN3fHAZ
This commit is contained in:
Jesse
2026-08-19 23:51:35 -04:00
co-authored by Claude Sonnet 5
parent 37b1e5b969
commit 859817d173
25 changed files with 1048 additions and 739 deletions
+14 -5
View File
@@ -486,11 +486,14 @@ function freightBudgetLeft(
}
/**
* Whether a train may do switching work at all (§7).
* Whether a train may set out or sort cars (§7).
*
* Six cards print "no switching" — the two expresses, the Light Engine, the Campaign, Circus and
* Military trains. They run the Division; they do not shunt. This covers moving, setting out and
* sorting alike, because all three are switching.
* Military trains — which means they may not ADD or DROP cars, not that they may never be touched: a
* train held at the Office may still need to clear onto Secondary Track ahead of other traffic. So
* this covers `switch.dropCars` and `switch.sortConsist` only; `switch.move` handles `noSwitching`
* itself, the same way it handles `dropOnly`, because the restriction has to bite on the pick-up a
* move would make, not on the move itself.
*/
function switchingRefusal(tray: CrewTray): RejectionCode | null {
return rulesOf(tray).noSwitching ? 'NO_SWITCHING' : null;
@@ -626,8 +629,6 @@ export function check(s: GameState, player: PlayerIndex, i: Intent): RejectionCo
if (turnOf(s, player).movesRemaining < 1) return 'NO_MOVES_REMAINING';
const tray = s.trays.get(i.trayId);
if (!tray) return 'NO_SUCH_TRAY';
const noSwitch = switchingRefusal(tray);
if (noSwitch) return noSwitch;
const from = trayCoord(s, i.trayId);
if (!from) return 'ILLEGAL_MOVE';
const dests = destinationsFor(s, player, i.trayId, from, i.reverse);
@@ -652,6 +653,14 @@ export function check(s: GameState, player: PlayerIndex, i: Intent): RejectionCo
*/
const fresh = dest.couples.slice(ownCutFor(s, player, i.trayId, i.reverse).length);
if (fresh.length > 0) {
/**
* "NO SWITCHING" MEANS NO ADDING OR DROPPING CARS, not "never move". A no-switching train
* held at the Office may still need to clear onto Secondary Track ahead of other traffic —
* §7 governs coupling, setting out and sorting, and a move that picks nothing up is none of
* those. Handled here, alongside `dropOnly`, rather than as a blanket refusal on the intent:
* the restriction has to bite on the pick-up itself, the same reasoning as the comment above.
*/
if (rules.noSwitching) return 'PICKUP_NOT_ALLOWED';
if (rules.dropOnly) return 'PICKUP_NOT_ALLOWED';
if (rules.pickUpEmptiesOnly && fresh.some((c) => c.loaded)) return 'EMPTIES_ONLY';
const freight = fresh.filter(isFreight).length;