v0.4.9g — a caboose is not a load, a Day that says it ended, and a train you may throw away
The v0.7.1 bugfix set ported to the playtest line: Gitea#8, #10, #9 and #2. Reasoning, and what differs from main, in CHANGELOG.md. - Gitea#8: X22 Pee-Dee refused every caboose, including the one it was made up with. A caboose carries the crew, not freight, so it is never a load. - Gitea#10: a dialog when the Day rolls over, carrying the standings, the Days left and your Revenue against the target with the pace. Suppressed on the first frame, on Undo stepping back across a rollover, and on the Day the game ends. - Gitea#9, superseding Gitea#6 from v0.4.9f: a Timetabled train may be discarded, an Extra may not. This line takes the plain rule — main has it as a New Game setting. - Gitea#2: the shortage stays, by Jesse's ruling. A blocked platform now gives its reason instead of simply having no button. Three things are written for this line rather than ported: the Timetabled discard has no setting and no URL parameter, the Day-end dialog reads f.objective (this line's per-player target) where main reads its combined victory dials, and Frame gained the viewer index the dialog needs to mark your row. 636 tests pass; no published replay was retired. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FLnYR4XtXQNamYJXGYT8oC
This commit is contained in:
co-authored by
Claude Opus 5
parent
e255076b5c
commit
9735468d25
+85
-1
@@ -22,8 +22,10 @@ import { cardDescription, cardName, describeIntent, variantLabel } from '../src/
|
||||
import { variantsFor } from '../src/engine/track.ts';
|
||||
import { divisionSvg, officeSvg } from '../src/sim/board-svg.ts';
|
||||
import { ENHANCEMENT_RULES } from '../src/engine/content.ts';
|
||||
import { facilitiesHtml, timetableHtml } from '../src/web/panels.ts';
|
||||
import { dayEndHtml, facilitiesHtml, timetableHtml } from '../src/web/panels.ts';
|
||||
import { record, renderHtml } from '../src/sim/replay.ts';
|
||||
import type { Frame } from '../src/sim/view.ts';
|
||||
import type { GameLength } from '../src/engine/content.ts';
|
||||
import { snapshot } from '../src/sim/view.ts';
|
||||
import { createGame as createEngineGame } from '../src/engine/setup.ts';
|
||||
import {
|
||||
@@ -2682,6 +2684,88 @@ describe('the three places a game is drawn stay in step', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('the Day rolling over says so (Gitea#10)', () => {
|
||||
// "As the game rolls off the end of the day, you get a dialog saying such. Hard to keep track of
|
||||
// time." A Day turns inside the phases that run themselves, so it passes between one click and
|
||||
// the next — the phase banner is gone in 2.6 seconds.
|
||||
//
|
||||
// The goal half is written against `f.objective`, which on this line resolves a PER-PLAYER target
|
||||
// from the game length: `standard` is 20 Revenue over 5 Days. (Main asks a different question of
|
||||
// the same dialog — a combined floor off the victory dials — so that half is not a shared port.)
|
||||
const frameAt = (day: number, length: GameLength = 'standard'): Frame => {
|
||||
const s = createEngineGame({
|
||||
id: 'dayend',
|
||||
seed: 4021,
|
||||
config: {
|
||||
mode: 'solitaire',
|
||||
victory: 'firstToTarget',
|
||||
length,
|
||||
optionalRules: { reducedVisibility: false, sisterTrains: false, employeeRotation: false, emergencyToolbox: false },
|
||||
},
|
||||
playerNames: ['Solitaire'],
|
||||
});
|
||||
s.clock.day = day;
|
||||
return snapshot(s, [], null);
|
||||
};
|
||||
|
||||
it('names the Day that ENDED, not the one starting', () => {
|
||||
// Written from the frame after the rollover, so an off-by-one here would congratulate a player
|
||||
// on finishing a Day they have not played yet.
|
||||
const html = dayEndHtml(frameAt(3));
|
||||
assert.ok(html.includes('Day 2 has ended'), `wrong Day named:\n${html}`);
|
||||
assert.ok(html.includes('Day 3 of 5'), 'the Day now beginning is not named');
|
||||
assert.ok(html.includes('3 Days left'), `the Days remaining are wrong:\n${html}`);
|
||||
});
|
||||
|
||||
it('counts the last Day as the last Day rather than promising more', () => {
|
||||
const html = dayEndHtml(frameAt(6));
|
||||
assert.ok(html.includes('Day 5 has ended'), 'the final Day is misnamed');
|
||||
assert.ok(html.includes('last Day on the timetable'), `still offering Days to run:\n${html}`);
|
||||
assert.ok(!html.includes('Days left'), 'promises more Days after the last one');
|
||||
});
|
||||
|
||||
it('reads the goal and the pace off the objective rather than working them out again', () => {
|
||||
// One implementation of "what is the target and am I keeping up" — `objectiveOf`. A dialog that
|
||||
// recomputed either would be one release away from disagreeing with the status block that shows
|
||||
// the same numbers every turn.
|
||||
const f = frameAt(3);
|
||||
const html = dayEndHtml(f);
|
||||
assert.ok(html.includes(`<b>${f.objective.target}</b>`), `the target is not shown:\n${html}`);
|
||||
assert.ok(html.includes(`<b>${f.revenue}</b>`), "the viewer's Revenue is not shown");
|
||||
// Nothing earned by Day 3 of 5 is behind a 20-Revenue pace, and the dialog says so in words.
|
||||
assert.equal(f.objective.onPace, false, 'the fixture is no longer behind pace');
|
||||
assert.ok(html.includes('behind pace'), 'the pace is not reported');
|
||||
// A campaign game is a different target, and the dialog must not have 20 baked into it.
|
||||
const long = frameAt(3, 'campaign');
|
||||
assert.ok(dayEndHtml(long).includes('<b>45</b>'), 'the target does not follow the game length');
|
||||
assert.ok(dayEndHtml(long).includes('Day 3 of 10'), 'the Day count does not follow the length');
|
||||
});
|
||||
|
||||
it('puts the leader at the top and marks which row is yours', () => {
|
||||
// The question at the end of a Day is who is ahead, which is not the order the seats are in.
|
||||
const f = frameAt(2);
|
||||
f.players = [
|
||||
{ index: 0, seat: 0, name: 'Ada', revenue: 4, hand: 3 },
|
||||
{ index: 1, seat: 1, name: 'Bo', revenue: 7, hand: 2 },
|
||||
];
|
||||
f.viewer = 0;
|
||||
const html = dayEndHtml(f);
|
||||
assert.ok(html.indexOf('Bo') < html.indexOf('Ada'), 'the standings are not in Revenue order');
|
||||
assert.ok(html.includes('(you)'), 'the viewer is not marked in the standings');
|
||||
// Marked from `f.viewer`, not from an assumed seat 0 — so moving the viewer moves the mark.
|
||||
f.viewer = 1;
|
||||
const moved = dayEndHtml(f);
|
||||
assert.ok(moved.indexOf('(you)') < moved.indexOf('Ada'), 'the mark did not follow the viewer');
|
||||
});
|
||||
|
||||
it('gives the page the dialog to fill', () => {
|
||||
const html = readFileSync(join(dist, 'play.html'), 'utf8');
|
||||
for (const id of ['dayenddlg', 'dayendbody']) {
|
||||
assert.ok(html.includes(`id="${id}"`), `play.html has no #${id}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('every file the build needs is actually in the repo (regression)', () => {
|
||||
it('does not gitignore a source page', () => {
|
||||
// REGRESSION. `.gitignore` carried `replay*.html` to catch the throwaway files generated at the
|
||||
|
||||
Reference in New Issue
Block a user