Files
meshcore-bot/modules/commands/dice_command.py
agessaman d2abc49048 feat: Enhance command documentation and usage information
- Updated the `generate_html` function to include detailed command usage information, including syntax, examples, and parameters for better user guidance.
- Added CSS styles for improved presentation of command usage and parameters in the generated website documentation.
- Enhanced command classes with structured documentation fields, allowing for consistent and informative command descriptions across the platform.
2026-01-15 20:04:58 -08:00

328 lines
12 KiB
Python

#!/usr/bin/env python3
"""
Dice command for the MeshCore Bot
Handles dice rolling for D&D and other tabletop games
"""
import random
from .base_command import BaseCommand
from ..models import MeshMessage
class DiceCommand(BaseCommand):
"""Handles dice rolling commands"""
# Plugin metadata
name = "dice"
keywords = ['dice']
description = "Roll dice for D&D and tabletop games. Use 'dice' for d6, 'dice d20' for d20, 'dice 2d6' for 2d6, 'dice d10 d6' for mixed dice, 'dice decade' for decade die (00-90), etc."
category = "games"
# Documentation
short_description = "Roll dice for tabletop games"
usage = "dice [NdX|dX|decade]"
examples = [
"dice",
"dice d20",
"dice 2d6",
"dice d10 d6",
"dice decade"
]
parameters = [
{"name": "dice", "description": "Dice notation: d6, 2d8, d10 d6, decade"}
]
# Standard D&D dice types
DICE_TYPES = {
'd4': 4,
'd6': 6,
'd8': 8,
'd10': 10,
'd12': 12,
'd16': 16,
'd20': 20
}
def __init__(self, bot):
"""Initialize the dice command.
Args:
bot: The bot instance.
"""
super().__init__(bot)
self.dice_enabled = self.get_config_value('Dice_Command', 'enabled', fallback=True, value_type='bool')
def can_execute(self, message: MeshMessage) -> bool:
"""Check if this command can be executed with the given message.
Args:
message: The message triggering the command.
Returns:
bool: True if command is enabled and checks pass, False otherwise.
"""
if not self.dice_enabled:
return False
return super().can_execute(message)
def get_help_text(self) -> str:
"""Get help text for the dice command.
Returns:
str: Help text string.
"""
return self.translate('commands.dice.help')
def matches_keyword(self, message: MeshMessage) -> bool:
"""Override to handle dice-specific matching.
Args:
message: The received message.
Returns:
bool: True if message is a dice command, False otherwise.
"""
content = message.content.strip().lower()
# Handle command-style messages
if content.startswith('!'):
content = content[1:].strip().lower()
# Check for exact "dice" match
if content == "dice":
return True
# Check for dice with parameters (dice d20, dice 20, dice d6, etc.)
# Match any message starting with "dice " - validation happens in execute()
if content.startswith("dice "):
words = content.split()
if len(words) >= 2 and words[0] == "dice":
return True # Match any dice command, validation in execute()
return False
def parse_dice_notation(self, dice_input: str) -> tuple:
"""Parse dice notation and return (sides, count, is_decade).
Supports: d20, 20, d6, 6, 2d6, 4d10, decade, etc.
Args:
dice_input: The dice string to parse.
Returns:
tuple: (sides, count, is_decade) or (None, None, False) if invalid.
"""
dice_input = dice_input.strip().lower()
# Handle special D&D dice types
if dice_input == "decade":
return (10, 1, True) # Decade die (00-90)
# Handle multiple dice notation (e.g., "2d6", "4d10", "3d20")
if 'd' in dice_input:
parts = dice_input.split('d')
if len(parts) == 2:
count_str, sides_str = parts
# Handle cases like "d6" (no count specified)
if not count_str:
count = 1
else:
try:
count = int(count_str)
if count < 1 or count > 10: # Reasonable limit
return None, None, False
except ValueError:
return None, None, False
# Parse sides
try:
sides = int(sides_str)
if sides in self.DICE_TYPES.values():
return sides, count, False
else:
return None, None, False
except ValueError:
return None, None, False
# Handle direct number (e.g., "20" -> d20)
if dice_input.isdigit():
sides = int(dice_input)
if sides in self.DICE_TYPES.values():
return sides, 1, False
else:
return None, None, False
# Handle dice type names (e.g., "d20", "d6")
if dice_input in self.DICE_TYPES:
return self.DICE_TYPES[dice_input], 1, False
return None, None, False
def parse_mixed_dice(self, dice_input: str) -> list:
"""Parse mixed dice notation and return list of (sides, count, is_decade) tuples.
Supports: "d10 d6", "2d6 d20", "d4 d8 d12", "decade", etc.
Args:
dice_input: The space-separated dice string.
Returns:
list: List of (sides, count, is_decade) tuples, or empty list if invalid.
"""
dice_input = dice_input.strip()
if not dice_input:
return []
# Split by spaces to get individual dice specifications
dice_specs = dice_input.split()
parsed_dice = []
for spec in dice_specs:
sides, count, is_decade = self.parse_dice_notation(spec)
if sides is None:
return [] # Invalid specification found
parsed_dice.append((sides, count, is_decade))
return parsed_dice
def roll_dice(self, sides: int, count: int = 1, is_decade: bool = False) -> list:
"""Roll dice and return list of results.
For decade dice, returns values 0, 10, 20, ..., 90 (formatted as 00, 10, 20, etc.)
Args:
sides: Number of sides on the die.
count: Number of dice to roll.
is_decade: Whether it's a decade die (00-90).
Returns:
list: List of integer results.
"""
if is_decade:
# Decade die: 00, 10, 20, 30, 40, 50, 60, 70, 80, 90
return [random.randint(0, 9) * 10 for _ in range(count)]
else:
return [random.randint(1, sides) for _ in range(count)]
def format_dice_result(self, sides: int, count: int, results: list, is_decade: bool = False) -> str:
"""Format dice roll results into a readable string.
Args:
sides: Number of sides.
count: Number of dice.
results: List of roll results.
is_decade: Whether it's a decade die.
Returns:
str: Formatted result string.
"""
if is_decade:
# Format decade dice results (00, 10, 20, etc.)
formatted_results = [f"{r:02d}" for r in results]
if count == 1:
return f"🎲 decade: {formatted_results[0]}"
else:
results_str = ", ".join(formatted_results)
total = sum(results)
return f"🎲 {count}decade: [{results_str}] = {total}"
else:
if count == 1:
# Single die roll
return self.translate('commands.dice.single_die', sides=sides, result=results[0])
else:
# Multiple dice
total = sum(results)
results_str = ", ".join(map(str, results))
return self.translate('commands.dice.multiple_dice', count=count, sides=sides, results=results_str, total=total)
def format_mixed_dice_result(self, dice_results: list) -> str:
"""Format mixed dice roll results into a readable string.
Args:
dice_results: list of tuples (sides, count, results_list, is_decade).
Returns:
str: Formatted result string for all dice.
"""
parts = []
grand_total = 0
for sides, count, results, is_decade in dice_results:
total = sum(results)
grand_total += total
if is_decade:
# Format decade dice
formatted_results = [f"{r:02d}" for r in results]
if count == 1:
parts.append(f"decade: {formatted_results[0]}")
else:
results_str = ", ".join(formatted_results)
parts.append(f"{count}decade: [{results_str}] = {total}")
else:
if count == 1:
parts.append(f"d{sides}: {results[0]}")
else:
results_str = ", ".join(map(str, results))
parts.append(f"{count}d{sides}: [{results_str}] = {total}")
result_str = " + ".join(parts)
if len(dice_results) > 1:
result_str += f" | Total: {grand_total}"
return f"🎲 {result_str}"
async def execute(self, message: MeshMessage) -> bool:
"""Execute the dice command.
Args:
message: The message triggering the command.
Returns:
bool: True if executed successfully, False otherwise.
"""
content = message.content.strip()
# Handle command-style messages
if content.startswith('!'):
content = content[1:].strip()
# Default to d6 if no specification
if content.lower() == "dice":
sides = 6
count = 1
results = self.roll_dice(sides, count)
response = self.format_dice_result(sides, count, results)
return await self.send_response(message, response)
# Parse dice specification
dice_part = content[5:].strip() # Get everything after "dice "
# Try parsing as mixed dice first (multiple dice types)
mixed_dice = self.parse_mixed_dice(dice_part)
if len(mixed_dice) > 1:
# Multiple dice types - roll each type
dice_results = []
for sides, count, is_decade in mixed_dice:
results = self.roll_dice(sides, count, is_decade)
dice_results.append((sides, count, results, is_decade))
# Format and send response
response = self.format_mixed_dice_result(dice_results)
return await self.send_response(message, response)
elif len(mixed_dice) == 1:
# Single dice type (could be multiple dice of same type like "2d6")
sides, count, is_decade = mixed_dice[0]
results = self.roll_dice(sides, count, is_decade)
response = self.format_dice_result(sides, count, results, is_decade)
return await self.send_response(message, response)
else:
# Invalid dice specification - return error with usage info
available_dice = ", ".join(list(self.DICE_TYPES.keys()) + ["decade"])
help_text = self.get_help_text()
error_msg = self.translate('commands.dice.invalid_dice_type', available=available_dice)
response = f"{error_msg}\n\n{help_text}"
return await self.send_response(message, response)