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:
Jesse
2026-08-05 08:28:22 -04:00
parent 2d348e1ecf
commit 9c76b1a5e2
28 changed files with 1927 additions and 394 deletions
+23 -8
View File
@@ -50,6 +50,23 @@ function hand(s: GameState, kind: string, key: string): string {
throw new Error(`no ${kind} card: ${key}`);
}
/**
* Puts a specific TRACK card in hand and returns its id.
*
* Track is an ordinary deck card, so building a district means holding the right piece — which is
* exactly the constraint these tests need to set up deliberately rather than draw for.
*/
function trackInHand(s: GameState, geometry: string, hand: string): string {
for (const [id, card] of s.cards) {
const k = card.kind as { kind: string; geometry?: string; hand?: string };
if (k.kind === 'track' && k.geometry === geometry && k.hand === hand) {
s.decks.hands.set(0, [id]);
return id;
}
}
throw new Error(`no track card: ${geometry}/${hand}`);
}
/** A Mainline node pinned to a known terrain, so deck composition cannot decide an outcome. */
function pinned(s: GameState, index: number, card: string) {
const node = s.division.nodes[index];
@@ -438,11 +455,12 @@ describe('the Limits sign moves with the Running Track (§2.1, Gap 4a)', () => {
const area = areaOf(s, 0);
s.turn.option = 'draw';
const straight = trackInHand(s, 'straight', 'none');
const beyondEast = { row: area.runningRow, col: area.limitsEast.col + 1 };
const beyondWest = { row: area.runningRow, col: area.limitsWest.col - 1 };
for (const placement of [beyondEast, beyondWest]) {
assert.notEqual(
check(s, 0, { type: 'track.lay', geometry: 'straight', hand: 'none', placement, variant: 0 }),
check(s, 0, { type: 'card.play', cardId: straight, placement, variant: 0 }),
null,
`(${placement.row},${placement.col}) is outside the Limits and must not be placeable`,
);
@@ -451,7 +469,7 @@ describe('the Limits sign moves with the Running Track (§2.1, Gap 4a)', () => {
// The sign itself IS legal — that is how the Running Track grows.
assert.equal(
check(s, 0, {
type: 'track.lay', geometry: 'straight', hand: 'none',
type: 'card.play', cardId: straight,
placement: { row: area.runningRow, col: area.limitsEast.col }, variant: 0,
}),
null,
@@ -460,11 +478,10 @@ describe('the Limits sign moves with the Running Track (§2.1, Gap 4a)', () => {
// A district opens with a TURNOUT on the Running Track — no office or industry card carries a
// stub of its own — so extend onto the east sign with one.
s.turn.laidThisTurn = false;
const turnoutCol = area.limitsEast.col;
assert.ok(
applyIntent(s, 0, {
type: 'track.lay', geometry: 'turnout', hand: 'left',
type: 'card.play', cardId: trackInHand(s, 'turnout', 'left'),
placement: { row: area.runningRow, col: turnoutCol }, variant: 0,
}).ok,
'a turnout must be layable on the Limits sign',
@@ -472,10 +489,9 @@ describe('the Limits sign moves with the Running Track (§2.1, Gap 4a)', () => {
// The district hangs off that turnout and is not bounded by the Limits at all. Variant 1 of a
// left-hand curve is the `ne` arc — the one on the same diagonal as the turnout's leg.
s.turn.laidThisTurn = false;
assert.equal(
check(s, 0, {
type: 'track.lay', geometry: 'curved', hand: 'left',
type: 'card.play', cardId: trackInHand(s, 'curved', 'left'),
placement: { row: area.runningRow - 1, col: turnoutCol }, variant: 1,
}),
null,
@@ -494,10 +510,9 @@ describe('the Limits sign moves with the Running Track (§2.1, Gap 4a)', () => {
s.turn.option = 'draw';
for (let n = 0; n < 4; n++) {
s.turn.laidThisTurn = false;
const target = { row: area.runningRow, col: area.limitsWest.col };
const r = applyIntent(s, 0, {
type: 'track.lay', geometry: 'straight', hand: 'none', placement: target, variant: 0,
type: 'card.play', cardId: trackInHand(s, 'straight', 'none'), placement: target, variant: 0,
});
assert.ok(r.ok, `extending onto the Limits should be legal (attempt ${n + 1})`);
}