v0.4.2 fixed passenger trains not stopping at office. changeed revenue for train completing run - 1 point to every player when train exits division

This commit is contained in:
Jesse
2026-08-13 21:29:52 -04:00
parent a7221dcf20
commit 393a669418
21 changed files with 1930 additions and 1531 deletions
+47 -10
View File
@@ -457,12 +457,35 @@ function refusesThisOffice(s: GameState, player: PlayerIndex, tray: CrewTray): b
* this works out whether a card is the reason. Without it a Military train standing at the platform
* reported "no train at the Office", which is both wrong and unhelpful.
*/
function passengerRefusal(s: GameState, player: PlayerIndex): RejectionCode {
function passengerRefusal(
s: GameState,
player: PlayerIndex,
at: GridCoord,
dir: 'board' | 'detrain',
): RejectionCode {
const area = areaOf(s, player);
const trains = area.adOccupancy.map((id) => s.trays.get(id)).filter((t): t is CrewTray => !!t);
if (trains.length > 0 && trains.every((t) => refusesThisOffice(s, player, t))) return 'NOT_A_TERMINAL';
if (trains.length > 0 && trains.every(refusesPassengers)) return 'NO_PASSENGER_WORK';
return 'NO_TRAIN_AT_OFFICE';
if (trains.length === 0) return 'NO_TRAIN_AT_OFFICE';
/**
* A TRAIN IS STANDING THERE, so say what is actually missing.
*
* This used to fall through to `NO_TRAIN_AT_OFFICE` — told to a player looking straight at a train
* on their own A/D track, which reads as a broken game rather than a rule. Measured over 60
* solitaire games it fired 51 times with a coach train in front of the player: 27 with nobody
* waiting to travel, 24 with passengers waiting and every coach already full.
*/
const f = facilityAt(s, player, at);
if (!f || f.kind !== 'passenger') return 'NO_SUCH_FACILITY';
if (dir === 'board') {
if (!f.outboundBox.some((c) => c.type === 'coach' && c.loaded)) return 'NO_PASSENGERS_WAITING';
return 'NO_EMPTY_COACH';
}
if (f.inboundBox.length >= f.capacity.inbound) return 'INBOUND_BOX_FULL';
if (!s.yards.divisionYard.some((c) => c.type === 'coach' && !c.loaded)) return 'NO_EMPTY_COACH_IN_YARD';
return 'NO_LOADED_COACH';
}
// ---------------------------------------------------------------------------
@@ -790,17 +813,31 @@ export function check(s: GameState, player: PlayerIndex, i: Intent): RejectionCo
}
// -- Load / Unload --------------------------------------------------------
case 'porter.board':
/**
* A WHISTLE POST HAS NO PORTERS AT ALL, which is not the same as having used them.
*
* The Office card carries a passenger facility at every tier so that an upgrade is a property
* change rather than a card swap — but a Whistle Post's has `porters: 0`, so this fell into
* `RESOURCE_SPENT`, "all Porters already used this Stage". A player who had used nothing was
* told they had spent it all, when the answer was to upgrade the Office. 15 times in 60 games.
*/
case 'porter.board': {
if (!inPhase(s, 'loadUnload')) return 'WRONG_PHASE';
if (!facilityAt(s, player, i.at)) return 'NO_SUCH_FACILITY';
if (portersLeft(facilityAt(s, player, i.at)!) < 1) return 'RESOURCE_SPENT';
return canBoard(s, player, i.at) ? null : passengerRefusal(s, player);
const f = facilityAt(s, player, i.at);
if (!f) return 'NO_SUCH_FACILITY';
if (f.porters < 1) return 'NO_PORTERS_HERE';
if (portersLeft(f) < 1) return 'RESOURCE_SPENT';
return canBoard(s, player, i.at) ? null : passengerRefusal(s, player, i.at, 'board');
}
case 'porter.detrain':
case 'porter.detrain': {
if (!inPhase(s, 'loadUnload')) return 'WRONG_PHASE';
if (!facilityAt(s, player, i.at)) return 'NO_SUCH_FACILITY';
if (portersLeft(facilityAt(s, player, i.at)!) < 1) return 'RESOURCE_SPENT';
return canDetrain(s, player, i.at) ? null : passengerRefusal(s, player);
const f = facilityAt(s, player, i.at);
if (!f) return 'NO_SUCH_FACILITY';
if (f.porters < 1) return 'NO_PORTERS_HERE';
if (portersLeft(f) < 1) return 'RESOURCE_SPENT';
return canDetrain(s, player, i.at) ? null : passengerRefusal(s, player, i.at, 'detrain');
}
case 'laborer.startLoad': {
if (!inPhase(s, 'loadUnload')) return 'WRONG_PHASE';