{ "cells": [ { "cell_type": "markdown", "id": "f2d6a493", "metadata": {}, "source": [ "# On Die Averages and Hit Points in 5e" ] }, { "cell_type": "markdown", "id": "28464919", "metadata": {}, "source": [ "This is a small notebook to show calculations for various aspects of die rolls and hit point generation\n", "in 5th edition D&D." ] }, { "cell_type": "code", "execution_count": 1, "id": "6fb82a7e", "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "import numpy as np" ] }, { "cell_type": "markdown", "id": "63d5f9c9", "metadata": {}, "source": [ "## Dice Averages" ] }, { "cell_type": "markdown", "id": "a0ec7d00", "metadata": {}, "source": [ "How we arrive at the numbers for die averages." ] }, { "cell_type": "markdown", "id": "5e5cec9c", "metadata": {}, "source": [ "### D8 Done Wrong" ] }, { "cell_type": "code", "execution_count": 2, "id": "b6e9fc90", "metadata": {}, "outputs": [ { "data": { "text/plain": "4.0" }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Logical flaw is some folks think the avg of a die is half it's max value.\n", "d8_wrong_values = [0, 1, 2, 3, 4, 5, 6, 7, 8]\n", "d8_wrong = pd.Series(d8_wrong_values)\n", "d8_wrong.mean()" ] }, { "cell_type": "code", "execution_count": 3, "id": "453da845", "metadata": {}, "outputs": [ { "data": { "text/plain": "count 8.00000\nmean 4.50000\nstd 2.44949\nmin 1.00000\n25% 2.75000\n50% 4.50000\n75% 6.25000\nmax 8.00000\ndtype: float64" }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# The average of a die is based on it's possible outcomes, not zero.\n", "d8_right = [1, 2, 3, 4, 5, 6, 7, 8]\n", "d8 = pd.Series(d8_right)\n", "d8.describe()" ] }, { "cell_type": "markdown", "id": "e6f69857", "metadata": { "pycharm": { "name": "#%% md\n" } }, "source": [ "This same pattern is repeated for every die type in the game. (i.e. 1d8, 1d10 ...)" ] }, { "cell_type": "markdown", "id": "2666a064", "metadata": { "pycharm": { "name": "#%% md\n" } }, "source": [ "## The confusion of Hit Points" ] }, { "cell_type": "markdown", "id": "c3f7f75e", "metadata": { "pycharm": { "name": "#%% md\n" } }, "source": [ "Beyond mistaking how die averages are derrived, average player HP and average Monster HPs are derrived\n", "differently. This causes some confusion too." ] }, { "cell_type": "markdown", "id": "71ee39f5", "metadata": {}, "source": [ "### Example Average Player Hit Points" ] }, { "cell_type": "markdown", "id": "235b9c06", "metadata": {}, "source": [ "Players take the average roll of a die rounded up each level. It's one of the few cases of rounding up in\n", "5e and is done each level. Not so with monsters." ] }, { "cell_type": "code", "execution_count": 4, "id": "80e0b9ab", "metadata": { "pycharm": { "name": "#%%\n" } }, "outputs": [ { "data": { "text/plain": "83.0" }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "player_hp = d8.max() + 14.0 * np.ceil(d8.mean()) + 5.0\n", "player_hp" ] }, { "cell_type": "markdown", "id": "433fd4e9", "metadata": { "pycharm": { "name": "#%% md\n" } }, "source": [ "Some folks opt for a house rule to reroll ones. This makes only the slightest difference and is not\n", "worth it to my mind. Model below is for rerollling all 1s infinatly, there are variatoins that have\n", "players roll once. The only model that approaches just taking avg hp is rerolling 1s infinatly. See\n", "simulation below." ] }, { "cell_type": "code", "execution_count": 5, "id": "159e65c9", "metadata": { "pycharm": { "name": "#%%\n" } }, "outputs": [ { "data": { "text/plain": "4.9375" }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "d8_house_rule_values = [4.5, 2, 3, 4, 5, 6, 7, 8]\n", "d8_house_rule = pd.Series(d8_house_rule_values)\n", "d8_house_rule.mean()" ] }, { "cell_type": "code", "execution_count": 6, "outputs": [ { "data": { "text/plain": "count 8.000000\nmean 4.937500\nstd 2.007797\nmin 2.000000\n25% 3.750000\n50% 4.750000\n75% 6.250000\nmax 8.000000\ndtype: float64" }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Description of HP rolls using infinite reroll of 1s\n", "d8_house_rule.describe()" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "code", "execution_count": 7, "outputs": [ { "data": { "text/plain": "count 8.00000\nmean 4.50000\nstd 2.44949\nmin 1.00000\n25% 2.75000\n50% 4.50000\n75% 6.25000\nmax 8.00000\ndtype: float64" }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Description of normal rolling rules for comparison.\n", "d8.describe()\n" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "id": "ee1ddaca", "metadata": { "pycharm": { "name": "#%% md\n" } }, "source": [ "#### 20th Level Character" ] }, { "cell_type": "code", "execution_count": 9, "outputs": [ { "data": { "text/plain": "103.0" }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# HP Results for a 20th level character using avg hp\n", "d8.max() + 19.0 * np.ceil(d8.mean())" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "code", "execution_count": 10, "outputs": [ { "data": { "text/plain": "103.0" }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# HP Results for a 20th level character rolling using the reroll 1s infinitely house rules\n", "d8_house_rule.max() + 19.0 * np.ceil(d8_house_rule.mean())" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "### Example Average Monster Hit Points" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%% md\n" } } }, { "cell_type": "markdown", "id": "becb151c", "metadata": {}, "source": [ "Monster hit points are not tallied every level (monsters don't have levels). Instead, their average HD value\n", "is multiplied by the number of HD. If these were players the HP value would be significantly higher\n", "because of the different mechanisms." ] }, { "cell_type": "code", "execution_count": 11, "id": "2749e441", "metadata": { "pycharm": { "name": "#%%\n" } }, "outputs": [ { "data": { "text/plain": "27.0" }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Example of a 5HD Bugbear\n", "bugbear_hp = 5.0 * d8.mean() + 5.0\n", "np.floor(bugbear_hp)" ] }, { "cell_type": "code", "execution_count": 14, "outputs": [ { "data": { "text/plain": "341.0" }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Example of a 22HD Dragon Turtle\n", "d20_values = range(1, 21)\n", "d20 = pd.Series(d20_values)\n", "d20.describe()\n", "\n", "22.0 * d20.mean() + 110" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "## Hit Point Generation House Rule Examples\n", "\n", "This is a small set of scripts to simulate and compare the results of various HP generation methods\n", "being discussed on various forums.\n", "\n", "* avg_hp = Average hp value for comparison\n", "* normal = Normal rolling of HPs\n", "* roll_all = Reroll any 1 infinatly\n", "* roll_once = Reroll a 1 once" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": 32, "outputs": [ { "data": { "text/plain": " normal once all avg\ncount 10000.000000 10000.000000 10000.00000 10000.0\nmean 93.266200 101.902300 103.10310 103.0\nstd 9.986136 8.954003 8.75359 0.0\nmin 58.000000 70.000000 64.00000 103.0\n25% 87.000000 96.000000 97.00000 103.0\n50% 93.000000 102.000000 103.00000 103.0\n75% 100.000000 108.000000 109.00000 103.0\nmax 129.000000 133.000000 133.00000 103.0", "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
normalonceallavg
count10000.00000010000.00000010000.0000010000.0
mean93.266200101.902300103.10310103.0
std9.9861368.9540038.753590.0
min58.00000070.00000064.00000103.0
25%87.00000096.00000097.00000103.0
50%93.000000102.000000103.00000103.0
75%100.000000108.000000109.00000103.0
max129.000000133.000000133.00000103.0
\n
" }, "execution_count": 32, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Hacky code. I'm trying to make this explicit for clarity.\n", "import random\n", "def get_hp_values(pc_level=20):\n", " normal= 8 # all players start with max hp\n", " avg_hp = normal\n", " roll_all= normal\n", " roll_once = normal\n", " for _ in range (pc_level - 1): # Roll for each of the 19 levels past first\n", " avg_hp = avg_hp + 5\n", " normal = normal + random.randint(1, 8)\n", " roll_all = roll_all + random.randint(2, 8)\n", " for _ in range(pc_level - 1):\n", " roll = random.randint(1, 8)\n", " if roll == 1:\n", " roll = random.randint(1, 8)\n", " roll_once = roll_once + roll\n", " return [normal, roll_all, roll_once, avg_hp]\n", "\n", "avg_hp, normal, roll_all, roll_once = [], [], [], []\n", "for _ in range(10000):\n", " result = get_hp_values(20) # Change this value for the level of PC you want to simulate.\n", " normal.append(result[0])\n", " roll_all.append(result[1])\n", " roll_once.append(result[2])\n", " avg_hp.append(result[3])\n", "\n", "hp_rolls = pd.DataFrame({'normal': normal, 'once': roll_once, 'all': roll_all, 'avg': avg_hp})\n", "hp_rolls.describe()" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.9.5" } }, "nbformat": 4, "nbformat_minor": 5 }