v0.8.0.8 — a played train does not come back; a discarded one does
Jesse's ruling on the question v0.8.0.7 filed: once a regularly scheduled train has been played its number is on the timetable, so putting it back into a reshuffled deck to be played again makes no sense. The same card sitting in a discard pile was never played and its slot is still open, so it should come back. An Extra is a single run rather than a standing slot, so a played one is free to run again. The test is therefore WHERE the card is, not only what it is — which is worth writing down, because it is exactly the rule a later tidy-up would simplify into filtering by kind everywhere. And the duplicate that started it: trainScheduled was pushing a synthetic train-<number> into the Salvage Yard beside the real card cardPlayed had already put there — four scheduled trains left eight entries in a pile holding four cards. Nothing read it, it inflated the depth, it displayed as "a card", and it would have been swept into the draw deck to be drawn as an id with nothing behind it. Removed, which retires the phantom-id class rather than papering over it, so v0.8.0.7's cardName resolver for it goes too. Games in progress resume: no predicate changed its answer, and a draw is a draw whatever is on top. What differs is the Yard's depth, which was double-counting, and what a reshuffle recovers — and reshuffles are effectively unreachable, with zero seen across eight games driven to 4000 moves. Closes #23. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X6cF1iYvJ1kNmzYBzu4QX6
This commit is contained in:
co-authored by
Claude Opus 5
parent
d0e5091824
commit
072029b1f7
+59
-1
@@ -159,6 +159,60 @@ describe('Local Operations: drawing (§6.2)', () => {
|
||||
assert.notEqual(s.decks.departments[1]![0], target, 'refilled with the same card');
|
||||
});
|
||||
|
||||
it('a PLAYED timetabled train never comes back, but a discarded one does — Gitea#23', () => {
|
||||
/**
|
||||
* Jesse's ruling, 2026-09-10: *"Once you've played a regularly scheduled train and it's in the
|
||||
* salvage deck, that train is already on the timetable. It does not make sense to put that back
|
||||
* into a reshuffled home deck to get played again. By contrast, a regularly scheduled train
|
||||
* that's in a discard pile could potentially get reused later, and so should have that
|
||||
* capability. Extras run one time and then they're done — if they are in the Salvage deck, they
|
||||
* should get shuffled back in so that they could get run again."*
|
||||
*
|
||||
* So the test is WHERE the card is, not only what it is: the same card is spent in the Salvage
|
||||
* Yard and still runnable in a Department. That is what this pins, because it is the kind of rule
|
||||
* a later tidy-up would happily "simplify" into filtering by card kind everywhere.
|
||||
*/
|
||||
const s = game();
|
||||
const kindOfCard = (id: string): string => s.cards.get(id)?.kind.kind ?? '?';
|
||||
const pool = [...s.decks.homeOffice];
|
||||
const trains = pool.filter((id) => kindOfCard(id) === 'timetabledTrain');
|
||||
const extras = pool.filter((id) => kindOfCard(id) === 'extraTrain');
|
||||
const others = pool.filter((id) => !['timetabledTrain', 'extraTrain'].includes(kindOfCard(id)));
|
||||
assert.ok(trains.length >= 2 && extras.length >= 1 && others.length >= 5, 'the deal lacks the cards this needs');
|
||||
|
||||
const spentTrain = trains[0]!; // played: in the Salvage Yard, its slot taken
|
||||
const discardedTrain = trains[1]!; // never played: sitting in a Department
|
||||
const playedExtra = extras[0]!; // a single run, free to run again
|
||||
|
||||
s.decks.salvageYard = [spentTrain, playedExtra, ...others.slice(0, 3)];
|
||||
s.decks.departments = [[discardedTrain], [others[3]!], [others[4]!]];
|
||||
s.decks.homeOffice = [others[5]!];
|
||||
|
||||
applyIntent(s, 0, { type: 'localOps.choose', option: 'draw' });
|
||||
const r = applyIntent(s, 0, { type: 'draw.fromHomeOffice' });
|
||||
assert.ok(r.ok);
|
||||
assert.ok(r.events.some((e) => e.type === 'deckReshuffled'), 'no reshuffle was emitted');
|
||||
|
||||
const recovered = new Set([...s.decks.homeOffice, ...s.decks.departments.flat()]);
|
||||
const hands = new Set([...s.decks.hands.values()].flat());
|
||||
|
||||
// THE RULING, both halves.
|
||||
assert.ok(!recovered.has(spentTrain), 'a played timetabled train was shuffled back in');
|
||||
assert.ok(!hands.has(spentTrain), 'a played timetabled train was dealt back into a hand');
|
||||
assert.ok(
|
||||
s.decks.salvageYard.includes(spentTrain),
|
||||
'a played timetabled train should stay in the Salvage Yard, not vanish',
|
||||
);
|
||||
assert.ok(
|
||||
recovered.has(discardedTrain) || hands.has(discardedTrain),
|
||||
'a DISCARDED timetabled train must come back — it was never played, so its slot is open',
|
||||
);
|
||||
assert.ok(
|
||||
recovered.has(playedExtra) || hands.has(playedExtra),
|
||||
'a played Extra must come back — an Extra is one run, not a standing slot',
|
||||
);
|
||||
});
|
||||
|
||||
it('reshuffles the Salvage Yard and Departments back in when the deck runs out', () => {
|
||||
// §6.2 — "If drawing a card has depleted the Home Office deck, immediately collect all cards
|
||||
// from the Salvage Yard and three Department decks, reshuffle, and reestablish the Home Office
|
||||
@@ -182,7 +236,11 @@ describe('Local Operations: drawing (§6.2)', () => {
|
||||
assert.ok(r.ok);
|
||||
assert.ok(r.events.some((e) => e.type === 'deckReshuffled'), 'no reshuffle was emitted');
|
||||
|
||||
assert.equal(s.decks.salvageYard.length, 0, 'the Salvage Yard must be swept');
|
||||
// Swept EXCEPT the trains whose slots are already on the timetable — see the ruling test below.
|
||||
assert.ok(
|
||||
s.decks.salvageYard.every((id) => s.cards.get(id)?.kind.kind === 'timetabledTrain'),
|
||||
'the Salvage Yard must be swept apart from spent timetabled trains',
|
||||
);
|
||||
assert.ok(s.decks.homeOffice.length > 0, 'the deck must be re-established');
|
||||
assert.ok(
|
||||
s.decks.departments.every((p) => p.length === 1),
|
||||
|
||||
@@ -3647,34 +3647,6 @@ describe('the Day rolling over says so (Gitea#10)', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('names the train on top of the Salvage Yard, instead of "a card"', () => {
|
||||
/**
|
||||
* The Salvage Yard is a FACE-UP pile and its tile reads the top card — but `trainScheduled`
|
||||
* pushes a synthetic `train-<number>` id rather than the id of the card that was played
|
||||
* (`apply.ts`), so there was nothing in `s.cards` to look up and the tile said "a card". A train
|
||||
* is scheduled several times a Day, so that id is on top most of the time: the pile was face up
|
||||
* and had nothing to say. Jesse, 2026-09-10: *"why is salvage deck not face up. I should see the
|
||||
* card played onto salvage."*
|
||||
*/
|
||||
const s = createEngineGame({
|
||||
id: 'salv',
|
||||
seed: 5,
|
||||
config: {
|
||||
mode: 'competitive', days: 5, minCombinedRevenue: 60,
|
||||
maxCollisionsPerDay: 0, maxCollisionsTotal: 0, pvpCardsAllowed: false,
|
||||
optionalRules: { reducedVisibility: false, employeeRotation: false, emergencyToolbox: false },
|
||||
},
|
||||
playerNames: ['Joe', 'Bot 1'],
|
||||
});
|
||||
s.decks.salvageYard.push('train-13');
|
||||
const f = snapshot(s, [], null);
|
||||
assert.equal(f.salvage.top, 'Train 13', 'a scheduled train on the pile must be named');
|
||||
|
||||
const html = pilesHtml(f);
|
||||
assert.ok(html.includes('Train 13'), `the Salvage tile does not name the train:\n${html}`);
|
||||
assert.equal(html.includes('>a card<'), false, 'the face-up pile still says "a card"');
|
||||
});
|
||||
|
||||
it('lights only the pile a watched move touched', () => {
|
||||
const s = createEngineGame({
|
||||
id: 'piles2',
|
||||
|
||||
Reference in New Issue
Block a user