341 lines
12 KiB
JavaScript
341 lines
12 KiB
JavaScript
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));
|
|
}
|
|
});
|
|
});
|
|
});
|