fixed train# specific rules. cleaned up display topline and train display on division map
This commit is contained in:
@@ -307,7 +307,49 @@ function mainlinePhase(s: GameState, events: GameEvent[]): AdvanceResult {
|
||||
|
||||
for (const [id, tray] of order) {
|
||||
if (s.movedThisPhase.has(id)) continue;
|
||||
const where = tray.position;
|
||||
const moved = moveTrain(s, id, tray, events);
|
||||
/**
|
||||
* X18 CIRCUS TRAIN — "one turn stopped on any track (circus set-up) earns 1 point".
|
||||
*
|
||||
* The flag was declared on the profile and read NOWHERE, so the one card in the deck that pays
|
||||
* for standing still paid nothing: reported from a playtest where TX18 sat on a siding for a
|
||||
* full Stage and no point arrived. Claimed once — an Extra runs once and is gone.
|
||||
*
|
||||
* "Stopped" is measured against the Mainline Phase: the train attempted to move and stayed where
|
||||
* it was. A train that is still in the district when the phase runs has not moved either, which
|
||||
* is the circus setting up on a siding rather than crossing the Division.
|
||||
*/
|
||||
if (!tray.stopPointClaimed && trainProfile(tray.trainNumber ?? 0, tray.trainIsExtra)?.rules.stopEarnsPoint) {
|
||||
const stillThere =
|
||||
tray.position.at === where.at &&
|
||||
(tray.position.at !== 'mainline' || where.at !== 'mainline' || tray.position.index === where.index) &&
|
||||
(tray.position.at !== 'grid' ||
|
||||
where.at !== 'grid' ||
|
||||
(tray.position.coord.row === where.coord.row && tray.position.coord.col === where.coord.col));
|
||||
if (moved !== 'expedited' && stillThere) {
|
||||
tray.stopPointClaimed = true;
|
||||
const owner = tray.position.at === 'grid' ? tray.position.owner : 0;
|
||||
const label =
|
||||
tray.position.at === 'grid'
|
||||
? `(${tray.position.coord.row},${tray.position.coord.col})`
|
||||
: tray.position.at === 'mainline'
|
||||
? `Mainline card ${tray.position.index}`
|
||||
: `the ${tray.position.side} Division Point`;
|
||||
events.push({ type: 'trainStoodStill', trainNumber: tray.trainNumber ?? 0, where: label });
|
||||
const p = s.players[owner];
|
||||
if (p) {
|
||||
p.revenue += 1;
|
||||
events.push({
|
||||
type: 'revenueChanged',
|
||||
player: owner,
|
||||
delta: 1,
|
||||
total: p.revenue,
|
||||
reason: 'circus set-up — a Stage spent standing still',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (moved === 'needsClearance') return { events, needsInput: true };
|
||||
// An expedited train may act twice in one Stage: it arrives and departs (Q3).
|
||||
if (moved === 'expedited') {
|
||||
|
||||
@@ -480,6 +480,13 @@ export function check(s: GameState, player: PlayerIndex, i: Intent): RejectionCo
|
||||
return f.inboundBox[i.index] ? null : 'BOX_EMPTY';
|
||||
}
|
||||
|
||||
case 'freightAgent.end':
|
||||
if (!inPhase(s, 'localOps')) return 'WRONG_PHASE';
|
||||
// §6.3 offers three things the Freight Agent may do and requires none of them. Ending with the
|
||||
// action unspent is a wasted Stage, which is the player's to waste — the alternative was
|
||||
// forcing an unjam that destroys a load.
|
||||
return s.turn.option === 'freightAgent' ? null : 'OPTION_NOT_CHOSEN';
|
||||
|
||||
case 'freightAgent.unjam': {
|
||||
if (!inPhase(s, 'localOps')) return 'WRONG_PHASE';
|
||||
if (s.turn.option !== 'freightAgent') return 'OPTION_NOT_CHOSEN';
|
||||
@@ -864,6 +871,7 @@ function execute(s: GameState, player: PlayerIndex, i: Intent): GameEvent[] {
|
||||
|
||||
case 'switch.end':
|
||||
case 'draw.end':
|
||||
case 'freightAgent.end':
|
||||
return [{ type: 'phaseEnded', player, phase: 'localOps' }];
|
||||
|
||||
case 'draw.fromHomeOffice': {
|
||||
|
||||
@@ -88,6 +88,8 @@ export type GameEvent =
|
||||
| { type: 'secondSectionOrdered'; player: PlayerIndex; trainNumber: number }
|
||||
| { type: 'trainMadeUp'; trainNumber: number; isExtra: boolean; at: string; direction: string }
|
||||
| { type: 'trainHeld'; trainNumber: number; reason: string }
|
||||
/** A train whose card pays for standing still (X18 Circus) collected on it. */
|
||||
| { type: 'trainStoodStill'; trainNumber: number; where: string }
|
||||
| { type: 'trainHighballed'; trainNumber: number; from: string; to: string }
|
||||
/**
|
||||
* `expedited` because it changes what the player must do RIGHT NOW. Q3 — an expedited train
|
||||
|
||||
@@ -55,6 +55,14 @@ export type Intent =
|
||||
| { 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 }
|
||||
|
||||
@@ -177,6 +177,9 @@ function localOpsCandidates(s: GameState, player: PlayerIndex): Intent[] {
|
||||
out.push({ type: 'draw.end' });
|
||||
|
||||
// -- freight agent (§6.3)
|
||||
// Ending without acting is always on offer: the section lists what the Freight Agent MAY do and
|
||||
// requires none of it.
|
||||
out.push({ type: 'freightAgent.end' });
|
||||
for (const coord of facilityCoords(s, player)) {
|
||||
for (const carType of CAR_TYPES) {
|
||||
out.push({ type: 'freightAgent.stockOutbound', at: coord, carType });
|
||||
|
||||
@@ -256,6 +256,13 @@ export type CrewTray = {
|
||||
facing?: 'n' | 's' | 'e' | 'w';
|
||||
position: NodeRef;
|
||||
movesUsed: number;
|
||||
/**
|
||||
* X18 Circus Train — "one turn stopped on any track (circus set-up) earns 1 point", claimed once.
|
||||
*
|
||||
* Recorded on the tray rather than the player because it is the TRAIN that sets up, and an Extra
|
||||
* runs once and is gone; there is no second visit to claim it on.
|
||||
*/
|
||||
stopPointClaimed?: boolean;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user