v0.8.3 — the engine half of the audit, and the deal 0.8.2 silently changed
Seven rules faults and one dealing fault, from a four-way code audit (engine, server, client, tests) read against the code before anything was acted on. Each is pinned by a test that failed first. CHANGELOG has the reasoning; this is the list. THE DEAL. 0.8.2 put the Second Section card into the deck after its save check had run and without a line in its notes. A deck one card larger shuffles differently from the same seed, so every save on the test server refused at move 3 — the boot log shows thirteen of thirteen — while the release notes said three would resume. `withSavedDeal` (was `withSavedOpening`) now sets `secondSectionCard: false` for a config that predates the setting, and the thirteen replay exactly as 0.8.2 described: three resume, ten refuse, the same ten at the same moves. THE RULES. `check` never tested that a switching tray was in the actor's own district, so a rival's train could be shunted and the rival charged the Moves. Occupancy matched on coordinates alone, so a rival's crew blocked your track. A Department draw that emptied the deck duplicated the drawn card and destroyed the refill card. The unjam cleared the first load rather than the one named. The collision floor could not fire in Stage 12. The Expedite fault was charged once per clearance question rather than once per phase. A train held at the Limits was only ever released by another arrival, never by a departure. Docs: rules.md describes each as built (and no longer says an Expedited train departs at Shift Change — that was v0.4.8's reading, corrected in v0.4.9's code and never in the document); game-state.md's collision-floor note now matches the code. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FrCWubm9GAftYCm2hWdKwK
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
6f2a8dff09
commit
4d222a7eba
+26
-6
@@ -7,7 +7,7 @@ import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import type { CarType } from '../src/engine/content.ts';
|
||||
import { DEFENCE_ONLY_CARDS, DEFENCE_ONLY_COPIES, MODIFIER_PROFILES, OPENING_OTHER, OPENING_TRACK, SOLITAIRE_DECK_SIZE, TRACK_CARDS, TRACK_IN_DECK, withSavedOpening } from '../src/engine/content.ts';
|
||||
import { DEFENCE_ONLY_CARDS, DEFENCE_ONLY_COPIES, MODIFIER_PROFILES, OPENING_OTHER, OPENING_TRACK, SOLITAIRE_DECK_SIZE, TRACK_CARDS, TRACK_IN_DECK, withSavedDeal } from '../src/engine/content.ts';
|
||||
import {
|
||||
DECK_SIZE,
|
||||
EXTRA_TRAINS,
|
||||
@@ -680,16 +680,36 @@ describe('the starting Office, and the deck that goes with it', () => {
|
||||
it('replays a save written before the setting as the Whistle Post game it was', () => {
|
||||
/**
|
||||
* The one house rule that changes how a game is DEALT rather than how it plays, so replaying it
|
||||
* under the wrong opening is a different railroad from intent one — silently. `withSavedOpening`
|
||||
* under the wrong opening is a different railroad from intent one — silently. `withSavedDeal`
|
||||
* fills it for a save that names other rules and cannot name this one.
|
||||
*/
|
||||
const saved: { houseRules: { startingHand: 'sixRandom'; startingOffice?: 'depot' | 'whistlePost' } } =
|
||||
const saved: { houseRules: { startingHand: 'sixRandom'; startingOffice?: 'depot' | 'whistlePost'; secondSectionCard?: boolean } } =
|
||||
{ houseRules: { startingHand: 'sixRandom' } };
|
||||
assert.equal(withSavedOpening(saved).houseRules.startingOffice, 'whistlePost');
|
||||
assert.equal(withSavedDeal(saved).houseRules.startingOffice, 'whistlePost');
|
||||
// ...and from a deck without the Second Section card, which went in at the same time (v0.8.3).
|
||||
assert.equal(withSavedDeal(saved).houseRules.secondSectionCard, false);
|
||||
|
||||
// A config that names it is left exactly as it is, in both directions.
|
||||
assert.equal(withSavedOpening({ houseRules: { startingOffice: 'depot' as const } }).houseRules.startingOffice, 'depot');
|
||||
assert.equal(withSavedDeal({ houseRules: { startingOffice: 'depot' as const } }).houseRules.startingOffice, 'depot');
|
||||
assert.ok(!('secondSectionCard' in withSavedDeal({ houseRules: { startingOffice: 'depot' as const } }).houseRules));
|
||||
// And a config with no house rules at all is a fresh game, not an old save.
|
||||
assert.deepEqual(withSavedOpening({}), {});
|
||||
assert.deepEqual(withSavedDeal({}), {});
|
||||
});
|
||||
|
||||
it('deals the same deck a pre-0.8.2 save was dealt from — no Second Section card (v0.8.3)', () => {
|
||||
/**
|
||||
* The card went into the deck in v0.8.2 after that release's save check had been run. A deck one
|
||||
* card larger shuffles into a different order from the same seed, so every save on the test
|
||||
* server refused at move 3 while the release notes said three would resume. Pinned here: the
|
||||
* legacy deal has no such card and the fresh deal has exactly one.
|
||||
*/
|
||||
const count = (g: ReturnType<typeof createGame>): number =>
|
||||
[...g.cards.values()].filter((c) => c.kind.kind === 'secondSection').length;
|
||||
const fresh = createGame({ id: 'f', seed: 7, config: { ...solitaireConfig, houseRules: {} } as never, playerNames: ['A'] });
|
||||
assert.equal(count(fresh), 1, 'a fresh deal should carry one Second Section card');
|
||||
const legacy = createGame({
|
||||
id: 'l', seed: 7, config: withSavedDeal({ ...solitaireConfig, houseRules: { startingHand: 'sixRandom' } }) as never, playerNames: ['A'],
|
||||
});
|
||||
assert.equal(count(legacy), 0, 'a pre-0.8.2 save was dealt from a deck with no Second Section card');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user