import { XOR128State } from "./xor128-state.js";
import { SplitMix64State } from "./splitmix64-state.js";
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
* @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]);
} 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()) {
console.warn(
"XOR128: seed is all zero. This is not recommended. Consider using a different seed. " +
"If no seed was provided, consider re-instantiating the generator."
);
}
}
/**
* 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] length of the string
* @param {string} [chars] characters to use. Default is A-Z, a-z, 0-9
* @returns {string} random string
*/
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] average value of the random numbers. Default is 0.5
* @param {number} [interval] semi interval of the random numbers. Default is 0.5
* @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 {any} item from input array
*/
pick_from_array(arr) {
if (!(arr instanceof Array))
throw new Error("XOR128: parameter must be an array");
if (arr.length === 0) return null;
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
*/
pick_from_string(str) {
if (typeof str !== "string")
throw new Error("XOR128: parameter must be a string");
if (str.length === 0) return null;
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
* @param {Array|string} x an array or a string
* @returns {any} item from input array or char from input string
*/
pick(x) {
if (x instanceof Array) return this.pick_from_array(x);
else if (typeof x === "string") return this.pick_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
* @returns {Array} shuffled array
*/
shuffle_array(arr) {
if (arr.length === 0) 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. The original string does not get shuffled.
* @param {string} string the string to be shuffled
* @returns {string} shuffled string
*/
shuffle_string(string) {
if (string.length === 0) 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. The original array or string does not get shuffled.
* @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 };