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:
+14
-19
@@ -678,7 +678,8 @@ function sampleDetail(s: GameState, kind: string, list: Intent[]): string {
|
||||
|
||||
/** One readable line for a single intent. */
|
||||
export function describeIntent(s: GameState, i: Intent): string {
|
||||
const at = (c: { row: number; col: number }): string => `(${c.row},${c.col})`;
|
||||
// X,Y — east/west then north/south, not the internal row/col storage order.
|
||||
const at = (c: { row: number; col: number }): string => `(${c.col},${c.row})`;
|
||||
// An intent belongs to whoever is acting, so it is described against THEIR district.
|
||||
// The acting PLAYER, not a seat — `describeIntent` describes an intent against the district of
|
||||
// whoever is making it. Named `seat` once, and then used as an `officeAreas` key, which is the
|
||||
@@ -1229,7 +1230,7 @@ export function snapshot(
|
||||
? `${t.position.side} Division Point`
|
||||
: t.position.at === 'mainline'
|
||||
? `Mainline card ${t.position.index}`
|
||||
: `Office Area (${t.position.coord.row},${t.position.coord.col})`,
|
||||
: `Office Area (${t.position.coord.col},${t.position.coord.row})`,
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -1424,7 +1425,9 @@ export function trainRules(t: {
|
||||
* DOES rather than that it exists, because the restriction is the whole character of the card: a
|
||||
* Military train that cannot be worked by Porters plays nothing like a Local.
|
||||
*/
|
||||
if (p.rules.noSwitching) parts.push('NO SWITCHING — it runs the Division and does not shunt');
|
||||
if (p.rules.noSwitching) {
|
||||
parts.push('NO SWITCHING — may not add or drop cars, but may still be moved clear of the mainline');
|
||||
}
|
||||
if (p.rules.terminalsOnly) parts.push('TERMINALS ONLY — Porters may work it at a Terminal and nowhere else');
|
||||
if (p.rules.coachStaysOnStationTrack) {
|
||||
// Said "may only be set out at the Office", which reads as a place you can do it. You cannot:
|
||||
@@ -1446,15 +1449,12 @@ export function trainRules(t: {
|
||||
}
|
||||
|
||||
if (p.rules.expedite) {
|
||||
// The single most-asked question in play — "why did that train leave before I could switch it?"
|
||||
// — so it names the phases rather than the Stage. It stands through Cargo and goes at the end of
|
||||
// the Stage, which is after the Porters and Laborers but before the next Local Operations.
|
||||
// It is released and switched exactly like any other train — the restriction is on where it may
|
||||
// be LEFT, not on when it leaves.
|
||||
parts.push(
|
||||
'EXPEDITED — it does not lay over. It stands through the Cargo phase and departs at the END OF ' +
|
||||
'THE STAGE IT ARRIVES IN, so Porters and Laborers can work it but it is normally gone before ' +
|
||||
'your next Local Operations turn and cannot be switched. UNLESS §8.1 wants a clearance ruling ' +
|
||||
'at that moment: there is no Superintendent in Supervisor Shift to give one, so the train ' +
|
||||
'stays and runs like any other from then on.',
|
||||
'EXPEDITED — must be kept ready to highball. It works and switches normally, but if it is not ' +
|
||||
'back on the Office square when the next Mainline Phase begins, that is a Station Master ' +
|
||||
'fault and costs 1 Revenue.',
|
||||
);
|
||||
}
|
||||
if (p.rules.stopEarnsPoint) parts.push('EARNS A POINT for one Stage spent standing still, once');
|
||||
@@ -1736,14 +1736,9 @@ function switchingMoves(s: GameState, player: PlayerIndex): Frame['moves'] {
|
||||
const out: Frame['moves'] = [];
|
||||
for (const [id, tray] of s.trays) {
|
||||
if (tray.position.at !== 'grid' || tray.position.seat !== seatOf(s, player)) continue;
|
||||
/**
|
||||
* A crew that may not switch is not a crew you can be switching.
|
||||
*
|
||||
* `movesFor` is pure track geometry — it answers where a train COULD roll — and six cards print
|
||||
* "no switching", which `check` enforces and it does not. Offering the Circus Train in a "which
|
||||
* train are you switching?" row is offering a train whose every move the rules refuse.
|
||||
*/
|
||||
if (trainProfile(tray.trainNumber ?? 0, tray.trainIsExtra)?.rules.noSwitching) continue;
|
||||
// A no-switching train may still be moved clear of the mainline (§7) — only coupling, setting
|
||||
// out and sorting are refused, and `check` rejects those the same way it rejects a pick-up X13
|
||||
// (dropOnly) is not allowed to make, so this row is not filtered any differently for either.
|
||||
const { to, blocked } = movesFor(s, player, id);
|
||||
out.push({ trayId: id, label: trainName(s, id), from: tray.position.coord, to, blocked });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user