Track becomes a deck card, Departments become decks, and trains must be made up to leave, industry restrictions enforced, update display and add a new game button

This commit is contained in:
Jesse
2026-08-05 08:28:22 -04:00
parent 2d348e1ecf
commit 9c76b1a5e2
28 changed files with 1927 additions and 394 deletions
+40 -16
View File
@@ -9,6 +9,7 @@
import type {
CarType,
Direction,
Hand,
MainlineKind,
FreightKind,
GameLength,
@@ -127,11 +128,6 @@ export type OfficeArea = {
heldAtLimits: TrayId[];
/** Dispatch bonuses spent this Day, by enhancement key — each is once a Day. */
dispatchUsedToday: string[];
/**
* The player's personal track supply (26 pieces), keyed `geometry:hand`. Track is NOT in the
* Home Office deck — it is laid from here.
*/
trackSupply: Map<string, number>;
};
// ---------------------------------------------------------------------------
@@ -192,10 +188,24 @@ export function spaceOn(card: TrackCard): number {
export function isOperationalRail(card: TrackCard): boolean {
if (!card.baseOperationalRail) return false;
if (card.facility && card.facility.menAtWork.some((slot) => slot !== null)) return false;
if (isLockedByWork(card)) return false;
return true;
}
/**
* §9.3 — "While ANY loads are in the MEN | AT | WORK track, the industry's track is locked down for
* safety reasons. It loses its status as Operational Rail. No cars can be picked up or dropped off,
* and **no trains may occupy or move on it**."
*
* That last clause is stronger than losing Operational Rail status. A turnout is not Operational
* Rail either, and a train runs straight through one — so "cannot stop here" and "cannot pass
* through here" are different properties, and only a locked industry has both. Kept separate from
* `isOperationalRail` for exactly that reason.
*/
export function isLockedByWork(card: TrackCard): boolean {
return !!card.facility && card.facility.menAtWork.some((slot) => slot !== null);
}
// ---------------------------------------------------------------------------
// Trains
// ---------------------------------------------------------------------------
@@ -223,7 +233,16 @@ export type CrewTray = {
* the four-car limit (§A.4) is a limit on cars, not on the locomotive hauling them.
*/
engineAt: number;
/** ORDERED, left-to-right. Max 4 including any caboose (§A.4). */
/**
* ORDERED NOSE FIRST: index 0 is the end nearest the front of the train, the last index is the
* tail. Max 4 including any caboose (§A.4); the engine is not one of them.
*
* Both diagrams in Appendix A read this way. A train drawn `[<ENG][yellow][brown][blue][red]`
* drops "red as he starts, then brown and blue", ending with only yellow — cars come off the TAIL,
* which is the end of the array. And a train that picks cars up on its nose gets them "in the
* order that they were in, pushing them into the Facility" — ahead of the engine, which is why
* `engineAt` moves when it happens.
*/
consist: RollingStock[];
direction: Direction;
/**
@@ -298,7 +317,8 @@ export type CardKind =
| { kind: 'office'; tier: OfficeTier }
| { kind: 'freightFacility'; facility: FreightKind }
| { kind: 'modifier'; modifier: ModifierKind }
| { kind: 'track'; geometry: TrackGeometry }
/** Handedness is printed on the card: it is the diagonal the 45° leg lies on. */
| { kind: 'track'; geometry: TrackGeometry; hand: Hand }
/**
* 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
@@ -315,8 +335,18 @@ export type Card = { id: CardId; kind: CardKind };
export type Decks = {
/** Face down. Order is SECRET — never projected to any client. */
homeOffice: CardId[];
/** Three face-up market slots fed from the deck (§2.6). */
departments: (CardId | null)[];
/**
* THE THREE DEPARTMENT DECKS — shared by every player, and stacks rather than single slots.
*
* §6.2: a discard goes "face up ON TOP of one of the three Department slots", and a draw takes
* "the TOP face-up card". Both phrases only mean anything if a Department is a pile, and modelling
* them as one-card slots silently DESTROYED whatever was underneath — discarding onto an occupied
* slot overwrote it, leaking cards out of a closed deck.
*
* Last element is the top of the pile: the card that is face up, the only one that may be drawn,
* and the one a discarding player is choosing to offer or to bury.
*/
departments: CardId[][];
/** Face up, so players can audit discards (§2.6). */
salvageYard: CardId[];
/** Private to the owner. Max 3, or 4 with a Red Flag. */
@@ -412,11 +442,6 @@ export type TurnState = {
option: 'switch' | 'draw' | 'freightAgent' | null;
movesRemaining: number;
drawnThisTurn: boolean;
/**
* Track was a CARD before it became a per-player supply, so it was naturally limited to one play
* a turn. The supply has 26 pieces and nothing else bounds it, so keep that limit explicitly.
*/
laidThisTurn: boolean;
freightAgentUsed: boolean;
/** Set when the actor finishes; the phase driver then moves to the next player. */
done: boolean;
@@ -427,7 +452,6 @@ export function freshTurn(moves: number): TurnState {
option: null,
movesRemaining: moves,
drawnThisTurn: false,
laidThisTurn: false,
freightAgentUsed: false,
done: false,
};