diff --git a/.eslintrc.json b/.eslintrc.json index 2699b3c..b7ef63d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -2,10 +2,10 @@ "root": true, "extends": ["@fisch0920/eslint-config/node"], "rules": { - "no-console": "off", "@typescript-eslint/naming-convention": "off", - "import/consistent-type-specifier-style": "off", "@typescript-eslint/array-type": "off", - "@typescript-eslint/no-inferrable-types": "off" + "@typescript-eslint/no-inferrable-types": "off", + "unicorn/prefer-code-point": "off", + "unicorn/prefer-math-trunc": "off" } } diff --git a/package.json b/package.json index abd0e39..0f1280f 100644 --- a/package.json +++ b/package.json @@ -36,9 +36,6 @@ "test:typecheck": "tsc --noEmit", "test:unit": "vitest run" }, - "dependencies": { - "seedrandom": "^3.0.5" - }, "devDependencies": { "@fisch0920/eslint-config": "^1.4.0", "@total-typescript/ts-reset": "^0.6.1", @@ -51,6 +48,7 @@ "np": "^10.0.7", "npm-run-all2": "^6.2.2", "prettier": "^3.3.3", + "seedrandom": "^3.0.5", "tsup": "^8.2.4", "tsx": "^4.19.0", "typescript": "^5.5.4", @@ -65,6 +63,7 @@ "prng", "stats", "d3-random", + "probability", "seedrandom", "distribution", "pseudorandom", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 67d65d7..e99ff14 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,10 +7,6 @@ settings: importers: .: - dependencies: - seedrandom: - specifier: ^3.0.5 - version: 3.0.5 devDependencies: '@fisch0920/eslint-config': specifier: ^1.4.0 @@ -45,6 +41,9 @@ importers: prettier: specifier: ^3.3.3 version: 3.3.3 + seedrandom: + specifier: ^3.0.5 + version: 3.0.5 tsup: specifier: ^8.2.4 version: 8.2.4(postcss@8.4.44)(tsx@4.19.0)(typescript@5.5.4)(yaml@2.5.0) diff --git a/readme.md b/readme.md index 54c1c2f..a7474f8 100644 --- a/readme.md +++ b/readme.md @@ -8,23 +8,18 @@ Welcome to the most **random** module on npm! 😜 ## Highlights -- Simple TS API -- Supports all modern JS/TS runtimes -- Seedable based on entropy or user input -- Plugin support for different pseudo random number generators (PRNGs) +- Simple TS API with zero dependencies +- **Seedable** +- Plugin support for different pseudo random number generators - Includes many common distributions - uniform, normal, poisson, bernoulli, etc -- Validates all user input -- Integrates with [seedrandom](https://github.com/davidbau/seedrandom) +- Replacement for `seedrandom` which hasn't been updated in over 5 years +- Supports all modern JS/TS runtimes ## Install ```bash -npm install --save random -# or -yarn add random -# or -pnpm add random +npm install random ``` ## Usage @@ -88,29 +83,22 @@ poisson() // 4 poisson() // 1 ``` -Note that returning a thunk here is more efficient when generating multiple -samples from the same distribution. +Note that returning a thunk here is more efficient when generating multiple samples from the same distribution. You can change the underlying PRNG or its seed as follows: ```ts -import seedrandom from 'seedrandom' +// change the underlying pseudo random number generator seed. +// by default, Math.random is used as the underlying PRNG, but it is not seedable, +// so if a seed is given, we use an ARC4 PRNG. +random.use('my-seed') -// change the underlying pseudo random number generator -// by default, Math.random is used as the underlying PRNG -random.use(seedrandom('foobar')) - -// create a new independent random number generator (uses seedrandom under the hood) +// create a new independent random number generator (uses ARC4 under the hood) const rng = random.clone('my-new-seed') -// create a second independent random number generator and use a seeded PRNG +// create a second independent random number generator using a custom PRNG +import seedrandom from 'seedrandom' const rng2 = random.clone(seedrandom('kittyfoo')) - -// replace Math.random with rng.uniform -rng.patch() - -// restore original Math.random -rng.unpatch() ``` You can also instantiate a fresh instance of `Random`: @@ -136,8 +124,6 @@ const rng3 = new Random(seedrandom('my-seed-string')) - [rng](#rng) - [clone](#clone) - [use](#use) - - [patch](#patch) - - [unpatch](#unpatch) - [next](#next) - [float](#float) - [int](#int) @@ -218,22 +204,6 @@ random.use(Math.random) --- -#### [patch](https://github.com/transitive-bullshit/random/blob/e11a840a1cfe0f5bd9c43640f9645a0b28f61406/src/random.js#L94-L101) - -Patches `Math.random` with this Random instance's PRNG. - -Type: `function ()` - ---- - -#### [unpatch](https://github.com/transitive-bullshit/random/blob/e11a840a1cfe0f5bd9c43640f9645a0b28f61406/src/random.js#L106-L111) - -Restores a previously patched `Math.random` to its original value. - -Type: `function ()` - ---- - #### [next](https://github.com/transitive-bullshit/random/blob/e11a840a1cfe0f5bd9c43640f9645a0b28f61406/src/random.js#L124-L126) Convenience wrapper around `this.rng.next()` @@ -485,16 +455,7 @@ Type: `function (alpha): function` - Generators - [x] pluggable prng - - [ ] port more prng from boost - - [ ] custom entropy - -- Misc - - [x] browser support via rollup - - [x] basic docs - - [x] basic tests - - [x] test suite - - [x] initial release! - - [x] typescript support + - [ ] port more prng from boost / seedrandom ## Related diff --git a/src/__snapshots__/seed.test.ts.snap b/src/__snapshots__/seed.test.ts.snap index c4b2149..7c06213 100644 --- a/src/__snapshots__/seed.test.ts.snap +++ b/src/__snapshots__/seed.test.ts.snap @@ -107,105 +107,105 @@ exports[`random.clone with seedrandom rng is consistent 1`] = ` exports[`random.clone with string seed is consistent 1`] = ` [ - 0.4599885692070501, - 1.498943037863171, - -0.45711036116856824, - 1.198323491226302, - 0.31744349762199836, - 0.2685493132319243, - -0.1045591660009993, - 0.49085805160430834, - -0.8654625222187131, - 1.0328218787419141, - 0.08718695044417465, - -2.15550607808736, - -2.1319639855429267, - 0.0338703357113602, - -1.48263309903668, - 0.4704950584394675, - 0.7476730169358645, - 1.351653203830535, - -0.9828311064313547, - 2.1405398829122455, - 0.40888838714250364, - 1.423884889193753, - -0.4202365483836478, - -0.6292491126479662, - -0.06490555468136183, - -2.407099109475531, - 0.32361017371377715, - -0.4438871374219309, - -0.2346580940412258, - 1.0281352299125754, - -1.6882299086494705, - -1.473245383801771, - 0.8357555103173332, - 0.7135368445932185, - -0.7578513303659669, - -0.3387116629465658, - -0.4153256830832315, - -0.6688468771750677, - 0.4167651567060368, - 0.28869308064318294, - -0.5815805569463813, - -1.3495568924563546, - -0.5380190897913749, - -0.9973987475284024, - 0.5901468209689925, - 0.05511991679885503, - -0.5592048510240638, - 0.647797683123219, - 0.9328861463637381, - -0.9107368893790824, - -1.5202624973614696, - 1.9619960339057412, - -0.2564540016287483, - 1.8386441386224903, - -0.5597862507291175, - -0.5163295347733488, - 0.20253589017784882, - -1.1290363774605272, - -1.3852662932430548, - 0.4105908858480398, - -0.8670786023272543, - -0.37114554286023166, - -1.429674546123385, - 0.1766914665610556, - -0.7577884069438263, - 2.015582353783945, - -1.3383970039777633, - -2.294167384222547, - 1.8627818074176121, - 1.034364085597537, - 0.12766688422696987, - -0.09821117867725376, - -0.49714043547906833, - 0.8428332981015355, - 0.8132636354024767, - -0.49862783315847153, - -0.674525692894474, - -0.16560513623146625, - -0.6912392012751473, - 1.0125818546132972, - 1.5559982434188717, - -0.6998095951192632, - 1.2507104808040281, - 0.630302824033449, - -0.11980026990636526, - 0.1604586588015088, - 0.14859821202202345, - -1.1874345546175447, - -0.0006786940627859577, - 0.5206751108384351, - -0.11254950785067822, - 1.0632337013426334, - -0.13003669756609917, - -1.32542353979745, - -0.5616800941504286, - 1.2987971837982453, - 0.04547221580867748, - -0.27036136048909387, - 0.20371364246834672, - 1.5437165642697632, + 1.0605080478180011, + 0.3824102148617453, + -0.7366504345478391, + -0.7946475045739224, + -1.2247324314330592, + 0.6716850060654046, + -1.009299614178395, + 0.053268550981502426, + 0.9740577508148593, + -0.6752585320928494, + 0.7446218073301792, + -2.129054359211667, + -1.8318170942537888, + 0.12094888734754691, + -0.10445788111868438, + -0.08857052877829866, + -1.2097296776821065, + -0.117177897638088, + 0.589262412952502, + -0.31461503408417685, + 1.5492120933673714, + -0.30159579028234024, + 0.10169022209103494, + -1.1878904212756178, + -0.4045334642187247, + -1.7727229412321357, + -2.1370757665469235, + 2.552039624962811, + 0.116684628880318, + -0.0738586836791935, + -0.7462649962349035, + -0.7308953270140024, + -1.397496377452742, + 1.080022871482886, + -0.5550935933831801, + -2.166285498075109, + -1.4202974080672033, + 0.13941215473562024, + 1.9511413086889442, + -0.24760951962395597, + -0.7668772257957995, + 1.0080903850107452, + -0.46433681035376906, + 0.9140407264550278, + -0.7321078574285467, + 2.2728031290005806, + 0.7831495249798555, + -1.1747809631263495, + 0.5033686835114866, + -0.5853630434209829, + 0.8048849983338022, + -0.6120558584970164, + -0.511942670460981, + 0.6621138875933301, + -0.2962382844043797, + 0.17405575536871304, + 0.29531818471271, + -0.14469245026793423, + 0.7323251168391561, + 1.4823525216244526, + -0.5573550292228768, + 0.4219991769658255, + -0.07575077050239393, + -0.9185841160043985, + 0.9385302429828497, + 0.2425634286777713, + 0.8670907746244069, + -0.27307985429422366, + 0.9879088784100294, + 0.5647239501014518, + -1.2854231936567626, + 0.3697503934710063, + 0.03015265871219791, + 1.4794228859998708, + -1.2900824469544805, + 1.2416773731568156, + -0.08836086943630893, + 0.44847317559380423, + 0.35891377843696987, + -1.3734860166194451, + -0.22825428479145904, + 0.24768202437532366, + -0.03862140436191103, + 1.3852892668021894, + 0.07026553111435546, + -0.7807561264439524, + 0.2570017824237705, + -0.8189485607920781, + -1.274011647761965, + -0.7026654258228537, + -0.5601442639711995, + -0.48577032219059707, + 0.8647210761817536, + -0.7017979621827999, + 1.3803710519208559, + 0.47852874862935263, + -0.7706566252379807, + -0.42212246077683613, + 0.4452340452580614, + 0.5214098824451193, ] `; diff --git a/src/distributions/bates.ts b/src/distributions/bates.ts index fadc8b3..a28427f 100644 --- a/src/distributions/bates.ts +++ b/src/distributions/bates.ts @@ -1,4 +1,4 @@ -import { type Random } from '../random' +import type { Random } from '../random' import { numberValidator } from '../validation' export function bates(random: Random, n = 1) { diff --git a/src/distributions/bernoulli.ts b/src/distributions/bernoulli.ts index 9fdc333..3246e88 100644 --- a/src/distributions/bernoulli.ts +++ b/src/distributions/bernoulli.ts @@ -1,4 +1,4 @@ -import { type Random } from '../random' +import type { Random } from '../random' import { numberValidator } from '../validation' export function bernoulli(random: Random, p = 0.5) { diff --git a/src/distributions/binomial.ts b/src/distributions/binomial.ts index 93ec4a1..f055c11 100644 --- a/src/distributions/binomial.ts +++ b/src/distributions/binomial.ts @@ -1,4 +1,4 @@ -import { type Random } from '../random' +import type { Random } from '../random' import { numberValidator } from '../validation' export function binomial(random: Random, n = 1, p = 0.5) { diff --git a/src/distributions/choice.test.ts b/src/distributions/choice.test.ts index 35a6b5e..055351c 100644 --- a/src/distributions/choice.test.ts +++ b/src/distributions/choice.test.ts @@ -10,7 +10,7 @@ type TestFn = (sample: number) => void * @param d Distribution function * @returns Mean of d */ -export const calcMean = (d: DistFn, testFn: TestFn) => { +const calcMean = (d: DistFn, testFn: TestFn) => { const n = 10_000 let sum = 0 @@ -34,7 +34,7 @@ test('random.choice() with seedrandom has correct uniform mean selection', () => }) test('random.choice() produces valid output for mixed arrays', () => { - const r = random.clone(seedrandom('NWNmMmU2MzVmNWY5MzQ1MzdhZjc0M2Zm')) + const r = random.clone('NWNmMmU2MzVmNWY5MzQ1MzdhZjc0M2Zm') const a = [13, 'foo', { example: true }, false, null, 14.152] for (let i = 0; i < 1_000_000; ++i) { const s = r.choice(a)! @@ -43,7 +43,7 @@ test('random.choice() produces valid output for mixed arrays', () => { }) test('random.choice() produces undefined for empty arrays', () => { - const r = random.clone(seedrandom('MzdkYTRkNTE4YWVjYThiNzkwMGI5YzA4')) + const r = random.clone('MzdkYTRkNTE4YWVjYThiNzkwMGI5YzA4') const a: any[] = [] for (let i = 0; i < 1000; ++i) { const s = r.choice(a) @@ -52,7 +52,7 @@ test('random.choice() produces undefined for empty arrays', () => { }) test('random.choice() with invalid input', () => { - const r = random.clone(seedrandom('ZDJjM2IyNmFlNmVjNWQwMGZkMmY1Y2Nk')) + const r = random.clone('ZDJjM2IyNmFlNmVjNWQwMGZkMmY1Y2Nk') assert.throws( () => r.choice(5 as any), 'Random.choice expected input to be an array, got number' diff --git a/src/distributions/exponential.ts b/src/distributions/exponential.ts index 69148a9..e2c8bf9 100644 --- a/src/distributions/exponential.ts +++ b/src/distributions/exponential.ts @@ -1,4 +1,4 @@ -import { type Random } from '../random' +import type { Random } from '../random' import { numberValidator } from '../validation' export function exponential(random: Random, lambda = 1) { diff --git a/src/distributions/geometric.ts b/src/distributions/geometric.ts index e54603f..34ddd4f 100644 --- a/src/distributions/geometric.ts +++ b/src/distributions/geometric.ts @@ -1,4 +1,4 @@ -import { type Random } from '../random' +import type { Random } from '../random' import { numberValidator } from '../validation' export function geometric(random: Random, p = 0.5) { diff --git a/src/distributions/irwin-hall.ts b/src/distributions/irwin-hall.ts index 47cf0ac..42e79cb 100644 --- a/src/distributions/irwin-hall.ts +++ b/src/distributions/irwin-hall.ts @@ -1,4 +1,4 @@ -import { type Random } from '../random' +import type { Random } from '../random' import { numberValidator } from '../validation' export function irwinHall(random: Random, n = 1) { diff --git a/src/distributions/log-normal.ts b/src/distributions/log-normal.ts index 5375bc2..e2e2547 100644 --- a/src/distributions/log-normal.ts +++ b/src/distributions/log-normal.ts @@ -1,4 +1,4 @@ -import { type Random } from '../random' +import type { Random } from '../random' export function logNormal(random: Random, mu = 0, sigma = 1) { const normal = random.normal(mu, sigma) diff --git a/src/distributions/normal.test.ts b/src/distributions/normal.test.ts index 483f39a..677307b 100644 --- a/src/distributions/normal.test.ts +++ b/src/distributions/normal.test.ts @@ -1,10 +1,10 @@ import seedrandom from 'seedrandom' import { assert, test } from 'vitest' -import type { SeedType } from '../types' -import { RNGFunction } from '../../src/generators/function' -import { RNGMathRandom } from '../../src/generators/math-random' -import { RNGXOR128 } from '../../src/generators/xor128' +import type { SeedOrRNG } from '../types' +import { FunctionRNG } from '../../src/generators/function' +import { MathRandomRNG } from '../../src/generators/math-random' +import { XOR128RNG } from '../../src/generators/xor128' import random from '../random' test('random.normal() produces numbers', () => { @@ -12,11 +12,11 @@ test('random.normal() produces numbers', () => { const d = r.normal() for (let i = 0; i < 10_000; ++i) { const v = d() - assert.equal(typeof v, 'number') + assert.isNumber(v) } }) -const meanN = (t: T) => { +const meanN = (t: T) => { const r = random.clone(t) const d = r.normal(120) let sum = 0 @@ -29,21 +29,31 @@ const meanN = (t: T) => { } test('random.normal(120) has mean 120', () => { + const mean = meanN(0) + assert.closeTo(mean, 120, 0.5) +}) + +test('random.normal(120) with seedrandom has mean 120', () => { const mean = meanN(seedrandom('MzUyYjZjZmM4YWI5NzEwNDliZGRmOTE3')) assert.closeTo(mean, 120, 0.5) }) -test('random.normal(120) with RNGXOR128 has mean 120', () => { - const mean = meanN(new RNGXOR128(100)) +test('random.normal(120) with seed has mean 120', () => { + const mean = meanN('YzNlNmVhOTg0MjZkMTNhNzE2NDc3Mjkw') + assert.closeTo(mean, 120, 0.5) +}) + +test('random.normal(120) with XOR128RNG has mean 120', () => { + const mean = meanN(new XOR128RNG(100)) assert.closeTo(mean, 120, 0.5) }) -test('random.normal(120) with RNGFunction has mean 120', () => { - const mean = meanN(new RNGFunction(Math.random)) +test('random.normal(120) with FunctionRNG has mean 120', () => { + const mean = meanN(new FunctionRNG(Math.random)) assert.closeTo(mean, 120, 0.5) }) -test('random.normal(120) with RNGMathRandom has mean 120', () => { - const mean = meanN(new RNGMathRandom()) +test('random.normal(120) with MathRandomRNG has mean 120', () => { + const mean = meanN(new MathRandomRNG()) assert.closeTo(mean, 120, 0.5) }) diff --git a/src/distributions/normal.ts b/src/distributions/normal.ts index a89f2f0..e16c0f9 100644 --- a/src/distributions/normal.ts +++ b/src/distributions/normal.ts @@ -1,4 +1,4 @@ -import { type Random } from '../random' +import type { Random } from '../random' export function normal(random: Random, mu = 0, sigma = 1) { return () => { diff --git a/src/distributions/pareto.ts b/src/distributions/pareto.ts index 2456a32..aa6d0e3 100644 --- a/src/distributions/pareto.ts +++ b/src/distributions/pareto.ts @@ -1,4 +1,4 @@ -import { type Random } from '../random' +import type { Random } from '../random' import { numberValidator } from '../validation' export function pareto(random: Random, alpha = 1) { diff --git a/src/distributions/poisson.ts b/src/distributions/poisson.ts index 106e81a..451fcd2 100644 --- a/src/distributions/poisson.ts +++ b/src/distributions/poisson.ts @@ -1,4 +1,4 @@ -import { type Random } from '../random' +import type { Random } from '../random' import { numberValidator } from '../validation' const logFactorialTable = [ diff --git a/src/distributions/uniform-boolean.ts b/src/distributions/uniform-boolean.ts index 53a3fea..fde3773 100644 --- a/src/distributions/uniform-boolean.ts +++ b/src/distributions/uniform-boolean.ts @@ -1,4 +1,4 @@ -import { type Random } from '../random' +import type { Random } from '../random' export function uniformBoolean(random: Random) { return () => { diff --git a/src/distributions/uniform-int.ts b/src/distributions/uniform-int.ts index 650c9cd..dd1e9e6 100644 --- a/src/distributions/uniform-int.ts +++ b/src/distributions/uniform-int.ts @@ -1,4 +1,4 @@ -import { type Random } from '../random' +import type { Random } from '../random' import { numberValidator } from '../validation' export function uniformInt(random: Random, min = 0, max = 1) { diff --git a/src/distributions/uniform.test.ts b/src/distributions/uniform.test.ts index 784bdbd..5b252e4 100644 --- a/src/distributions/uniform.test.ts +++ b/src/distributions/uniform.test.ts @@ -1,9 +1,9 @@ import seedrandom from 'seedrandom' import { assert, test } from 'vitest' -import { RNGFunction } from '../../src/generators/function' -import { RNGMathRandom } from '../../src/generators/math-random' -import { RNGXOR128 } from '../../src/generators/xor128' +import { FunctionRNG } from '../../src/generators/function' +import { MathRandomRNG } from '../../src/generators/math-random' +import { XOR128RNG } from '../../src/generators/xor128' import random from '../random' type distFn = () => number @@ -12,7 +12,7 @@ type distFn = () => number * @param d Distribution function * @returns Mean of d */ -export const calcMean = (d: distFn) => { +const calcMean = (d: distFn) => { const n = 10_000 let sum = 0 @@ -31,16 +31,12 @@ export const calcMean = (d: distFn) => { * @param max * @param t */ -export const assertZeroMax = (d: distFn, max: number) => { +const assertZeroMax = (d: distFn, max: number) => { const n = 10_000 for (let i = 0; i < n; ++i) { const v = d() - if (v < 0 || v > max) { - console.log(v) - } - assert.isTrue(v >= 0) assert.isTrue(v < max) } @@ -50,7 +46,7 @@ export const assertZeroMax = (d: distFn, max: number) => { * Assert random.uniform(min, max) returns numbers in [min, max) * @param d Distribution function */ -export const inMinMax = (d: distFn, min: number, max: number) => { +const inMinMax = (d: distFn, min: number, max: number) => { for (let i = 0; i < 10_000; ++i) { const v = d() assert.isTrue(v >= min) @@ -75,22 +71,29 @@ test('random.uniform() with seedrandom has mean 0.5', () => { assert.closeTo(mean, 0.5, 0.05) }) -test('random.uniform() with RNGXOR128 has mean 0.5', () => { - const r = random.clone(new RNGXOR128(3)) +test('random.uniform() with seed has mean 0.5', () => { + const r = random.clone('OTU2YTM0NjQ5MjM1ZTA3MTg4YjQyYjUw') + const d = r.uniform() + const mean = calcMean(d) + assert.closeTo(mean, 0.5, 0.05) +}) + +test('random.uniform() with XOR128RNG has mean 0.5', () => { + const r = random.clone(new XOR128RNG(3)) const d = r.uniform() const mean = calcMean(d) assert.closeTo(mean, 0.5, 0.05) }) -test('random.uniform() with RNGFunction has mean 0.5', () => { - const r = random.clone(new RNGFunction(Math.random)) +test('random.uniform() with FunctionRNG has mean 0.5', () => { + const r = random.clone(new FunctionRNG(Math.random)) const d = r.uniform() const mean = calcMean(d) assert.closeTo(mean, 0.5, 0.05) }) -test('random.uniform() with RNGMathRandom has mean 0.5', () => { - const r = random.clone(new RNGMathRandom()) +test('random.uniform() with MathRandomRNG has mean 0.5', () => { + const r = random.clone(new MathRandomRNG()) const d = r.uniform() const mean = calcMean(d) assert.closeTo(mean, 0.5, 0.05) @@ -102,20 +105,20 @@ test('random.uniform(max) returns numbers in [0, max)', () => { assertZeroMax(d, 42) }) -test('random.uniform(max) with RNGXOR128 returns numbers in [0, max)', () => { - const r = random.clone(new RNGXOR128(3)) +test('random.uniform(max) with XOR128RNG returns numbers in [0, max)', () => { + const r = random.clone(new XOR128RNG(3)) const d = r.uniform(undefined, 42) assertZeroMax(d, 42) }) -test('random.uniform(max) with RNGFunction returns numbers in [0, max)', () => { - const r = random.clone(new RNGFunction(Math.random)) +test('random.uniform(max) with FunctionRNG returns numbers in [0, max)', () => { + const r = random.clone(new FunctionRNG(Math.random)) const d = r.uniform(undefined, 42) assertZeroMax(d, 42) }) -test('random.uniform(max) with RNGMathRandom returns numbers in [0, max)', () => { - const r = random.clone(new RNGMathRandom()) +test('random.uniform(max) with MathRandomRNG returns numbers in [0, max)', () => { + const r = random.clone(new MathRandomRNG()) const d = r.uniform(undefined, 42) assertZeroMax(d, 42) }) @@ -127,22 +130,22 @@ test('random.uniform(max) has mean max / 2', () => { assert.closeTo(mean, 21, 0.5) }) -test('random.uniform(max) RNGXOR128 has mean max / 2', () => { - const r = random.clone(new RNGXOR128(3)) +test('random.uniform(max) XOR128RNG has mean max / 2', () => { + const r = random.clone(new XOR128RNG(3)) const d = r.uniform(undefined, 42) const mean = calcMean(d) assert.closeTo(mean, 21, 0.5) }) -test('random.uniform(max) RNGFunction has mean max / 2', () => { - const r = random.clone(new RNGFunction(Math.random)) +test('random.uniform(max) FunctionRNG has mean max / 2', () => { + const r = random.clone(new FunctionRNG(Math.random)) const d = r.uniform(undefined, 42) const mean = calcMean(d) assert.closeTo(mean, 21, 0.5) }) -test('random.uniform(max) RNGMathRandom has mean max / 2', () => { - const r = random.clone(new RNGMathRandom()) +test('random.uniform(max) MathRandomRNG has mean max / 2', () => { + const r = random.clone(new MathRandomRNG()) const d = r.uniform(undefined, 42) const mean = calcMean(d) assert.closeTo(mean, 21, 0.5) @@ -154,8 +157,8 @@ test('random.uniform(min, max) returns numbers in [min, max)', () => { inMinMax(d, 10, 42) }) -test('random.uniform(min, max) with RNGXOR128 returns numbers in [min, max)', () => { - const r = random.clone(new RNGXOR128(2)) +test('random.uniform(min, max) with XOR128RNG returns numbers in [min, max)', () => { + const r = random.clone(new XOR128RNG(2)) const d = r.uniform(10, 42) inMinMax(d, 10, 42) }) @@ -167,8 +170,8 @@ test('random.uniform(min, max) has mean (min + max) / 2', () => { assert.closeTo(mean, 26, 0.5) }) -test('random.uniform(min, max) with RNGXOR128 has mean (min + max) / 2', () => { - const r = random.clone(new RNGXOR128(2)) +test('random.uniform(min, max) with XOR128RNG has mean (min + max) / 2', () => { + const r = random.clone(new XOR128RNG(2)) const d = r.uniform(10, 42) const mean = calcMean(d) assert.closeTo(mean, 26, 0.5) diff --git a/src/distributions/uniform.ts b/src/distributions/uniform.ts index 699145f..afc58a0 100644 --- a/src/distributions/uniform.ts +++ b/src/distributions/uniform.ts @@ -1,4 +1,4 @@ -import { type Random } from '../random' +import type { Random } from '../random' export function uniform(random: Random, min = 0, max = 1) { return () => { diff --git a/src/generators/arc4.ts b/src/generators/arc4.ts new file mode 100644 index 0000000..deddefb --- /dev/null +++ b/src/generators/arc4.ts @@ -0,0 +1,112 @@ +import type { Seed } from '../types' +import { RNG } from '../rng' +import { mixKey, processSeed } from '../utils' + +// +// ARC4 +// +// An ARC4 implementation. The constructor takes a key in the form of an array +// of at most (width) integers that should be 0 <= x < (width). +// +// The g(count) method returns a pseudorandom integer that concatenates the +// next (count) outputs from ARC4. Its return value is a number x that is in +// the range 0 <= x < (width ^ count). + +// The following constants are related to IEEE 754 limits. +// const width = 256 // each RC4 output is 0 <= x < 256 +// const chunks = 6 // at least six RC4 outputs for each double +const _arc4_startdenom = 281_474_976_710_656 // 256 ** 6 == width ** chunks +const _arc4_significance = 4_503_599_627_370_496 // 2 ** 52 significant digits in a double +const _arc4_overflow = 9_007_199_254_740_992 // 2 ** 53 == significance * 2 + +export class ARC4RNG extends RNG { + protected _seed: number + + i: number + j: number + S: number[] + + constructor(seed?: Seed) { + super() + + const s = processSeed(seed) + this._seed = s + const key = mixKey(s, []) + + const S: number[] = [] + const keylen = key.length + this.i = 0 + this.j = 0 + this.S = S + + // Set up S using the standard key scheduling algorithm. + let i = 0 + while (i <= 0xff) { + S[i] = i++ + } + + for (let i = 0, j = 0; i <= 0xff; i++) { + const t = S[i]! + j = 0xff & (j + key[i % keylen]! + t) + S[i] = S[j]! + S[j] = t + } + + // For more robust unpredictability, the function call below discards an + // initial batch of values. This is called RC4-drop. + this.g(256) + } + + override get name() { + return 'arc4' + } + + override next() { + // This function returns a random double in [0, 1) that contains + // randomness in every bit of the mantissa of the IEEE 754 value. + + let n = this.g(6) // Start with a numerator n < 2 ^ 48 + let d = _arc4_startdenom // and denominator d = 2 ^ 48. + let x = 0 // and no 'extra last byte'. + + while (n < _arc4_significance) { + // Fill up all significant digits (2 ** 52) + n = (n + x) * 256 // by shifting numerator and + d *= 256 // denominator and generating a + x = this.g(1) // new least-significant-byte. + } + + while (n >= _arc4_overflow) { + // To avoid rounding past overflow, before adding + n /= 2 // last byte, shift everything + d /= 2 // right using integer math until + x >>>= 1 // we have exactly the desired bits. + } + + return (n + x) / d // Form the number within [0, 1). + } + + g(count: number) { + const { S } = this + let { i, j } = this + let r = 0 + + while (count--) { + i = 0xff & (i + 1) + const t = S[i]! + S[j] = t + j = 0xff & (j + t) + S[i] = S[j]! + r = r * 256 + S[0xff & (S[i]! + t)]! + } + + this.i = i + this.j = j + + return r + } + + override clone() { + return new ARC4RNG(this._seed) + } +} diff --git a/src/generators/function.ts b/src/generators/function.ts index 92d91e9..2c34bdd 100644 --- a/src/generators/function.ts +++ b/src/generators/function.ts @@ -1,31 +1,26 @@ -import type { SeedFn } from '../types' +import type { RNGFn } from '../types' import { RNG } from '../rng' -export class RNGFunction extends RNG { - _name!: string - _seedFn!: SeedFn +export class FunctionRNG extends RNG { + _name: string + _rngFn: RNGFn - constructor(seedFn: SeedFn, opts?: Record) { + constructor(rngFn: RNGFn) { super() - this.seed(seedFn, opts) + this._name = rngFn.name ?? 'function' + this._rngFn = rngFn } - get name() { + override get name() { return this._name } - next() { - return this._seedFn() + override next() { + return this._rngFn() } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - seed(seedFn: SeedFn, _opts?: Record) { - this._name = seedFn.name ?? 'function' - this._seedFn = seedFn - } - - clone(_: undefined, opts: Record) { - return new RNGFunction(this._seedFn, opts) + override clone() { + return new FunctionRNG(this._rngFn) } } diff --git a/src/generators/index.ts b/src/generators/index.ts new file mode 100644 index 0000000..54c7255 --- /dev/null +++ b/src/generators/index.ts @@ -0,0 +1,4 @@ +export * from './arc4' +export * from './function' +export * from './math-random' +export * from './xor128' diff --git a/src/generators/math-random.ts b/src/generators/math-random.ts index 1e58507..af06ca9 100644 --- a/src/generators/math-random.ts +++ b/src/generators/math-random.ts @@ -1,20 +1,15 @@ import { RNG } from '../rng' -export class RNGMathRandom extends RNG { - get name() { - return 'default' +export class MathRandomRNG extends RNG { + override get name() { + return 'Math.random' } - next() { + override next() { return Math.random() } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - seed(_seed: unknown, _opts: Record) { - // intentionally empty - } - - clone() { - return new RNGMathRandom() + override clone() { + return new MathRandomRNG() } } diff --git a/src/generators/xor128.ts b/src/generators/xor128.ts index 779846e..b58fe65 100644 --- a/src/generators/xor128.ts +++ b/src/generators/xor128.ts @@ -1,27 +1,35 @@ +import type { Seed } from '../types' import { RNG } from '../rng' +import { processSeed } from '../utils' + +export class XOR128RNG extends RNG { + protected _seed: number -export class RNGXOR128 extends RNG { x: number y: number z: number w: number - constructor(seed: number, opts?: Record) { + constructor(seed?: Seed) { super() - this.x = 0 + this._seed = processSeed(seed) + this.x = this._seed this.y = 0 this.z = 0 this.w = 0 - this.seed(seed, opts) + // discard an initial batch of 64 values + for (let i = 0; i < 64; ++i) { + this.next() + } } - get name() { + override get name() { return 'xor128' } - next() { + override next() { const t = this.x ^ (this.x << 1) this.x = this.y this.y = this.z @@ -30,16 +38,7 @@ export class RNGXOR128 extends RNG { return (this.w >>> 0) / 0x1_00_00_00_00 } - seed(seed: number, opts?: Record) { - this.x = this._seed(seed, opts) - - // discard an initial batch of 64 values - for (let i = 0; i < 64; ++i) { - this.next() - } - } - - clone(seed: number, opts: Record) { - return new RNGXOR128(seed, opts) + override clone() { + return new XOR128RNG(this._seed) } } diff --git a/src/index.ts b/src/index.ts index a85bcf6..f65a51f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ +export * from './generators' export * from './random' export { default } from './random' export * from './rng' -export * from './rng-factory' -export * from './rng-factory' export type * from './types' +export * from './utils' diff --git a/src/random.ts b/src/random.ts index a174c81..14c5dee 100644 --- a/src/random.ts +++ b/src/random.ts @@ -1,5 +1,5 @@ import type { RNG } from './rng' -import type { SeedType } from './types' +import type { SeedOrRNG } from './types' import { bates } from './distributions/bates' import { bernoulli } from './distributions/bernoulli' import { binomial } from './distributions/binomial' @@ -13,8 +13,8 @@ import { poisson } from './distributions/poisson' import { uniform } from './distributions/uniform' import { uniformBoolean } from './distributions/uniform-boolean' import { uniformInt } from './distributions/uniform-int' -import { RNGMathRandom } from './generators/math-random' -import { RNGFactory } from './rng-factory' +import { MathRandomRNG } from './generators/math-random' +import { createRNG } from './utils' /** * Distribution function @@ -37,26 +37,23 @@ interface ICacheEntry { /** * Seedable random number generator supporting many common distributions. * - * Defaults to Math.random as its underlying pseudorandom number generator. - * * @name Random * @class * - * @param {RNG|function|string|number} [rng=Math.random] - Underlying the default, built-in `Math.random` pseudorandom number generator. + * @param {RNG|function|string|number} [rng=Math.random] - Underlying random number generator or a seed for the default PRNG. Defaults to `Math.random`. */ export class Random { protected _rng!: RNG protected readonly _cache: { [k: string]: ICacheEntry } = {} - protected _patch?: typeof Math.random - constructor(rng: SeedType = new RNGMathRandom()) { - this.use(rng) + constructor(seedOrRNG: SeedOrRNG = new MathRandomRNG()) { + this._rng = createRNG(seedOrRNG) } /** - * @member {RNG} Underlying pseudo-random number generator + * @member {RNG} rng - Underlying pseudo-random number generator. */ get rng() { return this._rng @@ -66,55 +63,26 @@ export class Random { * Creates a new `Random` instance, optionally specifying parameters to * set a new seed. * - * @see RNG.clone - * - * @param {string} [seed] - Optional seed for new RNG. * @return {Random} */ - clone(rng: SeedType = this.rng.clone()): Random { - return new Random(rng) + clone(seedOrRNG: SeedOrRNG = this.rng.clone()): Random { + return new Random(seedOrRNG) } /** - * Sets the underlying pseudorandom number generator used via - * either an instance of `seedrandom`, a custom instance of RNG - * (for PRNG plugins), or a string specifying the PRNG to use - * along with an optional `seed` and `opts` to initialize the - * RNG. + * Sets the underlying pseudorandom number generator. * * @example + * ```ts * import random from 'random' * - * random.use('example_seedrandom_string') - * // or - * random.use(seedrandom('kittens')) + * random.use('example-seed') * // or * random.use(Math.random) + * ``` */ - use(rng?: SeedType) { - this._rng = RNGFactory(rng) - } - - /** - * Patches `Math.random` with this Random instance's PRNG. - */ - patch() { - if (this._patch) { - throw new Error('Math.random already patched') - } - - this._patch = Math.random - Math.random = this.uniform() - } - - /** - * Restores a previously patched `Math.random` to its original value. - */ - unpatch() { - if (this._patch) { - Math.random = this._patch - delete this._patch - } + use(seedOrRNG: SeedOrRNG) { + this._rng = createRNG(seedOrRNG) } // -------------------------------------------------------------------------- @@ -205,7 +173,7 @@ export class Random { * * Convence wrapper around `random.uniformInt()` * - * @param {Array} [array] - Lower bound (integer, inclusive) + * @param {Array} [array] - Input array * @return {T | undefined} */ choice(array: Array): T | undefined { @@ -215,7 +183,7 @@ export class Random { ) } - const length = array?.length + const length = array.length if (length > 0) { const index = this.uniformInt(0, length - 1)() @@ -234,7 +202,6 @@ export class Random { * * @param {number} [min=0] - Lower bound (float, inclusive) * @param {number} [max=1] - Upper bound (float, exclusive) - * @return {function} */ uniform = (min?: number, max?: number) => { return this._memoize('uniform', uniform, min, max) @@ -392,7 +359,7 @@ export class Random { * Returns a thunk which that returns independent, identically distributed * samples from the specified distribution. * - * @private + * @internal * * @param {string} label - Name of distribution * @param {function} getter - Function which generates a new distribution @@ -400,7 +367,11 @@ export class Random { * * @return {function} */ - _memoize(label: string, getter: IDistFn, ...args: any[]): IDist { + protected _memoize( + label: string, + getter: IDistFn, + ...args: any[] + ): IDist { const key = `${args.join(';')}` let value = this._cache[label] @@ -416,5 +387,4 @@ export class Random { } } -// defaults to Math.random as its RNG export default new Random() diff --git a/src/rng-factory.ts b/src/rng-factory.ts deleted file mode 100644 index de25079..0000000 --- a/src/rng-factory.ts +++ /dev/null @@ -1,35 +0,0 @@ -import seedrandom from 'seedrandom' - -import { RNGFunction } from './generators/function' -import { RNG } from './rng' - -/** - * Construct an RNG with variable inputs. Used in calls to Random constructor. - * - * @param {...*} args - Distribution-specific arguments - * @return RNG - * - * @example - * ```ts - * new Random(RNGFactory(...args)) - * ``` - */ -export const RNGFactory = (...args: T) => { - const [arg0 = 'default'] = args - - switch (typeof arg0) { - case 'object': - if (arg0 instanceof RNG) { - return arg0 - } - break - - case 'function': - return new RNGFunction(arg0) - - default: - return new RNGFunction(seedrandom(...args)) - } - - throw new Error(`invalid RNG "${arg0}"`) -} diff --git a/src/rng.ts b/src/rng.ts index 7d7810c..c014c8b 100644 --- a/src/rng.ts +++ b/src/rng.ts @@ -1,30 +1,7 @@ -import type { SeedType } from './types' - export abstract class RNG { abstract get name(): string abstract next(): number - abstract seed(_seed?: SeedType, _opts?: Record): void - - abstract clone(_seed?: SeedType, _opts?: Record): RNG - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _seed(seed: number, _opts?: Record) { - // TODO: add entropy and stuff - - if (seed === (seed || 0)) { - return seed - } else { - const strSeed = '' + seed - let s = 0 - - for (let k = 0; k < strSeed.length; ++k) { - // eslint-disable-next-line unicorn/prefer-code-point, unicorn/prefer-math-trunc - s ^= strSeed.charCodeAt(k) | 0 - } - - return s - } - } + abstract clone(): RNG } diff --git a/src/seed.test.ts b/src/seed.test.ts index b28559e..0fbef75 100644 --- a/src/seed.test.ts +++ b/src/seed.test.ts @@ -29,16 +29,16 @@ test('random.clone with string seed is consistent', () => { test('Random constructor', () => { const rng = new Random() - expect(rng).toBeDefined() + expect(rng.rng.name).toEqual('Math.random') const rng2 = new Random(seedrandom('my-seed-string')) - expect(rng2).toBeDefined() + expect(rng2.rng.name).toEqual('prng') const rng3 = new Random(Math.random) - expect(rng3).toBeDefined() + expect(rng3.rng.name).toEqual('random') const rng4 = new Random('example-seed-string') - expect(rng4).toBeDefined() + expect(rng4.rng.name).toEqual('arc4') }) test('random seed consistency', () => { diff --git a/src/types.ts b/src/types.ts index 2cd0b1a..8176e7c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,5 @@ import type { RNG } from './rng' -export type SeedFn = () => number -export type SeedType = number | string | SeedFn | RNG +export type RNGFn = () => number +export type Seed = number | string +export type SeedOrRNG = number | string | RNGFn | RNG diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..9312c44 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,58 @@ +import type { Seed, SeedOrRNG } from './types' +import { ARC4RNG } from './generators/arc4' +import { FunctionRNG } from './generators/function' +import { RNG } from './rng' + +export function createRNG(seedOrRNG?: SeedOrRNG) { + switch (typeof seedOrRNG) { + case 'object': + if (seedOrRNG instanceof RNG) { + return seedOrRNG + } + break + + case 'function': + return new FunctionRNG(seedOrRNG) + + default: + return new ARC4RNG(seedOrRNG) + } + + throw new Error(`invalid RNG seed or instance "${seedOrRNG}"`) +} + +export function processSeed(seed?: Seed): number { + if (seed === undefined) { + seed = crypto.randomUUID() + } + + if (typeof seed === 'number') { + return seed + } + + const strSeed = `${seed}` + let s = 0 + + for (let k = 0; k < strSeed.length; ++k) { + s ^= strSeed.charCodeAt(k) | 0 + } + + return s +} + +export function mixKey(seed: number, key: number[]): number[] { + const seedStr = `${seed}` + let smear = 0 + let j = 0 + + while (j < seedStr.length) { + key[0xff & j] = + 0xff & ((smear ^= (key[0xff & j] ?? 0) * 19) + seedStr.charCodeAt(j++)) + } + + if (!key.length) { + return [0] + } + + return key +}