Files
XZBT/tools/gc4-time-model.mjs

144 lines
4.6 KiB
JavaScript

/** Phase 0 executable contract model for XZBT 0.1 GC4. */
const UINT32_RANGE = 0x1_0000_0000;
const STEP_MS = 1000 / 60;
function rotateLeft(value, count) {
return ((value << count) | (value >>> (32 - count))) >>> 0;
}
function multiply32(left, right) {
return Math.imul(left, right) >>> 0;
}
export function fnv1a32(text) {
let hash = 0x811c9dc5;
for (const byte of new TextEncoder().encode(text)) {
hash ^= byte;
hash = multiply32(hash, 0x01000193);
}
return hash >>> 0;
}
export function splitMix32(seed) {
let state = seed >>> 0;
return () => {
state = (state + 0x9e3779b9) >>> 0;
let value = state;
value = multiply32(value ^ (value >>> 16), 0x21f0aaad);
value = multiply32(value ^ (value >>> 15), 0x735a2d97);
return (value ^ (value >>> 15)) >>> 0;
};
}
export function deriveStreamState(rootSeed, domain, stableInstanceKey) {
if (!Number.isInteger(rootSeed) || rootSeed < 0 || rootSeed >= UINT32_RANGE) {
throw new RangeError('Root seed must be an unsigned 32-bit integer.');
}
const hash = fnv1a32(`xzbt-0.1\0${rootSeed}\0${domain}\0${stableInstanceKey}`);
const expand = splitMix32(hash);
const state = [expand(), expand(), expand(), expand()];
if (state.every((word) => word === 0)) state[3] = 1;
return state;
}
export class Xoshiro128StarStar {
constructor(state) {
if (!Array.isArray(state) || state.length !== 4 || state.some((word) => !Number.isInteger(word))) {
throw new TypeError('xoshiro128** state must contain four integer words.');
}
this.state = state.map((word) => word >>> 0);
if (this.state.every((word) => word === 0)) throw new RangeError('xoshiro128** state cannot be all zero.');
}
nextUint32() {
const state = this.state;
const result = multiply32(rotateLeft(multiply32(state[1], 5), 7), 9);
const temporary = (state[1] << 9) >>> 0;
state[2] ^= state[0];
state[3] ^= state[1];
state[1] ^= state[2];
state[0] ^= state[3];
state[2] ^= temporary;
state[3] = rotateLeft(state[3], 11);
for (let index = 0; index < 4; index++) state[index] >>>= 0;
return result >>> 0;
}
nextFloat() {
return this.nextUint32() / UINT32_RANGE;
}
}
export function createRandomStream(rootSeed, domain, stableInstanceKey) {
return new Xoshiro128StarStar(deriveStreamState(rootSeed, domain, stableInstanceKey));
}
export class FixedStepClock {
constructor({ stepMs = STEP_MS, maxTicksPerTurn = 8, maxElapsedMs = 250 } = {}) {
this.stepMs = stepMs;
this.maxTicksPerTurn = maxTicksPerTurn;
this.maxElapsedMs = maxElapsedMs;
this.tickIndex = 0;
this.accumulatorMs = 0;
this.lastNowMs = undefined;
this.pauseReasons = new Set();
}
setPauseReason(reason, active, nowMs) {
const wasPaused = this.pauseReasons.size > 0;
if (active) this.pauseReasons.add(reason);
else this.pauseReasons.delete(reason);
const isPaused = this.pauseReasons.size > 0;
if (wasPaused !== isPaused) this.lastNowMs = nowMs;
}
advance(nowMs) {
if (this.lastNowMs === undefined) {
this.lastNowMs = nowMs;
return { ticks: [], stalled: false };
}
if (this.pauseReasons.size > 0) {
this.lastNowMs = nowMs;
return { ticks: [], stalled: false };
}
const observed = Math.max(0, nowMs - this.lastNowMs);
this.lastNowMs = nowMs;
const accepted = Math.min(observed, this.maxElapsedMs);
this.accumulatorMs += accepted;
const available = Math.floor((this.accumulatorMs + 1e-9) / this.stepMs);
const count = Math.min(available, this.maxTicksPerTurn);
const ticks = [];
for (let index = 0; index < count; index++) ticks.push(++this.tickIndex);
this.accumulatorMs -= count * this.stepMs;
const stalled = observed > this.maxElapsedMs || available > this.maxTicksPerTurn;
if (available > this.maxTicksPerTurn) {
this.accumulatorMs = Math.min(this.accumulatorMs, this.stepMs - 1e-9);
}
return { ticks, stalled };
}
}
export function planAudioUnlock(invocations, logicalSeconds) {
return invocations.map((invocation) => {
if (invocation.start < logicalSeconds) {
if (invocation.kind === 'one-shot') return { id: invocation.id, action: 'skip' };
if (invocation.end === undefined || invocation.end > logicalSeconds) {
return { id: invocation.id, action: 'start-continuous', phase: logicalSeconds - invocation.start, rampMs: 20 };
}
return { id: invocation.id, action: 'dispose' };
}
return { id: invocation.id, action: 'schedule', delay: invocation.start - logicalSeconds };
});
}
export const GC4_CONSTANTS = Object.freeze({
stepMs: STEP_MS,
maxTicksPerTurn: 8,
maxElapsedMs: 250,
audioLookaheadMs: 100,
unlockRampMs: 20
});