official.ghost.logic
Deploy D&D Campaign Manager v2
71b378e
"""
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)