58 lines
1.8 KiB
Python
58 lines
1.8 KiB
Python
import dataclasses
|
|
import random
|
|
import re
|
|
from typing import Self
|
|
|
|
from roll.throw import Throw
|
|
|
|
MIN_SIDES = 2
|
|
|
|
ROLL_PATTERN = re.compile(r"(\d+)d(\d+)([+-]\d+)?")
|
|
|
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
class Roll:
|
|
"""A roll of one or more dice"""
|
|
|
|
dice_count: int
|
|
sides: int
|
|
modifier: int | None = None
|
|
|
|
def __post_init__(self):
|
|
if self.dice_count < 1:
|
|
msg = "dice must be greater than 0"
|
|
raise ValueError(msg)
|
|
if self.sides < MIN_SIDES:
|
|
msg = "sides must be greater than 1"
|
|
raise ValueError(msg)
|
|
|
|
@classmethod
|
|
def from_str(cls, value: str) -> Self:
|
|
"""Parse a Roll from it's short representation, e.g. 2d6 or 1d20-2"""
|
|
match = ROLL_PATTERN.fullmatch(value)
|
|
if match is None:
|
|
msg = f"expected {value!r} to match pattern {ROLL_PATTERN.pattern!r}"
|
|
raise ValueError(msg)
|
|
dice_count, sides, modifier = match.groups()
|
|
return cls(int(dice_count), int(sides), int(modifier) if modifier else None)
|
|
|
|
def modifier_str(self) -> str:
|
|
"""Return the modifier as a string"""
|
|
if self.modifier is None:
|
|
return ""
|
|
sign = "+" if self.modifier > 0 else ""
|
|
return f"{sign}{self.modifier}"
|
|
|
|
def to_str(self) -> str:
|
|
"""Return the short representation of a roll, e.g. 3d4 or 2d20+3"""
|
|
return f"{self.dice_count}d{self.sides}{self.modifier_str()}"
|
|
|
|
def modify(self, modifier: int) -> Self:
|
|
"""Return a new Roll with the given modifier"""
|
|
return dataclasses.replace(self, modifier=modifier)
|
|
|
|
def throw(self) -> Throw:
|
|
"""Throw the dice"""
|
|
throw = [random.randint(1, self.sides) for _ in range(self.dice_count)]
|
|
return Throw(throw, self.modifier)
|