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:
+68
-2
@@ -231,7 +231,21 @@ describe('replay HTML', () => {
|
||||
const els = new Map<string, Record<string, unknown>>();
|
||||
const stub = {
|
||||
getElementById: (id: string) => {
|
||||
if (!els.has(id)) els.set(id, { textContent: '', innerHTML: '', value: '', style: {}, max: 0 });
|
||||
if (!els.has(id)) {
|
||||
// A faithful-enough element. `classList` matters: the page folds the district panel by
|
||||
// toggling a class, and a stub without one throws — which is a page that never renders,
|
||||
// not a cosmetic gap.
|
||||
const classes = new Set<string>();
|
||||
els.set(id, {
|
||||
textContent: '', innerHTML: '', value: '', style: {}, max: 0, onclick: null,
|
||||
classList: {
|
||||
add: (c: string) => void classes.add(c),
|
||||
remove: (c: string) => void classes.delete(c),
|
||||
contains: (c: string) => classes.has(c),
|
||||
},
|
||||
querySelector: () => null,
|
||||
});
|
||||
}
|
||||
return els.get(id);
|
||||
},
|
||||
};
|
||||
@@ -287,7 +301,21 @@ describe('replay HTML', () => {
|
||||
const els = new Map<string, Record<string, unknown>>();
|
||||
const stub = {
|
||||
getElementById: (id: string) => {
|
||||
if (!els.has(id)) els.set(id, { textContent: '', innerHTML: '', value: '', style: {}, max: 0 });
|
||||
if (!els.has(id)) {
|
||||
// A faithful-enough element. `classList` matters: the page folds the district panel by
|
||||
// toggling a class, and a stub without one throws — which is a page that never renders,
|
||||
// not a cosmetic gap.
|
||||
const classes = new Set<string>();
|
||||
els.set(id, {
|
||||
textContent: '', innerHTML: '', value: '', style: {}, max: 0, onclick: null,
|
||||
classList: {
|
||||
add: (c: string) => void classes.add(c),
|
||||
remove: (c: string) => void classes.delete(c),
|
||||
contains: (c: string) => classes.has(c),
|
||||
},
|
||||
querySelector: () => null,
|
||||
});
|
||||
}
|
||||
return els.get(id);
|
||||
},
|
||||
};
|
||||
@@ -350,3 +378,41 @@ describe('the replay wire format is lossless (regression)', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('the replay behaves like the game it is replaying', () => {
|
||||
it('carries the sounds each frame earned, and the one implementation that plays them', () => {
|
||||
// A replay is for watching a game back, so it should sound and fold like the game. Both use the
|
||||
// SAME functions — `cuesFor` decides what happened, `playCue` decides what it sounds like —
|
||||
// embedded by toString() exactly as the board renderers are. A second copy would drift, and a
|
||||
// replay that sounds different from the game is worse than a replay with no sound.
|
||||
const rec = record(880009, 'standard', 4000);
|
||||
const withCues = rec.frames.filter((f) => (f.cues?.length ?? 0) > 0);
|
||||
assert.ok(withCues.length > 20, `only ${withCues.length} frames carry a cue`);
|
||||
|
||||
const kinds = new Set(withCues.flatMap((f) => f.cues ?? []));
|
||||
assert.ok(kinds.has('stage'), 'no Stage ever sounded');
|
||||
assert.ok(kinds.has('day'), 'no Day ever sounded');
|
||||
assert.ok(kinds.has('train'), 'no train was ever announced');
|
||||
|
||||
// One cue per Stage boundary, the bell replacing the whistle at a Day — the same accounting the
|
||||
// live game is held to.
|
||||
let boundaries = 0;
|
||||
for (let k = 1; k < rec.frames.length; k++) {
|
||||
const a = rec.frames[k - 1]!;
|
||||
const b = rec.frames[k]!;
|
||||
if (a.day !== b.day || a.stage !== b.stage) boundaries++;
|
||||
}
|
||||
const clock = withCues.flatMap((f) => f.cues ?? []).filter((c) => c !== 'train').length;
|
||||
assert.equal(clock, boundaries, 'Stage boundaries and Stage-or-Day cues disagree');
|
||||
});
|
||||
|
||||
it('serves the controls for both, and embeds the player', () => {
|
||||
const html = renderHtml(record(880009, 'standard', 900));
|
||||
for (const id of ['sound', 'districttoggle', 'districtsummary']) {
|
||||
assert.ok(html.includes(`id="${id}"`), `the replay has no #${id}`);
|
||||
}
|
||||
assert.match(html, /const playCue = function/, 'the sound player is referenced but not embedded');
|
||||
assert.match(html, /#district\.folded/, 'nothing folds the district away');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user