/** * The intent vocabulary — every legal player choice, from docs/architecture/protocol.md §1. * * An intent is a PROPOSAL. It may be rejected. Contrast with an event (events.ts), which is a fact. */ import type { CarType, Direction, Hand, OfficeTier, TrackGeometry } from './content.ts'; import type { CardId, GridCoord, PlayerIndex, TrayId } from './state.ts'; // --------------------------------------------------------------------------- // Local Operations Phase (§6) — a three-way exclusive choice // --------------------------------------------------------------------------- export type LocalOpsOption = 'switch' | 'draw' | 'freightAgent'; export type Intent = | { type: 'localOps.choose'; option: LocalOpsOption } // -- switch (§6.1, Appendix A) | { type: 'switch.move'; trayId: TrayId; to: GridCoord; reverse: boolean } /** * Set out a cut. `fromNose` takes it off the FRONT of the train rather than the back — Appendix A's * worked example does exactly this: "Back up and drop off everything on the nose of your train * (red and blue) on Card B." Without it, cars taken onto the nose could never be set out again and * an engine buried in its own train had no way back to an end. */ | { type: 'switch.dropCars'; trayId: TrayId; count: number; fromNose?: boolean } /** * Small Yard enhancement — "a train that spends one move in the yard may sort itself in any * order, including cars in front of the engine". This is the designed answer to §A.3's * come-off-in-seated-order constraint, which is what makes facing-point work possible. */ | { type: 'switch.sortConsist'; trayId: TrayId; order: number[] } | { type: 'switch.end' } // -- draw (§6.2) | { type: 'draw.fromHomeOffice' } | { type: 'draw.fromDepartment'; slot: number } /** * `variant` indexes `variantsFor(geometry, hand)` — 0° or 180°, the only two ways a printed card * can be laid. Track is played through here like every other card; there is no separate lay * intent, because there is no separate supply to lay from. * * `placement` is a square in the player's Office Area; `node` indexes `division.nodes` and is for * the one card that goes out on the Mainline instead (ABS Signals). They are alternatives, and * they are SEPARATE FIELDS on purpose. A Mainline placement used to travel as the fake coordinate * `{ row: -1, col: node }`, and row −1 is an ordinary district row — the first one below the * Running Track, where most districts start. So a real placement on row −1 was labelled "out on * the Mainline, node −2", and an ABS Signals placement highlighted whichever district card * happened to sit at that column. A sentinel inside a coordinate's own value range is not a * sentinel. */ | { type: 'card.play'; cardId: CardId; placement?: GridCoord; variant?: number; node?: number } | { type: 'card.discard'; cardId: CardId; toSlot: number } | { type: 'draw.end' } // -- freight agent (§6.3) | { type: 'freightAgent.stockOutbound'; at: GridCoord; carType: CarType } | { type: 'freightAgent.clearInbound'; at: GridCoord; index: number } | { type: 'freightAgent.unjam'; at: GridCoord; from: 'outbound' | 'inbound' | 'menAtWork'; index: number } // -- New Train Phase (§7) | { type: 'newTrain.placeCar'; trayId: TrayId; carType: CarType; loaded: boolean } | { type: 'newTrain.passCar'; trayId: TrayId } /** Q9 — run a second, identical section behind a train that is due out this Stage. */ | { type: 'newTrain.secondSection'; trainNumber: number } // -- Mainline Phase (§8.1) — the Superintendent's clearance ruling | { type: 'mainline.clearance'; allow: boolean } /** * Lay a Mainline modifier (Brakeman / Airbrakes / Helpers / Realignment) on a Mainline card. * `node` indexes `division.nodes`. */ | { type: 'mainline.modify'; cardId: CardId; node: number } /** * Red Flags — protect a stopped train. The flagged train cannot be hit; an approaching train is * held instead of colliding. */ | { type: 'maneuver.redFlags'; cardId: CardId; trayId: TrayId } /** * Flying Switch — cut cars off behind the engine and roll them into an adjacent industry, without * the engine entering it. */ | { type: 'maneuver.flyingSwitch'; cardId: CardId; trayId: TrayId; count: number; to: GridCoord } | { type: 'redFlag.play' } // -- Load/Unload Phase (§9) | { type: 'porter.board'; at: GridCoord } | { type: 'porter.detrain'; at: GridCoord } /** §9.3 — the first Laborer step: Green Loading Slot -> MEN. */ | { type: 'laborer.startLoad'; at: GridCoord } | { type: 'laborer.advanceLoad'; at: GridCoord; box: number } | { type: 'laborer.beginUnload'; at: GridCoord; carIndex: number } | { type: 'loadUnload.end' }; export type IntentType = Intent['type']; // --------------------------------------------------------------------------- // Rejections — protocol.md §2 // --------------------------------------------------------------------------- export type RejectionCode = | 'NOT_YOUR_TURN' | 'WRONG_PHASE' | 'OPTION_ALREADY_CHOSEN' | 'OPTION_NOT_CHOSEN' | 'NO_MOVES_REMAINING' | 'ILLEGAL_MOVE' | 'NO_SUCH_TRAY' | 'NO_SUCH_CARD' | 'NO_SUCH_FACILITY' | 'CANNOT_DROP_HERE' | 'CONSIST_EMPTY' | 'CONSIST_FULL' | 'HAND_LIMIT' | 'CARD_NOT_IN_HAND' | 'NO_PLACEMENT' | 'NOT_CONNECTED' | 'DECK_EMPTY' | 'SLOT_EMPTY' | 'RESOURCE_SPENT' | 'BOX_FULL' | 'BOX_EMPTY' | 'NO_SUITABLE_CAR' | 'SUITABLE_CAR_EXISTS' /** §7 — cars go onto the train being assembled at a Division Point, not onto one already running. */ | 'NOT_BEING_MADE_UP' | 'WRONG_CAR_TYPE' | 'NOT_SUPERINTENDENT' | 'NO_PENDING_DECISION' | 'NOT_UPGRADEABLE' | 'FACILITY_LOCKED' /** An Industry goes on a straight stub, never on the Running Track (sheet: "Placed" column). */ | 'ON_RUNNING_TRACK' /** A card with no east-west road would dead-end the Running Track it was laid in. */ | 'BREAKS_RUNNING_TRACK' | 'NOT_IMPLEMENTED' | 'WRONG_INTENT' | 'NOT_A_GRADE' | 'TRAIN_ON_CARD' | 'CONSIST_ORDER' | 'NO_TRAIN_AT_OFFICE'; export type Rejection = { code: RejectionCode; message: string }; export function reject(code: RejectionCode, message: string): Rejection { return { code, message }; } // --------------------------------------------------------------------------- // Re-exports used by handlers // --------------------------------------------------------------------------- export type { CardId, Direction, GridCoord, OfficeTier, PlayerIndex, TrayId };