Say who is playing, what you can press, and where you are in the Stage
Mostly presentation, plus one rules fix found on the way.
This commit is contained in:
+41
-8
@@ -148,13 +148,21 @@ describe('the action menu presents choices the way they are made', () => {
|
||||
// "slot 2" is unusable information: the whole point of a face-up slot is choosing it on sight.
|
||||
const game = newGame(555);
|
||||
submit(game, actionGroups(game).options.find((o) => o.type === 'localOps.choose' && o.option === 'draw')!);
|
||||
const draw = actionMenu(game).direct.find((g) => g.title === 'Draw');
|
||||
assert.ok(draw, 'no draw group');
|
||||
for (const a of draw!.actions) {
|
||||
assert.ok(!/^draw\.|^slot \d+$/.test(a.label), `raw intent name on a button: ${a.label}`);
|
||||
// The draw actions are split by SOURCE now — gambling on the deck, taking a named face-up
|
||||
// card, and ending the turn are three different decisions — so gather every group that offers
|
||||
// one rather than looking for a single group called "Draw".
|
||||
const menu = actionMenu(game);
|
||||
const draws = menu.direct.filter((g) =>
|
||||
g.actions.some((a) => menu.options[a.index]?.type?.startsWith('draw.')),
|
||||
);
|
||||
assert.ok(draws.length > 0, 'no draw actions are offered at all');
|
||||
for (const g of draws) {
|
||||
for (const a of g.actions) {
|
||||
assert.ok(!/^draw\.|^slot \d+$/.test(a.label), `raw intent name on a button: ${a.label}`);
|
||||
}
|
||||
}
|
||||
assert.ok(
|
||||
draw!.actions.some((a) => /face-up slot/.test(a.label)),
|
||||
draws.flatMap((g) => g.actions).some((a) => /face-up slot/.test(a.label)),
|
||||
'face-up slots are not named',
|
||||
);
|
||||
});
|
||||
@@ -743,8 +751,10 @@ describe('the static build', () => {
|
||||
// STRICT: only ids that really exist in the served page. The stub used to conjure an element
|
||||
// for any id asked for, so a `$('target')` left behind after removing #target from the HTML
|
||||
// would pass here and throw on load in a browser — the page would simply never start.
|
||||
// `[\w-]` and not `[a-zA-Z]`: a hyphenated id is valid HTML, and the narrower pattern skipped
|
||||
// those silently — which would have let a genuinely MISSING `#tc-day` pass this test too.
|
||||
const served = new Set(
|
||||
[...readFileSync(join(dist, 'play.html'), 'utf8').matchAll(/id="([a-zA-Z]+)"/g)].map(
|
||||
[...readFileSync(join(dist, 'play.html'), 'utf8').matchAll(/id="([a-zA-Z][\w-]*)"/g)].map(
|
||||
(m) => m[1]!,
|
||||
),
|
||||
);
|
||||
@@ -867,9 +877,9 @@ describe('the static build', () => {
|
||||
// Cheap and total: compare every $('id') in the source against the ids in the served HTML.
|
||||
// Getting this wrong does not degrade the page, it stops the game starting at all.
|
||||
const src = readFileSync(join(root, 'src/web/main.ts'), 'utf8');
|
||||
const asked = new Set([...src.matchAll(/\$\('([a-zA-Z]+)'\)/g)].map((m) => m[1]!));
|
||||
const asked = new Set([...src.matchAll(/\$\('([a-zA-Z][\w-]*)'\)/g)].map((m) => m[1]!));
|
||||
const html = readFileSync(join(dist, 'play.html'), 'utf8');
|
||||
const present = new Set([...html.matchAll(/id="([a-zA-Z]+)"/g)].map((m) => m[1]!));
|
||||
const present = new Set([...html.matchAll(/id="([a-zA-Z][\w-]*)"/g)].map((m) => m[1]!));
|
||||
// `again` is created by the game-over screen before it is looked up.
|
||||
present.add('again');
|
||||
for (const id of asked) {
|
||||
@@ -877,6 +887,29 @@ describe('the static build', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('gives every phase the engine can be in a pill on the turn chart', () => {
|
||||
// The turn chart exists to say where in the Stage you are, so a phase with no pill is a hole
|
||||
// exactly when the player most needs it. `Phase` is a closed union in the engine; this fails if
|
||||
// one is added and the chart is not.
|
||||
const engine = readFileSync(join(root, 'src/engine/state.ts'), 'utf8');
|
||||
const decl = /export type Phase =([^;]+);/.exec(engine);
|
||||
assert.ok(decl, 'the Phase union moved — this test cannot see it any more');
|
||||
const phases = [...decl![1]!.matchAll(/'([a-zA-Z]+)'/g)].map((m) => m[1]!);
|
||||
assert.ok(phases.length >= 5, `only found ${phases.length} phases`);
|
||||
|
||||
const src = readFileSync(join(root, 'src/web/main.ts'), 'utf8');
|
||||
for (const p of phases) {
|
||||
assert.match(src, new RegExp(`key: '${p}'`), `no turn-chart pill for the ${p} phase`);
|
||||
}
|
||||
// And every pill must carry a tooltip: an icon alone does not explain a phase.
|
||||
const tips = [...src.matchAll(/key: '[a-zA-Z]+',\s*\n\s*label: '[^']+',\s*\n\s*tip: '/g)];
|
||||
assert.equal(tips.length, phases.length, 'a turn-chart pill has no tooltip');
|
||||
|
||||
const html = readFileSync(join(dist, 'play.html'), 'utf8');
|
||||
assert.match(html, /id="turnchart"/, 'the page has no turn chart');
|
||||
assert.doesNotMatch(html, /phase: <b id="phase">/, 'the old title-bar phase text is still there');
|
||||
});
|
||||
|
||||
it('serves all three pages, each stamped and cache-busted', () => {
|
||||
// The splash is the front door now; the game and the replay directory are separate pages.
|
||||
for (const [name, entry] of [
|
||||
|
||||
Reference in New Issue
Block a user