Track becomes a deck card, Departments become decks, and trains must be made up to leave, industry restrictions enforced, update display and add a new game button
This commit is contained in:
+42
-20
@@ -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 { MODIFIER_PROFILES, SOLITAIRE_DECK_SIZE, TRACK_PER_PLAYER } from '../src/engine/content.ts';
|
||||
import { MODIFIER_PROFILES, SOLITAIRE_DECK_SIZE, TRACK_CARDS, TRACK_IN_DECK } from '../src/engine/content.ts';
|
||||
import {
|
||||
DECK_SIZE,
|
||||
EXTRA_TRAINS,
|
||||
@@ -49,21 +49,27 @@ const newSolitaireGame = (seed = 1234) =>
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('card catalogue (component 1)', () => {
|
||||
it('composes the 140-card deck from the design', () => {
|
||||
// Transcribed from docs/Deck cards2.xlsx, whose own total is 115 — plus 18 extra industry cards
|
||||
// (Gap 12, industries 9 → 27) and 7 extra office cards (Q12, offices 7 → 14). Both are
|
||||
// deliberate departures from the sheet and both are flagged provisional in content.ts.
|
||||
it('composes the deck from the design', () => {
|
||||
// Transcribed from docs/Deck cards2.xlsx. The sheet's own totals are "Sum other 115" and
|
||||
// "Total track 104", i.e. 219, plus 12 start cards for its grand total of 231.
|
||||
//
|
||||
// 139, not 140: Poling is dealt ZERO copies. Its effect is "TBD in the source", so a card that
|
||||
// cannot be played is worse in a hand than absent from the deck. The entry stays in the
|
||||
// We are at 243 rather than 219 because of two deliberate departures, both flagged provisional
|
||||
// in content.ts: 18 extra industry cards (Gap 12, industries 9 → 27) and 7 extra office cards
|
||||
// (Q12, offices 7 → 14). Both were tuned against a deck that had NO track in it, so both are due
|
||||
// a re-measurement now that 104 track cards share the draw.
|
||||
//
|
||||
// 243 and not 244: Poling is dealt ZERO copies. Its effect is "TBD in the source", so a card
|
||||
// that cannot be played is worse in a hand than absent from the deck. The entry stays in the
|
||||
// catalogue so the gap remains visible.
|
||||
assert.equal(DECK_SIZE, 139);
|
||||
assert.equal(DECK_SIZE, 243);
|
||||
assert.equal(buildDeck().length, DECK_SIZE);
|
||||
});
|
||||
|
||||
it('matches the design deck composition exactly', () => {
|
||||
const byCategory = Object.fromEntries(deckComposition().map((c) => [c.category, c.count]));
|
||||
assert.deepEqual(byCategory, {
|
||||
// 104, straight from the sheet's column B — the largest category in the deck by some way.
|
||||
track: 104,
|
||||
// 14, not the sheet's 7 — Q12 office density; see OFFICE_PROFILES.
|
||||
office: 14,
|
||||
// 27, not the sheet's 9 — Gap 12 industry density; see INDUSTRY_PROFILES.
|
||||
@@ -81,19 +87,35 @@ describe('card catalogue (component 1)', () => {
|
||||
|
||||
it('removes opponent-directed cards from a solitaire deck', () => {
|
||||
// Q6 — Space-use and Action cards can only be played AT another player, so in a one-player
|
||||
// game they would be 22 of 139 draws (16%) that do nothing.
|
||||
assert.equal(SOLITAIRE_DECK_SIZE, 117);
|
||||
// game they would be 22 of 243 draws (9%) that do nothing.
|
||||
assert.equal(SOLITAIRE_DECK_SIZE, 221);
|
||||
const solo = buildDeck('solitaire');
|
||||
assert.equal(solo.length, SOLITAIRE_DECK_SIZE);
|
||||
assert.ok(!solo.some((c) => c.kind.kind === 'spaceUse' || c.kind.kind === 'action'));
|
||||
// A competitive deck keeps them. 139, not 140 — Poling is dealt no copies.
|
||||
assert.equal(buildDeck('competitive').length, 139);
|
||||
// A competitive deck keeps them. Poling is dealt no copies, so this is 243 and not 244.
|
||||
assert.equal(buildDeck('competitive').length, 243);
|
||||
});
|
||||
|
||||
it('keeps track OUT of the deck, as a per-player supply', () => {
|
||||
// The single biggest structural change: 26 pieces per player, 104 for four.
|
||||
assert.equal(TRACK_PER_PLAYER, 26);
|
||||
assert.ok(!buildDeck().some((c) => c.kind.kind === 'track'), 'track leaked into the deck');
|
||||
it('deals track FROM the deck, at the sheet\'s counts', () => {
|
||||
// Column B of Deck cards2.xlsx, "Number in Deck": 32 straights, 16+16 curves, 4+4 sharp curves,
|
||||
// 16+16 turnouts. An earlier reading took the sheet's LAST column, "Track Per Player" (26), as a
|
||||
// separate stack outside the deck — it is 104 shared out among four players, not a second pile.
|
||||
assert.equal(TRACK_IN_DECK, 104);
|
||||
const deck = buildDeck();
|
||||
for (const t of TRACK_CARDS) {
|
||||
const n = deck.filter(
|
||||
(c) => c.kind.kind === 'track' && c.kind.geometry === t.geometry && c.kind.hand === t.hand,
|
||||
).length;
|
||||
assert.equal(n, t.copiesInDeck, `${t.name}: ${n} in the deck, expected ${t.copiesInDeck}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('makes track the largest category in the deck', () => {
|
||||
// 104 of 243. Building a district is paid for in the industry or train you did not draw, which
|
||||
// is the whole reason it matters that track is a card rather than a private supply.
|
||||
const deck = buildDeck();
|
||||
const track = deck.filter((c) => c.kind.kind === 'track').length;
|
||||
assert.ok(track > deck.length / 3, `track is only ${track} of ${deck.length} cards`);
|
||||
});
|
||||
|
||||
it('has 12 timetabled trains, odd westbound and even eastbound', () => {
|
||||
@@ -348,22 +370,22 @@ describe('game setup (component 2)', () => {
|
||||
assert.equal(area.adOccupancy.length, 0);
|
||||
});
|
||||
|
||||
it('deals three cards and turns three Department slots face up', () => {
|
||||
it('deals three cards and starts three Department piles, one card each', () => {
|
||||
const g = newSolitaireGame();
|
||||
assert.equal(g.decks.hands.get(0)!.length, 3);
|
||||
assert.equal(g.decks.departments.length, 3);
|
||||
assert.ok(g.decks.departments.every((c) => c !== null));
|
||||
assert.ok(g.decks.departments.every((pile) => pile.length === 1), 'each Department starts with one face-up card');
|
||||
});
|
||||
|
||||
it('accounts for every card exactly once', () => {
|
||||
const g = newSolitaireGame();
|
||||
const all = [
|
||||
...g.decks.homeOffice,
|
||||
...g.decks.departments.filter((c): c is string => c !== null),
|
||||
...g.decks.departments.flat(),
|
||||
...g.decks.salvageYard,
|
||||
...[...g.decks.hands.values()].flat(),
|
||||
];
|
||||
// A solitaire deck omits the 22 opponent-directed cards (Q6), so it holds 118, not 140.
|
||||
// A solitaire deck omits the 22 opponent-directed cards (Q6).
|
||||
assert.equal(all.length, SOLITAIRE_DECK_SIZE, 'cards lost or duplicated');
|
||||
assert.equal(new Set(all).size, SOLITAIRE_DECK_SIZE, 'duplicate card ids');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user