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:
+75
-46
@@ -339,7 +339,7 @@ function mainlinePhase(s: GameState, events: GameEvent[]): AdvanceResult {
|
||||
(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) {
|
||||
if (!tray.departsThisStage && stillThere) {
|
||||
tray.stopPointClaimed = true;
|
||||
// The point goes to whoever is SITTING in the district it stopped in.
|
||||
const owner = tray.position.at === 'grid' ? playerAtSeat(s, tray.position.seat) : 0;
|
||||
@@ -364,11 +364,6 @@ function mainlinePhase(s: GameState, events: GameEvent[]): AdvanceResult {
|
||||
}
|
||||
}
|
||||
if (moved === 'needsClearance') return { events, needsInput: true };
|
||||
// An expedited train may act twice in one Stage: it arrives and departs (Q3).
|
||||
if (moved === 'expedited') {
|
||||
const again = moveTrain(s, id, tray, events);
|
||||
if (again === 'needsClearance') return { events, needsInput: true };
|
||||
}
|
||||
s.movedThisPhase.add(id);
|
||||
}
|
||||
|
||||
@@ -376,7 +371,7 @@ function mainlinePhase(s: GameState, events: GameEvent[]): AdvanceResult {
|
||||
return { events: [...events, ...enterPhase(s, 'loadUnload')], needsInput: false };
|
||||
}
|
||||
|
||||
type MoveOutcome = 'moved' | 'held' | 'needsClearance' | 'expedited';
|
||||
type MoveOutcome = 'moved' | 'held' | 'needsClearance';
|
||||
|
||||
/**
|
||||
* §8.2 — why this train is not fit to run, or null if it is.
|
||||
@@ -565,7 +560,6 @@ function moveTrain(
|
||||
from: 'the Office',
|
||||
to: 'the Mainline',
|
||||
});
|
||||
awardDeparture(s, seat, tray, events);
|
||||
return 'moved';
|
||||
}
|
||||
|
||||
@@ -580,8 +574,7 @@ function moveTrain(
|
||||
*/
|
||||
if (node.kind === 'divisionPoint') {
|
||||
area.adOccupancy = area.adOccupancy.filter((t) => t !== id);
|
||||
awardDeparture(s, seat, tray, events);
|
||||
retireTrain(s, id, tray, events);
|
||||
retireTrain(s, id, tray, node.side, events);
|
||||
return 'moved';
|
||||
}
|
||||
|
||||
@@ -663,7 +656,7 @@ function moveTrain(
|
||||
// The train has run the length of the Division and leaves the game (§2.3).
|
||||
dest.holding.push(id);
|
||||
tray.position = { at: 'divisionPoint', side: dest.side };
|
||||
retireTrain(s, id, tray, events);
|
||||
retireTrain(s, id, tray, dest.side, events);
|
||||
return 'moved';
|
||||
}
|
||||
|
||||
@@ -844,9 +837,24 @@ function arriveAtOffice(
|
||||
expedited: isExpedited(tray),
|
||||
});
|
||||
|
||||
// Q3 — an expedited train departs in the same Stage it arrived, so it is NOT added to
|
||||
// movedThisPhase and gets a second chance to move before the phase ends.
|
||||
if (isExpedited(tray)) return 'expedited';
|
||||
/**
|
||||
* Q3 — AN EXPEDITED TRAIN DEPARTS THE STAGE IT ARRIVED, BUT NOT BEFORE THE WORK IS DONE.
|
||||
*
|
||||
* It used to leave inside this very Mainline Phase — a second `moveTrain` on the spot. That made
|
||||
* "departs the Stage it arrives" true and the coaches worthless: Load/Unload runs AFTER Mainline,
|
||||
* so an expedited train was gone before a single Porter could reach it. Measured over 60 games,
|
||||
* Train 2 arrived 32 times and stood at an Office during Load/Unload exactly none of them, and
|
||||
* every coach-carrying express is expedited (1/2 Crack Limited, 5/6 The Sparrow, 19 Military).
|
||||
* The Crack Limited's own card says "stop at Terminals only", which it could never do.
|
||||
*
|
||||
* So it stands here through Load/Unload and leaves at the end of the Stage instead. It still does
|
||||
* not lay over to the next Stage — the keyword is honoured — and it is still subject to every
|
||||
* §8.1 condition when it goes, so it can be held.
|
||||
*/
|
||||
if (isExpedited(tray)) {
|
||||
tray.departsThisStage = true;
|
||||
return 'moved';
|
||||
}
|
||||
/**
|
||||
* X17 Campaign Train — the speeches happen at the first Office it reaches. Recorded AFTER the
|
||||
* expedite test above, so this arrival is the ordinary stop the card pays for and every later one
|
||||
@@ -907,47 +915,47 @@ function collide(
|
||||
}
|
||||
}
|
||||
|
||||
/** A completed run frees the crew; Extras go to the Salvage Yard (§2.3, Gap 2c). */
|
||||
/**
|
||||
* ONE REVENUE FOR EVERY TRAIN THAT GETS OUT OF YOUR SECTION.
|
||||
* ONE REVENUE TO EVERY PLAYER WHEN A TRAIN COMPLETES ITS RUN.
|
||||
*
|
||||
* Jesse's rule. Unlike freight and passengers it pays for traffic the player does not have to work —
|
||||
* which is the point: keeping the line clear is the Superintendent's job, and until now nothing paid
|
||||
* for doing it well. It rewards splitting a Subdivision with a Control Point, holding a following
|
||||
* train rather than gambling on it, and building the A/D capacity to turn arrivals around.
|
||||
* Jesse's rule, revised. It used to pay 1 to the Office a train departed — "it left YOUR section" —
|
||||
* which meant a five-Office railroad paid five times for one train and paid the most to whoever the
|
||||
* train happened to pass first. It pays once now, when the train runs off the end of the Division,
|
||||
* and it pays EVERYBODY: getting a train the whole length of the railroad is the shared achievement,
|
||||
* and every Office it crossed had to clear it.
|
||||
*
|
||||
* Paid on DEPARTURE from the Office, once per train. Not paid again when the train later runs off
|
||||
* the end of the Division and retires: it left YOUR section when it cleared your Office, and the far
|
||||
* Division Point belongs to whoever is seated there.
|
||||
* Unlike freight and passengers it pays for traffic no one has to work, which is the point: keeping
|
||||
* the line moving is the Superintendent's job, and nothing else paid for doing it well. It rewards
|
||||
* splitting a Subdivision with a Control Point, holding a following train rather than gambling on
|
||||
* it, and building the A/D capacity to turn arrivals around.
|
||||
*
|
||||
* A crew with no train number is a local switching move and not a departure, so it earns nothing.
|
||||
* A crew with no train number is a local switching move, not a run, so it earns nothing.
|
||||
*
|
||||
* PROVISIONAL, and worth watching: measured, this is roughly +5 Revenue a game against a mean of
|
||||
* 2.7, so it more than doubles the score on its own. Flagged in `TODO.md`.
|
||||
* PROVISIONAL — the previous version was worth about +5 Revenue a game against a mean of 2.7, and
|
||||
* this one pays far less often. Flagged in `TODO.md`.
|
||||
*/
|
||||
function awardDeparture(
|
||||
s: GameState,
|
||||
seat: SeatIndex,
|
||||
tray: CrewTray,
|
||||
events: GameEvent[],
|
||||
): void {
|
||||
function awardCompletedRun(s: GameState, tray: CrewTray, events: GameEvent[]): void {
|
||||
if (tray.trainNumber === null) return;
|
||||
// Paid to whoever OCCUPIES the Office the train left, not to the seat's index. Identical today,
|
||||
// and the difference is the whole point of the seat/player split.
|
||||
const owner = playerAtSeat(s, seat);
|
||||
const p = s.players[owner];
|
||||
if (!p) return;
|
||||
p.revenue += 1;
|
||||
events.push({
|
||||
type: 'revenueChanged',
|
||||
player: owner,
|
||||
delta: 1,
|
||||
total: p.revenue,
|
||||
reason: 'a train cleared your section',
|
||||
});
|
||||
for (const p of s.players) {
|
||||
p.revenue += 1;
|
||||
events.push({
|
||||
type: 'revenueChanged',
|
||||
player: p.index,
|
||||
delta: 1,
|
||||
total: p.revenue,
|
||||
reason: 'a train completed its run',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function retireTrain(s: GameState, id: TrayId, tray: CrewTray, events: GameEvent[]): void {
|
||||
/** A completed run frees the crew; Extras go to the Salvage Yard (§2.3, Gap 2c). */
|
||||
function retireTrain(
|
||||
s: GameState,
|
||||
id: TrayId,
|
||||
tray: CrewTray,
|
||||
side: Direction,
|
||||
events: GameEvent[],
|
||||
): void {
|
||||
for (const car of tray.consist) {
|
||||
if (car.type === 'caboose') s.yards.divisionYard.push(car);
|
||||
else s.yards.classificationYard.push(car);
|
||||
@@ -961,8 +969,11 @@ function retireTrain(s: GameState, id: TrayId, tray: CrewTray, events: GameEvent
|
||||
events.push({
|
||||
type: 'trainCompleted',
|
||||
trainNumber: tray.trainNumber ?? 0,
|
||||
isExtra: tray.trainIsExtra,
|
||||
side,
|
||||
consist: tray.consist.map((c) => ({ ...c })),
|
||||
});
|
||||
awardCompletedRun(s, tray, events);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -970,6 +981,24 @@ function retireTrain(s: GameState, id: TrayId, tray: CrewTray, events: GameEvent
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function shiftChange(s: GameState, events: GameEvent[]): AdvanceResult {
|
||||
/**
|
||||
* EXPEDITED DEPARTURES — the last thing that happens in a Stage (Q3, and `arriveAtOffice`).
|
||||
*
|
||||
* Run here rather than as a phase of its own because there is no decision in it: the trains that
|
||||
* arrived expedited this Stage try to leave, in train-number order like any other movement, and
|
||||
* §8.1 may still hold them. Being after Load/Unload is the whole point — it is what gives the
|
||||
* Porters their one Stage with the train.
|
||||
*/
|
||||
const expedited = [...s.trays.entries()]
|
||||
.filter(([, t]) => t.departsThisStage)
|
||||
.sort(([, a], [, b]) => (a.trainNumber ?? 0) - (b.trainNumber ?? 0));
|
||||
for (const [id, tray] of expedited) {
|
||||
tray.departsThisStage = false;
|
||||
// A clearance question cannot be asked here — the Superintendent's phase is over — so an
|
||||
// expedited train that would need a ruling simply stays, and runs normally next Stage.
|
||||
if (moveTrain(s, id, tray, events) === 'needsClearance') continue;
|
||||
}
|
||||
|
||||
// §5 — the Fedora passes every three Stages: shift changes at Stages 3, 6, 9 and 12.
|
||||
if (s.clock.stage % STAGES_PER_SHIFT === 0) {
|
||||
s.clock.superintendent = playerLeftOf(s, s.clock.superintendent);
|
||||
|
||||
Reference in New Issue
Block a user