<jupyter_start><jupyter_text>diceboxA simple dsl for dice.<jupyter_code>import pandas as pd
from dicebox import d
d(20)
d(20)()
d(6)
d(6)(100)
d(100) + 5
# out of 10k runs, how often is a d20 > 10?
(d(20) > 10)(10000).mean()
# 0 if a d20 <= 10, else 3d6
thresholded_3d6 = (d(20) > 10) * d(6, 6, 6)
print(thresholded_3d6)
# try it 100 times
thresholded_3d6(100)
# all of these are equivalent
str(d(6) + d(6)) == str(d({6: 2})) == str(d(6, 6)) == "2d6"
# use // to round down after a division
d(8, 6, 6, 6) // 2
d({8: 1, 6: 3})
d(10) + 5 + d(20) + d(8)
# 2 d20s with advantage
d(20).adv.roll(2)
%%time
n = 100000
adv = (pd.Series(d(20).adv.roll(n)).value_counts().sort_index() / n)
disadv = (pd.Series(d(20).disadv.roll(n)).value_counts().sort_index() / n)
norm = (pd.Series(d(20).roll(n)).value_counts().sort_index() / n)
pd.DataFrame({"disadv": disadv, "norm": norm, "adv": adv}).plot(kind="bar", figsize=(20, 8), width=0.9)
(d(20).adv - d(20).disadv) * 2<jupyter_output><empty_output><jupyter_text>---<jupyter_code>import pandas as pd
from dicebox.dnd import attack_roll_factory, critical_roll
from dicebox import d
rapier_attack = attack_roll_factory("Rapier", d(8) + 4, 3)
rapier_sneak_attack = attack_roll_factory("Rapier", d(8, 6, 6, 6) + 4, 3)
rapier_assassinate_attack = attack_roll_factory("Rapier", critical_roll(d(8, 6, 6, 6) + 4), 3, critical_hits=False)
# note that, even with the 23 to hit, no critical hit was triggered (because critical damage is already being used)
print("\n".join(rapier_assassinate_attack(adv=True, repeat=5)))
turn = [rapier_sneak_attack(), rapier_attack()]
print("\n".join(turn))
def avg_attack_vs_ac(sim, AC):
    hit = ((sim["to_hit_roll"] > AC) | sim["critical_hit"]) & ~ sim["critical_miss"]
    return (hit * sim["weapon_damage_roll"]).mean()
%%time
# 1m simulated attacks in <180ms; avg 172 nanoseconds per attack
sneak_attack_sim = pd.DataFrame(rapier_sneak_attack(repeat=1000000, numeric=True))
# avg damage per attack vs AC 15 opponent
avg_attack_vs_ac(sneak_attack_sim, AC=15)
sneak_attack_adv_sim = pd.DataFrame(rapier_sneak_attack(repeat=1000000, numeric=True, adv=True))
sneak_attack_disadv_sim = pd.DataFrame(rapier_sneak_attack(repeat=1000000, numeric=True, disadv=True))
# avg damage per attack (w/ advantage) vs AC 15 opponent
avg_attack_vs_ac(sneak_attack_adv_sim, AC=15)
print("critical hit %:")
print(sneak_attack_disadv_sim["critical_hit"].mean(), sneak_attack_sim["critical_hit"].mean(), sneak_attack_adv_sim["critical_hit"].mean())
print("critical miss %:")
print(sneak_attack_disadv_sim["critical_miss"].mean(), sneak_attack_sim["critical_miss"].mean(), sneak_attack_adv_sim["critical_miss"].mean())
sneak_attack_dmg_vs_ac = pd.DataFrame([{"disadv": avg_attack_vs_ac(sneak_attack_disadv_sim, AC=AC),
                    "normal": avg_attack_vs_ac(sneak_attack_sim, AC=AC),
                    "adv": avg_attack_vs_ac(sneak_attack_adv_sim, AC=AC)}
                   for AC in range(1, 26)])
ax = sneak_attack_dmg_vs_ac.plot(marker=".", title=str(d(8, 6, 6, 6) + 4), figsize=(12, 8), grid=True)
ax.set_xlabel("AC")
ax.set_ylabel("avg damage");
# note: advantage matters most low to mid; disadvantage matters most mid to high
sneak_attack_dmg_vs_ac<jupyter_output><empty_output><jupyter_text>---<jupyter_code>from dicebox import parse, d
parse("adv(1d20) + 3")(20)
expr = d(8, 6, 6, 6) // 2
str(expr)
parse(str(expr))<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.16470933646506167, 'fraction_numerical': 0.05314151497357604, 'mean_word_length': 6.127615062761507, 'pattern_counts': {'":': 6, '<': 15, '<?xml version=': 0, '>': 17, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '3', 'dir': 'jupyter-structured-clean-dedup', 'id': '59702', 'license': 'BSD-3-Clause', 'n_tokens_mistral': 1526, 'n_tokens_neox': 1349, 'n_words': 291, 'path': 'usage.ipynb', 'repo_name': 'quantology/dicebox', 'size': '3406'}