106 lines
4.0 KiB
JavaScript
106 lines
4.0 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import test from 'node:test';
|
|
|
|
import {
|
|
createRandomStream,
|
|
deriveStreamState,
|
|
FixedStepClock,
|
|
GC4_CONSTANTS,
|
|
planAudioUnlock
|
|
} from '../tools/gc4-time-model.mjs';
|
|
|
|
function simulateFrames(frameDurationMs, durationMs) {
|
|
const clock = new FixedStepClock();
|
|
const decisions = [];
|
|
clock.advance(0);
|
|
for (let now = frameDurationMs; now <= durationMs + 1e-7; now += frameDurationMs) {
|
|
for (const tick of clock.advance(Math.min(now, durationMs)).ticks) {
|
|
if (tick % 10 === 0) decisions.push(tick);
|
|
}
|
|
}
|
|
return { tickIndex: clock.tickIndex, decisions };
|
|
}
|
|
|
|
test('render frequency does not change fixed-tick decisions', () => {
|
|
const at60 = simulateFrames(1000 / 60, 1000);
|
|
const at120 = simulateFrames(1000 / 120, 1000);
|
|
assert.deepEqual(at60, at120);
|
|
assert.equal(at60.tickIndex, 60);
|
|
assert.deepEqual(at60.decisions, [10, 20, 30, 40, 50, 60]);
|
|
});
|
|
|
|
test('long stalls have bounded work and do not replay discarded wall time', () => {
|
|
const clock = new FixedStepClock();
|
|
clock.advance(0);
|
|
const stalled = clock.advance(1000);
|
|
assert.equal(stalled.stalled, true);
|
|
assert.equal(stalled.ticks.length, GC4_CONSTANTS.maxTicksPerTurn);
|
|
assert.ok(clock.accumulatorMs < GC4_CONSTANTS.stepMs);
|
|
});
|
|
|
|
test('visibility restoration does not clear explicit pause', () => {
|
|
const clock = new FixedStepClock();
|
|
clock.advance(0);
|
|
clock.advance(GC4_CONSTANTS.stepMs);
|
|
assert.equal(clock.tickIndex, 1);
|
|
|
|
clock.setPauseReason('explicit', true, 20);
|
|
clock.setPauseReason('hidden', true, 20);
|
|
clock.advance(10_000);
|
|
clock.setPauseReason('hidden', false, 10_000);
|
|
clock.advance(11_000);
|
|
assert.equal(clock.tickIndex, 1);
|
|
|
|
clock.setPauseReason('explicit', false, 11_000);
|
|
clock.advance(11_000 + GC4_CONSTANTS.stepMs);
|
|
assert.equal(clock.tickIndex, 2);
|
|
});
|
|
|
|
test('unlock skips pre-unlock one-shots and aligns a live continuous sound', () => {
|
|
const plan = planAudioUnlock([
|
|
{ id: 'expired', kind: 'one-shot', start: 1, end: 1.2 },
|
|
{ id: 'partial', kind: 'one-shot', start: 4.9, end: 5.2 },
|
|
{ id: 'ambience', kind: 'continuous', start: 2 },
|
|
{ id: 'future', kind: 'one-shot', start: 5.05, end: 5.2 }
|
|
], 5);
|
|
assert.deepEqual(plan, [
|
|
{ id: 'expired', action: 'skip' },
|
|
{ id: 'partial', action: 'skip' },
|
|
{ id: 'ambience', action: 'start-continuous', phase: 3, rampMs: 20 },
|
|
{ id: 'future', action: 'schedule', delay: 0.04999999999999982 }
|
|
]);
|
|
});
|
|
|
|
test('PRNG state and output are stable known vectors', () => {
|
|
assert.deepEqual(deriveStreamState(42, 'scenario', 'storm:1'), [
|
|
471422921, 3327820418, 120898024, 15893603
|
|
]);
|
|
const stream = createRandomStream(42, 'scenario', 'storm:1');
|
|
assert.deepEqual([
|
|
stream.nextUint32(), stream.nextUint32(), stream.nextUint32(), stream.nextUint32()
|
|
], [4101533927, 4149236977, 2113172072, 1672809915]);
|
|
});
|
|
|
|
test('manual sampling does not perturb cadence or scenario streams', () => {
|
|
const cadenceA = createRandomStream(7, 'cadence', 'routine:1');
|
|
const cadenceB = createRandomStream(7, 'cadence', 'routine:1');
|
|
const scenarioA = createRandomStream(7, 'scenario', 'surge:1');
|
|
const scenarioB = createRandomStream(7, 'scenario', 'surge:1');
|
|
const manual = createRandomStream(7, 'manual-sample', 'warning:1');
|
|
|
|
const before = [cadenceA.nextUint32(), scenarioA.nextUint32()];
|
|
for (let index = 0; index < 100; index++) manual.nextUint32();
|
|
const after = [cadenceA.nextUint32(), scenarioA.nextUint32()];
|
|
|
|
assert.deepEqual(before, [cadenceB.nextUint32(), scenarioB.nextUint32()]);
|
|
assert.deepEqual(after, [cadenceB.nextUint32(), scenarioB.nextUint32()]);
|
|
});
|
|
|
|
test('audio-reactive decisions use the same deterministic signal fixture at every render rate', () => {
|
|
const signals = JSON.parse(readFileSync(new URL('./fixtures/gc4/deterministic-signals.json', import.meta.url)));
|
|
const decide = () => signals.filter((signal) => signal.energy >= 0.5).map((signal) => signal.tick);
|
|
assert.deepEqual(decide(), [2, 3]);
|
|
assert.deepEqual(decide(), decide());
|
|
});
|