Avoiding Magic Numbers: simulating dice rolling

The following program estimates the probability of obtaining different totals on rolling two dice:

import random

# Initialize the rolls dictionary to a count of zero for each possible outcome.
rolls = dict.fromkeys(range(2, 13), 0)

# The simulation: roll two dice 100000 times.
for j in range(100000):
    roll_total = random.randint(1, 6) + random.randint(1, 6)
    rolls[roll_total] += 1

# Report the simulation results.
for i in range(2, 13):
    P = rolls[i] / 100000
    print(f'P({i}) = {P:.5f}')

Several magic numbers appear without explanation in the program above: the number of pips showing on each die is selected randomly from the integers 1–6; the total number of each of the possible outcomes 2–12 are stored in a dictionary, rolls, whose keys are generated by the function call range(2, 13); and the number of simulated rolls is hard-coded as 100 000.

Note that if we wanted, for example, to change the number of rolls we would have to edit the code in three places: the simulation loop, the comment above the simulation loop, and in the probability calculation. Maintaining and adapting code like this in a longer program is likely to be time-consuming and error-prone.

A little thought about how to assign these magic-number constants to variables also suggests a way to make the code more flexible, as shown below. As with other languages, it is common, but not necessary, to signal the definition of such a constant by defining its name in capitals:

import random

NDICE = 2
NFACES_PER_DIE = 6
NROLLS = 100000

# Calculate all the possible roll totals.
min_roll, max_roll = NDICE, NDICE * NFACES_PER_DIE
roll_total_range = range(min_roll, max_roll+1)

# Initialize the rolls dictionary to a count of zero for each possible outcome.
rolls = dict.fromkeys(roll_total_range, 0)

# The simulation: roll NDICE dice NROLLS times.
for j in range(NROLLS):
    roll_total = 0
    for i in range(NDICE):
        roll_total += random.randint(1, NFACES_PER_DIE)
    rolls[roll_total] += 1

# Report the simulation results.
for i in roll_total_range:
    P = rolls[i] / NROLLS
    print(f'P({i}) = {P:.5f}')

In this program, we can simulate the rolling of any number of dice with any number of sides any number of times by changing, in a single code location, the variables NDICE, NFACES_PER_DIE and NROLLS.

A typical output for a large number of two six-sided dice rolls is:

P(2) = 0.02754
P(3) = 0.05527
P(4) = 0.08240
P(5) = 0.11283
P(6) = 0.13626
P(7) = 0.16833
P(8) = 0.13891
P(9) = 0.11269
P(10) = 0.08301
P(11) = 0.05482
P(12) = 0.02794