""" Dice rolling utilities for D&D """ import random import re from typing import List, Tuple, Optional class DiceRoller: """D&D dice roller with standard notation support""" @staticmethod def roll(notation: str) -> Tuple[int, List[int], str]: """ Roll dice using standard notation (e.g., "2d6+3", "1d20", "4d6kh3") Returns: Tuple of (total, individual_rolls, explanation) """ notation = notation.lower().strip() # Parse notation: XdY+Z or XdY-Z or XdYkhN (keep highest N) match = re.match(r'(\d+)?d(\d+)(?:kh(\d+))?([+-]\d+)?', notation) if not match: raise ValueError(f"Invalid dice notation: {notation}") num_dice = int(match.group(1)) if match.group(1) else 1 die_size = int(match.group(2)) keep_highest = int(match.group(3)) if match.group(3) else None modifier = int(match.group(4)) if match.group(4) else 0 # Validate if num_dice < 1 or num_dice > 100: raise ValueError("Number of dice must be between 1 and 100") if die_size < 1 or die_size > 1000: raise ValueError("Die size must be between 1 and 1000") # Roll dice rolls = [random.randint(1, die_size) for _ in range(num_dice)] # Apply keep highest if keep_highest: if keep_highest >= num_dice: kept_rolls = rolls else: sorted_rolls = sorted(rolls, reverse=True) kept_rolls = sorted_rolls[:keep_highest] dropped_rolls = sorted_rolls[keep_highest:] explanation = f"Rolled {rolls}, kept {kept_rolls}, dropped {dropped_rolls}" else: kept_rolls = rolls explanation = f"Rolled {rolls}" total = sum(kept_rolls) + modifier if modifier != 0: explanation += f" {'+' if modifier > 0 else ''}{modifier} = {total}" else: explanation += f" = {total}" return total, rolls, explanation @staticmethod def roll_stat() -> int: """Roll a D&D ability score (4d6 keep highest 3)""" total, _, _ = DiceRoller.roll("4d6kh3") return total @staticmethod def roll_stats() -> dict: """Roll a complete set of D&D ability scores""" return { "strength": DiceRoller.roll_stat(), "dexterity": DiceRoller.roll_stat(), "constitution": DiceRoller.roll_stat(), "intelligence": DiceRoller.roll_stat(), "wisdom": DiceRoller.roll_stat(), "charisma": DiceRoller.roll_stat(), } @staticmethod def advantage(notation: str = "1d20") -> Tuple[int, List[int], str]: """Roll with advantage (roll twice, take higher)""" result1, rolls1, _ = DiceRoller.roll(notation) result2, rolls2, _ = DiceRoller.roll(notation) if result1 >= result2: return result1, rolls1, f"Advantage: rolled {rolls1} and {rolls2}, kept {result1}" else: return result2, rolls2, f"Advantage: rolled {rolls1} and {rolls2}, kept {result2}" @staticmethod def disadvantage(notation: str = "1d20") -> Tuple[int, List[int], str]: """Roll with disadvantage (roll twice, take lower)""" result1, rolls1, _ = DiceRoller.roll(notation) result2, rolls2, _ = DiceRoller.roll(notation) if result1 <= result2: return result1, rolls1, f"Disadvantage: rolled {rolls1} and {rolls2}, kept {result1}" else: return result2, rolls2, f"Disadvantage: rolled {rolls1} and {rolls2}, kept {result2}" @staticmethod def roll_initiative(dex_modifier: int = 0) -> Tuple[int, str]: """Roll initiative with dexterity modifier""" result, rolls, _ = DiceRoller.roll("1d20") total = result + dex_modifier return total, f"Initiative: {rolls[0]} + {dex_modifier} = {total}" @staticmethod def roll_hit_points(hit_die: int, constitution_modifier: int, level: int) -> int: """ Roll hit points for a character First level: max hit die + con mod Subsequent levels: roll hit die + con mod """ if level < 1: raise ValueError("Level must be at least 1") # First level gets max hp = hit_die + constitution_modifier # Roll for subsequent levels for _ in range(level - 1): roll, _, _ = DiceRoller.roll(f"1d{hit_die}") hp += roll + constitution_modifier return max(1, hp) # Minimum 1 HP # Convenience functions def roll(notation: str) -> Tuple[int, List[int], str]: """Roll dice using standard notation""" return DiceRoller.roll(notation) def roll_stats() -> dict: """Roll complete set of ability scores""" return DiceRoller.roll_stats() def roll_with_advantage(notation: str = "1d20") -> Tuple[int, List[int], str]: """Roll with advantage""" return DiceRoller.advantage(notation) def roll_with_disadvantage(notation: str = "1d20") -> Tuple[int, List[int], str]: """Roll with disadvantage""" return DiceRoller.disadvantage(notation)