From c2f046b9a5315edbf0c436d970cfa42b27ef5dd6 Mon Sep 17 00:00:00 2001 From: Supremist Date: Thu, 12 Mar 2026 00:15:04 +0200 Subject: Check Twin Takedown vs Double slice --- actions.py | 15 +++++++++++ character.py | 8 +++++- distribution.py | 8 +++++- leveling/rogue.json | 2 +- main.py | 75 +++++++++++++++++++++++++++++++++++++++++++---------- 5 files changed, 92 insertions(+), 16 deletions(-) diff --git a/actions.py b/actions.py index 9710faa..c1c9f30 100644 --- a/actions.py +++ b/actions.py @@ -14,6 +14,7 @@ class Attack: strike_type="melee" previous_attacks: int = 0 distance: int = 0 + sneak: bool = False def bonus(self): return self.attacker.attack_bonus(self.weapon, self.strike_type) + self.map() @@ -30,10 +31,24 @@ class Attack: def damage_bonus(self): return self.attacker.attack_damage_bonus(self.weapon, self.strike_type) + def __str__(self): + res = "" + if self.sneak: + res += "Sneak " + ac = self.target.ac() + bonus = self.bonus() + res += f"{self.strike_type} attack{self.previous_attacks+1} " + res += f"{bonus:+} vs AC {ac}: {10+bonus-ac:+}; " + res += Roll.short_str(self.roll()) + res += f" Dmg: {self.strike().expected()}" + return res + def damage(self, check=Roll.Success): persistant_damage_factor = 3 + 1/3 wp = self.weapon dmg_bonus = self.damage_bonus() + if self.sneak: + dmg_bonus += self.attacker.sneak_attack() if wp.backstabber: dmg_bonus += 1 if wp.potency >= 3: diff --git a/character.py b/character.py index 8614000..cab4cbb 100644 --- a/character.py +++ b/character.py @@ -3,7 +3,7 @@ import json from dataclasses import dataclass, asdict from items import Armor, Weapon -from game import LevelingPlan, Features, Modifier, Conditions +from game import LevelingPlan, Features, Modifier, Conditions, RollInfo class FeaturesPlan(LevelingPlan, dict): @@ -189,6 +189,12 @@ class Character: return (str_mod.total()//2 + dmg_mod).total() return dmg_mod.total() + def sneak_attack(self): + sneak = "sneak attack" + if sneak not in self.features: + return 0 + return RollInfo.parse(self.features[sneak]).distribution() + def toJSON(self): methods = ["classDC", "max_hp", "ac"] diff --git a/distribution.py b/distribution.py index d2503fa..47313b8 100644 --- a/distribution.py +++ b/distribution.py @@ -82,7 +82,7 @@ class Distribution: ax.bar(xax, yax) ax.set_ylabel(ylb) ax.set_xlabel(xlb) - ax.xaxis.set_major_locator(ticker.FixedLocator(xax)) + # ax.xaxis.set_major_locator(ticker.FixedLocator(xax)) # ax.yaxis.set_major_locator(ticker.FixedLocator(list(set(yax)))) plt.grid(True) plt.show() @@ -111,9 +111,15 @@ class Distribution: res.insert(operator(lv, rv, *args, **kwargs), lprob*rprob) return res + def __iadd__(self, other): + self = self.apply2(lambda x, y: x + y, other) + return self + def __add__(self, other): return self.apply2(lambda x, y: x + y, other) + __radd__ = __add__ + def __neg__(self): return self.apply(lambda x: -x) diff --git a/leveling/rogue.json b/leveling/rogue.json index e368f8a..87877f9 100644 --- a/leveling/rogue.json +++ b/leveling/rogue.json @@ -8,7 +8,7 @@ "3": {"deny advantage": true}, "5": {"sneak attack": "2d6", "unarmed":4, "simple": 4, "martial": 4, "critical specialization": "off-guard"}, "7": {"reflex": 6, "perception": 6, "weapon specialization": 1}, - "9": {"fortitude": 6, "debilitating strike": 1}, + "9": {"fortitude": 4, "debilitating strike": 1}, "11": {"classDC": 4, "sneak attack": "3d6"}, "13": {"reflex": 8, "perception": 8, "unarmored": 4, "light": 4, "unarmed": 6, "simple": 6, "martial": 6}, "15": {"weapon specialization": 2, "debilitating strike": 2}, diff --git a/main.py b/main.py index 65c9c45..3d4e154 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,6 @@ import math + +from numpy._core import multiarray from character import CharacterPlan from distribution import Distribution from game import Difficulty, Roll, d @@ -62,22 +64,27 @@ def main(): show_stat("Treat Wounds 20 +1", treat) class StatsTest: - def __init__(self) -> None: + def __init__(self, difficulty = Difficulty.Moderate) -> None: # self.char_js = pb2e.download_character(pfb_id=160180) self.char_js = pb2e.open_character("quizrel2") self.char_plan = CharacterPlan(self.char_js) - self.creature_plan = CreaturePlan(default_difficulty=Difficulty.Moderate) + self.creature_plan = CreaturePlan(default_difficulty=difficulty) # self.creature_plan["reflex"] = Difficulty.High # self.creature_plan["ac"] = Difficulty.High self.char = self.char_plan.build(1) def add_weapons(self): - falcata = Weapon("falcata", "martial", "sword", 8, fatal=12) - sword = Weapon("pick", "martial", "pick", 6, fatal=10) - rapier = Weapon("rapier", "martial", "sword", 6, finesse=True, deadly=8) - urumi = Weapon("urumi", "martial", "flail", 6, deadly=10) - sword2 = Weapon("tricky pick", "martial", "pick", 6, fatal=10, backstabber=True) - self.char.weapons.extend([sword, falcata, rapier, sword2, urumi]) + wps = [Weapon("falcata", "martial", "sword", 8, fatal=12), + Weapon("trident", "martial", "spear", 8), + Weapon("pick", "martial", "pick", 6, fatal=10), + Weapon("rapier", "martial", "sword", 6, finesse=True, deadly=8), + Weapon("urumi", "martial", "flail", 6, deadly=10), + Weapon("tricky pick", "martial", "pick", 6, fatal=10, backstabber=True), + Weapon("shortsword", "simple", "sword", 6, agile=True), + Weapon("gauntlet", "simple", "brawling", 4, agile=True), + Weapon("light pick", "martial", "pick", 6, agile=True, fatal=8) + ] + self.char.weapons.extend(wps) def get_weapon(self, name): for wp in self.char.weapons: @@ -110,7 +117,7 @@ def make_check(name, bonus, dc): if isinstance(bonus, Distribution): bonus = bonus.expected() print(f"{name} {bonus:+} vs DC {dc}:", - Roll.short_str(roll), f"Total: {dc-bonus-10:+}") + Roll.short_str(roll), f"Total: {10+bonus-dc:+}") return roll def demoralize_test(): @@ -139,7 +146,7 @@ def demoralize_test(): frightened = roll.apply(lambda x: x if x >= Roll.Success else 0).expected() print("Frightened:", frightened) -def test3(): +def weapon_damage(): t = StatsTest() def calc_damage(level): t.set_level(level) @@ -152,15 +159,55 @@ def test3(): res = dict() for weapon in t.char.weapons: - attack = Attack(t.char, t.creature, weapon) + attack = Attack(t.char, t.creature, weapon, sneak=False) res[weapon.name] = attack.strike().expected() # res["hp"] = creature["hp"] return res # calc_damage(20) + levelup_view("Weapons Comparison", "Expected Damage", calc_damage) + +def twin_takedown_test(): + t = StatsTest() + # t.creature_plan["ac"] = Difficulty.Extreme + def calc_damage(level): + t.set_level(level) + res = dict() + wp1 = t.get_weapon("tricky pick") + wp2 = t.get_weapon("gauntlet") + + # Twin takedown + attack = Attack(t.char, t.creature, wp1, sneak=True) + dmg = attack.strike().expected() + attack.previous_attacks += 1 + attack.weapon = wp2 + dmg += attack.strike().expected() + res["Twin Takedown"] = dmg + + # Double slice + attack = Attack(t.char, t.creature, wp1, sneak=True) + dmg = attack.strike().expected() + attack.sneak = False + attack.weapon = wp2 + dmg += attack.strike().expected() + res["Double Slice"] = dmg + + return res levelup_view("Weapons Comparison", "Expected Damage", calc_damage) +def multiple_attacks(): + t = StatsTest() + t.set_level(5) + wp = t.get_weapon("Shortsword") + wp.agile = False + # t.creature["ac"] -= 1 + attack = Attack(t.char, t.creature, wp, sneak=True) + print(attack) + for _ in range(4): + attack.previous_attacks += 1 + print(attack) + def levelup_view(title, ylabel, fn): @@ -192,5 +239,7 @@ def levelup_view(title, ylabel, fn): if __name__ == '__main__': - # test4() - demoralize_test() + # weapon_damage() + twin_takedown_test() + # multiple_attacks() + # demoralize_test() -- cgit v1.2.3