major-domo-v2/commands/dice/rolls.py
Cal Corum f64fee8d2e fix: remove 226 unused imports across the codebase (closes #33)
Ran `ruff check --select F401 --fix` to auto-remove 221 unused imports,
manually removed 4 unused `import discord` from package __init__.py files,
and fixed test import for DISAPPOINTMENT_TIERS to reference canonical location.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 11:35:04 -06:00

753 lines
29 KiB
Python

"""
Dice Rolling Commands
Implements slash commands for dice rolling functionality required for gameplay.
"""
import random
import discord
from discord.ext import commands
from services.team_service import team_service
from utils.logging import get_contextual_logger
from utils.decorators import logged_command
from utils.team_utils import get_user_major_league_team
from utils.text_utils import split_text_for_fields
from utils.dice_utils import DiceRoll, parse_and_roll_multiple_dice, parse_and_roll_single_dice
from views.embeds import EmbedColors, EmbedTemplate
from commands.dev.loaded_dice import get_and_consume_loaded_roll
from .chart_data import (
INFIELD_X_CHART,
OUTFIELD_X_CHART,
INFIELD_RANGES,
OUTFIELD_RANGES,
CATCHER_RANGES,
PITCHER_RANGES,
FIRST_BASE_ERRORS,
SECOND_BASE_ERRORS,
THIRD_BASE_ERRORS,
SHORTSTOP_ERRORS,
CORNER_OUTFIELD_ERRORS,
CENTER_FIELD_ERRORS,
CATCHER_ERRORS,
PITCHER_ERRORS,
)
class DiceRollCommands(commands.Cog):
"""Dice rolling command handlers for gameplay."""
def __init__(self, bot: commands.Bot):
self.bot = bot
self.logger = get_contextual_logger(f'{__name__}.DiceRollCommands')
@discord.app_commands.command(
name="roll",
description="Roll polyhedral dice using XdY notation (e.g., 2d6, 1d20, 3d8)"
)
@discord.app_commands.describe(
dice="Dice notation - single or multiple separated by semicolon (e.g., 2d6, 1d20;2d6;1d6)"
)
@logged_command("/roll")
async def roll_dice(
self,
interaction: discord.Interaction,
dice: str
):
"""Roll dice using standard XdY dice notation. Supports multiple rolls separated by semicolon."""
await interaction.response.defer()
# Parse and validate dice notation (supports multiple rolls)
roll_results = parse_and_roll_multiple_dice(dice)
if not roll_results:
await interaction.followup.send(
"❌ Invalid dice notation. Use format like: 2d6, 1d20, or 1d6;2d6;1d20",
ephemeral=True
)
return
# Create embed for the roll results
embed = self._create_multi_roll_embed(dice, roll_results, interaction.user)
await interaction.followup.send(embed=embed)
@commands.command(name="roll", aliases=["r", "dice"])
async def roll_dice_prefix(self, ctx: commands.Context, *, dice: str | None = None):
"""Roll dice using prefix commands (!roll, !r, !dice)."""
self.logger.info(f"Prefix roll command started by {ctx.author.display_name}", dice_input=dice)
if dice is None:
self.logger.debug("No dice input provided")
await ctx.send("❌ Please provide dice notation. Usage: `!roll 2d6` or `!roll 1d6;2d6;1d20`")
return
# Parse and validate dice notation (supports multiple rolls)
roll_results = parse_and_roll_multiple_dice(dice)
if not roll_results:
self.logger.warning("Invalid dice notation provided", dice_input=dice)
await ctx.send("❌ Invalid dice notation. Use format like: 2d6, 1d20, or 1d6;2d6;1d20")
return
self.logger.info(f"Dice rolled successfully", roll_count=len(roll_results))
# Create embed for the roll results
embed = self._create_multi_roll_embed(dice, roll_results, ctx.author)
await ctx.send(embed=embed)
@discord.app_commands.command(
name="d20",
description="Roll a single d20"
)
@logged_command("/d20")
async def d20_dice(self, interaction: discord.Interaction):
"""Roll a single d20."""
await interaction.response.defer()
embed_color = await self._get_channel_embed_color(interaction)
# Roll 1d20
dice_notation = "1d20"
roll_results = parse_and_roll_multiple_dice(dice_notation)
# Create embed for the roll results
embed = self._create_multi_roll_embed(
dice_notation,
roll_results,
interaction.user,
set_author=False,
embed_color=embed_color
)
embed.title = f'd20 roll for {interaction.user.display_name}'
await interaction.followup.send(embed=embed)
@discord.app_commands.command(
name="ab",
description="Roll baseball at-bat dice (1d6;2d6;1d20)"
)
@logged_command("/ab")
async def ab_dice(self, interaction: discord.Interaction):
"""Roll the standard baseball at-bat dice combination."""
await interaction.response.defer()
embed_color = await self._get_channel_embed_color(interaction)
# Check for loaded dice (dev testing)
loaded = get_and_consume_loaded_roll(interaction.user.id)
if loaded:
self.logger.info(
f"Using loaded dice for {interaction.user}: d6={loaded.d6_1}, 2d6={loaded.d6_2_total}, d20={loaded.d20}"
)
# Create DiceRoll objects from loaded values
# For 2d6, we split the total into two dice (arbitrary split that sums correctly)
d6_2a = min(loaded.d6_2_total - 1, 6) # First die (max 6)
d6_2b = loaded.d6_2_total - d6_2a # Second die gets remainder
roll_results = [
DiceRoll("1d6", 1, 6, [loaded.d6_1], loaded.d6_1),
DiceRoll("2d6", 2, 6, [d6_2a, d6_2b], loaded.d6_2_total),
DiceRoll("1d20", 1, 20, [loaded.d20], loaded.d20),
]
dice_notation = "1d6;2d6;1d20"
else:
# Use the standard baseball dice combination
dice_notation = "1d6;2d6;1d20"
roll_results = parse_and_roll_multiple_dice(dice_notation)
injury_risk = (roll_results[0].total == 6) and (roll_results[1].total in [7, 8, 9, 10, 11, 12])
d6_total = roll_results[1].total
embed_title = 'At bat roll'
if roll_results[2].total == 1:
embed_title = 'Wild pitch roll'
dice_notation = '1d20'
roll_results = [parse_and_roll_single_dice(dice_notation)]
elif roll_results[2].total == 2:
embed_title = 'PB roll'
dice_notation = '1d20'
roll_results = [parse_and_roll_single_dice(dice_notation)]
# Create embed for the roll results
embed = self._create_multi_roll_embed(
dice_notation,
roll_results,
interaction.user,
set_author=False,
embed_color=embed_color
)
embed.title = f'{embed_title} for {interaction.user.display_name}'
if injury_risk and embed_title == 'At bat roll':
embed.add_field(
name=f'Check injury for pitcher injury rating {13 - d6_total}',
value='Oops! All injuries!',
inline=False
)
await interaction.followup.send(embed=embed)
@commands.command(name="ab", aliases=["atbat"])
async def ab_dice_prefix(self, ctx: commands.Context):
"""Roll baseball at-bat dice using prefix commands (!ab, !atbat)."""
self.logger.info(f"At Bat dice command started by {ctx.author.display_name}")
team = await get_user_major_league_team(user_id=ctx.author.id)
embed_color = EmbedColors.PRIMARY
if team is not None and team.color is not None:
embed_color = int(team.color,16)
# Use the standard baseball dice combination
dice_notation = "1d6;2d6;1d20"
roll_results = parse_and_roll_multiple_dice(dice_notation)
self.logger.info("At Bat dice rolled successfully", roll_count=len(roll_results))
# Create embed for the roll results
embed = self._create_multi_roll_embed(dice_notation, roll_results, ctx.author, set_author=False, embed_color=embed_color)
embed.title = f'At bat roll for {ctx.author.display_name}'
await ctx.send(embed=embed)
@discord.app_commands.command(
name="scout",
description="Roll weighted scouting dice (1d6;2d6;1d20) based on card type"
)
@discord.app_commands.describe(
card_type="Type of card being scouted"
)
@discord.app_commands.choices(card_type=[
discord.app_commands.Choice(name="Batter", value="batter"),
discord.app_commands.Choice(name="Pitcher", value="pitcher")
])
@logged_command("/scout")
async def scout_dice(
self,
interaction: discord.Interaction,
card_type: discord.app_commands.Choice[str]
):
"""Roll weighted scouting dice based on card type (batter or pitcher)."""
await interaction.response.defer()
# Get the card type value
card_type_value = card_type.value
# Roll weighted scouting dice
roll_results = self._roll_weighted_scout_dice(card_type_value)
# Create embed for the roll results
embed = self._create_multi_roll_embed("1d6;2d6;1d20", roll_results, interaction.user, set_author=False)
embed.title = f'Scouting roll for {interaction.user.display_name}'
await interaction.followup.send(embed=embed)
@discord.app_commands.command(
name="fielding",
description="Roll Super Advanced fielding dice for a defensive position"
)
@discord.app_commands.describe(
position="Defensive position"
)
@discord.app_commands.choices(position=[
discord.app_commands.Choice(name="Pitcher (P)", value="P"),
discord.app_commands.Choice(name="Catcher (C)", value="C"),
discord.app_commands.Choice(name="First Base (1B)", value="1B"),
discord.app_commands.Choice(name="Second Base (2B)", value="2B"),
discord.app_commands.Choice(name="Third Base (3B)", value="3B"),
discord.app_commands.Choice(name="Shortstop (SS)", value="SS"),
discord.app_commands.Choice(name="Left Field (LF)", value="LF"),
discord.app_commands.Choice(name="Center Field (CF)", value="CF"),
discord.app_commands.Choice(name="Right Field (RF)", value="RF")
])
@logged_command("/fielding")
async def fielding_roll(
self,
interaction: discord.Interaction,
position: discord.app_commands.Choice[str]
):
"""Roll Super Advanced fielding dice for a defensive position."""
await interaction.response.defer()
embed_color = await self._get_channel_embed_color(interaction)
# Get the position value from the choice
pos_value = position.value
# Roll the dice - 1d20 and 3d6
dice_notation = "1d20;3d6;1d100"
roll_results = parse_and_roll_multiple_dice(dice_notation)
# Create fielding embed
embed = self._create_fielding_embed(pos_value, roll_results, interaction.user, embed_color)
await interaction.followup.send(embed=embed)
@commands.command(name="f", aliases=["fielding", "saf"])
async def fielding_roll_prefix(self, ctx: commands.Context, position: str | None = None):
"""Roll Super Advanced fielding dice using prefix commands (!f, !fielding, !saf)."""
self.logger.info(f"SA Fielding command started by {ctx.author.display_name}", position=position)
if position is None:
await ctx.send("❌ Please specify a position. Usage: `!f 3B` or `!fielding SS`")
return
# Parse and validate position
parsed_position = self._parse_position(position)
if not parsed_position:
await ctx.send("❌ Invalid position. Use: C, 1B, 2B, 3B, SS, LF, CF, RF")
return
# Roll the dice - 1d20 and 3d6 and 1d100
dice_notation = "1d20;3d6;1d100"
roll_results = parse_and_roll_multiple_dice(dice_notation)
self.logger.info("SA Fielding dice rolled successfully", position=parsed_position, d20=roll_results[0].total, d6_total=roll_results[1].total)
# Create fielding embed
embed = self._create_fielding_embed(parsed_position, roll_results, ctx.author)
await ctx.send(embed=embed)
@discord.app_commands.command(
name="jump",
description="Roll for baserunner's jump before stealing"
)
@logged_command("/jump")
async def jump_dice(self, interaction: discord.Interaction):
"""Roll to check for a baserunner's jump before attempting to steal a base."""
await interaction.response.defer()
embed_color = await self._get_channel_embed_color(interaction)
# Roll 1d20 for pickoff/balk check
check_roll = random.randint(1, 20)
# Roll 2d6 for jump rating
jump_result = parse_and_roll_single_dice("2d6")
# Roll another 1d20 for pickoff/balk resolution
resolution_roll = random.randint(1, 20)
# Create embed based on check roll
embed = self._create_jump_embed(
check_roll,
jump_result,
resolution_roll,
interaction.user,
embed_color,
show_author=False
)
await interaction.followup.send(embed=embed)
@commands.command(name="j", aliases=["jump"])
async def jump_dice_prefix(self, ctx: commands.Context):
"""Roll for baserunner's jump using prefix commands (!j, !jump)."""
self.logger.info(f"Jump command started by {ctx.author.display_name}")
team = await get_user_major_league_team(user_id=ctx.author.id)
embed_color = EmbedColors.PRIMARY
if team is not None and team.color is not None:
embed_color = int(team.color, 16)
# Roll 1d20 for pickoff/balk check
check_roll = random.randint(1, 20)
# Roll 2d6 for jump rating
jump_result = parse_and_roll_single_dice("2d6")
# Roll another 1d20 for pickoff/balk resolution
resolution_roll = random.randint(1, 20)
self.logger.info("Jump dice rolled successfully", check=check_roll, jump=jump_result.total if jump_result else None, resolution=resolution_roll)
# Create embed based on check roll
embed = self._create_jump_embed(
check_roll,
jump_result,
resolution_roll,
ctx.author,
embed_color
)
await ctx.send(embed=embed)
async def _get_channel_embed_color(self, interaction: discord.Interaction) -> int:
# Check if channel is a type that has a name attribute (DMChannel doesn't have one)
if isinstance(interaction.channel, (discord.TextChannel, discord.VoiceChannel, discord.Thread)):
channel_starter = interaction.channel.name[:6]
if '-' in channel_starter:
abbrev = channel_starter.split('-')[0]
channel_team = await team_service.get_team_by_abbrev(abbrev)
if channel_team is not None and channel_team.color is not None:
return int(channel_team.color,16)
team = await get_user_major_league_team(user_id=interaction.user.id)
if team is not None and team.color is not None:
return int(team.color,16)
return EmbedColors.PRIMARY
def _parse_position(self, position: str) -> str | None:
"""Parse and validate fielding position input for prefix commands."""
if not position:
return None
pos = position.upper().strip()
# Map common inputs to standard position names
position_map = {
'P': 'P', 'PITCHER': 'P',
'C': 'C', 'CATCHER': 'C',
'1': '1B', '1B': '1B', 'FIRST': '1B', 'FIRSTBASE': '1B',
'2': '2B', '2B': '2B', 'SECOND': '2B', 'SECONDBASE': '2B',
'3': '3B', '3B': '3B', 'THIRD': '3B', 'THIRDBASE': '3B',
'SS': 'SS', 'SHORT': 'SS', 'SHORTSTOP': 'SS',
'LF': 'LF', 'LEFT': 'LF', 'LEFTFIELD': 'LF',
'CF': 'CF', 'CENTER': 'CF', 'CENTERFIELD': 'CF',
'RF': 'RF', 'RIGHT': 'RF', 'RIGHTFIELD': 'RF'
}
return position_map.get(pos)
def _create_fielding_embed(
self,
position: str,
roll_results: list[DiceRoll],
user: discord.User | discord.Member,
embed_color: int = EmbedColors.PRIMARY
) -> discord.Embed:
"""Create an embed for fielding roll results."""
d20_result = roll_results[0].total
d6_total = roll_results[1].total
d6_rolls = roll_results[1].rolls
d100_result = roll_results[2].total
# Create base embed
embed = EmbedTemplate.create_base_embed(
title=f"SA Fielding roll for {user.display_name}",
color=embed_color
)
# Set user info
embed.set_author(
name=user.display_name,
icon_url=user.display_avatar.url
)
# Add dice results in standard format
dice_notation = "1d20;3d6"
embed_dice = self._create_multi_roll_embed(dice_notation, roll_results, user)
# Extract just the dice result part from the field
dice_field_value = embed_dice.fields[0].value
embed.add_field(
name="Dice Results",
value=dice_field_value,
inline=False
)
# Add fielding check summary
range_result = self._get_range_result(position, d20_result)
embed.add_field(
name=f"{position} Range Result",
value=f"```md\n 1 | 2 | 3 | 4 | 5\n{range_result}```",
inline=False
)
# Add rare play or error result
if d100_result == 1:
error_result = self._get_rare_play(position, d20_result)
base_field_name = "Rare Play Result"
else:
# Add error result
error_result = self._get_error_result(position, d6_total)
base_field_name = "Error Result"
if error_result:
# Split text if it exceeds Discord's field limit
result_chunks = split_text_for_fields(error_result, max_length=1024)
# Add each chunk as a separate field
for i, chunk in enumerate(result_chunks):
field_name = base_field_name
# Add part indicator if multiple chunks
if len(result_chunks) > 1:
field_name += f" (Part {i+1}/{len(result_chunks)})"
embed.add_field(
name=field_name,
value=chunk,
inline=False
)
# Add help commands
embed.add_field(
name="Help Commands",
value="Run /charts for full chart readout",
inline=False
)
# # Add references
# embed.add_field(
# name="References",
# value="Range Chart / Error Chart / Result Reference",
# inline=False
# )
return embed
def _create_jump_embed(
self,
check_roll: int,
jump_result: DiceRoll | None,
resolution_roll: int,
user: discord.User | discord.Member,
embed_color: int = EmbedColors.PRIMARY,
show_author: bool = True
) -> discord.Embed:
"""Create an embed for jump roll results."""
# Create base embed
embed = EmbedTemplate.create_base_embed(
title=f"Jump roll for {user.name}",
color=embed_color
)
if show_author:
# Set user info
embed.set_author(
name=user.name,
icon_url=user.display_avatar.url
)
# Check for pickoff or balk
if check_roll == 1:
# Pickoff attempt
embed.add_field(
name="Special",
value="```md\nCheck pickoff```",
inline=False
)
embed.add_field(
name="Pickoff roll",
value=f"```md\n# {resolution_roll}\nDetails:[1d20 ({resolution_roll})]```",
inline=False
)
elif check_roll == 2:
# Balk
embed.add_field(
name="Special",
value="```md\nCheck balk```",
inline=False
)
embed.add_field(
name="Balk roll",
value=f"```md\n# {resolution_roll}\nDetails:[1d20 ({resolution_roll})]```",
inline=False
)
else:
# Normal jump - show 2d6 result
if jump_result:
rolls_str = ' '.join(str(r) for r in jump_result.rolls)
embed.add_field(
name="Result",
value=f"```md\n# {jump_result.total}\nDetails:[2d6 ({rolls_str})]```",
inline=False
)
return embed
def _get_range_result(self, position: str, d20_roll: int) -> str:
"""Get the range result display for a position and d20 roll."""
if position == 'P':
return self._get_pitcher_range(d20_roll)
elif position in ['1B', '2B', '3B', 'SS']:
return self._get_infield_range(d20_roll)
elif position in ['LF', 'CF', 'RF']:
return self._get_outfield_range(d20_roll)
elif position == 'C':
return self._get_catcher_range(d20_roll)
return "Unknown position"
def _get_infield_range(self, d20_roll: int) -> str:
"""Get infield range result based on d20 roll."""
return INFIELD_RANGES.get(d20_roll, 'Unknown')
def _get_outfield_range(self, d20_roll: int) -> str:
"""Get outfield range result based on d20 roll."""
return OUTFIELD_RANGES.get(d20_roll, 'Unknown')
def _get_catcher_range(self, d20_roll: int) -> str:
"""Get catcher range result based on d20 roll."""
return CATCHER_RANGES.get(d20_roll, 'Unknown')
def _get_pitcher_range(self, d20_roll: int) -> str:
"""Get pitcher range result based on d20 roll."""
return PITCHER_RANGES.get(d20_roll, 'Unknown')
def _get_rare_play(self, position: str, d20_total: int) -> str:
"""Get the rare play result for a position and d20 total"""
starter = 'Rare play! Take the range result from above and consult the chart below.\n\n'
if position == 'P':
return starter + self._get_pitcher_rare_play(d20_total)
elif position == '1B':
return starter + self._get_infield_rare_play(d20_total)
elif position == '2B':
return starter + self._get_infield_rare_play(d20_total)
elif position == '3B':
return starter + self._get_infield_rare_play(d20_total)
elif position == 'SS':
return starter + self._get_infield_rare_play(d20_total)
elif position in ['LF', 'RF']:
return starter + self._get_outfield_rare_play(d20_total)
elif position == 'CF':
return starter + self._get_outfield_rare_play(d20_total)
raise ValueError(f'Unknown position: {position}')
def _get_error_result(self, position: str, d6_total: int) -> str:
"""Get the error result for a position and 3d6 total."""
# Get the appropriate error chart
if position == 'P':
return self._get_pitcher_error(d6_total)
elif position == '1B':
return self._get_1b_error(d6_total)
elif position == '2B':
return self._get_2b_error(d6_total)
elif position == '3B':
return self._get_3b_error(d6_total)
elif position == 'SS':
return self._get_ss_error(d6_total)
elif position in ['LF', 'RF']:
return self._get_corner_of_error(d6_total)
elif position == 'CF':
return self._get_cf_error(d6_total)
elif position == 'C':
return self._get_catcher_error(d6_total)
# Should never reach here due to position validation, but follow "Raise or Return" pattern
raise ValueError(f"Unknown position: {position}")
def _get_3b_error(self, d6_total: int) -> str:
"""Get 3B error result based on 3d6 total."""
return THIRD_BASE_ERRORS.get(d6_total, 'No error')
def _get_1b_error(self, d6_total: int) -> str:
"""Get 1B error result based on 3d6 total."""
return FIRST_BASE_ERRORS.get(d6_total, 'No error')
def _get_2b_error(self, d6_total: int) -> str:
"""Get 2B error result based on 3d6 total."""
return SECOND_BASE_ERRORS.get(d6_total, 'No error')
def _get_ss_error(self, d6_total: int) -> str:
"""Get SS error result based on 3d6 total."""
return SHORTSTOP_ERRORS.get(d6_total, 'No error')
def _get_corner_of_error(self, d6_total: int) -> str:
"""Get LF/RF error result based on 3d6 total."""
return CORNER_OUTFIELD_ERRORS.get(d6_total, 'No error')
def _get_cf_error(self, d6_total: int) -> str:
"""Get CF error result based on 3d6 total."""
return CENTER_FIELD_ERRORS.get(d6_total, 'No error')
def _get_catcher_error(self, d6_total: int) -> str:
"""Get Catcher error result based on 3d6 total."""
return CATCHER_ERRORS.get(d6_total, 'No error')
def _get_pitcher_error(self, d6_total: int) -> str:
"""Get Pitcher error result based on 3d6 total."""
return PITCHER_ERRORS.get(d6_total, 'No error')
def _get_pitcher_rare_play(self, d20_total: int) -> str:
return (
f'**G3**: {INFIELD_X_CHART["g3"]["rp"]}\n'
f'**G2**: {INFIELD_X_CHART["g2"]["rp"]}\n'
f'**G1**: {INFIELD_X_CHART["g1"]["rp"]}\n'
f'**SI1**: {INFIELD_X_CHART["si1"]["rp"]}\n'
)
def _get_infield_rare_play(self, d20_total: int) -> str:
return (
f'**G3**: {INFIELD_X_CHART["g3"]["rp"]}\n'
f'**G2**: {INFIELD_X_CHART["g2"]["rp"]}\n'
f'**G1**: {INFIELD_X_CHART["g1"]["rp"]}\n'
f'**SI1**: {INFIELD_X_CHART["si1"]["rp"]}\n'
f'**SI2**: {OUTFIELD_X_CHART["si2"]["rp"]}\n'
)
def _get_outfield_rare_play(self, d20_total: int) -> str:
return (
f'**F1**: {OUTFIELD_X_CHART["f1"]["rp"]}\n'
f'**F2**: {OUTFIELD_X_CHART["f2"]["rp"]}\n'
f'**F3**: {OUTFIELD_X_CHART["f3"]["rp"]}\n'
f'**SI2**: {OUTFIELD_X_CHART["si2"]["rp"]}\n'
f'**DO2**: {OUTFIELD_X_CHART["do2"]["rp"]}\n'
f'**DO3**: {OUTFIELD_X_CHART["do3"]["rp"]}\n'
f'**TR3**: {OUTFIELD_X_CHART["tr3"]["rp"]}\n'
)
def _roll_weighted_scout_dice(self, card_type: str) -> list[DiceRoll]:
"""
Roll scouting dice with weighted first d6 based on card type.
Args:
card_type: Either "batter" (1-3) or "pitcher" (4-6) for first d6
Returns:
List of 3 roll result dataclasses: weighted 1d6, normal 2d6, normal 1d20
"""
# First die (1d6) - weighted based on card type
if card_type == "batter":
first_roll = random.randint(1, 3)
else: # pitcher
first_roll = random.randint(4, 6)
first_d6_result = DiceRoll(
dice_notation='1d6',
num_dice=1,
die_sides=6,
rolls=[first_roll],
total=first_roll
)
# Second roll (2d6) - normal
second_result = parse_and_roll_single_dice("2d6")
# Third roll (1d20) - normal
third_result = parse_and_roll_single_dice("1d20")
return [first_d6_result, second_result, third_result]
def _create_multi_roll_embed(self, dice_notation: str, roll_results: list[DiceRoll], user: discord.User | discord.Member, set_author: bool = True, embed_color: int = EmbedColors.PRIMARY) -> discord.Embed:
"""Create an embed for multiple dice roll results."""
embed = EmbedTemplate.create_base_embed(
title="🎲 Dice Roll",
color=embed_color
)
if set_author:
# Set user info
embed.set_author(
name=user.name,
icon_url=user.display_avatar.url
)
# Create summary line with totals
totals = [str(result.total) for result in roll_results]
summary = f"# {','.join(totals)}"
# Create details line in the specified format: Details:[1d6;2d6;1d20 (5 - 5 6 - 13)]
dice_notations = [result.dice_notation for result in roll_results]
# Create the rolls breakdown part - group dice within each roll, separate roll groups with dashes
roll_groups = []
for result in roll_results:
rolls = result.rolls
if len(rolls) == 1:
# Single die: just the number
roll_groups.append(str(rolls[0]))
else:
# Multiple dice: space-separated within the group
roll_groups.append(' '.join(str(r) for r in rolls))
details = f"Details:[{';'.join(dice_notations)} ({' - '.join(roll_groups)})]"
# Set as description
embed.add_field(
name='Result',
value=f"```md\n{summary}\n{details}```"
)
return embed
async def setup(bot: commands.Bot):
"""Load the dice roll commands cog."""
await bot.add_cog(DiceRollCommands(bot))