import pandas as pd
import plotly.io as pio
from plotly import express as px
pio.renderers.default = "plotly_mimetype+notebook_connected"In [1]:
In [2]:
level = pd.Series(
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
)
strength = pd.Series([4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6])
weapon_proficiency = pd.Series(
[2, 2, 2, 2, 4, 4, 4, 4, 4, 4, 4, 4, 6, 6, 6, 6, 6, 6, 6, 6]
)
weapon_specialization = pd.Series(
[0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3]
)
weapon_tracking = pd.Series(
[0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3]
)
weapon_dice = pd.Series([1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4])
rage_damage = pd.Series(
[4, 4, 4, 4, 4, 4, 8, 8, 8, 8, 8, 8, 8, 8, 16, 16, 16, 16, 16, 16]
)
player = pd.DataFrame(
{
"player_level": level,
"strength": strength,
"weapon_proficiency": weapon_proficiency,
"weapon_specialization": weapon_specialization,
"weapon_tracking": weapon_tracking,
"weapon_dice": weapon_dice,
"rage_damage": rage_damage,
}
)
player| player_level | strength | weapon_proficiency | weapon_specialization | weapon_tracking | weapon_dice | rage_damage | |
|---|---|---|---|---|---|---|---|
| 0 | 1 | 4 | 2 | 0 | 0 | 1 | 4 |
| 1 | 2 | 4 | 2 | 0 | 1 | 1 | 4 |
| 2 | 3 | 4 | 2 | 0 | 1 | 1 | 4 |
| 3 | 4 | 4 | 2 | 0 | 1 | 2 | 4 |
| 4 | 5 | 4 | 4 | 2 | 1 | 2 | 4 |
| 5 | 6 | 4 | 4 | 2 | 1 | 2 | 4 |
| 6 | 7 | 4 | 4 | 2 | 1 | 2 | 8 |
| 7 | 8 | 4 | 4 | 2 | 1 | 2 | 8 |
| 8 | 9 | 4 | 4 | 2 | 1 | 2 | 8 |
| 9 | 10 | 5 | 4 | 2 | 2 | 2 | 8 |
| 10 | 11 | 5 | 4 | 2 | 2 | 2 | 8 |
| 11 | 12 | 5 | 4 | 2 | 2 | 3 | 8 |
| 12 | 13 | 5 | 6 | 3 | 2 | 3 | 8 |
| 13 | 14 | 5 | 6 | 3 | 2 | 3 | 8 |
| 14 | 15 | 5 | 6 | 3 | 2 | 3 | 16 |
| 15 | 16 | 5 | 6 | 3 | 3 | 3 | 16 |
| 16 | 17 | 5 | 6 | 3 | 3 | 3 | 16 |
| 17 | 18 | 5 | 6 | 3 | 3 | 3 | 16 |
| 18 | 19 | 5 | 6 | 3 | 3 | 4 | 16 |
| 19 | 20 | 6 | 6 | 3 | 3 | 4 | 16 |
In [3]:
monster_ac = pd.DataFrame(
[
{"monster_level": -1, "monster_tier": "extreme", "monster_ac": 18},
{"monster_level": 0, "monster_tier": "extreme", "monster_ac": 19},
{"monster_level": 1, "monster_tier": "extreme", "monster_ac": 19},
{"monster_level": 2, "monster_tier": "extreme", "monster_ac": 21},
{"monster_level": 3, "monster_tier": "extreme", "monster_ac": 22},
{"monster_level": 4, "monster_tier": "extreme", "monster_ac": 24},
{"monster_level": 5, "monster_tier": "extreme", "monster_ac": 25},
{"monster_level": 6, "monster_tier": "extreme", "monster_ac": 27},
{"monster_level": 7, "monster_tier": "extreme", "monster_ac": 28},
{"monster_level": 8, "monster_tier": "extreme", "monster_ac": 30},
{"monster_level": 9, "monster_tier": "extreme", "monster_ac": 31},
{"monster_level": 10, "monster_tier": "extreme", "monster_ac": 33},
{"monster_level": 11, "monster_tier": "extreme", "monster_ac": 34},
{"monster_level": 12, "monster_tier": "extreme", "monster_ac": 36},
{"monster_level": 13, "monster_tier": "extreme", "monster_ac": 37},
{"monster_level": 14, "monster_tier": "extreme", "monster_ac": 39},
{"monster_level": 15, "monster_tier": "extreme", "monster_ac": 40},
{"monster_level": 16, "monster_tier": "extreme", "monster_ac": 42},
{"monster_level": 17, "monster_tier": "extreme", "monster_ac": 43},
{"monster_level": 18, "monster_tier": "extreme", "monster_ac": 45},
{"monster_level": 19, "monster_tier": "extreme", "monster_ac": 46},
{"monster_level": 20, "monster_tier": "extreme", "monster_ac": 48},
{"monster_level": 21, "monster_tier": "extreme", "monster_ac": 49},
{"monster_level": 22, "monster_tier": "extreme", "monster_ac": 51},
{"monster_level": 23, "monster_tier": "extreme", "monster_ac": 52},
{"monster_level": 24, "monster_tier": "extreme", "monster_ac": 54},
{"monster_level": -1, "monster_tier": "hard", "monster_ac": 15},
{"monster_level": 0, "monster_tier": "hard", "monster_ac": 16},
{"monster_level": 1, "monster_tier": "hard", "monster_ac": 16},
{"monster_level": 2, "monster_tier": "hard", "monster_ac": 18},
{"monster_level": 3, "monster_tier": "hard", "monster_ac": 19},
{"monster_level": 4, "monster_tier": "hard", "monster_ac": 21},
{"monster_level": 5, "monster_tier": "hard", "monster_ac": 22},
{"monster_level": 6, "monster_tier": "hard", "monster_ac": 24},
{"monster_level": 7, "monster_tier": "hard", "monster_ac": 25},
{"monster_level": 8, "monster_tier": "hard", "monster_ac": 27},
{"monster_level": 9, "monster_tier": "hard", "monster_ac": 28},
{"monster_level": 10, "monster_tier": "hard", "monster_ac": 30},
{"monster_level": 11, "monster_tier": "hard", "monster_ac": 31},
{"monster_level": 12, "monster_tier": "hard", "monster_ac": 33},
{"monster_level": 13, "monster_tier": "hard", "monster_ac": 34},
{"monster_level": 14, "monster_tier": "hard", "monster_ac": 36},
{"monster_level": 15, "monster_tier": "hard", "monster_ac": 37},
{"monster_level": 16, "monster_tier": "hard", "monster_ac": 39},
{"monster_level": 17, "monster_tier": "hard", "monster_ac": 40},
{"monster_level": 18, "monster_tier": "hard", "monster_ac": 42},
{"monster_level": 19, "monster_tier": "hard", "monster_ac": 43},
{"monster_level": 20, "monster_tier": "hard", "monster_ac": 45},
{"monster_level": 21, "monster_tier": "hard", "monster_ac": 46},
{"monster_level": 22, "monster_tier": "hard", "monster_ac": 48},
{"monster_level": 23, "monster_tier": "hard", "monster_ac": 49},
{"monster_level": 24, "monster_tier": "hard", "monster_ac": 51},
{"monster_level": -1, "monster_tier": "moderate", "monster_ac": 14},
{"monster_level": 0, "monster_tier": "moderate", "monster_ac": 15},
{"monster_level": 1, "monster_tier": "moderate", "monster_ac": 15},
{"monster_level": 2, "monster_tier": "moderate", "monster_ac": 17},
{"monster_level": 3, "monster_tier": "moderate", "monster_ac": 18},
{"monster_level": 4, "monster_tier": "moderate", "monster_ac": 20},
{"monster_level": 5, "monster_tier": "moderate", "monster_ac": 21},
{"monster_level": 6, "monster_tier": "moderate", "monster_ac": 23},
{"monster_level": 7, "monster_tier": "moderate", "monster_ac": 24},
{"monster_level": 8, "monster_tier": "moderate", "monster_ac": 26},
{"monster_level": 9, "monster_tier": "moderate", "monster_ac": 27},
{"monster_level": 10, "monster_tier": "moderate", "monster_ac": 29},
{"monster_level": 11, "monster_tier": "moderate", "monster_ac": 30},
{"monster_level": 12, "monster_tier": "moderate", "monster_ac": 32},
{"monster_level": 13, "monster_tier": "moderate", "monster_ac": 33},
{"monster_level": 14, "monster_tier": "moderate", "monster_ac": 35},
{"monster_level": 15, "monster_tier": "moderate", "monster_ac": 36},
{"monster_level": 16, "monster_tier": "moderate", "monster_ac": 38},
{"monster_level": 17, "monster_tier": "moderate", "monster_ac": 39},
{"monster_level": 18, "monster_tier": "moderate", "monster_ac": 41},
{"monster_level": 19, "monster_tier": "moderate", "monster_ac": 42},
{"monster_level": 20, "monster_tier": "moderate", "monster_ac": 44},
{"monster_level": 21, "monster_tier": "moderate", "monster_ac": 45},
{"monster_level": 22, "monster_tier": "moderate", "monster_ac": 47},
{"monster_level": 23, "monster_tier": "moderate", "monster_ac": 48},
{"monster_level": 24, "monster_tier": "moderate", "monster_ac": 50},
{"monster_level": -1, "monster_tier": "easy", "monster_ac": 12},
{"monster_level": 0, "monster_tier": "easy", "monster_ac": 13},
{"monster_level": 1, "monster_tier": "easy", "monster_ac": 13},
{"monster_level": 2, "monster_tier": "easy", "monster_ac": 15},
{"monster_level": 3, "monster_tier": "easy", "monster_ac": 16},
{"monster_level": 4, "monster_tier": "easy", "monster_ac": 18},
{"monster_level": 5, "monster_tier": "easy", "monster_ac": 19},
{"monster_level": 6, "monster_tier": "easy", "monster_ac": 21},
{"monster_level": 7, "monster_tier": "easy", "monster_ac": 22},
{"monster_level": 8, "monster_tier": "easy", "monster_ac": 24},
{"monster_level": 9, "monster_tier": "easy", "monster_ac": 25},
{"monster_level": 10, "monster_tier": "easy", "monster_ac": 27},
{"monster_level": 11, "monster_tier": "easy", "monster_ac": 28},
{"monster_level": 12, "monster_tier": "easy", "monster_ac": 30},
{"monster_level": 13, "monster_tier": "easy", "monster_ac": 31},
{"monster_level": 14, "monster_tier": "easy", "monster_ac": 33},
{"monster_level": 15, "monster_tier": "easy", "monster_ac": 34},
{"monster_level": 16, "monster_tier": "easy", "monster_ac": 36},
{"monster_level": 17, "monster_tier": "easy", "monster_ac": 37},
{"monster_level": 18, "monster_tier": "easy", "monster_ac": 39},
{"monster_level": 19, "monster_tier": "easy", "monster_ac": 40},
{"monster_level": 20, "monster_tier": "easy", "monster_ac": 42},
{"monster_level": 21, "monster_tier": "easy", "monster_ac": 43},
{"monster_level": 22, "monster_tier": "easy", "monster_ac": 45},
{"monster_level": 23, "monster_tier": "easy", "monster_ac": 46},
{"monster_level": 24, "monster_tier": "easy", "monster_ac": 48},
]
)
monster_ac| monster_level | monster_tier | monster_ac | |
|---|---|---|---|
| 0 | -1 | extreme | 18 |
| 1 | 0 | extreme | 19 |
| 2 | 1 | extreme | 19 |
| 3 | 2 | extreme | 21 |
| 4 | 3 | extreme | 22 |
| ... | ... | ... | ... |
| 99 | 20 | easy | 42 |
| 100 | 21 | easy | 43 |
| 101 | 22 | easy | 45 |
| 102 | 23 | easy | 46 |
| 103 | 24 | easy | 48 |
104 rows × 3 columns
In [4]:
level_diff = pd.DataFrame({"level_diff": [-4, -3, -2, -1, 0, 1, 2, 3, 4]})
level_diff| level_diff | |
|---|---|
| 0 | -4 |
| 1 | -3 |
| 2 | -2 |
| 3 | -1 |
| 4 | 0 |
| 5 | 1 |
| 6 | 2 |
| 7 | 3 |
| 8 | 4 |
In [5]:
weapon_dice = pd.DataFrame(
[
{"die_name": "d4", "die_value": 2.5},
{"die_name": "d6", "die_value": 3.5},
{"die_name": "d8", "die_value": 4.5},
{"die_name": "d10", "die_value": 5.5},
{"die_name": "d12", "die_value": 6.5},
]
)
weapon_dice| die_name | die_value | |
|---|---|---|
| 0 | d4 | 2.5 |
| 1 | d6 | 3.5 |
| 2 | d8 | 4.5 |
| 3 | d10 | 5.5 |
| 4 | d12 | 6.5 |
In [6]:
player["attack_bonus"] = (
player["player_level"]
+ player["strength"]
+ player["weapon_proficiency"]
+ player["weapon_tracking"]
)
player| player_level | strength | weapon_proficiency | weapon_specialization | weapon_tracking | weapon_dice | rage_damage | attack_bonus | |
|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 4 | 2 | 0 | 0 | 1 | 4 | 7 |
| 1 | 2 | 4 | 2 | 0 | 1 | 1 | 4 | 9 |
| 2 | 3 | 4 | 2 | 0 | 1 | 1 | 4 | 10 |
| 3 | 4 | 4 | 2 | 0 | 1 | 2 | 4 | 11 |
| 4 | 5 | 4 | 4 | 2 | 1 | 2 | 4 | 14 |
| 5 | 6 | 4 | 4 | 2 | 1 | 2 | 4 | 15 |
| 6 | 7 | 4 | 4 | 2 | 1 | 2 | 8 | 16 |
| 7 | 8 | 4 | 4 | 2 | 1 | 2 | 8 | 17 |
| 8 | 9 | 4 | 4 | 2 | 1 | 2 | 8 | 18 |
| 9 | 10 | 5 | 4 | 2 | 2 | 2 | 8 | 21 |
| 10 | 11 | 5 | 4 | 2 | 2 | 2 | 8 | 22 |
| 11 | 12 | 5 | 4 | 2 | 2 | 3 | 8 | 23 |
| 12 | 13 | 5 | 6 | 3 | 2 | 3 | 8 | 26 |
| 13 | 14 | 5 | 6 | 3 | 2 | 3 | 8 | 27 |
| 14 | 15 | 5 | 6 | 3 | 2 | 3 | 16 | 28 |
| 15 | 16 | 5 | 6 | 3 | 3 | 3 | 16 | 30 |
| 16 | 17 | 5 | 6 | 3 | 3 | 3 | 16 | 31 |
| 17 | 18 | 5 | 6 | 3 | 3 | 3 | 16 | 32 |
| 18 | 19 | 5 | 6 | 3 | 3 | 4 | 16 | 33 |
| 19 | 20 | 6 | 6 | 3 | 3 | 4 | 16 | 35 |
In [7]:
player_damage = player.merge(weapon_dice, how="cross")
player_damage["average_damage"] = (
player_damage["die_value"] * player_damage["weapon_dice"]
+ player_damage["strength"]
+ player_damage["weapon_specialization"]
+ player_damage["rage_damage"]
)
player_damage| player_level | strength | weapon_proficiency | weapon_specialization | weapon_tracking | weapon_dice | rage_damage | attack_bonus | die_name | die_value | average_damage | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 4 | 2 | 0 | 0 | 1 | 4 | 7 | d4 | 2.5 | 10.5 |
| 1 | 1 | 4 | 2 | 0 | 0 | 1 | 4 | 7 | d6 | 3.5 | 11.5 |
| 2 | 1 | 4 | 2 | 0 | 0 | 1 | 4 | 7 | d8 | 4.5 | 12.5 |
| 3 | 1 | 4 | 2 | 0 | 0 | 1 | 4 | 7 | d10 | 5.5 | 13.5 |
| 4 | 1 | 4 | 2 | 0 | 0 | 1 | 4 | 7 | d12 | 6.5 | 14.5 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 95 | 20 | 6 | 6 | 3 | 3 | 4 | 16 | 35 | d4 | 2.5 | 35.0 |
| 96 | 20 | 6 | 6 | 3 | 3 | 4 | 16 | 35 | d6 | 3.5 | 39.0 |
| 97 | 20 | 6 | 6 | 3 | 3 | 4 | 16 | 35 | d8 | 4.5 | 43.0 |
| 98 | 20 | 6 | 6 | 3 | 3 | 4 | 16 | 35 | d10 | 5.5 | 47.0 |
| 99 | 20 | 6 | 6 | 3 | 3 | 4 | 16 | 35 | d12 | 6.5 | 51.0 |
100 rows × 11 columns
In [8]:
player_level_diff = player_damage.merge(level_diff, how="cross")
player_level_diff["monster_level"] = (
player_level_diff["player_level"] + player_level_diff["level_diff"]
)
player_monster_matchups = player_level_diff.merge(
monster_ac, how="inner", on="monster_level"
)
player_monster_matchups| player_level | strength | weapon_proficiency | weapon_specialization | weapon_tracking | weapon_dice | rage_damage | attack_bonus | die_name | die_value | average_damage | level_diff | monster_level | monster_tier | monster_ac | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 4 | 2 | 0 | 0 | 1 | 4 | 7 | d4 | 2.5 | 10.5 | -2 | -1 | extreme | 18 |
| 1 | 1 | 4 | 2 | 0 | 0 | 1 | 4 | 7 | d4 | 2.5 | 10.5 | -2 | -1 | hard | 15 |
| 2 | 1 | 4 | 2 | 0 | 0 | 1 | 4 | 7 | d4 | 2.5 | 10.5 | -2 | -1 | moderate | 14 |
| 3 | 1 | 4 | 2 | 0 | 0 | 1 | 4 | 7 | d4 | 2.5 | 10.5 | -2 | -1 | easy | 12 |
| 4 | 1 | 4 | 2 | 0 | 0 | 1 | 4 | 7 | d4 | 2.5 | 10.5 | -1 | 0 | extreme | 19 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 3535 | 20 | 6 | 6 | 3 | 3 | 4 | 16 | 35 | d12 | 6.5 | 51.0 | 3 | 23 | easy | 46 |
| 3536 | 20 | 6 | 6 | 3 | 3 | 4 | 16 | 35 | d12 | 6.5 | 51.0 | 4 | 24 | extreme | 54 |
| 3537 | 20 | 6 | 6 | 3 | 3 | 4 | 16 | 35 | d12 | 6.5 | 51.0 | 4 | 24 | hard | 51 |
| 3538 | 20 | 6 | 6 | 3 | 3 | 4 | 16 | 35 | d12 | 6.5 | 51.0 | 4 | 24 | moderate | 50 |
| 3539 | 20 | 6 | 6 | 3 | 3 | 4 | 16 | 35 | d12 | 6.5 | 51.0 | 4 | 24 | easy | 48 |
3540 rows × 15 columns
In [9]:
def evaluate_hit_result(
roll: int, attack_bonus: int, monster_ac: int, average_damage: float
) -> float:
match roll:
case 1:
if attack_bonus + roll >= monster_ac + 10:
return average_damage
case 20:
if attack_bonus + roll >= monster_ac:
return average_damage * 2.0
if attack_bonus + roll >= monster_ac - 10:
return average_damage
case _:
if attack_bonus + roll >= monster_ac + 10:
return average_damage * 2.0
if attack_bonus + roll >= monster_ac:
return average_damage
return 0.0
def calculate_dps(x: pd.DataFrame) -> float:
return (
sum(
evaluate_hit_result(
roll=roll,
attack_bonus=x["attack_bonus"],
monster_ac=x["monster_ac"],
average_damage=x["average_damage"],
)
for roll in range(1, 21)
)
/ 20.0
)
player_monster_matchups["dps"] = player_monster_matchups.apply(calculate_dps, axis=1)
player_monster_matchups| player_level | strength | weapon_proficiency | weapon_specialization | weapon_tracking | weapon_dice | rage_damage | attack_bonus | die_name | die_value | average_damage | level_diff | monster_level | monster_tier | monster_ac | dps | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 4 | 2 | 0 | 0 | 1 | 4 | 7 | d4 | 2.5 | 10.5 | -2 | -1 | extreme | 18 | 5.775 |
| 1 | 1 | 4 | 2 | 0 | 0 | 1 | 4 | 7 | d4 | 2.5 | 10.5 | -2 | -1 | hard | 15 | 8.400 |
| 2 | 1 | 4 | 2 | 0 | 0 | 1 | 4 | 7 | d4 | 2.5 | 10.5 | -2 | -1 | moderate | 14 | 9.450 |
| 3 | 1 | 4 | 2 | 0 | 0 | 1 | 4 | 7 | d4 | 2.5 | 10.5 | -2 | -1 | easy | 12 | 11.550 |
| 4 | 1 | 4 | 2 | 0 | 0 | 1 | 4 | 7 | d4 | 2.5 | 10.5 | -1 | 0 | extreme | 19 | 5.250 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 3535 | 20 | 6 | 6 | 3 | 3 | 4 | 16 | 35 | d12 | 6.5 | 51.0 | 3 | 23 | easy | 46 | 28.050 |
| 3536 | 20 | 6 | 6 | 3 | 3 | 4 | 16 | 35 | d12 | 6.5 | 51.0 | 4 | 24 | extreme | 54 | 7.650 |
| 3537 | 20 | 6 | 6 | 3 | 3 | 4 | 16 | 35 | d12 | 6.5 | 51.0 | 4 | 24 | hard | 51 | 15.300 |
| 3538 | 20 | 6 | 6 | 3 | 3 | 4 | 16 | 35 | d12 | 6.5 | 51.0 | 4 | 24 | moderate | 50 | 17.850 |
| 3539 | 20 | 6 | 6 | 3 | 3 | 4 | 16 | 35 | d12 | 6.5 | 51.0 | 4 | 24 | easy | 48 | 22.950 |
3540 rows × 16 columns
In [10]:
df = player_monster_matchups
player_monster_matchups.to_parquet("./barbarian_2e_matchups.parquet", index=False)In [11]:
px.line(df, x="player_level", y="attack_bonus", markers=True).show()In [12]:
px.line(df, x="player_level", y="average_damage", color="die_name", markers=True).show()