mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-03-30 12:05:38 +00:00
- 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.
328 lines
12 KiB
Python
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)
|