adding classification and division yard updates, train collisions, engine and car updates, turnout improvements.
This commit is contained in:
+52
-22
@@ -30,6 +30,8 @@ import {
|
||||
collectiveRevenueFloor,
|
||||
lengthProfile,
|
||||
officeProfile,
|
||||
REGIONS_PER_MAINLINE_CARD,
|
||||
mainlineProfile,
|
||||
} from './content.ts';
|
||||
import type { Direction } from './content.ts';
|
||||
import type { GameEvent } from './events.ts';
|
||||
@@ -186,7 +188,7 @@ function newTrainPhase(s: GameState, events: GameEvent[]): AdvanceResult {
|
||||
id: trayId,
|
||||
trainNumber: due,
|
||||
trainIsExtra: false,
|
||||
engineFront: true,
|
||||
engineAt: 0,
|
||||
consist: [],
|
||||
direction,
|
||||
position: { at: 'divisionPoint', side },
|
||||
@@ -219,7 +221,7 @@ function newTrainPhase(s: GameState, events: GameEvent[]): AdvanceResult {
|
||||
id: trayId,
|
||||
trainNumber: number,
|
||||
trainIsExtra: false,
|
||||
engineFront: true,
|
||||
engineAt: 0,
|
||||
consist: [],
|
||||
direction,
|
||||
position: { at: 'divisionPoint', side },
|
||||
@@ -250,7 +252,7 @@ function newTrainPhase(s: GameState, events: GameEvent[]): AdvanceResult {
|
||||
id: trayId,
|
||||
trainNumber: number,
|
||||
trainIsExtra: true,
|
||||
engineFront: true,
|
||||
engineAt: 0,
|
||||
consist: [],
|
||||
direction: 'east',
|
||||
position: { at: 'divisionPoint', side: 'west' },
|
||||
@@ -488,6 +490,53 @@ function moveTrain(
|
||||
// Q1/Q2 — crossing takes a whole number of Stages set by the card's speed and the train's
|
||||
// Fast/Slow class. Count it down rather than stepping through printed cells.
|
||||
if (transit.stagesRemaining > 1) {
|
||||
/**
|
||||
* Q13 — REAR-END COLLISIONS, answered: a train collides when it CATCHES UP.
|
||||
*
|
||||
* §10 makes a Mainline collision the Superintendent's fault and removes both trains, and ABS
|
||||
* Signals exists to stop trains rear-ending each other — but §8.3's trigger list never named
|
||||
* one and none was implemented, so granting clearance was free: both trains survived, no
|
||||
* penalty, and ABS Signals protected against nothing.
|
||||
*
|
||||
* Colliding on CATCHING UP is the version that rewards judging the gap. A following train
|
||||
* that closes on the one ahead runs into it; a following train that never closes is fine, so
|
||||
* clearance becomes a bet on relative speed rather than a formality. §2.1 divides the card
|
||||
* into two regions, and sharing one is what "caught up" means.
|
||||
*
|
||||
* ABS Signals does what it says instead: the follower stops SHORT of the collision and holds.
|
||||
*/
|
||||
const regionOf = (t: { stagesTotal: number; stagesRemaining: number }): number => {
|
||||
const entry = REGIONS_PER_MAINLINE_CARD - t.stagesTotal;
|
||||
const elapsed = t.stagesTotal - t.stagesRemaining;
|
||||
return Math.min(REGIONS_PER_MAINLINE_CARD - 1, Math.max(0, entry + elapsed));
|
||||
};
|
||||
// NOT on a card that prints "trains may pass". Double Track and Uncontrolled Siding hold two
|
||||
// trains because they HAVE two roads, so a train catching another there goes past it — that
|
||||
// is what the card is for. Without this the mechanic fired 0.41 times a game while the bot
|
||||
// never once granted clearance, which is the tell: those were all passing cards.
|
||||
const mayPass = mainlineProfile(node.card).trainsMayPass;
|
||||
const next = regionOf({ stagesTotal: transit.stagesTotal, stagesRemaining: transit.stagesRemaining - 1 });
|
||||
const ahead = mayPass
|
||||
? undefined
|
||||
: node.transits.find(
|
||||
(t) => t.tray !== id && t.direction === transit.direction && regionOf(t) === next,
|
||||
);
|
||||
|
||||
if (ahead) {
|
||||
if (node.absSignals) {
|
||||
// "Trains on this card will not rear-end each other; they stop short of a collision."
|
||||
events.push({
|
||||
type: 'trainHeld',
|
||||
trainNumber: tray.trainNumber ?? 0,
|
||||
reason: 'ABS Signals — held short of the train ahead',
|
||||
});
|
||||
return 'held';
|
||||
}
|
||||
// §10 — the Superintendent let it in behind the other, so it is the Superintendent's fault.
|
||||
collide(s, s.clock.superintendent, [id, ahead.tray], events, 'ran into the train ahead', 'the Mainline');
|
||||
return 'moved';
|
||||
}
|
||||
|
||||
transit.stagesRemaining -= 1;
|
||||
return 'moved';
|
||||
}
|
||||
@@ -756,25 +805,6 @@ function shiftChange(s: GameState, events: GameEvent[]): AdvanceResult {
|
||||
// Telegraph/Telephone/Radio are each usable once a Day.
|
||||
for (const area of s.officeAreas.values()) area.dispatchUsedToday = [];
|
||||
|
||||
/**
|
||||
* THE CLASSIFICATION YARD GOES BACK INTO SERVICE OVERNIGHT.
|
||||
*
|
||||
* ASSUMPTION, flagged rather than derived: Gap 2c says engines and cabooses return to the
|
||||
* Division Yard and everything else to Classification, and the recovered rules never say how
|
||||
* Classification empties. It was write-only — seven places pushed into it and nothing ever read
|
||||
* it — so rolling stock drained one way and left the game. Measured at 30 cars a game dead by
|
||||
* the end, 37% of the 80 dealt at setup.
|
||||
*
|
||||
* Sorting cars for redistribution is what a classification yard is FOR, and a Day is its
|
||||
* natural cycle, so they return at the Day boundary. This does not bind a 5-Day game — the
|
||||
* Division Yard still held 16.6 loaded freight cars at the end and ran dry in 2 games of 100 —
|
||||
* but a campaign-length game would have starved.
|
||||
*/
|
||||
if (s.yards.classificationYard.length > 0) {
|
||||
s.yards.divisionYard.push(...s.yards.classificationYard);
|
||||
s.yards.classificationYard = [];
|
||||
}
|
||||
|
||||
s.clock.day += 1;
|
||||
s.clock.stage = 1;
|
||||
s.collisionsToday = 0;
|
||||
|
||||
Reference in New Issue
Block a user