177 lines
7.6 KiB
TypeScript
177 lines
7.6 KiB
TypeScript
/**
|
||
* 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 }
|
||
/**
|
||
* Finish the turn having chosen the Freight Agent and done nothing.
|
||
*
|
||
* §6.3 lists three things the Freight Agent MAY do; it does not say one must be done. Without this
|
||
* the option was a trap — having chosen it, the only legal moves were to stock, to clear, or to
|
||
* UNJAM, and unjamming a healthy facility throws away a load that cost a whole action to stock.
|
||
*/
|
||
| { type: 'freightAgent.end' }
|
||
// -- 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'
|
||
/**
|
||
* A turnout may be laid on top of a straight, or of a curve on the same arc as its diverging leg
|
||
* — but only those, and only while the square is idle. See `checkTurnoutUpgrade`.
|
||
*/
|
||
| 'NOT_UPGRADEABLE_TRACK'
|
||
/** You cannot swap the track out from under a standing car. */
|
||
| 'UPGRADE_OCCUPIED'
|
||
/** An Interlocking or Telegraph is built on that card; the upgrade would have to lift it. */
|
||
| 'UPGRADE_ENHANCED'
|
||
/**
|
||
* §7 — the operating rules printed on a train's own card. Each names the restriction it broke, so
|
||
* the UI can say which card is refusing rather than "illegal move".
|
||
*/
|
||
| 'NO_SWITCHING'
|
||
| 'NOT_A_TERMINAL'
|
||
| 'COACH_MUST_STAY'
|
||
| 'FREIGHT_WORKED_HERE'
|
||
| 'NO_PASSENGER_WORK'
|
||
| 'PICKUP_NOT_ALLOWED'
|
||
| 'EMPTIES_ONLY'
|
||
| '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 };
|