import re import copy from enum import Enum from dataclasses import dataclass from abc import ABC, abstractmethod from distribution import Distribution def d(edge_count): res = Distribution() if edge_count == 0: res.insert(0, 1) return res prob = 1/edge_count for val in range(1, edge_count+1): res.insert(val, prob) return res class Roll: Fumble = -1 Fail = 0 Success = 1 Crit = 2 @staticmethod def get_natural_success_mod(value): if value <= 1: return -1 if value >= 20: return 1 return 0 @staticmethod def check(value, dc, bonus=0): res = 0 rolled = value + bonus if rolled >= dc + 10: res = Roll.Crit elif rolled >= dc: res = Roll.Success elif rolled <= dc - 10: res = Roll.Fumble res += Roll.get_natural_success_mod(value) return Roll.clamp(res) @staticmethod def clamp(value): return max(Roll.Fumble, min(Roll.Crit, value)) class Difficulty(Enum): Terrible = -2 Low = -1 Moderate = 0 High = 1 Extreme = 2 @classmethod def from_str(cls, name): name = name.strip().lower() for member in cls: if member.name.lower() == name: return member raise ValueError(f"{name!r} is not a valid {cls.__name__}") @dataclass class RollInfo: die_size: int dice_count: int = 1 bonus: int = 0 @classmethod def parse(cls, str_data): # match strings like '2d6+8 (15)' match = re.fullmatch(r"^(\d+)d(\d+)\s*(\+\s*\d+)?\s*(\s*\(\s*\d+\s*\))?", str_data) if not match: raise ValueError(f"{str_data!r} is not a valid RollInfo") res = RollInfo(int(match.group(2).strip())) res.dice_count = int(match.group(1).strip()) bonus = match.group(3) if bonus: res.bonus = int(bonus.strip(" +")) return res def distribution(self): return self.dice_count * d(self.die_size) + self.bonus class LevelingPlan(ABC): @abstractmethod def build(self, level: int): pass class Features: abilities = ["str", "dex", "int", "wis", "cha", "con"] saves = ["reflex", "will", "fortitude"] defences = ["unarmored", "light", "medium", "heavy"] weapons = ["unarmed", "simple", "martial", "advanced"] skill_ability = { "perception": "wis", "fortitude": "con", "reflex": "dex", "will": "wis", "acrobatics": "dex", "arcana": "int", "athletics": "str", "crafting": "int", "deception": "cha", "diplomacy": "cha", "intimidation": "cha", "medicine": "wis", "nature": "wis", "occultism": "int", "performance": "cha", "religion": "wis", "society": "int", "stealth": "dex", "survival": "wis", "thievery": "dex", } @staticmethod def all(): res = set(Features.defences) | set(Features.weapons) res |= set(Features.saves) res |= Features.skill_ability.keys() return list(res) @staticmethod def expand(shortcuts: list[str]): if "all" in shortcuts: return Features.all() res = set() for shortcut in shortcuts: for skill, ability in Features.skill_ability.items(): if shortcut == ability: res.add(skill) for feature in ["saves", "defences", "weapons"]: if shortcut == feature: res |= set(getattr(Features, feature)) return list(res) class Modifier: names = ["circumstance", "status", "item"] def __init__(self, circumstance=0, status=0, item=0, untyped=0) -> None: self.untyped = untyped self.bonuses = dict() self.penalties = dict() for name in Modifier.names: val = locals()[name] if val > 0: self.bonuses[name] = val elif val < 0: self.penalties[name] = val def total(self): res = self.untyped for val in self.bonuses.values(): res += val for val in self.penalties.values(): res += val return res def __iadd__(self, other): if isinstance(other, int): self.untyped += other return self if not isinstance(other, Modifier): raise NotImplementedError self.untyped += other.untyped for name in (self.bonuses.keys() & other.bonuses.keys()): self.bonuses[name] = max(self.bonuses.get(name, 0), other.bonuses.get(name, 0)) for name in (self.penalties.keys() & other.penalties.keys()): self.penalties[name] = min(self.penalties.get(name, 0), other.penalties.get(name, 0)) return self def __add__(self, other): res = copy.deepcopy(self) res += other return res __radd__ = __add__ @dataclass class Condition: name: str filter: list modifier: Modifier class Conditions(list): def modifier(self, features, exclude = None): exclude = set(exclude) if exclude is not None else set() features = set(features) res = Modifier() for condition in self: positive = set(condition.filter) & features negative = set(condition.filter) & exclude if positive and not negative: res += condition.modifier return res