more fixes and tweaks
This commit is contained in:
@@ -726,3 +726,38 @@ The temptation is to rebuild everything at once. I would not.
|
||||
|
||||
**Do not retune anything against current measurements.** Gap 12's numbers describe a placeholder
|
||||
ruleset. Re-measure after step 5 and treat everything before that as void.
|
||||
|
||||
---
|
||||
|
||||
## §10 Q13 — rear-end collisions on a Mainline card are not implemented
|
||||
|
||||
**Found by playing.** The Superintendent's §8.1 ruling asks whether one train may follow another onto
|
||||
an occupied Mainline card. Granting it is currently **free**: both trains cross, nothing collides, no
|
||||
penalty. Verified directly — two trains on one card, clearance granted, both alive and revenue
|
||||
unchanged.
|
||||
|
||||
Two places in the rules say it should not be free:
|
||||
|
||||
- **§10** — "A collision on a Mainline card is the fault of the Superintendent who cleared the trains
|
||||
out; he loses 5 Revenue points" and "**Train-on-train.** Both trains are immediately removed."
|
||||
- **ABS Signals** — "Trains on this card will not rear-end each other. They will stop short of a
|
||||
collision." A card whose entire purpose is preventing rear-enders presupposes that rear-enders
|
||||
happen.
|
||||
|
||||
But **§8.3's trigger list does not include it.** The three automatic triggers are all about arriving
|
||||
at an Office or highballing past fouled track; none covers a follower catching a leader mid-card. So
|
||||
the rule is asserted in §10 and priced by ABS Signals, yet never given a trigger.
|
||||
|
||||
The missing piece is **timing**, and it is a design decision rather than a transcription gap.
|
||||
Crossing is counted in Stages (Q1), so both trains simply run their counters down. Candidates:
|
||||
|
||||
1. **On entry** — following onto an occupied card wrecks both at once. Makes clearance a pure gamble
|
||||
and ABS Signals very strong.
|
||||
2. **On catching up** — collide only when the follower's remaining Stages would take it past the
|
||||
leader. Rewards judging the gap, which is what a Superintendent actually does.
|
||||
3. **Never** — clearance is genuinely safe, §10's Mainline clause is vestigial, and ABS Signals is
|
||||
worth less than it reads.
|
||||
|
||||
Until this is settled the clearance buttons describe only what the engine actually does — they say
|
||||
the train "closes up behind" rather than promising a −5 risk that cannot occur. Wording that invents
|
||||
a consequence is worse than wording that under-sells one.
|
||||
|
||||
+19
-3
@@ -35,7 +35,7 @@ import type { Direction } from './content.ts';
|
||||
import type { GameEvent } from './events.ts';
|
||||
import { acceptsCar, areaOf } from './apply.ts';
|
||||
import { legalActions } from './legal.ts';
|
||||
import type { CrewTray, DivisionNode, GameState, PlayerIndex, TrayId } from './state.ts';
|
||||
import type { CrewTray, DivisionNode, GameState, PlayerIndex, RollingStock, TrayId } from './state.ts';
|
||||
import { coordKey, freshTurn, totalRevenue } from './state.ts';
|
||||
|
||||
export type AdvanceResult = {
|
||||
@@ -630,7 +630,7 @@ function arriveAtOffice(
|
||||
});
|
||||
return 'moved';
|
||||
}
|
||||
collide(s, owner, [id], events, 'no free A/D track');
|
||||
collide(s, owner, [id], events, 'no free A/D track', 'the Office');
|
||||
return 'moved';
|
||||
}
|
||||
|
||||
@@ -647,7 +647,7 @@ function arriveAtOffice(
|
||||
// is not expecting them (§A.4), so this is a collision too, not a coupling.
|
||||
const officeCard = area.grid.get(coordKey(area.officeCoord));
|
||||
if (officeCard && officeCard.standing.length > 0) {
|
||||
collide(s, owner, [id], events, 'cars fouling the Running Track');
|
||||
collide(s, owner, [id], events, 'cars fouling the Running Track', 'the Running Track');
|
||||
return 'moved';
|
||||
}
|
||||
|
||||
@@ -672,10 +672,22 @@ function collide(
|
||||
trains: TrayId[],
|
||||
events: GameEvent[],
|
||||
reason: string,
|
||||
where = 'the Office',
|
||||
): void {
|
||||
// Record WHAT was lost before removing it. The log said only "COLLISION: NO FREE A/D TRACK", so a
|
||||
// player could see the 5 points go without learning which train had just been written off, or
|
||||
// what it was carrying.
|
||||
const lost: { label: string; consist: RollingStock[] }[] = [];
|
||||
for (const id of trains) {
|
||||
const tray = s.trays.get(id);
|
||||
if (!tray) continue;
|
||||
lost.push({
|
||||
label:
|
||||
tray.trainNumber === null
|
||||
? 'the local crew'
|
||||
: `Train ${tray.trainIsExtra ? 'X' : ''}${tray.trainNumber}`,
|
||||
consist: [...tray.consist],
|
||||
});
|
||||
// Gap 2c — engines and cabooses return to the Division Yard, everything else to Classification.
|
||||
for (const car of tray.consist) {
|
||||
if (car.type === 'caboose') s.yards.divisionYard.push(car);
|
||||
@@ -685,6 +697,10 @@ function collide(
|
||||
s.freeTrays.push(id);
|
||||
}
|
||||
|
||||
if (lost.length > 0) {
|
||||
events.push({ type: 'trainsDestroyed', player: faultPlayer, trains: lost, reason, where });
|
||||
}
|
||||
|
||||
s.collisionsToday += 1;
|
||||
const player = s.players[faultPlayer];
|
||||
if (player) {
|
||||
|
||||
@@ -31,6 +31,13 @@ export type GameEvent =
|
||||
| { type: 'trackLaid'; player: PlayerIndex; geometry: string; hand: string; at: GridCoord; variant: number; remaining: number }
|
||||
| { type: 'mainlineModified'; player: PlayerIndex; cardId: CardId; node: number; key: string; became?: string }
|
||||
| { type: 'redFlagsSet'; player: PlayerIndex; cardId: CardId; trayId: TrayId; node: number }
|
||||
| {
|
||||
type: 'trainsDestroyed';
|
||||
player: PlayerIndex;
|
||||
trains: { label: string; consist: RollingStock[] }[];
|
||||
reason: string;
|
||||
where: string;
|
||||
}
|
||||
| { type: 'flyingSwitch'; player: PlayerIndex; cardId: CardId; trayId: TrayId; to: GridCoord; stock: RollingStock[] }
|
||||
| { type: 'officeUpgraded'; player: PlayerIndex; from: OfficeTier; to: OfficeTier }
|
||||
| { type: 'cardDiscarded'; player: PlayerIndex; cardId: CardId; toSlot: number }
|
||||
|
||||
@@ -294,6 +294,27 @@ export function narrate(e: GameEvent, ctx: NarrateContext = {}): Narration {
|
||||
tone: 'good',
|
||||
text: `${e.key.toUpperCase()} used (+${e.bonus}) — Train ${e.trainNumber} wins the meet against Train ${e.againstTrain}, which now counts as number ${e.againstTrain + e.bonus}. Once a Day only.`,
|
||||
};
|
||||
case 'trainsDestroyed': {
|
||||
// Say what hit what, where, and what was written off. "COLLISION: NO FREE A/D TRACK" told a
|
||||
// player the score had changed and nothing else.
|
||||
const why =
|
||||
e.reason === 'no free A/D track'
|
||||
? `it arrived at ${e.where} with every A/D track already occupied — there was nowhere to put it (§8.3)`
|
||||
: e.reason === 'cars fouling the Running Track'
|
||||
? `it ran into cars left standing on ${e.where} between the Limits and the Office (§8.3)`
|
||||
: e.reason;
|
||||
const wrecked = e.trains
|
||||
.map((t) => `${t.label} (${t.consist.length ? carsLabel(t.consist) : 'no cars'})`)
|
||||
.join(' and ');
|
||||
return {
|
||||
tone: 'bad',
|
||||
text:
|
||||
`COLLISION — ${wrecked} destroyed: ${why}. Engines and cabooses go back to the Division ` +
|
||||
`Yard, all other cars to the Classification Yard (§10). A Timetabled train card returns ` +
|
||||
`to its slot and runs again next Day; an Extra is gone for good.`,
|
||||
};
|
||||
}
|
||||
|
||||
case 'clearanceRequested':
|
||||
return {
|
||||
tone: 'bad',
|
||||
|
||||
+6
-2
@@ -302,9 +302,13 @@ export function describeIntent(s: GameState, i: Intent): string {
|
||||
const pending = s.clock.pendingDecision;
|
||||
const who = pending ? trainName(s, pending.train) : 'the train';
|
||||
const ahead = pending ? trainName(s, pending.occupiedBy) : 'the train ahead';
|
||||
// NOT "risks a collision, −5". A rear-end on a Mainline card is described by §10 and is what
|
||||
// ABS Signals exists to prevent, but no such collision is implemented — granting clearance is
|
||||
// currently free. Saying otherwise invents a consequence the engine will never deliver.
|
||||
// See implications.md §10 Q13.
|
||||
return i.allow
|
||||
? `ALLOW — ${who} follows ${ahead} into the same Subdivision (risks a collision, −5 Revenue)`
|
||||
: `HOLD — ${who} waits where it is: safe, but it loses the Stage`;
|
||||
? `ALLOW — ${who} follows ${ahead} onto the same Mainline card, closing up behind it`
|
||||
: `HOLD — ${who} waits where it is, losing the Stage but keeping the line clear`;
|
||||
}
|
||||
case 'draw.fromDepartment': {
|
||||
// Naming the card is the whole point of a FACE-UP slot: "slot 2" tells a player nothing, and
|
||||
|
||||
+2
-1
@@ -81,7 +81,8 @@ const GROUP_ORDER: readonly { prefix: string; title: string }[] = [
|
||||
{ prefix: 'card.', title: 'Cards' },
|
||||
{ prefix: 'track.lay', title: 'Lay track from your supply' },
|
||||
{ prefix: 'mainline.modify', title: 'Mainline modifiers' },
|
||||
{ prefix: 'maneuver.', title: 'Manoeuvres' },
|
||||
// American spelling throughout, to match MANEUVER_CARDS and the source deck.
|
||||
{ prefix: 'maneuver.', title: 'Maneuvers' },
|
||||
{ prefix: 'freightAgent.', title: 'Freight Agent' },
|
||||
{ prefix: 'newTrain.', title: 'Making up the train' },
|
||||
{ prefix: 'porter.', title: 'Porters' },
|
||||
|
||||
+4
-2
@@ -435,8 +435,10 @@ describe('the page explains itself', () => {
|
||||
assert.match(label, /Train 4/, `the ruling does not name the train: ${label}`);
|
||||
assert.doesNotMatch(label, /tray\d/, `internal id on a button: ${label}`);
|
||||
}
|
||||
assert.match(allow, /collision/, 'granting clearance does not mention the risk');
|
||||
assert.match(hold, /safe|loses/, 'holding does not mention the cost');
|
||||
// Deliberately NOT asserting that granting clearance mentions a collision: no rear-end is
|
||||
// implemented, and promising one would describe a consequence the engine never delivers.
|
||||
assert.match(allow, /same Mainline card/, 'granting clearance does not say what happens');
|
||||
assert.match(hold, /losing the Stage/, 'holding does not mention the cost');
|
||||
});
|
||||
|
||||
it('states the objective and whether you are keeping up', () => {
|
||||
|
||||
Reference in New Issue
Block a user