major-domo-v2/utils/dice_utils.py
Cal Corum 9991b5f4a0 CLAUDE: Refactor dice rolling into reusable utility module and add /d20 command
- Created utils/dice_utils.py with reusable dice rolling functions
  - DiceRoll dataclass for roll results
  - parse_and_roll_multiple_dice() for multiple dice notation
  - parse_and_roll_single_dice() for single dice notation
  - Graceful error handling with empty list returns

- Refactored commands/dice/rolls.py to use new utility module
  - Removed duplicate DiceRoll class and parsing methods
  - Updated all method calls to use standalone functions
  - Added new /d20 command for quick d20 rolls
  - Fixed fielding prefix command to include d100 roll

- Updated tests/test_commands_dice.py
  - Updated imports to use utils.dice_utils
  - Fixed all test calls to use standalone functions
  - Added comprehensive test for /d20 command
  - All 35 tests passing

- Updated utils/CLAUDE.md documentation
  - Added Dice Utilities section with full API reference
  - Documented functions, usage patterns, and design benefits
  - Listed all commands using dice utilities

Benefits:
- Reusability: Dice functions can be imported by any command file
- Maintainability: Centralized dice logic in one place
- Testability: Functions testable independent of command cogs
- Consistency: All dice commands use same underlying logic

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-29 01:15:11 -05:00

85 lines
2.2 KiB
Python

"""
Dice Rolling Utilities
Provides reusable dice rolling functionality for commands that need dice mechanics.
"""
import random
import re
from dataclasses import dataclass
@dataclass
class DiceRoll:
"""Represents the result of a dice roll."""
dice_notation: str
num_dice: int
die_sides: int
rolls: list[int]
total: int
def parse_and_roll_multiple_dice(dice_notation: str) -> list[DiceRoll]:
"""Parse dice notation (supports multiple rolls) and return roll results.
Args:
dice_notation: Dice notation string, supports multiple rolls separated by semicolon
(e.g., "2d6", "1d20;2d6;1d6")
Returns:
List of DiceRoll results, or empty list if any part is invalid
"""
# Split by semicolon for multiple rolls
dice_parts = [part.strip() for part in dice_notation.split(';')]
results = []
for dice_part in dice_parts:
try:
result = parse_and_roll_single_dice(dice_part)
results.append(result)
except ValueError:
return [] # Return empty list if any part is invalid
return results
def parse_and_roll_single_dice(dice_notation: str) -> DiceRoll:
"""Parse single dice notation and return roll results.
Args:
dice_notation: Single dice notation string (e.g., "2d6", "1d20")
Returns:
DiceRoll result
Raises:
ValueError: If dice notation is invalid or values are out of reasonable limits
"""
# Clean the input
dice_notation = dice_notation.strip().lower().replace(' ', '')
# Pattern: XdY
pattern = r'^(\d+)d(\d+)$'
match = re.match(pattern, dice_notation)
if not match:
raise ValueError(f'Cannot parse dice string **{dice_notation}**')
num_dice = int(match.group(1))
die_sides = int(match.group(2))
# Validate reasonable limits
if num_dice > 100 or die_sides > 1000 or num_dice < 1 or die_sides < 2:
raise ValueError('I don\'t know, bud, that just doesn\'t seem doable.')
# Roll the dice
rolls = [random.randint(1, die_sides) for _ in range(num_dice)]
total = sum(rolls)
return DiceRoll(
dice_notation=dice_notation,
num_dice=num_dice,
die_sides=die_sides,
rolls=rolls,
total=total
)