import { random } from "../../src/utils/random.js"; import { init } from "../init.js"; // Initialise the testing environment init(); /** * All expected values below were generated by running the equivalent Python code * using CPython's `random` module (Mersenne Twister 19937). For example: * * ```python * import random * random.seed(42) * [random.random() for _ in range(10)] * ``` * * The JS implementation must match Python's output exactly for reproducibility. */ describe("random (Mersenne Twister 19937, Python-compatible)", () => { // Must be the first test — verifies auto-seeding at module load produces non-zero output. it("should return non-zero values before any explicit seed() call", () => { const value = random.random(); expect(value).not.toBe(0); expect(value).toBeGreaterThanOrEqual(0); expect(value).toBeLessThan(1); }); describe("random()", () => { it("should produce the correct sequence for seed(42)", () => { const expected = [0.6394267984578837, 0.025010755222666936, 0.27502931836911926, 0.22321073814882275, 0.7364712141640124, 0.6766994874229113, 0.8921795677048454, 0.08693883262941615, 0.4219218196852704, 0.029797219438070344]; random.seed(42); for (let i = 0; i < expected.length; i++) { expect(random.random()).toBe(expected[i]); } }); it("should produce the correct sequence for seed(0)", () => { const expected = [0.8444218515250481, 0.7579544029403025, 0.420571580830845, 0.25891675029296335, 0.5112747213686085, 0.4049341374504143, 0.7837985890347726, 0.30331272607892745, 0.4765969541523558, 0.5833820394550312]; random.seed(0); for (let i = 0; i < expected.length; i++) { expect(random.random()).toBe(expected[i]); } }); it("should produce the correct sequence for seed(12345)", () => { const expected = [0.41661987254534116, 0.010169169457068361, 0.8252065092537432, 0.2986398551995928, 0.3684116894884757, 0.19366134904507426, 0.5660081687288613, 0.1616878239293682, 0.12426688428353017, 0.4329362680099159]; random.seed(12345); for (let i = 0; i < expected.length; i++) { expect(random.random()).toBe(expected[i]); } }); it("should handle a large seed (2^40 + 17)", () => { const expected = [0.6614192930187859, 0.24151842637090726, 0.6909905728254405, 0.11131596896699081, 0.8184932948202619, 0.4957890371902045, 0.3754628980018908, 0.6967482542555166, 0.5464037278028259, 0.4648564722045134]; random.seed(2 ** 40 + 17); for (let i = 0; i < expected.length; i++) { expect(random.random()).toBe(expected[i]); } }); it("should always return values in [0, 1)", () => { random.seed(777); for (let i = 0; i < 10000; i++) { const v = random.random(); expect(v).toBeGreaterThanOrEqual(0); expect(v).toBeLessThan(1); } }); }); describe("determinism", () => { it("should produce the same sequence when re-seeded with the same value", () => { random.seed(42); const first = Array.from({ length: 20 }, () => random.random()); random.seed(42); const second = Array.from({ length: 20 }, () => random.random()); expect(first).toEqual(second); }); it("should produce different sequences for different seeds", () => { random.seed(1); const seq1 = Array.from({ length: 10 }, () => random.random()); random.seed(2); const seq2 = Array.from({ length: 10 }, () => random.random()); expect(seq1).not.toEqual(seq2); }); }); describe("gauss()", () => { it("should produce the correct sequence for seed(42) with default mu=0, sigma=1", () => { const expected = [-0.14409032957792836, -0.1729036003315193, -0.11131586156766246, 0.7019837250988631, -0.12758828378288709, -1.4973534143409575, 0.33231834406771527, -0.2673374784971682, -0.216958684145195, 0.11588478670085507]; random.seed(42); for (let i = 0; i < expected.length; i++) { expect(random.gauss()).toBe(expected[i]); } }); it("should produce the correct sequence for seed(99) with mu=5, sigma=2", () => { const expected = [3.8995546045187948, 5.758236653956967, 5.653854573190089, 6.362796994590186, 5.0943415236981435, 3.4818290248514048, 2.746397968893985, 7.035976389331746, 0.4167692939861194, 3.8652143740326643]; random.seed(99); for (let i = 0; i < expected.length; i++) { expect(random.gauss(5, 2)).toBe(expected[i]); } }); it("should have approximately zero mean and unit variance over many samples", () => { random.seed(314159); const n = 50000; let sum = 0; let sumSq = 0; for (let i = 0; i < n; i++) { const v = random.gauss(); sum += v; sumSq += v * v; } const mean = sum / n; const variance = sumSq / n - mean * mean; expect(Math.abs(mean)).toBeLessThan(0.02); expect(Math.abs(variance - 1)).toBeLessThan(0.05); }); }); describe("shuffle()", () => { it("should produce the correct permutation for seed(42) on [0..9]", () => { const expected = [7, 3, 2, 8, 5, 6, 9, 4, 0, 1]; const arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; random.seed(42); random.shuffle(arr); expect(arr).toEqual(expected); }); it("should produce the correct permutation for seed(7) on letters", () => { const expected = ["i", "d", "b", "e", "h", "a", "j", "g", "c", "f"]; const arr = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]; random.seed(7); random.shuffle(arr); expect(arr).toEqual(expected); }); it("should preserve all elements (no duplicates, no missing)", () => { const original = Array.from({ length: 100 }, (_, i) => i); const arr = [...original]; random.seed(2025); random.shuffle(arr); expect(arr.sort((a, b) => a - b)).toEqual(original); }); it("should be a no-op for single-element and empty arrays", () => { const single = [42]; random.seed(0); random.shuffle(single); expect(single).toEqual([42]); const empty = []; random.shuffle(empty); expect(empty).toEqual([]); }); }); describe("choices()", () => { it("should produce the correct picks for seed(42) with weights [10,1,1,1]", () => { const expected = ["a", "a", "a", "a", "a", "a", "c", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "b", "a"]; const population = ["a", "b", "c", "d"]; const weights = [10, 1, 1, 1]; random.seed(42); for (let i = 0; i < expected.length; i++) { expect(random.choices(population, weights)).toBe(expected[i]); } }); it("should produce the correct picks for seed(42) with float weights", () => { // Python: random.seed(42); [random.choices([0,1,2,3,4], weights=[0.1,0.2,0.3,0.25,0.15])[0] for _ in range(20)] const expected = [3, 0, 1, 1, 3, 3, 4, 0, 2, 0, 1, 2, 0, 1, 3, 2, 1, 2, 3, 0]; const population = [0, 1, 2, 3, 4]; const weights = [0.1, 0.2, 0.3, 0.25, 0.15]; random.seed(42); for (let i = 0; i < expected.length; i++) { expect(random.choices(population, weights)).toBe(expected[i]); } }); it("should produce the correct picks for seed(12345) with uniform weights", () => { // Python: random.seed(12345); [random.choices(['x','y','z'], weights=[1,1,1])[0] for _ in range(15)] const expected = ["y", "x", "z", "x", "y", "x", "y", "x", "x", "y", "y", "x", "y", "y", "z"]; const population = ["x", "y", "z"]; const weights = [1, 1, 1]; random.seed(12345); for (let i = 0; i < expected.length; i++) { expect(random.choices(population, weights)).toBe(expected[i]); } }); it("should only ever return elements from the population", () => { const population = [10, 20, 30]; const weights = [1, 1, 1]; random.seed(123); for (let i = 0; i < 1000; i++) { expect(population).toContain(random.choices(population, weights)); } }); it("should respect heavily skewed weights", () => { // With weight 1000 on "x" and 1 on "y", nearly all picks should be "x" const population = ["x", "y"]; const weights = [1000, 1]; random.seed(555); let xCount = 0; const n = 5000; for (let i = 0; i < n; i++) { if (random.choices(population, weights) === "x") xCount++; } expect(xCount / n).toBeGreaterThan(0.99); }); it("should work with a single-element population", () => { random.seed(0); expect(random.choices(["only"], [1])).toBe("only"); expect(random.choices(["only"], [999])).toBe("only"); }); }); describe("Random class", () => { it("should be accessible as random.Random", () => { expect(typeof random.Random).toBe("function"); }); it("should produce the same sequence as the global functions given the same seed", () => { const rng = new random.Random(42); random.seed(42); for (let i = 0; i < 20; i++) { expect(rng.random()).toBe(random.random()); } }); it("should be independent from the global state — seeding instance does not affect global", () => { random.seed(42); const before = random.random(); // consume one global value expect(before).toBe(0.6394267984578837); const rng = new random.Random(99); rng.random(); // consume from instance rng.seed(0); // re-seed instance rng.random(); // consume again // global should continue from where it left off, unaffected expect(random.random()).toBe(0.025010755222666936); random.seed(42); expect(random.random()).toBe(before); }); it("should be independent from the global state — seeding global does not affect instance", () => { const rng = new random.Random(42); const v0 = rng.random(); random.seed(999); // re-seed global random.random(); // consume global values random.random(); // instance should continue from where it left off const rng2 = new random.Random(42); expect(rng2.random()).toBe(v0); }); it("two instances with the same seed produce identical sequences", () => { const rng1 = new random.Random(42); const rng2 = new random.Random(42); for (let i = 0; i < 20; i++) { expect(rng1.random()).toBe(rng2.random()); } }); it("two instances with different seeds produce different sequences", () => { const rng1 = new random.Random(1); const rng2 = new random.Random(2); const seq1 = Array.from({ length: 10 }, () => rng1.random()); const seq2 = Array.from({ length: 10 }, () => rng2.random()); expect(seq1).not.toEqual(seq2); }); it("two instances are independent — advancing one does not affect the other", () => { const rng1 = new random.Random(42); const rng2 = new random.Random(42); // Advance rng1 by 100 values for (let i = 0; i < 100; i++) rng1.random(); // rng2 should still be at the start const rng3 = new random.Random(42); expect(rng2.random()).toBe(rng3.random()); }); it("instance.seed() re-initialises state deterministically", () => { const rng = new random.Random(42); const first = Array.from({ length: 10 }, () => rng.random()); rng.seed(42); const second = Array.from({ length: 10 }, () => rng.random()); expect(first).toEqual(second); }); it("instance.seed() with no arguments seeds from entropy (non-zero output)", () => { const rng = new random.Random(); const v = rng.random(); expect(v).toBeGreaterThanOrEqual(0); expect(v).toBeLessThan(1); }); it("instance.gauss() matches global gauss() for the same seed", () => { const rng = new random.Random(42); random.seed(42); for (let i = 0; i < 10; i++) { expect(rng.gauss(5, 2)).toBe(random.gauss(5, 2)); } }); it("instance.shuffle() matches global shuffle() for the same seed", () => { const arr1 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; const arr2 = [...arr1]; const rng = new random.Random(42); rng.shuffle(arr1); random.seed(42); random.shuffle(arr2); expect(arr1).toEqual(arr2); }); it("instance.choices() matches global choices() for the same seed", () => { const population = ["a", "b", "c", "d"]; const weights = [10, 1, 1, 1]; const rng = new random.Random(42); random.seed(42); for (let i = 0; i < 20; i++) { expect(rng.choices(population, weights)).toBe(random.choices(population, weights)); } }); }); });