feat(sim): seeded PRNG
This commit is contained in:
1 parent
cb1c4177d1
commit
13192ee0bc
2 files changed
+83
No files matched your search
@@ -0,0 +1,35 @@
|
||||
export interface RngHolder {
|
||||
rngState: number;
|
||||
}
|
||||
|
||||
/** mulberry32 — mutates holder.rngState, returns a float in [0, 1). */
|
||||
export function random(h: RngHolder): number {
|
||||
let t = (h.rngState = (h.rngState + 0x6d2b79f5) | 0);
|
||||
t = Math.imul(t ^ (t >>> 15), t | 1);
|
||||
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
}
|
||||
|
||||
export function randomInt(h: RngHolder, min: number, max: number): number {
|
||||
return min + Math.floor(random(h) * (max - min + 1));
|
||||
}
|
||||
|
||||
export function weightedPick<T>(
|
||||
h: RngHolder,
|
||||
items: readonly T[],
|
||||
weight: (item: T) => number,
|
||||
): T | undefined {
|
||||
const weights = items.map((it) => Math.max(0, weight(it)));
|
||||
const total = weights.reduce((sum, w) => sum + w, 0);
|
||||
if (total <= 0) return undefined;
|
||||
let r = random(h) * total;
|
||||
let lastPositive: T | undefined;
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const w = weights[i];
|
||||
if (w === 0) continue;
|
||||
lastPositive = items[i];
|
||||
if (r < w) return items[i];
|
||||
r -= w;
|
||||
}
|
||||
return lastPositive;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { random, randomInt, weightedPick } from '../../src/sim/rng';
|
||||
|
||||
describe('rng', () => {
|
||||
it('is deterministic for a given seed', () => {
|
||||
const a = { rngState: 42 };
|
||||
const b = { rngState: 42 };
|
||||
const seqA = [random(a), random(a), random(a)];
|
||||
const seqB = [random(b), random(b), random(b)];
|
||||
expect(seqA).toEqual(seqB);
|
||||
});
|
||||
|
||||
it('returns values in [0, 1)', () => {
|
||||
const h = { rngState: 7 };
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const v = random(h);
|
||||
expect(v).toBeGreaterThanOrEqual(0);
|
||||
expect(v).toBeLessThan(1);
|
||||
}
|
||||
});
|
||||
|
||||
it('randomInt stays within inclusive bounds', () => {
|
||||
const h = { rngState: 1 };
|
||||
const seen = new Set<number>();
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const v = randomInt(h, 3, 5);
|
||||
expect(v).toBeGreaterThanOrEqual(3);
|
||||
expect(v).toBeLessThanOrEqual(5);
|
||||
seen.add(v);
|
||||
}
|
||||
expect(seen.size).toBe(3);
|
||||
});
|
||||
|
||||
it('weightedPick never picks zero-weight items', () => {
|
||||
const h = { rngState: 3 };
|
||||
const items = ['a', 'b', 'c'];
|
||||
for (let i = 0; i < 500; i++) {
|
||||
const v = weightedPick(h, items, (x) => (x === 'b' ? 0 : 1));
|
||||
expect(v).not.toBe('b');
|
||||
}
|
||||
});
|
||||
|
||||
it('weightedPick returns undefined when all weights are zero', () => {
|
||||
const h = { rngState: 3 };
|
||||
expect(weightedPick(h, ['a'], () => 0)).toBeUndefined();
|
||||
expect(weightedPick(h, [], () => 1)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user