99 lines
3.1 KiB
Python
99 lines
3.1 KiB
Python
# 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
|