123 lines
4.0 KiB
Python
Raw 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]+))*$")
D666_COMBINATIONS = [(0, 1, 2), (0, 2, 1), (1, 0, 2), (1, 2, 0), (2, 0, 1), (2, 1, 0)]
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
def d666(strict_order: bool = False) -> list[int]:
"""Roll three six sided dice and use them as separate digits
“d666” is a special die type often used in random tables; here you dont roll one 666 sided die, but three 6-sided
ones, then use the three values of the three digits of the final result.
:param strict_order: if ``True``, use the dics in strict order. Otherwise, return all possible combinations.
:returns: the list of all valid combinations
"""
roll_values = [random.randint(1, 6) for _ in range(3)]
results = []
combinations = [(0, 1, 2)] if strict_order else D666_COMBINATIONS
for a, b, c in combinations:
result = roll_values[a] * 100 + roll_values[b] * 10 + roll_values[c]
if result not in results:
results.append(result)
return results