99 lines
3.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# SPDX-FileCopyrightText: 2025 2025
# SPDX-FileContributor: Gergely Polonkai
#
# SPDX-License-Identifier: GPL-3.0-or-later
"""dice -- Utilities to deal with dice rolls"""
import random
import re
DICE_DESCRIPTOR_PATTERN = re.compile(r"^([0-9]*d[0-9]+)([+-]([0-9]+|[0-9]*d[0-9]+))*$")
class Die:
"""A generic die rolling class"""
def __init__(self, descriptor: str) -> None:
self.positive_pool, self.negative_pool, self.modifier = self.parse(descriptor)
@staticmethod
def parse(descriptor: str) -> tuple[list[int], list[int], int]:
"""Parse dice descriptors such as ``d6`` or ``3d8+2``
:param descriptor: the dice descriptor
:returns: a tuple with the number and types of posititve dice, the number and types of negative dice, and the
constant modifier
"""
normalized_descriptor = "".join(c for c in descriptor if c != " ")
if not normalized_descriptor:
raise ValueError("Empty string cannot be parsed")
if not DICE_DESCRIPTOR_PATTERN.match(normalized_descriptor):
raise ValueError(f"Invalid dice descriptor {descriptor}")
sign = "+"
positive_pool = []
negative_pool = []
modifier = 0
while normalized_descriptor:
if (new_sign := normalized_descriptor[0]) in ("+", "-"):
sign = new_sign
normalized_descriptor = normalized_descriptor[1:]
continue
match = re.search(r"^([0-9]*)(d?)([0-9]+)", normalized_descriptor)
# This will always pass as we already checked the descriptor against DICE_DESCRIPTOR_PATTERN; we need this
# assertion to prevent mypy warnings
assert match
multiplier, has_d, die_max = match.groups()
normalized_descriptor = normalized_descriptor[match.end() :]
if has_d:
count = int(multiplier) if multiplier else 1
dice = [int(die_max) for _ in range(count)]
if sign == "+":
positive_pool.extend(dice)
elif sign == "-": # pragma: no branch
negative_pool.extend(dice)
else:
value = int(die_max)
if sign == "-":
value = 0 - value
modifier += value
return positive_pool, negative_pool, modifier
@property
def minimum(self) -> int:
"""The lowest number this dice roller can yield"""
return len(self.positive_pool) - sum(self.negative_pool) + self.modifier
@property
def maximum(self) -> int:
"""The highest number this dice roller can yield"""
return sum(self.positive_pool) - len(self.negative_pool) + self.modifier
def roll(self) -> int:
"""Roll the dice in this rollers dice pool and return the result"""
result = 0
for die in self.positive_pool:
result += random.randint(1, die)
for die in self.negative_pool:
result -= random.randint(1, die)
result += self.modifier
return result