v0.4.4 — New Game dialog for the opening hand and the three revenue rates, Yard renamed Interchange, and two movement bugs behind a mirrored consist

This commit is contained in:
Jesse
2026-08-14 22:39:50 -04:00
parent d1314066bd
commit 6655e20ea8
29 changed files with 5450 additions and 3902 deletions
+46
View File
@@ -13,6 +13,7 @@ import type {
MainlineKind,
FreightKind,
GameLength,
HouseRuleOverrides,
ModifierKind,
OfficeTier,
TrackGeometry,
@@ -282,6 +283,28 @@ export type CrewTray = {
* without one falls back to `direction`.
*/
facing?: 'n' | 's' | 'e' | 'w';
/**
* WHICH WAY THE ENGINE POINTS IN RAILROAD TERMS — east or west, and never anything else.
*
* `facing` is a PORT, because movement needs one: a crew on a north-south spur must be able to
* leave by 'n' or 's'. But a Division runs east and west, and a player reads a train the way a
* railroader does — "the engine is on the west end" — so a ▲ on a crew that had turned onto a
* spur read as a train that had somehow stood itself on end. Worse, nothing resets `facing` when
* a train leaves the district, so a crew that shunted onto a north-south spur and then departed
* carried its 'n' out onto the Division map and drew ▲ there too.
*
* So this is the DISPLAY facing, and it is a separate field because it cannot be derived: on
* north-south track the east-west sense is not in the current port, it is in the last one. It
* holds its value across north-south track and updates whenever `facing` becomes 'e' or 'w' —
* which is exactly the railroad's own convention, where compass north on a branch is still
* timetable east. A train that runs forward through 180° of curves genuinely does come out
* pointing the other way, and this follows it; backing up does not change it, because a train
* that backs up has not turned around.
*
* NOT `direction`: that is the timetable direction of the RUN and does not move when a run-around
* puts the engine on the other end, which is the one thing the arrow exists to show.
*/
railFacing?: 'e' | 'w';
position: NodeRef;
movesUsed: number;
/**
@@ -468,6 +491,14 @@ export type GameConfig = {
employeeRotation: boolean;
emergencyToolbox: boolean;
};
/**
* The opening deal and the three revenue rates, chosen when the game is dealt (`content.ts`).
*
* Optional and PARTIAL on purpose. Every caller that does not care about them — and most of the
* engine tests do not — gets `DEFAULT_HOUSE_RULES` through `houseRules()`, which is the one place
* a default is written down. A caller that cares names only the dials it is setting.
*/
houseRules?: HouseRuleOverrides;
};
export type OutcomeReason =
@@ -524,6 +555,21 @@ export function freshTurns(players: number, moves: number): Map<PlayerIndex, Tur
return turns;
}
/**
* WHICH WAY TO DRAW THE ENGINE — east or west, for every train, everywhere.
*
* The single place the display facing is decided, so the Division map, the Office cards and the
* tooltips can never disagree about which end of a train the engine is on. Three sources, in the
* order they can be trusted: the carried east-west sense; the current port, when it happens to be
* an east-west one (a tray placed straight onto the board has no history yet); and failing both,
* the direction of the run.
*/
export function railFacingOf(tray: Pick<CrewTray, 'railFacing' | 'facing' | 'direction'>): 'e' | 'w' {
if (tray.railFacing) return tray.railFacing;
if (tray.facing === 'e' || tray.facing === 'w') return tray.facing;
return tray.direction === 'west' ? 'w' : 'e';
}
export function turnOf(s: GameState, player: PlayerIndex): TurnState {
const t = s.turns.get(player);
if (!t) throw new Error(`no turn state for player ${player}`);