On Die Averages and Hit Points in 5e

This is a small notebook to show calculations for various aspects of die rolls and hit point generation in 5th edition D&D.

In [1]:
import pandas as pd
import numpy as np

Dice Averages

How we arrive at the numbers for die averages.

D8 Done Wrong

In [2]:
# Logical flaw is some folks think the avg of a die is half it's max value.
d8_wrong_values = [0, 1, 2, 3, 4, 5, 6, 7, 8]
d8_wrong = pd.Series(d8_wrong_values)
d8_wrong.mean()
Out[2]:
4.0
In [3]:
# The average of a die is based on it's possible outcomes, not zero.
d8_right = [1, 2, 3, 4, 5, 6, 7, 8]
d8 = pd.Series(d8_right)
d8.describe()
Out[3]:
count    8.00000
mean     4.50000
std      2.44949
min      1.00000
25%      2.75000
50%      4.50000
75%      6.25000
max      8.00000
dtype: float64

This same pattern is repeated for every die type in the game. (i.e. 1d8, 1d10 ...)

The confusion of Hit Points

Beyond mistaking how die averages are derrived, average player HP and average Monster HPs are derrived differently. This causes some confusion too.

Example Average Player Hit Points

Players take the average roll of a die rounded up each level. It's one of the few cases of rounding up in 5e and is done each level. Not so with monsters.

In [4]:
player_hp = d8.max() + 14.0 * np.ceil(d8.mean()) + 5.0
player_hp
Out[4]:
83.0

Some folks opt for a house rule to reroll ones. This makes only the slightest difference and is not worth it to my mind. Model below is for rerollling all 1s infinatly, there are variatoins that have players roll once. The only model that approaches just taking avg hp is rerolling 1s infinatly. See simulation below.

In [5]:
d8_house_rule_values = [4.5, 2, 3, 4, 5, 6, 7, 8]
d8_house_rule = pd.Series(d8_house_rule_values)
d8_house_rule.mean()
Out[5]:
4.9375
In [6]:
# Description of HP rolls using infinite reroll of 1s
d8_house_rule.describe()
Out[6]:
count    8.000000
mean     4.937500
std      2.007797
min      2.000000
25%      3.750000
50%      4.750000
75%      6.250000
max      8.000000
dtype: float64
In [7]:
# Description of normal rolling rules for comparison.
d8.describe()
Out[7]:
count    8.00000
mean     4.50000
std      2.44949
min      1.00000
25%      2.75000
50%      4.50000
75%      6.25000
max      8.00000
dtype: float64

20th Level Character

In [9]:
# HP Results for a 20th level character using avg hp
d8.max() + 19.0 * np.ceil(d8.mean())
Out[9]:
103.0
In [10]:
# HP Results for a 20th level character rolling using the reroll 1s infinitely house rules
d8_house_rule.max() + 19.0 * np.ceil(d8_house_rule.mean())
Out[10]:
103.0

Example Average Monster Hit Points

Monster hit points are not tallied every level (monsters don't have levels). Instead, their average HD value is multiplied by the number of HD. If these were players the HP value would be significantly higher because of the different mechanisms.

In [11]:
# Example of a 5HD Bugbear
bugbear_hp = 5.0 * d8.mean() + 5.0
np.floor(bugbear_hp)
Out[11]:
27.0
In [14]:
# Example of a 22HD Dragon Turtle
d20_values = range(1, 21)
d20 = pd.Series(d20_values)
d20.describe()

22.0 * d20.mean() + 110
Out[14]:
341.0

Hit Point Generation House Rule Examples

This is a small set of scripts to simulate and compare the results of various HP generation methods being discussed on various forums.

  • avg_hp = Average hp value for comparison
  • normal = Normal rolling of HPs
  • roll_all = Reroll any 1 infinatly
  • roll_once = Reroll a 1 once
In [32]:
# Hacky code.  I'm trying to make this explicit for clarity.
import random
def get_hp_values(pc_level=20):
    normal= 8 # all players start with max hp
    avg_hp = normal
    roll_all= normal
    roll_once = normal
    for _ in range (pc_level - 1): # Roll for each of the 19 levels past first
        avg_hp = avg_hp + 5
        normal = normal + random.randint(1, 8)
        roll_all = roll_all + random.randint(2, 8)
    for _ in range(pc_level - 1):
        roll = random.randint(1, 8)
        if roll == 1:
            roll = random.randint(1, 8)
        roll_once = roll_once + roll
    return [normal, roll_all, roll_once, avg_hp]

avg_hp, normal, roll_all, roll_once = [], [], [], []
for _ in range(10000):
    result = get_hp_values(20) # Change this value for the level of PC you want to simulate.
    normal.append(result[0])
    roll_all.append(result[1])
    roll_once.append(result[2])
    avg_hp.append(result[3])

hp_rolls = pd.DataFrame({'normal': normal, 'once': roll_once, 'all': roll_all, 'avg': avg_hp})
hp_rolls.describe()
Out[32]:
normal once all avg
count 10000.000000 10000.000000 10000.00000 10000.0
mean 93.266200 101.902300 103.10310 103.0
std 9.986136 8.954003 8.75359 0.0
min 58.000000 70.000000 64.00000 103.0
25% 87.000000 96.000000 97.00000 103.0
50% 93.000000 102.000000 103.00000 103.0
75% 100.000000 108.000000 109.00000 103.0
max 129.000000 133.000000 133.00000 103.0