Initial commit

This commit is contained in:
Jesse
2026-07-31 07:19:57 -04:00
commit e7bc07df85
39 changed files with 12438 additions and 0 deletions
+75
View File
@@ -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);
}