v0.8.0.2 — the speed control that was only ever a URL parameter, and a Day-end
contradiction Two things found by playing v0.8.0.1, neither in the mechanism itself. ?pace= never worked. index.html's doors are play.html?lobby and play.html?solitaire, so arriving through the splash replaces the query string and the play page only ever saw ?lobby — a whole game was played at 1x while believing it was at 7x. v0.8.0 shipped that parameter as the only way to change speed and the game's own front door destroyed it. There is a control on the play screen now, beside zoom, persisted per viewer; the doors carry pace through as well, so the URL lever is honest for handing two playtesters different speeds. PACE_LEVELS moved to sim/pacing.ts with DWELL and MAX_PACE — the tuning surface in one file, and testable. The committed default is unchanged: what it should be is a question for a game played at a speed that took effect. And the Day-end dialog said "0 today, 2 in all". advance.ts increments the Day and then zeroes collisionsToday, and noteDayEnd() fires when the Day goes up — so the dialog reporting the Day that just finished was drawn from the very frame in which that Day's count was reset. Reproduced on four of five seeds before changing anything. The count is captured at the rollover now; it is not derivable on the client, because in multiplayer the push announcing the new Day is the same push that carries the reset. And "today" was the wrong word regardless: it names the Day instead — "Collisions: 2 on Day 1, 2 in all". Unrelated to v0.8.0 — that one has been wrong since the dialog was built for Gitea#10, and needed somebody to play a Day with a collision in it and then read the summary. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X6cF1iYvJ1kNmzYBzu4QX6
This commit is contained in:
co-authored by
Claude Opus 5
parent
0cfeb4c496
commit
c10f52791e
+26
-1
@@ -14,7 +14,7 @@ import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { DWELL, MAX_PACE, dwellFor, dwellForStep, kindOf, watchableCount } from '../src/sim/pacing.ts';
|
||||
import { DWELL, MAX_PACE, PACE_LEVELS, dwellFor, dwellForStep, kindOf, watchableCount } from '../src/sim/pacing.ts';
|
||||
import type { StepKind } from '../src/sim/pacing.ts';
|
||||
import type { Intent } from '../src/engine/intents.ts';
|
||||
|
||||
@@ -120,6 +120,31 @@ describe('pacing — dwell by kind', () => {
|
||||
assert.equal(watchableCount(queue, 0), 0, 'with animation off, nothing is behind');
|
||||
});
|
||||
|
||||
it('offers speeds a player actually reached for, and none the code would clamp', () => {
|
||||
/**
|
||||
* Jesse played a whole game believing he was at 7× and was in fact at 1×: `?pace=` shipped as the
|
||||
* only lever, and `index.html`'s doors are `play.html?lobby` / `play.html?solitaire`, so arriving
|
||||
* from the splash REPLACES the query string. Hence a real control on the play screen, and hence
|
||||
* this ladder — which must reach the speeds people ask for and must not offer one that
|
||||
* `dwellFor` would silently clamp.
|
||||
*/
|
||||
assert.equal(PACE_LEVELS[0], 0, 'off must be the first rung — #18 wants it turned off');
|
||||
assert.ok(PACE_LEVELS.includes(1), 'the default must be on the ladder');
|
||||
assert.ok(PACE_LEVELS.includes(7), '7x was asked for by name');
|
||||
for (const p of PACE_LEVELS) {
|
||||
assert.ok(p <= MAX_PACE, `${p}x is past MAX_PACE, so the control would lie about it`);
|
||||
assert.equal(dwellFor('switch.move', p), Math.round(DWELL.switching * p));
|
||||
}
|
||||
// Strictly increasing, so stepping the control always changes the speed.
|
||||
for (let i = 1; i < PACE_LEVELS.length; i++) {
|
||||
assert.ok(PACE_LEVELS[i]! > PACE_LEVELS[i - 1]!, 'the ladder must be strictly increasing');
|
||||
}
|
||||
// The slowest rung has to be slow enough to be worth having: six switching moves at the top of
|
||||
// the ladder is a full minute, which is the "watch them struggle" case.
|
||||
const slowest = dwellFor('switch.move', PACE_LEVELS[PACE_LEVELS.length - 1]!) * 6;
|
||||
assert.ok(slowest >= 60_000, `the slowest a switching turn can be watched is ${slowest}ms`);
|
||||
});
|
||||
|
||||
it('a silent step beats only when the clock turns over — TODO #18', () => {
|
||||
/**
|
||||
* Both obvious rules were wrong, so both are pinned. "No narration, no dwell" flashed past
|
||||
|
||||
@@ -524,6 +524,9 @@ describe('#91 — nothing private survives serialisation, in any state', () => {
|
||||
// The rules the game was dealt under, and the score.
|
||||
'houseRules', 'mode', 'optionalRules', 'days', 'minCombinedRevenue',
|
||||
'maxCollisionsPerDay', 'maxCollisionsTotal', 'collisionsToday', 'collisionsTotal',
|
||||
// What the Day that just ended finished on. Public for the same reason the running counts are:
|
||||
// a collision happens on the Mainline in front of everybody.
|
||||
'collisionsPrevDay',
|
||||
'status', 'outcome', 'extraDays', 'extensionVotes', 'official', 'tally',
|
||||
// Names, seats, revenue and HAND SIZE — never hand contents.
|
||||
'players',
|
||||
|
||||
@@ -3614,6 +3614,46 @@ describe('the Day rolling over says so (Gitea#10)', () => {
|
||||
assert.ok(html.includes('3 Days left'), `the Days remaining are wrong:\n${html}`);
|
||||
});
|
||||
|
||||
it('reports the ENDED Day\'s collisions, not the fresh Day\'s zero', () => {
|
||||
/**
|
||||
* Jesse, 2026-09-09, at the end of a Day 1 with two collisions in it: *"It shows a total of two
|
||||
* collisions, but zero today. Since we just finished day one, that does seem to be a
|
||||
* contradiction."*
|
||||
*
|
||||
* The cause is a one-line ordering fact: `advance.ts` increments the Day and then zeroes
|
||||
* `collisionsToday`, and this dialog is drawn from the frame whose Day went UP — so it read the
|
||||
* fresh Day's zero and printed it beside a running total that could not agree with it. The count
|
||||
* is captured at the rollover now, and the dialog names the Day rather than saying "today".
|
||||
*/
|
||||
const s = createEngineGame({
|
||||
id: 'collide',
|
||||
seed: 5,
|
||||
config: {
|
||||
mode: 'competitive',
|
||||
days: 5,
|
||||
minCombinedRevenue: 60,
|
||||
maxCollisionsPerDay: 3,
|
||||
maxCollisionsTotal: 10,
|
||||
pvpCardsAllowed: false,
|
||||
optionalRules: { reducedVisibility: false, employeeRotation: false, emergencyToolbox: false },
|
||||
},
|
||||
playerNames: ['Joe', 'Bot 1'],
|
||||
});
|
||||
// The state as the rollover out of Day 1 leaves it: two collisions happened, `today` is reset.
|
||||
s.clock.day = 2;
|
||||
s.collisionsPrevDay = 2;
|
||||
s.collisionsToday = 0;
|
||||
s.collisionsTotal = 2;
|
||||
|
||||
const html = dayEndHtml(snapshot(s, [], null));
|
||||
assert.ok(html.includes('Day 1 has ended'), `wrong Day named:\n${html}`);
|
||||
assert.ok(html.includes('<b>2</b> on Day 1'), `the ended Day's collisions are wrong:\n${html}`);
|
||||
assert.ok(html.includes('<b>2</b> in all'), `the running total is wrong:\n${html}`);
|
||||
assert.doesNotMatch(html, /<b>0<\/b> today/, `still reporting the fresh Day's zero:\n${html}`);
|
||||
// The contradiction itself: a Day-end dialog must never claim fewer in all than on that Day.
|
||||
assert.doesNotMatch(html, /<b>0<\/b> on Day 1/, 'reported no collisions on a Day that had two');
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
Reference in New Issue
Block a user