Name the caboose, the train and the Running Track hazard
This commit is contained in:
+14
-6
@@ -287,11 +287,13 @@ export function describeIntent(s: GameState, i: Intent): string {
|
|||||||
case 'porter.detrain':
|
case 'porter.detrain':
|
||||||
return `detrain passengers at ${at(i.at)}`;
|
return `detrain passengers at ${at(i.at)}`;
|
||||||
case 'newTrain.placeCar':
|
case 'newTrain.placeCar':
|
||||||
return `add ${i.loaded ? 'loaded' : 'empty'} ${i.carType}`;
|
// carLabel knows a caboose carries the crew, not freight. Formatting it here by hand put
|
||||||
|
// "add loaded caboose" on a button.
|
||||||
|
return `add ${carLabel({ type: i.carType, loaded: i.loaded })}`;
|
||||||
case 'mainline.modify':
|
case 'mainline.modify':
|
||||||
return `${cardName(s, i.cardId)} on Mainline card ${i.node}`;
|
return `${cardName(s, i.cardId)} on Mainline card ${i.node}`;
|
||||||
case 'maneuver.redFlags':
|
case 'maneuver.redFlags':
|
||||||
return `Red Flags on ${i.trayId}`;
|
return `set Red Flags to protect ${trainName(s, i.trayId)} — an approaching train must stop short`;
|
||||||
case 'maneuver.flyingSwitch':
|
case 'maneuver.flyingSwitch':
|
||||||
return `Flying Switch ${i.count} car(s) into ${at(i.to)}`;
|
return `Flying Switch ${i.count} car(s) into ${at(i.to)}`;
|
||||||
case 'mainline.clearance': {
|
case 'mainline.clearance': {
|
||||||
@@ -325,7 +327,7 @@ export function describeIntent(s: GameState, i: Intent): string {
|
|||||||
case 'redFlag.play':
|
case 'redFlag.play':
|
||||||
return 'play your red flag';
|
return 'play your red flag';
|
||||||
case 'maneuver.redFlags':
|
case 'maneuver.redFlags':
|
||||||
return `Red Flags on ${i.trayId}`;
|
return `set Red Flags to protect ${trainName(s, i.trayId)} — an approaching train must stop short`;
|
||||||
default: {
|
default: {
|
||||||
// Every Intent now has a sentence, so `i` narrows to never here. Keeping the assignment makes
|
// Every Intent now has a sentence, so `i` narrows to never here. Keeping the assignment makes
|
||||||
// that a COMPILE error the day someone adds an intent without describing it — the playable UI
|
// that a COMPILE error the day someone adds an intent without describing it — the playable UI
|
||||||
@@ -385,7 +387,7 @@ export function snapshot(
|
|||||||
kind,
|
kind,
|
||||||
label,
|
label,
|
||||||
running: row === area.runningRow,
|
running: row === area.runningRow,
|
||||||
what: cellDescription(card, officeProfile(area.tier).name),
|
what: cellDescription(card, officeProfile(area.tier).name, row === area.runningRow),
|
||||||
enhancements: card.enhancements.map(prettyKey),
|
enhancements: card.enhancements.map(prettyKey),
|
||||||
tray: trayAt.get(key) ?? null,
|
tray: trayAt.get(key) ?? null,
|
||||||
cars: (card.facility?.industryTrack.length ? card.facility.industryTrack.cars : card.standing).map(carLabel),
|
cars: (card.facility?.industryTrack.length ? card.facility.industryTrack.cars : card.standing).map(carLabel),
|
||||||
@@ -579,7 +581,7 @@ export function trainName(s: GameState, trayId: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** What a card on the board does, in one short line. */
|
/** What a card on the board does, in one short line. */
|
||||||
function cellDescription(card: TrackCard, officeName: string): string {
|
function cellDescription(card: TrackCard, officeName: string, onRunning: boolean): string {
|
||||||
const g = card.geometry;
|
const g = card.geometry;
|
||||||
switch (g.kind) {
|
switch (g.kind) {
|
||||||
case 'limits':
|
case 'limits':
|
||||||
@@ -612,7 +614,13 @@ function cellDescription(card: TrackCard, officeName: string): string {
|
|||||||
if (f.kind === 'passenger') return 'passengers board and detrain here';
|
if (f.kind === 'passenger') return 'passengers board and detrain here';
|
||||||
const p = industryProfile(g.facility as never);
|
const p = industryProfile(g.facility as never);
|
||||||
const flow = p.flow === 'both' ? 'ships out AND receives' : p.flow === 'outbound' ? 'ships out' : 'receives';
|
const flow = p.flow === 'both' ? 'ships out AND receives' : p.flow === 'outbound' ? 'ships out' : 'receives';
|
||||||
return `${flow} ${p.carTypes.join('/')} · spot a matching car on its siding to work a load`;
|
// §11.2 — "Facility cards carry their own rails", so an industry on the Running Track does not
|
||||||
|
// block anything. It does inherit the Running Track's hazard: §10 makes cars left standing
|
||||||
|
// between the Limits and the Office a collision when a train arrives.
|
||||||
|
const where = onRunning
|
||||||
|
? ' · ON THE RUNNING TRACK — trains pass straight through, but a car left standing here is hit by the next arrival'
|
||||||
|
: '';
|
||||||
|
return `${flow} ${p.carTypes.join('/')} · spot a matching car on its siding to work a load${where}`;
|
||||||
}
|
}
|
||||||
case 'track':
|
case 'track':
|
||||||
switch (g.geometry) {
|
switch (g.geometry) {
|
||||||
|
|||||||
@@ -338,6 +338,69 @@ describe('the page explains itself', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('never puts an internal tray id on a BUTTON either', () => {
|
||||||
|
// The history check below missed this: `maneuver.redFlags` was labelled "Red Flags on tray3",
|
||||||
|
// so the id leaked through the action list rather than the log. Every button, every turn.
|
||||||
|
const game = newGame(2345);
|
||||||
|
for (let i = 0; i < 400; i++) {
|
||||||
|
if (currentActor(game) === null) break;
|
||||||
|
const { options } = actionGroups(game);
|
||||||
|
if (options.length === 0) break;
|
||||||
|
for (const o of options) {
|
||||||
|
const label = describeIntent(game.state, o);
|
||||||
|
assert.doesNotMatch(label, /\btray\d+\b/, `internal tray id on a button: ${label}`);
|
||||||
|
}
|
||||||
|
if (!submit(game, options[0]!)) break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('never offers to load a caboose', () => {
|
||||||
|
// A caboose carries the crew, not freight. "add loaded caboose" appeared because the button was
|
||||||
|
// formatted by hand instead of using the labeller that already knew.
|
||||||
|
const game = newGame(2345);
|
||||||
|
for (let i = 0; i < 400; i++) {
|
||||||
|
if (currentActor(game) === null) break;
|
||||||
|
const { options } = actionGroups(game);
|
||||||
|
if (options.length === 0) break;
|
||||||
|
for (const o of options) {
|
||||||
|
assert.doesNotMatch(
|
||||||
|
describeIntent(game.state, o),
|
||||||
|
/(loaded|empty) caboose/,
|
||||||
|
'a caboose was described as loaded or empty',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!submit(game, options[0]!)) break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('warns that an industry on the Running Track is in the way of arrivals', () => {
|
||||||
|
// §11.2 — a Facility carries its own rails, so it does NOT block traffic. But §10 makes a car
|
||||||
|
// left standing between the Limits and the Office a collision, and nothing said so.
|
||||||
|
const game = newGame(9);
|
||||||
|
const area = game.state.officeAreas.get(0)!;
|
||||||
|
const industry = {
|
||||||
|
geometry: { kind: 'facility' as const, facility: 'grocersWarehouse' as const, axis: 'ew' as const },
|
||||||
|
baseOperationalRail: true, standing: [], modifiers: [], enhancements: [],
|
||||||
|
facility: {
|
||||||
|
kind: 'freight' as const, subtype: 'grocersWarehouse',
|
||||||
|
allows: { outbound: false, inbound: true },
|
||||||
|
outboundBox: [], inboundBox: [], capacity: { outbound: 0, inbound: 1 },
|
||||||
|
menAtWork: [null, null, null], industryTrack: { length: 2, cars: [] },
|
||||||
|
laborers: 1, porters: 0, usedThisStage: { laborers: 0, porters: 0 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
area.grid.set(`${area.runningRow},2`, industry as never);
|
||||||
|
area.grid.set(`${area.runningRow - 1},0`, JSON.parse(JSON.stringify(industry)) as never);
|
||||||
|
|
||||||
|
const cells = view(game).cells;
|
||||||
|
const onRunning = cells.find((c) => c.row === area.runningRow && c.col === 2)!;
|
||||||
|
const below = cells.find((c) => c.row === area.runningRow - 1 && c.col === 0)!;
|
||||||
|
|
||||||
|
assert.match(onRunning.what, /RUNNING TRACK/, 'no warning on a Running Track industry');
|
||||||
|
assert.match(onRunning.what, /pass straight through/, 'does not say traffic still passes');
|
||||||
|
assert.doesNotMatch(below.what, /RUNNING TRACK/, 'a Secondary Track industry must not warn');
|
||||||
|
});
|
||||||
|
|
||||||
it('never puts an internal tray id in front of a player', () => {
|
it('never puts an internal tray id in front of a player', () => {
|
||||||
// The §8.1 clearance question read "may tray2 follow tray3 into the next Subdivision?" — the
|
// The §8.1 clearance question read "may tray2 follow tray3 into the next Subdivision?" — the
|
||||||
// sharpest decision in the game, phrased in internal identifiers.
|
// sharpest decision in the game, phrased in internal identifiers.
|
||||||
|
|||||||
Reference in New Issue
Block a user