mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-04-04 14:35:41 +00:00
- Implemented a configuration option for enabling or disabling commands across multiple command classes. - Each command now checks its enabled state before execution, improving control over command availability. - Updated the configuration loading mechanism to retrieve the enabled state from the config file for commands like Advert, AQI, Catfact, and others.
314 lines
11 KiB
Python
314 lines
11 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"
|
|
|
|
# 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)
|