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:
+75
-42
@@ -63,56 +63,73 @@ const gameDealtWith = (startingHand: StartingHand, seed = 1234) =>
|
||||
|
||||
describe('card catalogue (component 1)', () => {
|
||||
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.
|
||||
// THE WHOLE CATALOGUE IS docs/Deck cards5.xlsx NOW (Gitea#14). Sheet 5's own totals are
|
||||
// "Total track 48" and "Total other (in play) 107", i.e. 155 shuffled, plus 12 start cards for
|
||||
// its grand total of 167. Card for card, 84 rows agree with it exactly and the only ones that
|
||||
// do not are listed below — every one of them a deliberate hold, in one direction or the other.
|
||||
//
|
||||
// We are at 235 rather than 219 because of three deliberate departures, all flagged in
|
||||
// content.ts: 18 extra industry cards (Gap 12, industries 9 → 27), 7 extra office cards (Q12,
|
||||
// offices 7 → 14), and the 8 sharp curves taken back OUT. The first two were tuned against a deck
|
||||
// that had NO track in it, so both are due a re-measurement now that 96 track cards share the
|
||||
// draw.
|
||||
// EVERY COUNT IN THE CATALOGUE IS NOW THE SHEET'S. The two deliberate departures that used to
|
||||
// sit here are gone with Gitea#14 — the Q12 office doubling (offices 14 → 7) and the Gap 12
|
||||
// industry tripling (27 → 9) — because both were measured against a deck holding 96 track
|
||||
// cards, and sheet 5 halves that. content.ts carries the measurements that decided it.
|
||||
//
|
||||
// Two entries are dealt ZERO copies and kept in the catalogue so the design stays visible:
|
||||
// Poling, whose effect is "TBD in the source", and the sharp curves, whose only difference from
|
||||
// an ordinary curve was a Move cost nothing ever charged.
|
||||
// DECK_SIZE is the CATALOGUE, 235. The deck actually dealt is smaller: the 22 opponent-directed
|
||||
// cards are held back in every mode until they are implemented, so `buildDeck` returns 213.
|
||||
assert.equal(DECK_SIZE, 235);
|
||||
// We are at 143 rather than the sheet's 155 for ONE reason: the ten Safety, Event, Inspection
|
||||
// and Space-use cards sheet 5 adds are not built, and stay out until they are (Jesse,
|
||||
// 2026-08-26) — Cargo Theft, Civic Improvement, Civilian angel, Delayed Clearance, Flares 2,
|
||||
// Robbery, Service Delays, Shipper complaints, Strike, Union Hall 2. Twelve copies in all.
|
||||
//
|
||||
// NOTHING RUNS THE OTHER WAY ANY MORE. Every card sheet 5 does not list is dealt ZERO copies
|
||||
// rather than deleted, so the design stays visible and the rules stay implemented: the
|
||||
// Telegraph/Telephone/Radio ladder, Facing Point Locks (both), Flying Switch, Section House and
|
||||
// Vandalism, all removed from the design on purpose; Poling, whose effect the source records as
|
||||
// "TBD"; and the sharp curves, whose only difference from an ordinary curve was a Move cost
|
||||
// nothing ever charged — sheet 5 deals those zero too, so the catalogue and the design agree.
|
||||
//
|
||||
// DECK_SIZE is the CATALOGUE, 143. The deck actually dealt is smaller: the 20 opponent-directed
|
||||
// cards are held back in every mode until they are implemented, so `buildDeck` returns 123.
|
||||
assert.equal(DECK_SIZE, 143);
|
||||
assert.equal(buildDeck().length, SOLITAIRE_DECK_SIZE);
|
||||
});
|
||||
|
||||
it('matches the design deck composition exactly', () => {
|
||||
const byCategory = Object.fromEntries(deckComposition().map((c) => [c.category, c.count]));
|
||||
assert.deepEqual(byCategory, {
|
||||
// 96, not the sheet's 104: the 8 SHARP CURVES are dealt zero copies. The only thing that made
|
||||
// one different from an ordinary curve was a Move cost that nothing ever charged, so they were
|
||||
// geometric duplicates taking 8 draws. Kept in the catalogue at zero, as Poling is.
|
||||
track: 96,
|
||||
// 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.
|
||||
industry: 27,
|
||||
// Sheet 5's track counts exactly (Gitea#14): 16 straights, 8+8 curves, 8+8 turnouts, and the
|
||||
// sharp curves dealt none — which is where the catalogue already had them, and where sheet 5
|
||||
// now puts them too.
|
||||
track: 48,
|
||||
// The sheet's 7 — the Q12 doubling came out in Gitea#14; see OFFICE_PROFILES.
|
||||
office: 7,
|
||||
// The sheet's 9 — the Gap 12 tripling came out in Gitea#14; see INDUSTRY_PROFILES.
|
||||
industry: 9,
|
||||
modifier: 23,
|
||||
train: 22,
|
||||
spaceUse: 12,
|
||||
enhancement: 18,
|
||||
mainlineModifier: 7,
|
||||
// 6, not 7 — Poling is dealt no copies until its rule is known.
|
||||
maneuver: 6,
|
||||
action: 10,
|
||||
spaceUse: 11,
|
||||
// 6 — the dispatching ladder and Facing Point Locks are dealt 0 copies (see
|
||||
// ENHANCEMENT_CARDS), and Interlocking, Water column and ABS Signals came down to the sheet's
|
||||
// single copies. What is left is the sheet's Enhancements exactly, bar Railroad crossing,
|
||||
// which sheet 5 moved here from the Action cards and which is still counted there below.
|
||||
enhancement: 6,
|
||||
// 5 — Facing Point Locks came out of the Mainline modifiers too.
|
||||
mainlineModifier: 5,
|
||||
// 3 — Red Flags is the sheet's 3; Flying Switch and Poling are both dealt none.
|
||||
maneuver: 3,
|
||||
// 9 — Vandalism is dealt none. The rest are opponent-directed and held out of every deck.
|
||||
action: 9,
|
||||
});
|
||||
});
|
||||
|
||||
it('removes opponent-directed cards from a solitaire deck', () => {
|
||||
// Q6 took Space-use and Action cards out of solitaire, where they have no legal target. They are
|
||||
// now out of the COMPETITIVE deck too, until they are implemented: `checkPlay` answers both
|
||||
// categories NOT_IMPLEMENTED, so dealing them would make 22 of 235 draws (9%) reject outright.
|
||||
// 206, not 213: the 22 opponent-directed cards come out, and so do the SEVEN that exist only to
|
||||
// answer them — Facing Point Locks (both the Enhancement and the Mainline modifier, 2 each), two
|
||||
// Water Columns and one Overpass. A defence with nothing to defend against is the same dead draw
|
||||
// as the attack would be. `SimpleCard.answers` names the pairing, so they return together.
|
||||
assert.equal(SOLITAIRE_DECK_SIZE, 206);
|
||||
assert.equal(DEFENCE_ONLY_COPIES, 7);
|
||||
// categories NOT_IMPLEMENTED, so dealing them would be a dead draw.
|
||||
// 121, not 123: the 20 opponent-directed cards come out, and so do the TWO that exist only to
|
||||
// answer them — one Water Column and one Overpass. A defence with nothing to defend against is
|
||||
// the same dead draw as the attack would be. `SimpleCard.answers` names the pairing, so they
|
||||
// return together. It was seven until Gitea#14 dealt Facing Point Locks zero copies: a card at
|
||||
// zero is already out, so it no longer needs holding back.
|
||||
assert.equal(SOLITAIRE_DECK_SIZE, 121);
|
||||
assert.equal(DEFENCE_ONLY_COPIES, 2);
|
||||
for (const c of DEFENCE_ONLY_CARDS) {
|
||||
assert.ok(c.answers, `${c.name} is held back without saying what it answers`);
|
||||
assert.ok(
|
||||
@@ -131,11 +148,12 @@ describe('card catalogue (component 1)', () => {
|
||||
});
|
||||
|
||||
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, 16+16 turnouts —
|
||||
// and 4+4 sharp curves, which are dealt none. An earlier reading took the sheet's LAST column,
|
||||
// "Track Per Player" (26), as a separate stack outside the deck; it is the sheet's 104 shared out
|
||||
// among four players, not a second pile.
|
||||
assert.equal(TRACK_IN_DECK, 96);
|
||||
// Column B of Deck cards5.xlsx, "Number in Deck": 16 straights, 8+8 curves, 8+8 turnouts, and
|
||||
// 0+0 sharp curves. Sheet 2 had each of those at double, which is what the deck dealt until
|
||||
// Gitea#14. An earlier reading took the sheet's LAST column, "Track Per Player" (26), as a
|
||||
// separate stack outside the deck; it is the sheet's total shared out among four players, not a
|
||||
// second pile.
|
||||
assert.equal(TRACK_IN_DECK, 48);
|
||||
const deck = buildDeck();
|
||||
for (const t of TRACK_CARDS) {
|
||||
const n = deck.filter(
|
||||
@@ -146,11 +164,26 @@ describe('card catalogue (component 1)', () => {
|
||||
});
|
||||
|
||||
it('makes track the largest category in the deck', () => {
|
||||
// 96 of 235. Building a district is paid for in the industry or train you did not draw, which
|
||||
// 48 of 121. 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.
|
||||
//
|
||||
// This asked for a THIRD of the deck until Gitea#14, which was only ever a rule of thumb. It
|
||||
// asks its own question now — is track still the biggest single thing you can draw — plus a
|
||||
// loose band, because the exact share is not settled yet and should not be pinned as though it
|
||||
// were. Sheet 5 puts track at 48 of the 155 cards it would have you shuffle, i.e. 31%; we read
|
||||
// 40% because the Space-use, Safety, Event and Inspection cards are held out, which concentrates
|
||||
// everything that is left. The share falls TOWARDS the sheet as those land, so the band is set
|
||||
// to hold across that whole journey rather than to be re-edited at each step.
|
||||
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`);
|
||||
const counts = new Map<string, number>();
|
||||
for (const c of deck) counts.set(c.kind.kind, (counts.get(c.kind.kind) ?? 0) + 1);
|
||||
const track = counts.get('track') ?? 0;
|
||||
for (const [kind, n] of counts) {
|
||||
if (kind === 'track') continue;
|
||||
assert.ok(track > n, `${kind} has ${n} cards against track's ${track}`);
|
||||
}
|
||||
const share = track / deck.length;
|
||||
assert.ok(share > 0.28 && share < 0.45, `track is ${(share * 100).toFixed(1)}% of the deck`);
|
||||
});
|
||||
|
||||
it('has 12 timetabled trains, odd westbound and even eastbound', () => {
|
||||
|
||||
Reference in New Issue
Block a user