OS-007 · Probability

Dice Roll Simulator

One fair die is flat: every face is equally likely. Add a second and the sum piles up in the middle — a 7 is six times as likely as a 2. The simulator rolls up to ten dice at once and checks every bar of the histogram against the exact distribution of the sum.

Interactive model

OS-007 · dice-roll · seeded
simulated frequencyexact probability
Parameters
The roll is the sum of k dice
Same seed, same run — here, in simulation.js and in simulation.py.
Rolls
10,000
Mean of the sum
6.981
Exact mean k(s+1)/2
7.000
Variance of the sum
5.844
Exact variance k(s²−1)/12
5.833
Total variation distance
0.0117
TV from sampling noise alone (approx.)
0.0121

Example run: default parameters, seed 42.

Why the sum of dice is not flat

Two six-sided dice have 36 equally likely outcomes. Only one of them, 1 + 1, gives a sum of 2, but six give 7: 1 + 6, 2 + 5, 3 + 4 and their reverses. Middle sums can be made in more ways, so they come up more often. With three dice the peak is flatter and wider — 10 and 11 each have 27 of 216 outcomes, 12.5% — and by five or six dice the histogram looks like a bell curve. That is the central limit theorem at work on a very small scale.

Try one die against ten dice with the same number of sides, or a single d20 against 2d10: the ranges overlap, the shapes do not.

The exact distribution by convolution

The probability of a sum t with k dice follows from the distribution for k − 1 dice: the last die shows each face f = 1…s with probability 1/s.

Pk(t) = (1/s) · Σf=1…s Pk−1(t − f)

The model runs this recursion on integer counts of outcomes and divides by sk at the end. The largest case, ten d20s, has 2010 ≈ 1.02 × 1013 outcomes, well inside the range where floating-point numbers hold integers exactly, so the “exact” line has no rounding error beyond the final division.

One die has mean (s + 1)/2 and variance (s² − 1)/12. Means and variances of independent dice add, so the sum has

mean = k(s + 1)/2    variance = k(s² − 1)/12

For two six-sided dice that is 7 and 35/6 ≈ 5.833.

How close is the histogram?

The total variation (TV) distance is half the sum of the gaps between simulated and exact probabilities. It equals the largest difference the two distributions assign to any event, such as “the sum is at least 10”. It is never exactly zero for a finite number of rolls: sampling noise alone leaves each frequency off by about √(2p(1 − p)/(πn)) on average. Adding that over all sums gives the TV from sampling noise alone readout. In repeated runs at the default settings, the average TV distance agrees with it to within a few percent. Both shrink like 1/√n: 100 times more rolls make them 10 times smaller.

A TV distance far above the noise level would mean biased dice or a flawed random-number generator.

Example result

With seed 42, 10,000 rolls of two six-sided dice averaged 6.981 against an exact mean of 7.000, with variance 5.844 against 5.833. The total variation distance to the exact distribution was 0.0117, close to the 0.0121 expected from sampling noise alone.

Output of dice-roll with dice=2 sides=6 rolls=10000 seed=42
Rolls10,000
Mean of the sum6.981
Exact mean k(s+1)/27.000
Variance of the sum5.844
Exact variance k(s²−1)/125.833
Total variation distance0.0117
TV from sampling noise alone (approx.)0.0121

Implementation

The page runs simulation.js from the package below; the Python file is a line-by-line port with the same random-number generator, so a given seed produces the same result in both languages. The excerpts show the core of each.

JavaScript — simulation.js
// Exact distribution of the sum of k fair s-sided dice by repeated convolution:
// ways[j] counts the s^k equally likely outcomes whose sum is k + j.
// Integer counts stay exact up to 20^10 ≈ 10^13, well below 2^53.
export function exactWays(k, s) {
  let ways = [1];
  for (let d = 0; d < k; d++) {
    const next = new Array(ways.length + s - 1).fill(0);
    for (let i = 0; i < ways.length; i++) {
      for (let f = 0; f < s; f++) next[i + f] += ways[i];
    }
    ways = next;
  }
  return ways;
}

// One roll: k independent dice, each face 1…s equally likely.
export function rollDice(k, s, rng, faces) {
  let sum = 0;
  for (let d = 0; d < k; d++) {
    const face = 1 + rng.int(s);
    if (faces) faces[d] = face;
    sum += face;
  }
  return sum;
}

// Total variation distance: half the summed absolute gaps between the
// empirical frequencies and the exact probabilities.
export function tvDistance(counts, n, probs) {
  let tv = 0;
  for (let j = 0; j < probs.length; j++) tv += Math.abs(counts[j] / n - probs[j]);
  return tv / 2;
}
Python — simulation.py
def exact_ways(k, s):
    """ways[j] = number of the s**k equally likely outcomes whose sum is k + j."""
    ways = [1]
    for _ in range(k):
        nxt = [0] * (len(ways) + s - 1)
        for i, w in enumerate(ways):
            for f in range(s):
                nxt[i + f] += w
        ways = nxt
    return ways


def roll_dice(k, s, rng):
    """Sum of k independent fair dice with faces 1..s."""
    total = 0
    for _ in range(k):
        total += 1 + rng.int(s)
    return total


def tv_distance(counts, n, probs):
    """Half the summed absolute gaps between empirical and exact probabilities."""
    tv = 0
    for c, q in zip(counts, probs):
        tv += abs(c / n - q)
    return tv / 2

Run the full model from a terminal:

Shell
node simulation.js --dice 2 --sides 6 --seed 42
python simulation.py --dice 2 --sides 6 --seed 42

Download the model

dice-roll.zip6 files · 9.0 KBDownload .zip
Files in dice-roll.zip
FileContentsSize
README.mdHow to run it, parameters, example result2.2 KB
model.jsonParameters with ranges and defaults, outputs, the seed-42 example2.3 KB
simulation.jsThe model used on this page plus a command-line runner (Node 18+)8.0 KB
simulation.pyPython 3.9+ port, standard library only6.2 KB
example.csvResult table from the example run405 B
LICENSE.txtMIT License1.0 KB

Assumptions & limitations

  • Fair, independent dice. Every face has probability exactly 1/s and dice do not influence each other. Physical dice are manufactured objects and can be slightly unbalanced.
  • Faces from 32-bit random numbers. A face is ⌊u · s⌋ + 1 for a uniform u with 232 possible values, which biases a face by at most s/232 — invisible at a million rolls.
  • The noise reference is approximate. It uses the normal approximation to the binomial for each sum, which is loose for sums with tiny probabilities.
  • Sample variance. The simulated variance divides by n − 1.

References

  1. Grinstead, C. M. & Snell, J. L. (1997). Introduction to Probability, 2nd ed. American Mathematical Society. math.dartmouth.edu/~prob/prob/prob.pdf
  2. Ross, S. M. A First Course in Probability. Pearson.