@augment-vir/common
Version:
A collection of augments, helpers types, functions, and classes for any JavaScript environment.
141 lines (140 loc) • 5.1 kB
JavaScript
/**
* A class that produces deterministic, pseudo random numbers based on a given seed. This uses the
* Alea v0.9 algorithm from Johannes Baagøe.
*
* @category Random
* @category Package : @augment-vir/common
* @example
*
* ```ts
* import {SeededRandom} from '@augment-vir/common';
*
* const random = SeededRandom.fromSeed('hello there');
* console.info(random.next()); // this value will always be the same
*
* const random2 = SeededRandom.fromState(random.exportState());
* console.info(random.next(), random2.next()); // both of these values will always be identical
*
* const random3 = random2.clone();
* console.info(random.next(), random2.next(), random3.next()); // all of these values will always be identical
* ```
*
* @package [`@augment-vir/common`](https://www.npmjs.com/package/@augment-vir/common)
*/
export class SeededRandom {
alea;
/** Generate a new {@link SeededRandom} instance from a seed. */
static fromSeed(seed) {
return new SeededRandom(new AleaRandom(seed));
}
/** Generate a new {@link SeededRandom} instance from a specific state. */
static fromState(state) {
const alea = new AleaRandom('');
alea.importState(state);
return new SeededRandom(alea);
}
/**
* Exports the current state of the seeded random number generator so it can be cloned to
* another instance. Use {@link SeededRandom.fromState} to consume this state.
*/
exportState() {
return this.alea.exportState();
}
/**
* This constructor is private. Use {@link SeededRandom.fromSeed} or
* {@link SeededRandom.fromState} instead.
*/
constructor(alea) {
this.alea = alea;
}
/** Generates the next deterministic pseudo random number. */
next() {
return this.alea.next();
}
/**
* Create a {@link SeededRandom} copy with the same state as this one. This is the same as
* calling {@link SeededRandom.exportState} and passing it directly into
* {@link SeededRandom.fromState}.
*/
clone() {
return SeededRandom.fromState(this.exportState());
}
}
/**
* Alea algorithm has the following license from http://baagoe.com/en/RandomMusings/javascript. The
* code has been modified to fit into a class structure and simplify code flow and arguments. The
* algorithm itself has not changed.
*
* Copyright (C) 2010 by Johannes Baagøe (baagoe@baagoe.org)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
* associated documentation files (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge, publish, distribute,
* sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
* NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/** Alea v0.9 */
class AleaRandom {
n = 0xef_c8_24_9d;
s = [
this.mash(' '),
this.mash(' '),
this.mash(' '),
1,
];
/** Mash v0.9. */
mash(input) {
const data = input.toString();
for (let i = 0; i < data.length; i++) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.n += data.codePointAt(i);
let h = 0.025_196_032_824_169_38 * this.n;
this.n = h >>> 0;
h -= this.n;
h *= this.n;
this.n = h >>> 0;
h -= this.n;
this.n += h * 0x1_00_00_00_00; // 2^32
}
return (this.n >>> 0) * 2.328_306_436_538_696_3e-10; // 2^-32
}
constructor(seed) {
this.s[0] -= this.mash(seed);
if (this.s[0] < 0) {
this.s[0] += 1;
}
this.s[1] -= this.mash(seed);
if (this.s[1] < 0) {
this.s[1] += 1;
}
this.s[2] -= this.mash(seed);
/* node:coverage ignore next 3: I don't care if we don't cover this */
if (this.s[2] < 0) {
this.s[2] += 1;
}
}
next() {
const t = 2_091_639 * this.s[0] + this.s[3] * 2.328_306_436_538_696_3e-10; // 2^-32
this.s[0] = this.s[1];
this.s[1] = this.s[2];
// eslint-disable-next-line sonarjs/no-nested-assignment
return (this.s[2] = t - (this.s[3] = t | 0));
}
importState(state) {
this.s = state;
}
exportState() {
return [
...this.s,
];
}
}