Rejection sampling, in full
The general idea is to sample from something easy and throw away whatever does not fit. If you can generate points uniformly in a square but want points uniformly in the circle inscribed in it, generate in the square, keep the ones that land inside the circle, discard the rest. Every accepted point is then uniform over the circle. The cost is the discarded work, measured by the acceptance rate. The payoff is that accepted samples carry no bias from the shortcut.
The random tools on Vibe Numbers use the integer form. rollInt(max) in components/random/random.ts fills a one-element Uint32Array from crypto.getRandomValues, which the Web Crypto specification defines as a source of cryptographically strong values. That gives x uniform over the 2^32 integers from 0 to 4294967295. The code then computes limit = Math.floor(2^32 / max) x max, the largest multiple of max inside that range, redraws while x is greater than or equal to limit, and returns 1 + (x % max).
Truncating at a multiple of max is what makes the result exactly uniform. The kept range 0 to limit-1 holds exactly limit values, and limit divides evenly by max, so every remainder from 0 to max-1 is backed by the same number of inputs. Without the truncation the leftover tail would hand a few remainders one extra input each, which is modulo bias. The loop terminates with probability 1 and in practice almost always accepts on the first draw.
- Probability
- Accept-reject method, Acceptance-rejection sampling
A worked example
Take max = 6, a d6. 2^32 = 4,294,967,296, and 4,294,967,296 / 6 = 715,827,882.66..., so limit = 715,827,882 x 6 = 4,294,967,292. Values 0 through 4,294,967,291 are kept and exactly four values, 4,294,967,292 to 4,294,967,295, are discarded. The rejection chance per draw is 4 in 4,294,967,296, under one in a billion, and each face ends up backed by exactly 715,827,882 integers.
Sources
crypto.getRandomValues fills a typed array with cryptographically strong random values, and is the counterpart to Math.random, which is described as a non-cryptographic source.
MDN Web Docs — Crypto: getRandomValues() method
See also
Where it comes up on this site
Rejection sampling: frequently asked
No. A d4, a d8 and a coin all divide 2^32 exactly, so nothing is ever rejected.
Because 2^32 is not divisible by most face counts, and the leftover tail hands a few outcomes one extra backing value each. On a d6 the skew is about one part in 716 million, which nobody would ever detect, and the redraw removes it for the cost of one comparison. There is no reason to keep a known defect that cheap to fix.
It has no guaranteed stopping point, but the chance of a single rejection on a d6 is roughly 1 in 1.07 billion, so two in a row is not a thing you will see.