v0.7.9.1 — the engine was right twice; the screen was not

Gitea#22: the Division map drew every westbound train in the wrong half of
its Mainline card. `regionOfTransit` counts from the end a train entered,
which is what the collision rules ask; the map wanted "which printed box,
left to right" and used the same number, so an eastbound train came out
right by luck and a westbound one came out mirrored. It cost a collision —
Train 3 was cleared to follow T5 and ran into TX17, which the picture had
drawn ahead of T5 rather than behind it. One mirror in `view.ts`, at the
boundary the map is drawn from; the collision rules are untouched.

Gitea#21: a second tank car would not come off at a refinery, and "Blocked
— why nothing is moving" answered by describing the refinery's green box.
The real answer was Train 3's printed rule — the Express works one freight
car per location — so the refusal was correct and the panel sent the player
to spend a Freight Agent action that could not have helped. No rule
changed. The panel now names the budget, asking the reducer's own
predicate so its words cannot drift from the rule.

Both were replayed from the saves attached to the issues and verified in
the exact position each report names. The map fix is proved by mutation:
reverting the mirror fails two tests, and making the renderer ignore the
region fails a third. Every existing region test ran eastbound, where the
mirror is the identity, which is why the bug survived them.

891 tests pass, up from 884.

Closes #21
Closes #22

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E3Qk7uresKCHksdZajXCLg
This commit is contained in:
Jesse.Markowitz
2026-09-07 11:41:23 -04:00
co-authored by Claude Opus 5
parent 6058f6c17e
commit 7ade60e21f
11 changed files with 412 additions and 21 deletions
+37 -1
View File
@@ -18,7 +18,7 @@
import { MAX_CONSIST } from '../engine/content.ts';
import { adTrackCount, coordKey, seatOf, turnOf } from '../engine/state.ts';
import type { GameState, GridCoord, PlayerIndex, RollingStock, SeatIndex, TrayId } from '../engine/state.ts';
import { areaOf, canAdvanceLoad, canBoard, canDetrain, canStartLoad, facilityCarType, facilityCarTypes, laborersLeft, movesFor, passengerRefusal, portersLeft } from '../engine/apply.ts';
import { areaOf, canAdvanceLoad, canBoard, canDetrain, canStartLoad, facilityCarType, facilityCarTypes, freightRuleSpentHere, isFreight, laborersLeft, movesFor, passengerRefusal, portersLeft } from '../engine/apply.ts';
import type { GameEvent } from '../engine/events.ts';
// ---------------------------------------------------------------------------
@@ -746,6 +746,42 @@ export function impediments(s: GameState, player: PlayerIndex = 0): Impediment[]
* Only while switching, and only the cards actually in the crew's way: `movesFor` reports the
* squares the movement walk reached and refused, not every square on the board.
*/
/**
* WHY A CAR WILL NOT COME OFF (Gitea#21).
*
* "I dropped the first tank car, but that was all I was allowed to do" — and this panel, asked
* why, talked about the refinery's green box. The rule that actually refused is printed on the
* train: trains 3/4, the Express, "may drop or pick up one freight car at every location". The
* refusal was correct. Nothing said it.
*
* That is a worse failure than a missing button, because the panel did not stay silent — it
* offered a true statement about the FACILITY, which sent the player to spend a Freight Agent
* action that could not have helped. The rule was on the train card's tooltip, which is not
* where anyone looks when a button they expected is simply absent.
*
* Only when the crew has a freight car it could otherwise set out. A budget spent by a train
* with nothing left to drop is not blocking anything, and this panel earns its keep by being
* short enough to read.
*/
const turnNow = turnOf(s, player);
if (s.clock.phase === 'localOps' && turnNow.option === 'switch') {
for (const [id, tray] of s.trays) {
if (tray.position.at !== 'grid' || tray.position.seat !== seatOf(s, player)) continue;
if (!tray.consist.some(isFreight)) continue;
if (!freightRuleSpentHere(s, player, id)) continue;
const { row, col } = tray.position.coord;
out.push({
where: `Train ${tray.trainNumber} at (${col},${row})`,
why:
'ONE FREIGHT CAR PER LOCATION — this train has already worked a freight car on this ' +
'square, so no more come off or on here until next turn. It may still work one at the ' +
'next square it reaches.',
// The printed rule doing its job, and it lifts by itself. Amber, not red.
severity: 'waiting',
});
}
}
const turn = turnOf(s, player);
if (s.clock.phase === 'localOps' && turn.option === 'switch' && turn.movesRemaining > 0) {
for (const [id, tray] of s.trays) {