From 1ba4f4ba1576141d37524b7582077b8181e35f00 Mon Sep 17 00:00:00 2001 From: npmrun <1549469775@qq.com> Date: Sun, 26 Apr 2026 23:05:49 +0800 Subject: [PATCH] test(chase): harden rng coverage and seed semantics Made-with: Cursor --- src/game/chase/rng.ts | 17 +++++++++++++---- tests/chase/rng.test.ts | 44 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/src/game/chase/rng.ts b/src/game/chase/rng.ts index dddaf37..ccb1126 100644 --- a/src/game/chase/rng.ts +++ b/src/game/chase/rng.ts @@ -2,6 +2,10 @@ export type Rng = () => number; const DEFAULT_SEED = 0x9e3779b9; +function toUint32(value: number): number { + return value >>> 0; +} + function hashString(input: string): number { let hash = 2166136261; 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 { + // 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)) { - return input >>> 0; + return toUint32(input); } if (typeof input === "string") { @@ -24,7 +33,7 @@ export function normalizeSeed(input?: string | number): number { const numericSeed = Number(trimmed); if (Number.isFinite(numericSeed)) { - return numericSeed >>> 0; + return toUint32(numericSeed); } return hashString(trimmed); @@ -37,9 +46,9 @@ export function createRng(seed: number): Rng { let state = normalizeSeed(seed); return () => { - state = (state + 0x6d2b79f5) >>> 0; + state = toUint32(state + 0x6d2b79f5); let t = Math.imul(state ^ (state >>> 15), 1 | state); t ^= t + Math.imul(t ^ (t >>> 7), 61 | t); - return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + return toUint32(t ^ (t >>> 14)) / 4294967296; }; } diff --git a/tests/chase/rng.test.ts b/tests/chase/rng.test.ts index 33ccec6..4fc48ea 100644 --- a/tests/chase/rng.test.ts +++ b/tests/chase/rng.test.ts @@ -13,15 +13,49 @@ describe("chase rng", () => { expect(sequenceA).toEqual(sequenceB); }); - it("normalizeSeed returns a number for undefined input", () => { - expect(typeof normalizeSeed(undefined)).toBe("number"); + it("keeps createRng outputs in [0, 1)", () => { + 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", () => { - expect(typeof normalizeSeed("")).toBe("number"); + it("matches a stable golden sequence for a fixed seed", () => { + 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"); }); + + 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); + }); });