Spaces:
Sleeping
Sleeping
| """ | |
| 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""" | |
| 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 | |
| def roll_stat() -> int: | |
| """Roll a D&D ability score (4d6 keep highest 3)""" | |
| total, _, _ = DiceRoller.roll("4d6kh3") | |
| return total | |
| 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(), | |
| } | |
| 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}" | |
| 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}" | |
| 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}" | |
| 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) | |