v0.7.2 — a leg that is part of the row, the deck the sheet prints, regions not miles per hour, and a Division you read left to right

Gitea#17 — a 45° leg is an end of the west-to-east row, so backing into a cut
through a curve's south leg no longer couples it back to front. The same
assumption left a crew's own cut standing when it pulled out through a leg,
which is the "cars left behind" report we had failed to reproduce.

Gitea#14 — every count is docs/Deck cards5.xlsx. Track halved, and the Q12
office doubling and Gap 12 industry tripling both come out with it: they were
measured against a deck with twice the track, and keeping them at the sheet's
track count wipes out the reefer chain entirely. 84 rows now match card for
card; the ten Safety, Event and Inspection cards it adds are not built and are
held out. Cards the sheet no longer lists are dealt zero copies rather than
deleted, so their rules stay implemented.

Gitea#15 — RAR reversed it: a rail may stop dead against its neighbour and the
placement is legal. What must hold is that no train crosses the gap, which was
already true and is now pinned against the reported board.

Gitea#3 — the printed speeds are scenery. A card costs one Stage per printed
region and where a train STARTS is what varies; Fast/Slow is read on Hilly
alone. Entering a one-region card behind another is a collision now, which is
what ABS exists to prevent, and ABS no longer holds trains silently.

Gitea#18 — the Division draws as one row, west to east, with no office-area
detail. East is finally always to the right.

Closes #3
Closes #14
Closes #15
Closes #17
Closes #18
This commit is contained in:
Jesse.Markowitz
2026-08-26 15:20:56 -04:00
parent 441447648d
commit 2ab25e320c
23 changed files with 4619 additions and 3264 deletions
+22 -8
View File
@@ -228,14 +228,28 @@ describe('the game conserves Rolling Stock', () => {
for (let t = 0; t < 50_000; t++) {
const before = census(s);
const pumped = pump(s);
// §10 — a collision destroys both trains and everything they were carrying. That is the one
// legitimate way the count falls, so the expectation follows it down.
for (const e of pumped) {
if (e.type === 'trainsDestroyed') for (const tr of e.trains) expected -= tr.consist.length;
}
assert.ok(
census(s) === before || pumped.some((e) => e.type === 'trainsDestroyed'),
`seed ${seed}: the engine changed the census by ${census(s) - before} outside a collision`,
/**
* A COLLISION DESTROYS NO CAR, and this used to assume it destroyed all of them.
*
* The subtraction that stood here — `expected -= tr.consist.length` for every train in a
* `trainsDestroyed` event — describes a rule the engine does not have. Gap 2c (`advance.ts`,
* "TAKE THE WRECK OFF THE CARD") sends the wreck's cabooses back to the Division Yard and
* everything else to Classification, so the stock is conserved through a collision like any
* other move. The train is destroyed; its cars are not.
*
* It passed for as long as it did because none of the six seeds below ever collided, so the
* branch never ran. Changing the deck to the sheet's counts (Gitea#14) moved the deals, seed
* 24757 collided, and the test failed claiming the engine had CONJURED three cars — the
* exact opposite of what had happened.
*
* So the census is now held flat, unconditionally, which is both the real invariant and a
* stronger test than the one it replaces: there is no longer any event that excuses a
* change, and `expected` cannot drift away from the supply it was dealt.
*/
assert.equal(
census(s),
before,
`seed ${seed}: the engine changed the census by ${census(s) - before} while pumping`,
);
if (s.status === 'finished') break;
const actor = s.clock.pendingDecision !== null ? s.clock.superintendent : s.clock.currentActor;