roll/roll/roll.py

57 lines
1.7 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 = 1
sides: int = 20
modifier: int = 0
def __post_init__(self) -> None:
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 its 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 0)
def modifier_str(self) -> str:
"""return the modifier as a string"""
if self.modifier == 0:
return ""
return f"{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.sides, self.modifier)