-
-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(random): add an RNG based on Alea (#82)
* feat(random): add good, seedable RNG We often use some seedable RNGs based on sinus. There are several problems with these: - The values are generally highly correlated. - Implementation differs and therefore fails pinning tests. - **Vast majority** of the returned values is **very** close to 1. - Values close to 0 are as good as imposibble. This provides a solid alternatives that will hopefully be much better and consistent in different environments (allowing for pinning tests). * test(deep-merge): fix failing test Creating a snapshot somehow made this fail. I suspect some kind of global scope pollution. Sadly unless we want to switch to AVA or something this seems to be the most elaborate and best maintained snapshotting library. Truth is though that the test should have always looked like this.
- Loading branch information
Showing
8 changed files
with
5,014 additions
and
433 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
/** | ||
* Seedable, fast and reasonably good (not crypto but more than okay for our | ||
* needs) random number generator. | ||
* | ||
* @remarks | ||
* Adapted from {@link https://web.archive.org/web/20110429100736/http://baagoe.com:80/en/RandomMusings/javascript}. | ||
* Original algorithm created by Johannes Baagøe \<baagoe\@baagoe.com\> in 2010. | ||
*/ | ||
|
||
/** | ||
* Random number generator. | ||
*/ | ||
export interface RNG { | ||
/** Returns \<0, 1). Faster than [[fract53]]. */ | ||
(): number; | ||
/** Returns \<0, 1). Provides more precise data. */ | ||
fract53(): number; | ||
/** Returns \<0, 2^32). */ | ||
uint32(): number; | ||
|
||
/** The algorithm gehind this instance. */ | ||
algorithm: string; | ||
/** The seed used to seed this instance. */ | ||
seed: Mashable[]; | ||
/** The version of this instance. */ | ||
version: string; | ||
} | ||
|
||
/** | ||
* Create a seeded pseudo random generator based on Alea by Johannes Baagøe. | ||
* | ||
* @param seed - All supplied arguments will be used as a seed. In case nothing | ||
* is supplied the current time will be used to seed the generator. | ||
* | ||
* @returns A ready to use seeded generator. | ||
*/ | ||
export function Alea(...seed: Mashable[]): RNG { | ||
return AleaImplementation(seed.length ? seed : [Date.now()]); | ||
} | ||
|
||
/** | ||
* An implementation of [[Alea]] without user input validation. | ||
* | ||
* @param seed - The data that will be used to seed the generator. | ||
* | ||
* @returns A ready to use seeded generator. | ||
*/ | ||
function AleaImplementation(seed: Mashable[]): RNG { | ||
let [s0, s1, s2] = mashSeed(seed); | ||
let c = 1; | ||
|
||
const random: RNG = (): number => { | ||
const t = 2091639 * s0 + c * 2.3283064365386963e-10; // 2^-32 | ||
s0 = s1; | ||
s1 = s2; | ||
return (s2 = t - (c = t | 0)); | ||
}; | ||
|
||
random.uint32 = (): number => random() * 0x100000000; // 2^32 | ||
|
||
random.fract53 = (): number => | ||
random() + ((random() * 0x200000) | 0) * 1.1102230246251565e-16; // 2^-53 | ||
|
||
random.algorithm = "Alea"; | ||
random.seed = seed; | ||
random.version = "0.9"; | ||
|
||
return random; | ||
} | ||
|
||
/** | ||
* Turn arbitrary data into values [[AleaImplementation]] can use to generate | ||
* random numbers. | ||
* | ||
* @param seed - Arbitrary data that will be used as the seed. | ||
* | ||
* @returns Three numbers to use as initial values for [[AleaImplementation]]. | ||
*/ | ||
function mashSeed(...seed: Mashable[]): [number, number, number] { | ||
const mash = Mash(); | ||
|
||
let s0 = mash(" "); | ||
let s1 = mash(" "); | ||
let s2 = mash(" "); | ||
|
||
for (let i = 0; i < seed.length; i++) { | ||
s0 -= mash(seed[i]); | ||
if (s0 < 0) { | ||
s0 += 1; | ||
} | ||
s1 -= mash(seed[i]); | ||
if (s1 < 0) { | ||
s1 += 1; | ||
} | ||
s2 -= mash(seed[i]); | ||
if (s2 < 0) { | ||
s2 += 1; | ||
} | ||
} | ||
|
||
return [s0, s1, s2]; | ||
} | ||
|
||
/** | ||
* Values of these types can be used as a seed. | ||
*/ | ||
export type Mashable = number | string | boolean | object | bigint; | ||
|
||
/** | ||
* Create a new mash function. | ||
* | ||
* @returns A nonpure function that takes arbitrary [[Mashable]] data and turns | ||
* them into numbers. | ||
*/ | ||
function Mash(): (data: Mashable) => number { | ||
let n = 0xefc8249d; | ||
|
||
return function(data): number { | ||
const string = data.toString(); | ||
for (let i = 0; i < string.length; i++) { | ||
n += string.charCodeAt(i); | ||
let h = 0.02519603282416938 * n; | ||
n = h >>> 0; | ||
h -= n; | ||
h *= n; | ||
n = h >>> 0; | ||
h -= n; | ||
n += h * 0x100000000; // 2^32 | ||
} | ||
return (n >>> 0) * 2.3283064365386963e-10; // 2^-32 | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./alea"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import snapshot from "snap-shot-it"; | ||
|
||
import { Alea, RNG } from "../../src/random"; | ||
|
||
/* global BigInt */ | ||
// Note: BigInt is supported by all maintained Node versions and even a bunch | ||
// of dead so it's safe to use it in tests. However it is not a standard yet | ||
// and therefore ESLint complains about it hence the declaration above. We | ||
// should remove this comment block once it becomes a standard and ESLint adds | ||
// it into it's list of builtin globals. | ||
|
||
describe("Alea", function(): void { | ||
this.timeout(60000); | ||
|
||
const count = 100; | ||
|
||
for (const { name, get } of [ | ||
{ name: "rng()", get: (rng: RNG): number => rng() }, | ||
{ name: "rng.fract53()", get: (rng: RNG): number => rng.fract53() }, | ||
{ name: "rng.uint32()", get: (rng: RNG): number => rng.uint32() } | ||
]) { | ||
describe(name, function(): void { | ||
for (const seed of [ | ||
[ | ||
"I'm an alligator", | ||
" I'm a mama-papa coming for you", | ||
" I'm the space invader", | ||
" I'll be a rock 'n' rollin' bitch for you." | ||
], | ||
+new Date("2020-01-01"), | ||
false, | ||
true, | ||
Math.PI, | ||
BigInt("4235467986087964853726437"), | ||
-0.00432, | ||
-423, | ||
0.1244942, | ||
77, | ||
0, | ||
"We can be zeros, just for one day" | ||
]) { | ||
it(`Seed: ${seed.toString()}`, function(): void { | ||
const alea = Alea(seed); | ||
|
||
const values = Array(count); | ||
for (let i = 0; i < count; ++i) { | ||
values[i] = get(alea); | ||
} | ||
|
||
// Note: All the code above could be replaced by two args for data | ||
// driven snapshots however the overhead of that is insane. The tests | ||
// would go from milliseconds to seconds. | ||
snapshot(values); | ||
}); | ||
} | ||
}); | ||
} | ||
}); |