Fixed division end of track labels and other minor bugs. Trains now show their information. Play cards now required to get to 3 or less, no other restrictions. auto hide now more prominent. it and sounds now are in replay.

This commit is contained in:
Jesse
2026-08-04 13:43:28 -04:00
parent c3c5c59a45
commit 4bba991f41
11 changed files with 466 additions and 161 deletions
+69
View File
@@ -16,6 +16,7 @@ import { divisionSvg, officeSvg } from '../src/sim/board-svg.ts';
import { snapshot } from '../src/sim/view.ts';
import { createGame as createEngineGame } from '../src/engine/setup.ts';
import {
overHandLimit,
actionGroups,
actionMenu,
handPlayable,
@@ -1122,3 +1123,71 @@ describe('the sounds fire on the events they name', () => {
assert.deepEqual(game.cues, [], 'the game announced a Stage ending before one had');
});
});
describe('the hand limit is a limit, not a toll on drawing (regression)', () => {
it('only blocks the end of a turn when more than three cards are held', () => {
// REGRESSION. The page set "must play a card" on ANY draw, so a player who had already played
// two cards and drew back to three was still forced to spend one. §6.2 is a hand LIMIT —
// "reduce his hand to no more than three cards" — and the engine says the same: `draw.end` is
// refused with HAND_LIMIT and otherwise allowed.
const game = newGame(555);
const hand = game.state.decks.hands.get(0)!;
hand.length = 0;
assert.equal(overHandLimit(game), false, 'an empty hand blocked the turn from ending');
const ids = [...game.state.cards.keys()].slice(0, 5);
hand.push(...ids.slice(0, 3));
assert.equal(overHandLimit(game), false, 'three cards is the limit, not over it');
hand.push(ids[3]!);
assert.equal(overHandLimit(game), true, 'four cards must be played down');
// A Red Flag raises the limit by one (§6.2).
game.state.decks.redFlags.set(0, true);
assert.equal(overHandLimit(game), false, 'a Red Flag allows a fourth card');
hand.push(ids[4]!);
assert.equal(overHandLimit(game), true, 'five cards is over even with a Red Flag');
});
it('agrees with the engine about when the turn may end', () => {
// Two statements of one rule is how they drift. This asserts they cannot.
const game = newGame(909);
for (let i = 0; i < 400; i++) {
if (currentActor(game) === null) break;
const { options, groups } = actionGroups(game);
if (groups.length === 0 || options.length === 0) break;
if (game.state.clock.phase === 'localOps' && game.state.turn.option === 'draw') {
const engineAllows = options.some((o) => o.type === 'draw.end');
assert.equal(
engineAllows,
!overHandLimit(game),
'the page and the engine disagree about whether the turn may end',
);
}
submit(game, options[groups[0]!.actions[0]!.index]!);
}
});
});
describe('a train says what its card calls for', () => {
it('explains a consist that offers only one kind of car', () => {
// Reported from playtesting on seed 22222: Extra X22 offered a caboose and nothing else, with
// no reason given. The rules are right — "Pee-Dee" is a per-diem train whose consist is one
// caboose and no cars — but the screen never said so, which reads as a broken game.
const game = newGame(22222);
const titles = new Set<string>();
for (let i = 0; i < 3000; i++) {
if (currentActor(game) === null) break;
const { options, groups } = actionGroups(game);
if (groups.length === 0 || options.length === 0) break;
for (const g of groups) if (g.kind === 'newTrain.') titles.add(g.title);
submit(game, options[groups[0]!.actions[0]!.index]!);
}
assert.ok(titles.size > 0, 'no train was ever made up');
for (const t of titles) {
assert.match(t, /its card calls for/, `a train was made up with no consist explained: ${t}`);
assert.ok(/“[^”]+”/.test(t), `the train is not named: ${t}`);
}
});
});