import type { Rng } from "@suaveplan/types"; import { pwgenError } from "../errors/codes.js"; /** * Bounded retry budget for the rejection loop. A correctly-implemented * `Rng.nextUint32` produces a uniformly-distributed uint32 every call; the * worst-case rejection probability is `(2^32 mod max) / 2^32 < max/2^32`, * which is < 0.0000019 for `max = 7776`. 1024 attempts give a worst-case * success probability of `1 - (max/2^32)^1024`, indistinguishable from 1. * Hitting this cap therefore proves the supplied `Rng` is broken. */ const REJECTION_RETRY_BUDGET = 1024; /** Exclusive upper bound of the uint32 space. */ const TWO_POW_32 = 0x1_0000_0000; /** * Return a uniformly-distributed integer in the half-open interval * `[0, max)` using Lemire-style rejection sampling. * * Reference: Daniel Lemire, _Fast Random Integer Generation in an * Interval_, ACM Trans. Model. Comput. Simul. 2019, * https://arxiv.org/abs/1805.10941 §3 ("Nearly Divisionless"). * * The naïve `Math.floor(rng() * max)` is biased for non-power-of-two * `max`. This implementation draws a uint32 and rejects values above the * threshold `2^32 - (2^32 mod max)`, re-drawing on rejection. The bias * disappears at the cost of an expected `< 2` draws per call. * * @throws PwgenError(PWGEN.RNG_FAILED) if `rng.nextUint32` returns * non-uint32 values for {@link REJECTION_RETRY_BUDGET} consecutive * attempts. */ export function randomInt(rng: Rng, max: number): number { if (!Number.isInteger(max) || max < 1 || max >= TWO_POW_32) { throw pwgenError( "PWGEN.RNG_FAILED", `randomInt requires max in [1, 2^32); received ${max}` ); } const threshold = TWO_POW_32 - (TWO_POW_32 % max); for (let attempt = 0; attempt < REJECTION_RETRY_BUDGET; attempt++) { const draw = rng.nextUint32() >>> 0; if (draw < threshold) return draw % max; } throw pwgenError( "PWGEN.RNG_FAILED", `Rng failed to produce an in-range uint32 after ${REJECTION_RETRY_BUDGET} attempts` ); }