mirror of
https://github.com/Threnklyn/dungeon-sheets.git
synced 2026-05-18 20:23:27 +02:00
82 lines
2.4 KiB
Python
82 lines
2.4 KiB
Python
import random
|
|
import re
|
|
from collections import namedtuple
|
|
from itertools import groupby
|
|
|
|
from dungeonsheets.exceptions import DiceError
|
|
|
|
dice_re = re.compile(r"(\d+)d(\d+)([+-]\d+)*", flags=re.I)
|
|
dice_part_re = re.compile(r"[0-9d]+", flags=re.I)
|
|
Dice = namedtuple("Dice", ("num", "faces", "modifier"))
|
|
|
|
|
|
def read_dice_str(dice_str):
|
|
"""Interpret a D&D dice string, eg. 3d10+2.
|
|
|
|
Returns
|
|
-------
|
|
dice : tuple
|
|
A named tuple with the scheme (num, faces), so '3d10-2' return
|
|
(num=3, faces=10, modifier=-2)
|
|
|
|
"""
|
|
dice_str = dice_str.replace(" ", "").replace("\n", "")
|
|
match = dice_re.search(dice_str)
|
|
if match is None:
|
|
raise DiceError(f"Cannot interpret dice string {dice_str}")
|
|
num, faces = int(match.group(1)), int(match.group(2))
|
|
if match.group(3) is None:
|
|
modifier = 0
|
|
else:
|
|
modifier = int(match.group(3))
|
|
dice = Dice(num, faces, modifier)
|
|
return dice
|
|
|
|
def _dice_mean(dice, force_min=True):
|
|
"""Support function for calculating dice string mean."""
|
|
dmg_min = dice.num + dice.modifier
|
|
dmg_max = dice.num*dice.faces + dice.modifier
|
|
return (dmg_max - dmg_min)/2.0 + dmg_min
|
|
|
|
def combine_dice(dice_str):
|
|
"""Condense a dice string into its simplest representation.
|
|
|
|
For example: "1d8 + 5 + 2d8 + 2" would be reduced to "3d8 + 7".
|
|
|
|
Returns
|
|
=======
|
|
new_dice_str
|
|
A new, condensed string for the given dice.
|
|
|
|
"""
|
|
dice = []
|
|
bonuses = 0
|
|
# Sort out each portion of the dice string
|
|
for part in dice_part_re.findall(dice_str):
|
|
try:
|
|
dice.append(read_dice_str(part))
|
|
except DiceError:
|
|
bonuses += int(part)
|
|
# Recombine the dice into a more concise string
|
|
new_parts = []
|
|
dice = sorted(dice, key=lambda x: x.faces)
|
|
for faces, grp in groupby(dice, key=lambda x: x.faces):
|
|
new_parts.append(f"{sum([d.num for d in grp])}d{faces}")
|
|
if bonuses > 0:
|
|
new_parts.append(str(bonuses))
|
|
new_dice_str = " + ".join(new_parts)
|
|
return new_dice_str
|
|
|
|
def roll(a, b=None):
|
|
"""roll(20) means roll 1d20, roll(2, 6) means roll 2d6"""
|
|
if b is None:
|
|
return random.randint(1, a)
|
|
else:
|
|
return sum([random.randint(1, b) for _ in range(a)])
|
|
|
|
def dice_roll_mean(dice_text):
|
|
"""Takes a dice string like '3d6 +3' and returns its average roll."""
|
|
dice = read_dice_str(dice_text)
|
|
return round(_dice_mean(dice))
|
|
|