Browse Source

test(chase): harden rng coverage and seed semantics

Made-with: Cursor
master
npmrun 2 weeks ago
parent
commit
1ba4f4ba15
  1. 17
      src/game/chase/rng.ts
  2. 44
      tests/chase/rng.test.ts

17
src/game/chase/rng.ts

@ -2,6 +2,10 @@ export type Rng = () => number;
const DEFAULT_SEED = 0x9e3779b9; const DEFAULT_SEED = 0x9e3779b9;
function toUint32(value: number): number {
return value >>> 0;
}
function hashString(input: string): number { function hashString(input: string): number {
let hash = 2166136261; let hash = 2166136261;
for (let i = 0; i < input.length; i += 1) { for (let i = 0; i < input.length; i += 1) {
@ -12,8 +16,13 @@ function hashString(input: string): number {
} }
export function normalizeSeed(input?: string | number): number { export function normalizeSeed(input?: string | number): number {
// Seed normalization contract (always returns uint32):
// 1) finite number -> ToUint32 (e.g. -1 => 0xffffffff, 1.9 => 1)
// 2) numeric string -> parse then ToUint32
// 3) non-numeric string -> stable hash -> uint32
// 4) undefined / empty / whitespace / NaN / non-finite -> DEFAULT_SEED
if (typeof input === "number" && Number.isFinite(input)) { if (typeof input === "number" && Number.isFinite(input)) {
return input >>> 0; return toUint32(input);
} }
if (typeof input === "string") { if (typeof input === "string") {
@ -24,7 +33,7 @@ export function normalizeSeed(input?: string | number): number {
const numericSeed = Number(trimmed); const numericSeed = Number(trimmed);
if (Number.isFinite(numericSeed)) { if (Number.isFinite(numericSeed)) {
return numericSeed >>> 0; return toUint32(numericSeed);
} }
return hashString(trimmed); return hashString(trimmed);
@ -37,9 +46,9 @@ export function createRng(seed: number): Rng {
let state = normalizeSeed(seed); let state = normalizeSeed(seed);
return () => { return () => {
state = (state + 0x6d2b79f5) >>> 0; state = toUint32(state + 0x6d2b79f5);
let t = Math.imul(state ^ (state >>> 15), 1 | state); let t = Math.imul(state ^ (state >>> 15), 1 | state);
t ^= t + Math.imul(t ^ (t >>> 7), 61 | t); t ^= t + Math.imul(t ^ (t >>> 7), 61 | t);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296; return toUint32(t ^ (t >>> 14)) / 4294967296;
}; };
} }

44
tests/chase/rng.test.ts

@ -13,15 +13,49 @@ describe("chase rng", () => {
expect(sequenceA).toEqual(sequenceB); expect(sequenceA).toEqual(sequenceB);
}); });
it("normalizeSeed returns a number for undefined input", () => { it("keeps createRng outputs in [0, 1)", () => {
expect(typeof normalizeSeed(undefined)).toBe("number"); const rng = createRng(12345);
const samples = Array.from({ length: 500 }, () => rng());
for (const value of samples) {
expect(value).toBeGreaterThanOrEqual(0);
expect(value).toBeLessThan(1);
}
}); });
it("normalizeSeed returns a number for empty string", () => { it("matches a stable golden sequence for a fixed seed", () => {
expect(typeof normalizeSeed("")).toBe("number"); const rng = createRng(12345);
const sequence = Array.from({ length: 5 }, () => rng());
expect(sequence).toEqual([
0.9797282677609473,
0.3067522644996643,
0.484205421525985,
0.817934412509203,
0.5094283693470061,
]);
});
it("produces different sequences for different seeds", () => {
const rngA = createRng(12345);
const rngB = createRng(12346);
const sequenceA = Array.from({ length: 5 }, () => rngA());
const sequenceB = Array.from({ length: 5 }, () => rngB());
expect(sequenceA).not.toEqual(sequenceB);
}); });
it("normalizeSeed returns a number for non-numeric string", () => { it("normalizes undefined, empty and non-numeric string to numbers", () => {
expect(typeof normalizeSeed(undefined)).toBe("number");
expect(typeof normalizeSeed("")).toBe("number");
expect(typeof normalizeSeed("abc")).toBe("number"); expect(typeof normalizeSeed("abc")).toBe("number");
}); });
it("normalizes edge case seeds into uint32 semantics", () => {
expect(normalizeSeed(NaN)).toBe(0x9e3779b9);
expect(normalizeSeed(-1)).toBe(0xffffffff);
expect(normalizeSeed(1.9)).toBe(1);
expect(normalizeSeed(" ")).toBe(0x9e3779b9);
});
}); });

Loading…
Cancel
Save