more info on train moves in history. train info added to tooltip on timetable and division map. support for multiple trains in area switching. more info on train7/8 wrt location of coach on initial train build.
This commit is contained in:
@@ -79,6 +79,8 @@ export function divisionSvg(nodes: DivisionView[]): string {
|
||||
engineAt?: number;
|
||||
/** East or west, always — a Division runs east and west and so does its rolling stock. */
|
||||
facing?: 'e' | 'w';
|
||||
/** The train's printed card, so a train out on the Mainline can still be asked what it is. */
|
||||
what?: string;
|
||||
region?: number;
|
||||
direction?: string;
|
||||
stagesLeft?: number;
|
||||
@@ -311,7 +313,11 @@ export function divisionSvg(nodes: DivisionView[]): string {
|
||||
: '';
|
||||
out += `<g class="bs-train" data-tip="${esc(t.label)} \u2014 carrying ${esc(cars.join(', ') || 'no cars')}${
|
||||
cars.length ? ` (${loaded} loaded)` : ''
|
||||
}${inRegion ? ` \u00b7 region ${(t.region ?? 0) + 1} of ${c.regions}${dir}` : ''}${esc(stages)}">` +
|
||||
}${inRegion ? ` \u00b7 region ${(t.region ?? 0) + 1} of ${c.regions}${dir}` : ''}${esc(stages)}${
|
||||
// What the card prints. A train on the Mainline is exactly where "why did that leave without
|
||||
// me?" gets asked, and EXPEDITED is the answer more often than not.
|
||||
t.what ? `\n\n${esc(t.what)}` : ''
|
||||
}">` +
|
||||
`<rect x="${tx - w / 2}" y="${c.y + 22}" width="${w}" height="19" rx="3"/>` +
|
||||
`<text class="bs-tlab" x="${tx}" y="${c.y + 35}" text-anchor="middle">${esc(label)}</text>`;
|
||||
out += '</g>';
|
||||
|
||||
+16
-8
@@ -244,24 +244,32 @@ export function narrate(e: GameEvent, ctx: NarrateContext = {}): Narration {
|
||||
text: `Train ${e.trainNumber} was due out but is HELD — ${e.reason}`,
|
||||
};
|
||||
case 'trainHighballed':
|
||||
// The RULE that released it, not a restatement of the move. "Some trains seem to be moving
|
||||
// before I can switch them" was reported against a log in which every departure read alike.
|
||||
return {
|
||||
tone: 'plain',
|
||||
text: `Train ${e.trainNumber} HIGHBALLED — departed ${e.from} onto ${e.to}`,
|
||||
text: `Train ${e.trainNumber} HIGHBALLED — departed ${e.from} onto ${e.to}. Why now: ${e.why}`,
|
||||
};
|
||||
case 'trainArrived':
|
||||
/**
|
||||
* AN EXPEDITED TRAIN IS NOT COMING BACK ROUND.
|
||||
* AN EXPEDITED TRAIN IS NOT COMING BACK ROUND — AND IS NOT GONE YET EITHER.
|
||||
*
|
||||
* This said "it will highball again next Mainline Phase, so any work must happen now" to every
|
||||
* arrival. For an expedited train that is exactly backwards — Q3 departs it in the SAME Stage
|
||||
* it arrived, so there is no turn in which to work it — and the log then contradicted itself
|
||||
* two lines later when the train left. Reported after Train 4, the Express, did precisely that.
|
||||
* This said "it will highball again next Mainline Phase" to every arrival, which is backwards
|
||||
* for an Expedited train. Corrected once to "it leaves again this same Mainline Phase", which
|
||||
* was ALSO wrong and in a way that cost the player work: the expedited departure was moved to
|
||||
* Supervisor Shift precisely so the Porters and Laborers get their Stage with the train, so
|
||||
* Cargo IS available and only Local Operations is not. Telling a player there is "no turn in
|
||||
* which to work it" talked them out of the one turn they had.
|
||||
*
|
||||
* So both branches now name the phases: what you can still do with this train, and when it
|
||||
* goes. That is the whole of the reported confusion — "some trains seem to be moving before I
|
||||
* can switch or do other operations on them".
|
||||
*/
|
||||
return {
|
||||
tone: 'plain',
|
||||
text: e.expedited
|
||||
? `Train ${e.trainNumber} ARRIVED at the ${e.office} carrying ${carsLabel(e.consist)} — and EXPEDITES, so it leaves again this same Mainline Phase; there is no turn in which to work it`
|
||||
: `Train ${e.trainNumber} ARRIVED at the ${e.office} carrying ${carsLabel(e.consist)} — it stands until the next Mainline Phase, so any work must happen before then`,
|
||||
? `Train ${e.trainNumber} ARRIVED at the ${e.office} carrying ${carsLabel(e.consist)} — its card prints EXPEDITE, so it stands through the Cargo phase and departs at the END OF THIS STAGE. Porters and Laborers can work it now; it is normally gone before your next Local Operations turn, so it cannot be switched — unless §8.1 holds it for a ruling, in which case it stays and runs like any other train.`
|
||||
: `Train ${e.trainNumber} ARRIVED at the ${e.office} carrying ${carsLabel(e.consist)} — it stands here for the rest of this Stage. You can work it in Cargo now, switch it in the NEXT Stage's Local Operations, and it departs in that Stage's Mainline Phase.`,
|
||||
};
|
||||
case 'trainDiverted':
|
||||
return {
|
||||
|
||||
+101
-12
@@ -198,6 +198,14 @@ export type TrainChip = {
|
||||
cars: string[];
|
||||
engineAt: number;
|
||||
facing: 'e' | 'w';
|
||||
/**
|
||||
* The train's printed card — what it may and may not do.
|
||||
*
|
||||
* The Office card has carried this for a while; the Division map chip carried only the consist, so
|
||||
* a train out on the Mainline could not be asked what it was. Same text either way, from
|
||||
* `trainRules`, so the two views cannot describe one train differently.
|
||||
*/
|
||||
what: string;
|
||||
/**
|
||||
* Which region of a Mainline card the train is standing in, and which way it is going. Absent
|
||||
* everywhere else: a Division Point is a single queue, and inside a district a train moves by
|
||||
@@ -337,11 +345,23 @@ export type Frame = {
|
||||
* `blocked` comes out of the movement walk itself (`movesFor`), so a reason on screen is the rule
|
||||
* that actually refused the square rather than a second guess at it.
|
||||
*/
|
||||
/**
|
||||
* ONE ENTRY PER CREW, not one for the district.
|
||||
*
|
||||
* This used to be a single object built from the FIRST tray in the map, with a comment admitting
|
||||
* it: "One crew. Solitaire has one, and with more the answer would depend on which is selected".
|
||||
* More than one crew in a district is ordinary — trains stand on the A/D tracks while a local
|
||||
* shunts — and when it happened the board highlighted one crew's squares while the action list
|
||||
* offered every crew's moves, with nothing saying which was which.
|
||||
*/
|
||||
moves: {
|
||||
trayId: string;
|
||||
/** "Train 8", "the local crew" — what to call it on screen. */
|
||||
label: string;
|
||||
from: { row: number; col: number };
|
||||
to: { row: number; col: number }[];
|
||||
blocked: { coord: { row: number; col: number }; kind: string; why: string }[];
|
||||
} | null;
|
||||
}[];
|
||||
facilities: FacilityView[];
|
||||
hand: string[];
|
||||
/** What each hand card does, in the same order — names alone are not a playable hand. */
|
||||
@@ -382,6 +402,16 @@ export type Frame = {
|
||||
};
|
||||
/** 12 slots; the train number due out at each Stage, or null. */
|
||||
timetable: (number | null)[];
|
||||
/**
|
||||
* THE TRAIN'S CARD, SLOT BY SLOT — so a card played on Day 1 can still be read on Day 4.
|
||||
*
|
||||
* Reported from play: "once a train card's been played, how would I see that particular train card
|
||||
* again — what it's allowed to do and not allowed to do, and how it has to be loaded?" The card
|
||||
* goes onto the Timetable and is then gone, and its restrictions are what decide whether a train
|
||||
* can be switched, worked by Porters, or loaded at all. The Timetable is where the player already
|
||||
* looks for that train, so the card rides there.
|
||||
*/
|
||||
timetableWhat: (string | null)[];
|
||||
blocked: Impediment[];
|
||||
trains: { label: string; where: string }[];
|
||||
/**
|
||||
@@ -697,8 +727,37 @@ export function describeIntent(s: GameState, i: Intent): string {
|
||||
}
|
||||
case 'freightAgent.unjam':
|
||||
return `unjam ${i.from} at ${at(i.at)}`;
|
||||
case 'freightAgent.clearInbound':
|
||||
return `clear red box at ${at(i.at)}`;
|
||||
/**
|
||||
* SAY THAT IT PAYS NOTHING, because the obvious guess is that it does.
|
||||
*
|
||||
* Reported from play: "it wasn't obvious if that was a mechanical thing or if that's the actual
|
||||
* revenue generation — I believe that's actually where you get the revenue, and that completes
|
||||
* unloading the car." It is the first: the Revenue for an inbound load was already paid, one
|
||||
* step earlier, when the Laborer walked it off the car into the red box (`unloadCompleted` pays
|
||||
* `freightPerLoad`). Clearing the box banks nothing — it empties the one slot an inbound load
|
||||
* can finish in, so the NEXT car can be unloaded, and costs the whole Freight Agent action for
|
||||
* the Stage.
|
||||
*
|
||||
* Verified against the engine rather than read off the rules: `freightAgent.clearInbound` emits
|
||||
* `inboundCleared` alone, with no `revenueChanged` beside it.
|
||||
*/
|
||||
case 'freightAgent.clearInbound': {
|
||||
const f = areaOf(s, actor).grid.get(`${i.at.row},${i.at.col}`)?.facility ?? null;
|
||||
const car = f?.inboundBox[i.index];
|
||||
const full = f ? f.inboundBox.length >= f.capacity.inbound : false;
|
||||
// The red box serves both halves of §9: an inbound freight load that has come off its car,
|
||||
// and a coach whose passengers have detrained. Both were paid for a step earlier, and both
|
||||
// sit in the box until the Freight Agent moves them on.
|
||||
const paid =
|
||||
f?.kind === 'passenger'
|
||||
? 'the Revenue was paid when the passengers detrained'
|
||||
: 'the Revenue was paid when the load reached the box';
|
||||
const frees = f?.kind === 'passenger' ? 'more passengers can detrain here' : 'another car can be unloaded here';
|
||||
return (
|
||||
`send the ${car ? carLabel(car) : 'car'} in the red Inbound box at ${at(i.at)} to the Classification Yard` +
|
||||
` — pays nothing (${paid}); it frees${full ? ' the last' : ' a'} slot so ${frees}`
|
||||
);
|
||||
}
|
||||
case 'laborer.startLoad':
|
||||
return `start a load at ${at(i.at)}`;
|
||||
case 'laborer.advanceLoad':
|
||||
@@ -1052,6 +1111,7 @@ export function snapshot(
|
||||
classificationTotal: s.yards.classificationYard.length,
|
||||
},
|
||||
timetable: [...s.timetable],
|
||||
timetableWhat: s.timetable.map((n) => (n === null ? null : trainRules({ trainNumber: n, trainIsExtra: false }))),
|
||||
decision,
|
||||
wasted,
|
||||
option: turnOf(s, viewer).option,
|
||||
@@ -1276,7 +1336,13 @@ export function trainRules(t: {
|
||||
if (p.rules.noSwitching) parts.push('NO SWITCHING — it runs the Division and does not shunt');
|
||||
if (p.rules.terminalsOnly) parts.push('TERMINALS ONLY — Porters may work it at a Terminal and nowhere else');
|
||||
if (p.rules.coachStaysOnStationTrack) {
|
||||
parts.push('THE COACH STAYS AT THE STATION — a cut carrying it may only be set out at the Office');
|
||||
// Said "may only be set out at the Office", which reads as a place you can do it. You cannot:
|
||||
// §A.4 refuses the Office square outright, so the coach can never be set out anywhere — which
|
||||
// is why the make-up order decides whether this train can switch at all.
|
||||
parts.push(
|
||||
'THE COACH IS NEVER SET OUT — so keep it OFF the outer end of the train, or nothing can come ' +
|
||||
'off at all. Add the coach before the freight car when making up.',
|
||||
);
|
||||
}
|
||||
if (p.rules.oneFreightPerLocation) {
|
||||
parts.push('ONE FREIGHT CAR PER LOCATION — dropped or picked up, one each square per turn');
|
||||
@@ -1288,7 +1354,18 @@ export function trainRules(t: {
|
||||
parts.push('STOPS ONCE FOR SPEECHES, then runs expedited from its next Office onward');
|
||||
}
|
||||
|
||||
if (p.rules.expedite) parts.push('EXPEDITED — it departs in the same Stage it arrives (§7)');
|
||||
if (p.rules.expedite) {
|
||||
// The single most-asked question in play — "why did that train leave before I could switch it?"
|
||||
// — so it names the phases rather than the Stage. It stands through Cargo and goes at the end of
|
||||
// the Stage, which is after the Porters and Laborers but before the next Local Operations.
|
||||
parts.push(
|
||||
'EXPEDITED — it does not lay over. It stands through the Cargo phase and departs at the END OF ' +
|
||||
'THE STAGE IT ARRIVES IN, so Porters and Laborers can work it but it is normally gone before ' +
|
||||
'your next Local Operations turn and cannot be switched. UNLESS §8.1 wants a clearance ruling ' +
|
||||
'at that moment: there is no Superintendent in Supervisor Shift to give one, so the train ' +
|
||||
'stays and runs like any other from then on.',
|
||||
);
|
||||
}
|
||||
if (p.rules.stopEarnsPoint) parts.push('EARNS A POINT for one Stage spent standing still, once');
|
||||
return parts.join(' · ');
|
||||
}
|
||||
@@ -1556,24 +1633,35 @@ function countStock(
|
||||
* Only while a switching turn is actually running and only while Moves remain: a highlight that
|
||||
* survives into the Cargo phase is an invitation to click something that is no longer offered.
|
||||
*
|
||||
* One crew. Solitaire has one, and with more the answer would depend on which is selected — a
|
||||
* question the page does not yet ask.
|
||||
* EVERY crew in the district, each with its own squares. It used to return the first one it found,
|
||||
* which is the same thing in solitaire's opening but not once a train is standing at the Office
|
||||
* while a local shunts: the board then drew one crew's reachable squares and the action list offered
|
||||
* both crews' moves, so half the highlights belonged to a train the player was not moving.
|
||||
*/
|
||||
function switchingMoves(s: GameState, player: PlayerIndex): Frame['moves'] {
|
||||
const turn = turnOf(s, player);
|
||||
if (s.clock.phase !== 'localOps' || turn.option !== 'switch') return null;
|
||||
if (turn.movesRemaining < 1) return null;
|
||||
if (s.clock.phase !== 'localOps' || turn.option !== 'switch') return [];
|
||||
if (turn.movesRemaining < 1) return [];
|
||||
const out: Frame['moves'] = [];
|
||||
for (const [id, tray] of s.trays) {
|
||||
if (tray.position.at !== 'grid' || tray.position.seat !== seatOf(s, player)) continue;
|
||||
/**
|
||||
* A crew that may not switch is not a crew you can be switching.
|
||||
*
|
||||
* `movesFor` is pure track geometry — it answers where a train COULD roll — and six cards print
|
||||
* "no switching", which `check` enforces and it does not. Offering the Circus Train in a "which
|
||||
* train are you switching?" row is offering a train whose every move the rules refuse.
|
||||
*/
|
||||
if (trainProfile(tray.trainNumber ?? 0, tray.trainIsExtra)?.rules.noSwitching) continue;
|
||||
const { to, blocked } = movesFor(s, player, id);
|
||||
return { from: tray.position.coord, to, blocked };
|
||||
out.push({ trayId: id, label: trainName(s, id), from: tray.position.coord, to, blocked });
|
||||
}
|
||||
return null;
|
||||
return out;
|
||||
}
|
||||
|
||||
function trainChip(s: GameState, id: string): TrainChip {
|
||||
const t = s.trays.get(id);
|
||||
if (!t) return { label: id, consist: [], cars: [], engineAt: 0, facing: 'e' };
|
||||
if (!t) return { label: id, consist: [], cars: [], engineAt: 0, facing: 'e', what: '' };
|
||||
/**
|
||||
* The engine is drawn IN the consist, at the position it occupies.
|
||||
*
|
||||
@@ -1592,6 +1680,7 @@ function trainChip(s: GameState, id: string): TrainChip {
|
||||
cars,
|
||||
engineAt: at,
|
||||
facing: railFacingOf(t),
|
||||
what: t.trainNumber === null ? 'A local crew — no timetable, no card, no special rules.' : trainRules(t),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user