/** * 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 } | { type: 'switch.dropCars'; trayId: TrayId; count: number } /** * 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)` — Gap 11: orientation is chosen on placement. */ | { type: 'card.play'; cardId: CardId; placement?: GridCoord; variant?: number } | { type: 'card.discard'; cardId: CardId; toSlot: number } /** * Lay a piece from your personal track supply (§12.2 / content.ts TRACK_SUPPLY). * * Laid during the "draw a card" option, one piece a turn — how track behaved when it WAS a card. * Confirmed; see implications.md §10 Q10. */ | { type: 'track.lay'; geometry: TrackGeometry; hand: Hand; placement: GridCoord; variant?: 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' | 'WRONG_CAR_TYPE' | 'NOT_SUPERINTENDENT' | 'NO_PENDING_DECISION' | 'NOT_UPGRADEABLE' | 'FACILITY_LOCKED' | '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 };