/**
* XOR128 js implementation
* @version 1.1.0
* @author Lorenzo Rossi - https://www.lorenzoros.si - https://github.com/lorossi/
* @license MIT
*/
class XOR128State {
/**
* Internal state of the XOR128 pseudo-random number generator.
* @private
* @param {number} x
* @param {number} y
* @param {number} z
* @param {number} w
* @throws {Error} if any of x, y, z or w is not a number
*/
constructor(x, y, z, w) {
if (
typeof x !== "number" ||
typeof y !== "number" ||
typeof z !== "number" ||
typeof w !== "number"
)
throw new Error("XOR128: seed must be a number");
this._state = [x, y, z, w];
}
/**
* Get the current internal state as an array of 4 numbers.
*
* @returns {Array} current internal state
*/
get state() {
return this._state;
}
/**
* Return true if the internal state is all zero, false otherwise.
*
* @returns {boolean} true if the internal state is all zero, false otherwise
*/
isAllZero() {
return this._state.every((x) => x === 0);
}
/**
* Rotate the internal state.
*
* @returns {void}
*/
rotateState() {
const [x, y, z, w] = this._state;
this._state = [y, z, w, x];
}
/**
* Set the first word of the internal state.
*
* @returns {void}
* @throws {Error} if x is not a number
*/
setFirstWord(x) {
if (typeof x !== "number")
throw new Error("XOR128: first word must be a number");
this._state[0] = x;
}
}
class SplitMix64State {
/**
* Internal state of the SplitMix64 pseudo-random number generator.
* @private
* @param {number} seed
* @returns {void}
* @throws {Error} if seed is not a number
*/
constructor(seed) {
if (typeof seed !== "number")
throw new Error("SplitMix64State: seed must be a number");
this._seed = seed;
}
/**
* Get the current internal state.
*
* @returns {number} current internal state
*/
get seed() {
return this._seed;
}
/**
* Set the internal state.
*
* @param {number} seed
* @returns {void}
* @throws {Error} if seed is not a number
*/
set seed(seed) {
if (typeof seed !== "number")
throw new Error("SplitMix64State: seed must be a number");
this._seed = seed;
}
/**
* Mix the internal state.
* @returns {Array} array of two numbers
*/
mix() {
let big_seed = BigInt(this._seed);
big_seed += 0x9e3779b97f4a7c15n;
this._seed = Number(big_seed & BigInt(0xffffffffffffffffn));
let z = big_seed;
z = (z ^ (z >> BigInt(30n))) * BigInt(0xbf58476d1ce4e5b9n);
z = (z ^ (z >> BigInt(30n))) * BigInt(0x94d049bb133111ebn);
z = z ^ (z >> BigInt(31n));
const z_lower = Number(z & 0xffffffffn);
const z_upper = Number(z >> 32n);
return [z_lower, z_upper];
}
}
class XOR128 {
/**
* XOR128 pseudo-random number generator.
* Formerly based on the implementation by WizCorp https://github.com/Wizcorp/xor128/,
* now based on the xor128 as described on Wikipedia https://en.wikipedia.org/wiki/Xorshift
* All parameters are optional, if nothing is passed a random value from
* js functions Math.random() will be used
*
* @param {number|Array} [x] seed or array of seeds. \
* If an array is passed, the first 4 elements will be used as seeds
* @returns {XOR128}
* @throws {Error} if x is not a number or an array of 4 numbers
*/
constructor(x = null) {
if (x instanceof Array) {
// an array was passed, use the first 4 elements as seeds
if (x.length != 4) throw new Error("XOR128: array must have 4 elements");
this._xor_state = new XOR128State(...x);
} else if (x === null) {
// no seed was passed, use Math.random()
const xx = Math.floor(Math.random() * (2 ** 53 - 1));
return new XOR128(xx);
} else if (typeof x === "number") {
// a number was passed, use it as seed
if (x <= 0) throw new Error("XOR128: seed must be a non-negative number");
// create a SplitMix64State from the seed
this._split_state = new SplitMix64State(x);
const s1 = this._split_state.mix();
const s2 = this._split_state.mix();
// create a XOR128State from the seeds
this._xor_state = new XOR128State(s1[0], s1[1], s2[0], s2[1]);
// check if the seed is all
// this might happen but it is not recommended
if (this._xor_state.isAllZero())
console.warn(
"XOR128: seed is all zero, this is not recommended. ",
"If no seed was passed, try instantiating the class again"
);
} else throw new Error("XOR128: parameter must be a number or an array");
// check if the seed is all zero
if (this._xor_state.isAllZero())
throw new Error("XOR128: seed must not be all zero");
}
/**
* Returns a random number in range [a, b) (i.e. a included, b excluded)
* If only one parameter is passed, the random number will be generated in range [0, a)
* If no parameters are passed, the random number will be generated in range [0, 1)
*
* @param {number|undefined} [a] if two parameters are passed, minimum range value; maximum range value otherwise
* @param {number|undefined} [b] maximum range value
* @returns {number} random number
*/
random(a = undefined, b = undefined) {
if (a === undefined && b === undefined) {
// if no parameters are passed, generate a random number in range [0, 1)
a = 0;
b = 1;
} else if (b === undefined) {
// if only one parameter is passed, generate a random number in range [0, a)
b = a;
a = 0;
}
if (a > b)
throw new Error("XOR128: first parameter must be smaller than second");
if (typeof a !== "number" || typeof b !== "number")
throw new Error("XOR128: parameters must be numbers");
// all calculations are done with BigInts to avoid precision errors
let t = BigInt(this._xor_state.state[3]);
let s = BigInt(this._xor_state.state[0]);
this._xor_state.rotateState();
t ^= t << BigInt(11);
t ^= t >> BigInt(8);
t ^= s ^ (s >> BigInt(19));
// convert the first word of the internal state to a 32 bit number
const x = Number(BigInt.asUintN(32, t));
this._xor_state.setFirstWord(x);
// convert the result to a number in range [0, 1]
let r = Number(BigInt.asUintN(32, t)) / (2 ** 32 - 1);
// re map the result to the desired range
return a + r * (b - a);
}
/**
* Returns a random integer in range [a, b) (i.e. a included, b excluded)
* If only one parameter is passed, the random number will be generated in range [0, a)
* If no parameters are passed, the random number will be generated in range [0, 1]
*
* @param {number|undefined} [a] if two parameters are passed, minimum range value; maximum range value otherwise
* @param {number|undefined} [b] maximum range value
* @returns {number} random number
*/
random_int(a = undefined, b = undefined) {
if (a === undefined && b === undefined) {
// if no parameters are passed, generate a random number in range [0, 1]
a = 0;
b = 2;
} else if (b === undefined) {
// if only one parameter is passed, generate a random number in range [0, a)
b = a;
a = 0;
}
return Math.floor(this.random(a, b));
}
/**
* Returns a random boolean
*
* @returns {boolean} random boolean
*/
random_bool() {
return this.random() > 0.5;
}
/**
* Returns a random string
*
* @param {number} [length=10] length of the string
* @param {string} [chars="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"] characters to use
*/
random_string(
length = 10,
chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
) {
return new Array(length)
.fill(0)
.map(() => chars.charAt(this.random_int(0, chars.length)))
.join("");
}
/**
* Returns a random integer in range (average - interval, average + interval)
* If only one parameter is passed, the random number will be generated in range (average - 0.5, average + 0.5)
* If no parameters are passed, the random number will be generated in range [0, 1]
*
* @param {number} [average=0.5] average value of the random numbers
* @param {number} [interval=0.5] semi interval of the random numbers
* @returns {number} random number
*/
random_interval(average = 0.5, interval = 0.5) {
return this.random(average - interval, average + interval);
}
/**
* Returns a random item from the provided array
*
* @param {Array} arr an array
* @returns {*} item from input array
*/
random_from_array(arr) {
if (!(arr instanceof Array))
throw new Error("XOR128: parameter must be an array");
return arr[this.random_int(0, arr.length)];
}
/**
* Returns a random char from the provided string
*
* @param {string} str a string
* @returns {string} char from input string
*/
random_from_string(str) {
if (typeof str !== "string")
throw new Error("XOR128: parameter must be a string");
return str.charAt(this.random_int(0, str.length));
}
/**
* Returns a random item from the provided array or a random char from the provided string
*
* @returns {*} item from input array or char from input string
*/
pick(x) {
if (x instanceof Array) return this.random_from_array(x);
else if (typeof x === "string") return this.random_from_string(x);
else throw new Error("XOR128: parameter must be an array or a string");
}
/**
* Shuffles the provided array (the original array does not get shuffled)
*
* @param {Array} arr an array
*/
shuffle_array(arr) {
if (!arr) return null;
return [...arr]
.map((s) => ({ sort: this.random(), value: s }))
.sort((a, b) => a.sort - b.sort)
.map((a) => a.value);
}
/**
* Shuffles and returns a string
*
* @param {string} string the string to be shuffled
* @returns {string}
*/
shuffle_string(string) {
if (!string) return "";
return string
.split("")
.map((s) => ({ sort: this.random(), value: s }))
.sort((a, b) => a.sort - b.sort)
.map((a) => a.value)
.join("");
}
/**
* Shuffles and returns an array or a string.
*
* @param {Array|string} x an array or a string
* @returns {*} shuffled array or string
*/
shuffle(x) {
if (x instanceof Array) return this.shuffle_array(x);
if (typeof x === "string") return this.shuffle_string(x);
throw new Error("XOR128: parameter must be an array or a string");
}
}
export { XOR128 };