Initial commit
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Component 7 — Seeded RNG.
|
||||
*
|
||||
* Every random decision in the game derives from one stored seed, so any game is exactly
|
||||
* replayable. See architecture/components.md §2 A.7.
|
||||
*
|
||||
* IMPORTANT: never call Math.random() anywhere in the engine. A single ambient random call
|
||||
* silently breaks replay, and the failure is invisible until someone tries to reproduce a bug.
|
||||
*/
|
||||
|
||||
/** An RNG is a value, not a global. Thread it through explicitly. */
|
||||
export type Rng = {
|
||||
/** Raw 32-bit unsigned draw. */
|
||||
next(): number;
|
||||
/** Uniform integer in [0, bound). Throws if bound < 1. */
|
||||
nextInt(bound: number): number;
|
||||
/** A single twelve-sided die roll, 1..12 (§4.3, §4.4, §7). */
|
||||
d12(): number;
|
||||
/** Fisher-Yates. Returns a new array; does not mutate the input. */
|
||||
shuffle<T>(items: readonly T[]): T[];
|
||||
/** The current internal state, so a game can be snapshotted mid-play. */
|
||||
getState(): number;
|
||||
};
|
||||
|
||||
/**
|
||||
* mulberry32 — small, fast, and good enough for a board game. Chosen because its entire state
|
||||
* is one 32-bit integer, which makes snapshotting and restoring an in-progress game trivial.
|
||||
*/
|
||||
export function createRng(seed: number): Rng {
|
||||
let state = seed >>> 0;
|
||||
|
||||
const next = (): number => {
|
||||
state = (state + 0x6d2b79f5) >>> 0;
|
||||
let t = state;
|
||||
t = Math.imul(t ^ (t >>> 15), t | 1);
|
||||
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
||||
return (t ^ (t >>> 14)) >>> 0;
|
||||
};
|
||||
|
||||
const nextInt = (bound: number): number => {
|
||||
if (!Number.isInteger(bound) || bound < 1) {
|
||||
throw new Error(`nextInt bound must be a positive integer, got ${bound}`);
|
||||
}
|
||||
// Rejection sampling, so the distribution stays uniform rather than modulo-biased.
|
||||
const limit = Math.floor(0x100000000 / bound) * bound;
|
||||
let draw = next();
|
||||
while (draw >= limit) draw = next();
|
||||
return draw % bound;
|
||||
};
|
||||
|
||||
return {
|
||||
next,
|
||||
nextInt,
|
||||
d12: () => nextInt(12) + 1,
|
||||
shuffle: <T,>(items: readonly T[]): T[] => {
|
||||
const out = items.slice();
|
||||
for (let i = out.length - 1; i > 0; i--) {
|
||||
const j = nextInt(i + 1);
|
||||
const a = out[i]!;
|
||||
const b = out[j]!;
|
||||
out[i] = b;
|
||||
out[j] = a;
|
||||
}
|
||||
return out;
|
||||
},
|
||||
getState: () => state,
|
||||
};
|
||||
}
|
||||
|
||||
/** Restore an RNG mid-stream, for rebuilding a game from a snapshot. */
|
||||
export function restoreRng(state: number): Rng {
|
||||
// mulberry32 advances from its state before drawing, and createRng seeds state directly,
|
||||
// so restoring is just seeding with the saved state.
|
||||
return createRng(state);
|
||||
}
|
||||
Reference in New Issue
Block a user