Spaces:
Sleeping
Sleeping
File size: 5,187 Bytes
71b378e |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
"""
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)
|