Back to Article
index.ipynb
Download Notebook
In [1]:
import pandas as pd
import plotly.io as pio
from plotly import express as px

pio.renderers.default = "plotly_mimetype+notebook_connected"
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()