138 lines
4 KiB
JavaScript
138 lines
4 KiB
JavaScript
|
|
/**
|
||
|
|
* ShapeSeq PRNG — seedable pseudo-random number generator (mulberry32)
|
||
|
|
*
|
||
|
|
* Port-ready: explicit state as plain objects, no closures in hot path,
|
||
|
|
* all state is serializable. Pure-functional next/nextInt/fork — no mutation.
|
||
|
|
*
|
||
|
|
* @module shapeseq/prng
|
||
|
|
*/
|
||
|
|
|
||
|
|
// --- Mulberry32 core ---
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Single step of the mulberry32 PRNG.
|
||
|
|
* Pure function: takes a 32-bit state, returns { value, nextState }.
|
||
|
|
*
|
||
|
|
* @param {number} state - unsigned 32-bit integer
|
||
|
|
* @returns {{ value: number, nextState: number }}
|
||
|
|
* value: float in [0, 1)
|
||
|
|
* nextState: next 32-bit state
|
||
|
|
*/
|
||
|
|
function mulberry32Step(state) {
|
||
|
|
let t = (state + 0x6D2B79F5) | 0;
|
||
|
|
const nextState = t >>> 0; // the incremented state IS the new state
|
||
|
|
t = Math.imul(t ^ (t >>> 15), t | 1);
|
||
|
|
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
||
|
|
const raw = ((t ^ (t >>> 14)) >>> 0);
|
||
|
|
return { value: raw / 0x100000000, nextState };
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- Public API ---
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create a PRNG instance from a seed.
|
||
|
|
*
|
||
|
|
* @param {number} seed - any 32-bit integer (will be coerced to unsigned)
|
||
|
|
* @returns {{ state: number }} serializable PRNG state object
|
||
|
|
*/
|
||
|
|
export function createPRNG(seed) {
|
||
|
|
return { state: (seed >>> 0) };
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Produce the next random float in [0, 1). Pure — does not mutate input.
|
||
|
|
*
|
||
|
|
* @param {{ state: number }} prng
|
||
|
|
* @returns {{ value: number, nextState: { state: number } }}
|
||
|
|
*/
|
||
|
|
export function next(prng) {
|
||
|
|
const { value, nextState } = mulberry32Step(prng.state);
|
||
|
|
return { value, nextState: { state: nextState } };
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Produce a random integer in [min, max] (inclusive). Pure — does not mutate input.
|
||
|
|
*
|
||
|
|
* @param {{ state: number }} prng
|
||
|
|
* @param {number} min - inclusive lower bound (integer)
|
||
|
|
* @param {number} max - inclusive upper bound (integer)
|
||
|
|
* @returns {{ value: number, nextState: { state: number } }}
|
||
|
|
*/
|
||
|
|
export function nextInt(prng, min, max) {
|
||
|
|
const { value, nextState } = next(prng);
|
||
|
|
const range = max - min + 1;
|
||
|
|
const intVal = min + Math.floor(value * range);
|
||
|
|
return { value: intVal, nextState };
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Derive an independent PRNG stream from the current state + an id.
|
||
|
|
* Used to give each primitive in a chain its own deterministic stream.
|
||
|
|
*
|
||
|
|
* The child seed is produced by hashing the current state with the id
|
||
|
|
* using a simple but effective mixing function (splitmix-style).
|
||
|
|
*
|
||
|
|
* @param {{ state: number }} prng
|
||
|
|
* @param {number|string} id - primitive position index or string identifier
|
||
|
|
* @returns {{ state: number }} a new, independent PRNG state
|
||
|
|
*/
|
||
|
|
export function fork(prng, id) {
|
||
|
|
// Convert string ids to a numeric hash
|
||
|
|
let idNum;
|
||
|
|
if (typeof id === 'string') {
|
||
|
|
idNum = 0;
|
||
|
|
for (let i = 0; i < id.length; i++) {
|
||
|
|
idNum = ((idNum << 5) - idNum + id.charCodeAt(i)) | 0;
|
||
|
|
}
|
||
|
|
idNum = idNum >>> 0;
|
||
|
|
} else {
|
||
|
|
idNum = id >>> 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Mix state + id using splitmix-style finalizer
|
||
|
|
let h = (prng.state ^ idNum) >>> 0;
|
||
|
|
h = (h + 0x9E3779B9) >>> 0;
|
||
|
|
h = Math.imul(h ^ (h >>> 16), 0x85EBCA6B) >>> 0;
|
||
|
|
h = Math.imul(h ^ (h >>> 13), 0xC2B2AE35) >>> 0;
|
||
|
|
h = (h ^ (h >>> 16)) >>> 0;
|
||
|
|
|
||
|
|
return { state: h };
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get serializable state from a PRNG instance (for freeze/restore).
|
||
|
|
*
|
||
|
|
* @param {{ state: number }} prng
|
||
|
|
* @returns {{ state: number }}
|
||
|
|
*/
|
||
|
|
export function getState(prng) {
|
||
|
|
return { state: prng.state };
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Restore a PRNG instance from previously saved state.
|
||
|
|
*
|
||
|
|
* @param {{ state: number }} _prng - ignored (stateless restore)
|
||
|
|
* @param {{ state: number }} savedState
|
||
|
|
* @returns {{ state: number }}
|
||
|
|
*/
|
||
|
|
export function setState(_prng, savedState) {
|
||
|
|
return { state: savedState.state >>> 0 };
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Generate a cryptographically random seed for initial seeding.
|
||
|
|
* Uses crypto.getRandomValues when available, falls back to Date.now().
|
||
|
|
*
|
||
|
|
* @returns {number} unsigned 32-bit integer
|
||
|
|
*/
|
||
|
|
export function randomSeed() {
|
||
|
|
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
||
|
|
const buf = new Uint32Array(1);
|
||
|
|
crypto.getRandomValues(buf);
|
||
|
|
return buf[0];
|
||
|
|
}
|
||
|
|
// Fallback: not cryptographic, but sufficient for musical randomness
|
||
|
|
return (Date.now() * 2654435761) >>> 0;
|
||
|
|
}
|