Expand game engine, replay, tests, and documentation

This commit is contained in:
Jesse
2026-07-31 08:10:54 -04:00
parent e7bc07df85
commit 401b879140
30 changed files with 2253 additions and 552 deletions
+34 -6
View File
@@ -9,6 +9,7 @@
import type {
CarType,
Direction,
MainlineKind,
FreightKind,
GameLength,
ModifierKind,
@@ -65,11 +66,14 @@ export type CardGeometry =
geometry: TrackGeometry;
turnout?: TurnoutOrientation;
axis?: TrackAxis;
bypass?: 'n' | 's' | 'e' | 'w';
/** Curves are printed left- or right-handed (design supply); not chosen on placement. */
hand?: 'left' | 'right';
}
| { kind: 'office' }
| { kind: 'limits' }
| { kind: 'facility'; facility: FreightKind; axis?: TrackAxis };
| { kind: 'facility'; facility: FreightKind; axis?: TrackAxis }
/** Not track — a Modifier sits beside a Facility and raises its capacity (§9). */
| { kind: 'modifier'; modifier: ModifierKind };
export type TrackCard = {
geometry: CardGeometry;
@@ -165,7 +169,7 @@ export function isOperationalRail(card: TrackCard): boolean {
export type NodeRef =
| { at: 'divisionPoint'; side: Direction }
| { at: 'mainline'; index: number; region: number }
| { at: 'mainline'; index: number }
| { at: 'grid'; owner: PlayerIndex; coord: GridCoord };
export type CrewTray = {
@@ -186,11 +190,15 @@ export type CrewTray = {
// The Division
// ---------------------------------------------------------------------------
export type Region = { occupant: TrayId | null };
/**
* A train part-way across a Mainline card. Crossing time is measured in STAGES (Q1/Q2), so the
* regions the placeholder used are gone — the cells printed on the cards are decoration.
*/
export type Transit = { tray: TrayId; stagesRemaining: number; direction: Direction };
export type DivisionNode =
| { kind: 'divisionPoint'; side: Direction; holding: TrayId[] }
| { kind: 'mainline'; regions: Region[] }
| { kind: 'mainline'; card: MainlineKind; transits: Transit[] }
| { kind: 'office'; owner: PlayerIndex };
/** Ordered west to east. For N players: N Office nodes and N+1 Mainline cards. */
@@ -206,7 +214,17 @@ export type CardKind =
| { kind: 'office'; tier: OfficeTier }
| { kind: 'freightFacility'; facility: FreightKind }
| { kind: 'modifier'; modifier: ModifierKind }
| { kind: 'track'; geometry: TrackGeometry };
| { kind: 'track'; geometry: TrackGeometry }
/**
* The four categories recovered from the design. They are in the deck so the composition is
* right; their BEHAVIOUR is not implemented yet (implications.md §10 is still open), so
* `check` rejects playing them and they can only be discarded.
*/
| { kind: 'spaceUse'; key: string }
| { kind: 'enhancement'; key: string }
| { kind: 'mainlineModifier'; key: string }
| { kind: 'maneuver'; key: string }
| { kind: 'action'; key: string };
export type Card = { id: CardId; kind: CardKind };
@@ -342,6 +360,16 @@ export type GameState = {
yards: Yards;
/** Index 0 = Stage 1. A train number, or null for an empty slot. */
timetable: (number | null)[];
/**
* §7 — Extra Trains played from hand, waiting for a free Crew Tray. An Extra is not scheduled:
* it runs once, immediately, then its card goes to the Salvage Yard (§2.3).
*/
pendingExtras: number[];
/**
* Q9 — train numbers ordered to run a second section. The next New Train Phase makes up an
* identical train behind the first, if a Crew Tray is free.
*/
pendingSecondSections: number[];
clock: Clock;
turn: TurnState;
/** Transient: trains already moved in the current Mainline Phase. Cleared when it ends. */