summaryrefslogtreecommitdiff
path: root/game.py
blob: 75162eec85349182d7daa1b09d769e4482332945 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
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