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:
co-authored by
Claude Opus 5
parent
6058f6c17e
commit
7ade60e21f
+85
-2
@@ -13,8 +13,8 @@ import { DEFAULT_MAX_COLLISIONS_PER_DAY, DEFAULT_MAX_COLLISIONS_TOTAL, collectiv
|
||||
import type { GameEvent } from '../src/engine/events.ts';
|
||||
import { areaOf } from '../src/engine/apply.ts';
|
||||
import { createGame } from '../src/engine/setup.ts';
|
||||
import { coordKey } from '../src/engine/state.ts';
|
||||
import type { GameConfig, GameState } from '../src/engine/state.ts';
|
||||
import { coordKey, turnOf } from '../src/engine/state.ts';
|
||||
import type { GameConfig, GameState, GridCoord } from '../src/engine/state.ts';
|
||||
import { developerBot, playGame } from '../src/sim/bot.ts';
|
||||
import { impediments, isVisible, narrate, phaseLabel } from '../src/sim/narrate.ts';
|
||||
import { compress, rehydrateCells, record, renderHtml } from '../src/sim/replay.ts';
|
||||
@@ -138,6 +138,89 @@ describe('impediments', () => {
|
||||
s.freeTrays = [];
|
||||
assert.ok(impediments(s, 0).some((b) => /HELD/.test(b.why)));
|
||||
});
|
||||
|
||||
/** A crew standing on `at`, working the given train, with one empty tank car on the drawbar. */
|
||||
const express = (s: GameState, trainNumber: number, at: GridCoord): string => {
|
||||
const id = s.freeTrays.pop()!;
|
||||
s.trays.set(id, {
|
||||
id, trainNumber, trainIsExtra: false, engineAt: 0,
|
||||
consist: [{ type: 'tank', loaded: false, origin: 0 }],
|
||||
direction: 'east', position: { at: 'grid', seat: 0, coord: at }, movesUsed: 0,
|
||||
});
|
||||
return id;
|
||||
};
|
||||
|
||||
/**
|
||||
* GITEA#21 — THE GAME REFUSED, AND THE PANEL EXPLAINED SOMETHING ELSE.
|
||||
*
|
||||
* "I wanted to drop two empty tank cars so that the freight agents and men at work could load
|
||||
* them later. I dropped the first tank car, but that was all I was allowed to do. Checked
|
||||
* 'Blocked — why nothing is moving' and saw: refinery 1,0 — green box empty — nothing to load
|
||||
* (needs a Freight Agent action)."
|
||||
*
|
||||
* Replayed from the attached save (seed 550943578, 181 intents): the crew was Train 3, and the
|
||||
* engine's answer was `FREIGHT_WORKED_HERE`. Train 3 is the Express, and the Express prints "May
|
||||
* drop or pick up one freight car at every location" — so THE REFUSAL WAS CORRECT and the rule
|
||||
* is not what is wrong here. It resets next turn, and the Express may work a car at the next
|
||||
* square this turn; that is what makes it an Express rather than a one-car-a-Stage train.
|
||||
*
|
||||
* What was wrong is that nothing said so. The panel whose entire job is "why is nothing moving?"
|
||||
* listed the refinery's green box — a true statement about the FACILITY, and nothing to do with
|
||||
* why the drop was refused — so the player was sent to fix a Freight Agent action that would not
|
||||
* have helped. The rule was on the train card's own tooltip, which is not where somebody looks
|
||||
* when a button they expected is missing.
|
||||
*
|
||||
* The panel is where a refusal gets explained, so the budget belongs in it.
|
||||
*/
|
||||
it('says when the Express has spent its one freight car on this square (Gitea#21)', () => {
|
||||
const s = createGame({ id: 'g', seed: 5, config, playerNames: ['p'] });
|
||||
const area = areaOf(s, 0);
|
||||
const at = area.officeCoord;
|
||||
|
||||
// A crew standing at the Office working Train 3 — the Express — with a tank car still on it.
|
||||
const trayId = express(s, 3, at);
|
||||
|
||||
s.clock.phase = 'localOps';
|
||||
const turn = turnOf(s, 0);
|
||||
turn.option = 'switch';
|
||||
|
||||
// Nothing to say before it has worked anything here.
|
||||
assert.ok(
|
||||
!impediments(s, 0).some((b) => /FREIGHT CAR PER LOCATION/i.test(b.why)),
|
||||
'the budget was reported spent before the train had worked a car at all',
|
||||
);
|
||||
|
||||
// Now it has set one car out here — exactly the state the save is in at intent 181.
|
||||
turn.freightWorked[`${trayId}@${coordKey(at)}`] = 1;
|
||||
|
||||
const row = impediments(s, 0).find((b) => /FREIGHT CAR PER LOCATION/i.test(b.why));
|
||||
assert.ok(
|
||||
row,
|
||||
`nothing explained the refusal:\n${JSON.stringify(impediments(s, 0), null, 2)}`,
|
||||
);
|
||||
// It must name the train, or a player with three crews out cannot tell which one it means.
|
||||
assert.match(row!.where, /Train 3/);
|
||||
// And it must say the limit lifts, or it reads as "this train can never work here again".
|
||||
assert.match(row!.why, /turn/i);
|
||||
// Amber: this is the printed rule doing its job, not a fault.
|
||||
assert.equal(row!.severity, 'waiting');
|
||||
});
|
||||
|
||||
it('leaves every other train alone — the rule is printed on 3 and 4 only (Gitea#21)', () => {
|
||||
const s = createGame({ id: 'g', seed: 5, config, playerNames: ['p'] });
|
||||
const area = areaOf(s, 0);
|
||||
const at = area.officeCoord;
|
||||
// Train 5 is The Sparrow, which prints no per-location freight limit.
|
||||
const trayId = express(s, 5, at);
|
||||
s.clock.phase = 'localOps';
|
||||
turnOf(s, 0).option = 'switch';
|
||||
turnOf(s, 0).freightWorked[`${trayId}@${coordKey(at)}`] = 1;
|
||||
|
||||
assert.ok(
|
||||
!impediments(s, 0).some((b) => /FREIGHT CAR PER LOCATION/i.test(b.why)),
|
||||
'a train with no such rule was told it had spent a budget it does not have',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user