import re 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