# 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 roller’s 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