import { Vector } from "./Vector.js";
/**
* A PRNGAlgorithm is a class that implements some technique for random number
* generation. It should have a seed and max value, as well as a next function.
* @typedef PRNGAlgorithm
* @prop {number} seed - The initial state of the PRNG.
* @prop {number} max - The maximum possible value of the PRNG.
* @prop {function(): number} next - The next psuedo-random number between 0 and 1.
*/
/**
* @class
*/
export class PRNG {
/**
* Creates an instance of PRNG.
* @param {Object} [options={}] - The configuration options for the PRNG.
* @param {number} [options.seed=Date.now()] - The default seed value for the PRNG is the current timestamp.
* @param {PRNGAlgorithm} [options.algorithm=XORShift32] - The algorithm to use
* for generating psuedo-random numbers.
*/
constructor({ seed = Date.now(), algorithm = XORShift32 } = {}) {
this.seed = seed;
this.algorithm = new algorithm(this.seed);
}
/**
* Generate a random float in the range [0, 1).
* @param {number} [max=1] - A single argument specifies a range of between 0 and that number.
* @param {number} [min=0] - Two numeric arguments specifies a range.
* @returns {number} A floating-point number in a given range, 0 (inclusive) and 1 (exclusive) if called without arguments.
*/
randomFloat(max = 1, min = 0) {
let range = max - min;
return range * (this.algorithm.next() / this.algorithm.max) + min;
}
/**
* Generate a random float in the range [-1, 1).
* @returns {number} A number between -1 (inclusive) and 1 (exclusive).
*/
randomBipolarFloat() {
return this.randomFloat() * 2 - 1;
}
/**
* Generate a random float that fits a normal distribution.
*
* The most likely values are at the mean. The standard deviation describes
* the spread of the distribution.
*
* With a mean of 0, and standard deviation of 1 (default values);
* - 68% of results fall within +/- 1
* - 95% of results fall within +/- 2
* - 99% of results fall within +/- 3
*
* @param {number} [mean=0] - The mean of the distribution.
* @param {number} [sd=1] - The standard deviation of the distribution.
* @returns {number}
*/
randomGaussian(mean = 0, sd = 1) {
let u, v, radius;
while (true) {
u = this.randomBipolarFloat();
v = this.randomBipolarFloat();
radius = u * u + v * v;
if (radius < 1 && radius >= 0) {
break;
}
}
const scale = Math.sqrt((-2 * Math.log(radius)) / radius);
return mean + sd * u * scale;
}
/**
* Generate a random unit vector.
* @returns {Vector} A {@link Vector} with a magnitude of 1 and a random angle between `0` and `TWO_PI`.
*/
randomUnitVector() {
const angle = this.randomFloat() * Math.PI * 2;
return Vector.fromAngle(angle);
}
/**
* Generate a random vector within the bounds of a given AABB.
* @param {AABB} box - The box to generate the vector within.
* @returns {Vector} A {@link Vector} within the {@link AABB}.
*/
randomVectorInAABB(box) {
return new Vector(
this.randomFloat(box.x - box.width, box.x + box.width),
this.randomFloat(box.y - box.height, box.y + box.height)
);
}
/**
* Generate a random {@link Vector} within a given {@link Circle}.
* @param {Circle} circle - The circle to generate the {@link Vector} within.
* @returns {Vector} A {@link Vector} within the {@link Circle}.
*/
randomVectorInCircle(circle) {
let angle = this.randomFloat() * Math.PI * 2;
let magnitude = Math.sqrt(this.randomFloat()) * circle.radius;
return Vector.add(
Vector.fromAngle(angle).multiply(magnitude),
new Vector(circle.x, circle.y)
);
}
/**
* Generate a random integer from a specified range of values.
* @param {number} min - The minimum integer value (inclusive).
* @param {number} max - The maximum integer value (exclusive).
* @returns {number} A random integer between min (inclusive) and max (exclusive).
*/
randomInteger(min, max) {
const range = max - min;
return min + Math.floor(this.randomFloat() * range);
}
/**
* Select a random element from an array.
* @param {array} array - The array from which to select an element.
* @returns {*} A randomly selected element from the array.
*/
randomElement(array) {
const index = Math.floor(this.randomFloat() * array.length);
return array[index];
}
/**
* Returns a sample without replacement from a given array and sample size.
* @param {array} array - The array from which to sample elements.
* @param {number} size - The number of samples to generate (must be a positive integer).
* @returns {array} An array of randomly selected elements from an array.
*/
randomSample(array, size) {
if (size < 1 || !Number.isInteger(size)) {
throw new TypeError("Sample size should be a positive integer.");
}
if (array.length < size) {
throw new Error("Sample size must not be greater than array length.");
}
let availableElements = [...array];
let result = [];
for (let i = 0; i < size; i++) {
const index = Math.floor(this.randomFloat() * availableElements.length);
result.push(availableElements[index]);
availableElements.splice(index, 1);
}
return result;
}
/**
* Returns a boolean based on a specified probability.
* @param {number} [chance=0.5] - The probability of returning true (between 0 and 1).
* @returns {boolean} True if the random float is less than the chance, otherwise false.
*/
randomChance(chance = 0.5) {
return this.randomFloat() < chance;
}
/**
* Selects an option probabalistically from a set of weighted choices.
* @param {Array<Object>} choices - An array of objects with `option` and `weight` properties.
* @param {function|number|string} choices[].option - The outcome of the choice.
* @param {number} choices[].weight - The weight of the choice. Higher weights (relative to the other choices) increase the likelihood of selection.
* @returns {function|number|string} The selected option.
*/
randomWeighted(choices) {
let totalWeight = 0;
for (let choice of choices) {
totalWeight += choice.weight;
}
const randomNumberInRange = this.randomFloat() * totalWeight;
let cumulativeWeight = 0;
for (let choice of choices) {
cumulativeWeight += choice.weight;
if (randomNumberInRange < cumulativeWeight) {
if (typeof choice.option === "function") {
return choice.option();
}
return choice.option;
}
}
}
}
/**
* LCG (Linear Congruential Generator) Pseudorandom Number Generator Class
* @implements {PRNGAlgorithm}
*
* @summary Implements the LCG algorithm to generate pseudorandom numbers.
* @description
* This class provides the LCG algorithm for generating psuedorandom
* numbers to the {@link PRNG} class.
*
* @example
* const rng = new LCG(123456789);
* console.log(rng.next()); // Generates a pseudorandom number
*
* @see https://en.wikipedia.org/wiki/Linear_congruential_generator
*/
export class LCG {
/**
* Creates an instance of LCG.
* @param {number} seed - The seed value for the LCG PRNG.
*/
constructor(seed) {
this.modulus = 2 ** 32;
this.multiplier = 1664525;
this.increment = 1013904223;
this.state = seed;
this.max = this.modulus;
}
/**
* Generates the next pseudorandom number.
* @returns {number} A number in the range [0 to this.modulus - 1).
*/
next() {
this.state = (this.multiplier * this.state + this.increment) % this.modulus;
return this.state;
}
}
/**
* Mulberry32 Pseudorandom Number Generator Class
* @implements {PRNGAlgorithm}
*
* @summary Implements the Mulberry32 algorithm to generate pseudorandom numbers.
* @description
* This class provides the Mulberry32 method of generating psuedorandom
* numbers to the {@link PRNG} class.
*
* The Mulberry32 algorithm was written by Tommy Ettinger in 2017 and is released to the public domain, meaning it can be freely used, modified, and distributed without restrictions.
*
* @example
* const rng = new Mulberry32(123456789);
* console.log(rng.next()); // Generates a pseudorandom number
*
* @see https://gist.github.com/tommyettinger/46a874533244883189143505d203312c
*/
export class Mulberry32 {
/**
* Creates an instance of Mulberry32.
* @param {number} seed - The seed value for the Mulberry32 PRNG.
*/
constructor(seed) {
this.state = seed;
this.max = 2 ** 32;
}
/**
* Generates the next psuedo-random number.
* @returns {number} A number in the range [0-1).
*
* @example
* // Returns a random number in the range [0-1)
* const rng = new Mulberry32();
* console.log(rng.next());
*/
next() {
let t = (this.state += 0x6d2b79f5);
t = Math.imul(t ^ (t >>> 15), 1 | t);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
this.state = t ^ (t >>> 14);
return this.state >>> 0;
}
}
/**
* XORShift32 Pseudorandom Number Generator Class
* @implements {PRNGAlgorithm}
*
* @summary Implements the XORShift32 algorithm to generate pseudorandom
* numbers.
* @description
* This class provides the XORShift32 method of generating psuedorandom
* numbers to the {@link PRNG} class. It uses bitwise operations
* to generate a sequence of pseudorandom 32-bit unsigned integers.
*
* The XORShift algorithm was written by George Marsaglia in 2003 and is
* released to the public domain, meaning it can be freely used, modified,
* and distributed without restrictions.
*
* @example
* const prng = new XORShift32(123456789);
* console.log(prng.next()); // Generates a pseudorandom number
*
* @see Marsaglia, G. (2003) "XORShift RNGs", Journal of Statistical
* Software https://www.jstatsoft.org/article/view/v008i14
*/
export class XORShift32 {
/**
* Creates an instance of XORShift32.
* @param {number} seed - The seed value for the PRNG.
*/
constructor(seed) {
this.state = seed;
this.max = 2 ** 32;
}
/**
* Generates the next psuedo-random number.
* @returns {number} A number in the range [0-1).
*
* @example
* // Returns a random number in the range [0-1)
* const rng = new XORShift32();
* console.log(rng.next());
*/
next() {
let a = this.state;
a ^= a << 13;
a ^= a >>> 17;
a ^= a << 5;
this.state = a >>> 0; // Ensure it stays a 32-bit unsigned integer
return this.state;
}
}
/**
* @summary Create a random hexadecimal string of a specified length.
* @description Uses Math.random() to make a hexadecimal string for
* setting the seed of a {@link Plot} to be used in a seedable {@link PRNG} such as {@link LCG}
* @param {number} n - Number of digits
* @returns {string} Hexadecimal string of length n
*/
export function unseededRandomHex(n) {
const hexArray = Array.from(
{
length: n,
},
() => {
return Math.floor(Math.random() * 16).toString(16);
}
);
const hex = hexArray.join("");
const decimal = parseInt(hex, 16);
return {
hex,
decimal,
};
}