v0.8.0.10 — playtest fixes: clearance rulings, the log, the map, and a save file
From the first two multiplayer playtests of v0.8.0.9, each traced before fixing. The engine: - A train on a card BEHIND the one departing no longer triggers a clearance ruling or an opposite-direction bar (#26). Reproduced from the exported save: X15 was held over X18 behind it, and X18 then collided into the full Whistle Post. Games in progress holding a ruling the engine no longer asks for will not resume (28 of 40 recorded four-seat games); shipped as is at Jesse's call. - `mainlineModified` carries the card's previous kind, so the log can say what a Realignment converted (#27). The screen: - The turn chart and the Division map name the player whose move is on screen while bot turns replay, not the live actor (#25). - The owning player's name is no longer outlined by the turn arrow's stroke, which made it unreadable (#24). - A Mainline card flashes on the map when a Realignment changes it (#28). - The history is held back with the board and revealed step by step, instead of arriving whole while the board is still catching up (#29). - A ruling made by holding the office reads "Superintendent Player X" (#30), and no line names a player twice (#31). - A seated player can download their own game as a save file: the play page's Save replay button, fed by GET /api/save?token=… (#32). The StartOS action cannot do this — an action result is text only. Closes #24 Closes #25 Closes #26 Closes #27 Closes #28 Closes #29 Closes #30 Closes #31 Closes #32 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nnuCv8UodHucFfx3LWEoX
This commit is contained in:
co-authored by
Claude Opus 5
parent
76c6e103b3
commit
4adf149ba5
+75
-1
@@ -15,7 +15,7 @@ import { currentActor, newMultiplayerGame, submit } from '../src/web/game.ts';
|
||||
import { publicSnapshot } from '../src/sim/view.ts';
|
||||
import { takeSteps } from '../src/sim/display-step.ts';
|
||||
import type { DisplayStep } from '../src/sim/display-step.ts';
|
||||
import { createStepQueue } from '../src/web/step-queue.ts';
|
||||
import { actorOnScreen, createStepQueue } from '../src/web/step-queue.ts';
|
||||
import { DWELL } from '../src/sim/pacing.ts';
|
||||
|
||||
const config: GameConfig = {
|
||||
@@ -278,3 +278,77 @@ describe('the step queue', () => {
|
||||
assert.equal(q.showing(), null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('whose move the screen is showing (Gitea#25)', () => {
|
||||
const step = (player: number | null) => ({ player }) as DisplayStep;
|
||||
const queue = (behind: number, busy: boolean, showing: DisplayStep | null) => ({
|
||||
behind: () => behind,
|
||||
busy: () => busy,
|
||||
showing: () => showing,
|
||||
});
|
||||
|
||||
it('names the live actor once the board has caught up', () => {
|
||||
assert.deepEqual(actorOnScreen(queue(0, false, step(2)), 0), { actor: 0, replaying: false });
|
||||
});
|
||||
|
||||
it('names the player of the step on screen while the board is behind — not the live actor', () => {
|
||||
// One human (seat 0) against bots: the live game already waits on seat 0 while bot 2's moves replay.
|
||||
assert.deepEqual(actorOnScreen(queue(3, true, step(2)), 0), { actor: 2, replaying: true });
|
||||
});
|
||||
|
||||
it('keeps naming the last step while it is still on screen, after the counter reaches zero', () => {
|
||||
assert.deepEqual(actorOnScreen(queue(0, true, step(1)), 0), { actor: 1, replaying: true });
|
||||
});
|
||||
|
||||
it('names nobody for an automatic phase being shown', () => {
|
||||
assert.deepEqual(actorOnScreen(queue(2, true, step(null)), 0), { actor: null, replaying: true });
|
||||
});
|
||||
|
||||
it('falls back to the live actor before any step has been shown', () => {
|
||||
assert.deepEqual(actorOnScreen(queue(1, true, null), 3), { actor: 3, replaying: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe('the log is held back with the board, and a changed card flashes (playtest, 2026-09-15)', () => {
|
||||
it('owes exactly the lines of the steps not yet shown', () => {
|
||||
const { steps } = realSteps(1917398, 400);
|
||||
const q = createStepQueue();
|
||||
q.reset(baseline(1917398));
|
||||
assert.equal(q.pendingLines(), 0, 'an empty queue holds nothing back');
|
||||
|
||||
q.push(steps);
|
||||
const owed = steps.reduce((n, s) => n + s.lines.length, 0);
|
||||
assert.equal(q.pendingLines(), owed, 'every queued step still owes its lines');
|
||||
|
||||
// Drive the clock as a render loop would; the debt falls monotonically and ends at nothing.
|
||||
let now = 0;
|
||||
let last = owed;
|
||||
for (let i = 0; i < 20_000 && q.busy(); i++) {
|
||||
q.advance(now);
|
||||
const left = q.pendingLines();
|
||||
assert.ok(left <= last, 'the held-back count grew while the board caught up');
|
||||
last = left;
|
||||
now += 50;
|
||||
}
|
||||
assert.equal(q.pendingLines(), 0, 'the board caught up but lines were still withheld');
|
||||
});
|
||||
|
||||
it('skipping reveals the whole log at once', () => {
|
||||
const { steps } = realSteps(1917398, 400);
|
||||
const q = createStepQueue();
|
||||
q.reset(baseline(1917398));
|
||||
q.push(steps);
|
||||
q.skip();
|
||||
assert.equal(q.pendingLines(), 0, 'Skip left lines withheld — the history would stay short');
|
||||
});
|
||||
|
||||
it('flashes nothing on an ordinary step', () => {
|
||||
// Realignment is rare in bot play, so this pins the quiet case: the map must not pulse at random.
|
||||
const { steps } = realSteps(1917398, 400);
|
||||
const q = createStepQueue();
|
||||
q.reset(baseline(1917398));
|
||||
q.push(steps.slice(0, 5));
|
||||
q.advance(0);
|
||||
assert.deepEqual(q.flashing(), [], 'a step that changed no Mainline card flashed one');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user