Skip to main content
diceroller.fun
diceroller.fun

How We Ensure Truly Fair Dice Rolls

An engineering breakdown of our cryptographically secure random number generation (CSPRNG), rejection sampling mathematics, and statistical testing.

1. The Physical Problem with Real Dice

Many tabletop players assume physical plastic or resin dice are the gold standard of fairness. In reality, mass-manufactured dice frequently have:

  • Interior Air Pockets: Bubbles trapped during resin injection shift the center of gravity, causing the die to roll heavier faces down.
  • Uneven Tumbling: Rounding edges in rock tumblers causes some facets to be slightly wider than others.
  • Inconsistent Carving: Drilled pips remove material unevenly (a face with 6 drilled holes weighs less than a face with 1).

2. The Mathematical Flaw in Naive Online Rollers

Basic dice websites frequently write JavaScript like this:

// BIASED IMPLEMENTATION: DO NOT USE
function rollDie(sides) {
  return Math.floor(Math.random() * sides) + 1; // Modulo bias & non-cryptographic
}

Math.random() is not cryptographically secure and can be influenced by seed predictability. Furthermore, naive integer truncations introduce modulo bias when mapping large integers into arbitrary side ranges.

3. Our Algorithm: Web Crypto + Rejection Sampling

To achieve mathematically unbiased distribution across every die size from d2 to d1000, diceroller.fun utilizes the browser's hardware-entropy Web Crypto API (crypto.getRandomValues) paired with rejection sampling:

const UINT32_MAX = 0x100000000; // 2^32

export function randInt(min: number, max: number): number {
  const range = max - min + 1;
  const limit = Math.floor(UINT32_MAX / range) * range;
  const buffer = new Uint32Array(1);

  let randomVal: number;
  do {
    crypto.getRandomValues(buffer);
    randomVal = buffer[0];
  } while (randomVal >= limit); // Discard & redraw remainder

  return min + (randomVal % range);
}

By discarding values that fall into the incomplete remainder slice [limit, 2^32), every integer in the target range receives an identically uniform slice of the 32-bit integer space. Modulo bias is mathematically reduced to exactly zero.

4. Automated Chi-Square Statistical Testing

Our test suite runs automated Chi-Square Goodness-of-Fit tests on 100,000 rolls of six-sided (d6) and twenty-sided (d20) dice during every continuous integration build. This confirms that empirical frequencies conform to the expected uniform distribution with a p-value well within standard statistical confidence limits (p > 0.01).

Frequently Asked Questions

Why not just use Math.random()? ↓

Standard Math.random() in JavaScript is typically an implementation of xorshift128+ or similar non-cryptographic PRNGs. While fast, it has known statistical weaknesses, period limitations, and when combined with standard modulo arithmetic (% sides), it suffers from modulo bias.

What is modulo bias and how do you eliminate it? ↓

When you take a large uniform integer space (such as 32-bit unsigned integers from 0 to 4,294,967,295) and map it onto a smaller range using the modulo operator (% range), any remainder at the end of the 2^32 boundary gives an unfair advantage to the smallest numbers. We eliminate this by using rejection sampling: we compute the highest clean multiple of the range and discard (reject) any drawn integer that falls into the remainder zone, redrawing until an unbiased sample is obtained.

Is this true randomness? ↓

No deterministic computer algorithm without external physical entropy can claim "true" quantum randomness. Instead, we use a Cryptographically Secure Pseudo-Random Number Generator (CSPRNG), which draws on hardware entropy provided by the operating system kernel. For tabletop games, board games, and probability, CSPRNG with rejection sampling is mathematically undistinguishable from ideal randomness and eliminates the physical defects found in real acrylic dice.

Explore Related Dice Rollers