roll/roll/roll.py

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)