Files
XZBT/test/gc4-time-random.test.mjs
T

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());
});