major-domo-v2/commands/dev/loaded_dice.py
Cal Corum 6b35a14066 Add dev-only loaded dice command for testing /ab rolls
- New !loaded <d6> <2d6> <d20> [user_id] command for predetermined dice
- Loaded values consumed on next /ab roll (one-shot)
- Supports targeting other users by ID for testing
- Admin-restricted prefix commands (!loaded, !unload, !checkload)
- Self-contained in commands/dev/ package

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 22:45:01 -06:00

282 lines
8.9 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Loaded Dice - Developer Testing System
HIDDEN DEV COMMAND for testing dice-dependent gameplay mechanics.
Use `!loaded <d6> <2d6_total> <d20>` to set up a loaded roll for your next /ab.
Example:
!loaded 3 7 15 → Next /ab will roll 3 for d6, 7 for 2d6, 15 for d20
The loaded values are consumed after one use (one-shot).
Only users with Administrator permission can use this command.
"""
import logging
from dataclasses import dataclass
from typing import Optional
import discord
from discord.ext import commands
from utils.logging import get_contextual_logger
from views.embeds import EmbedColors, EmbedTemplate
logger = logging.getLogger(__name__)
@dataclass
class LoadedRoll:
"""Represents a pending loaded roll for a user."""
d6_1: int # First d6 (1-6)
d6_2_total: int # 2d6 total (2-12)
d20: int # d20 value (1-20)
user_id: int # User who set this up
# In-memory storage for pending loaded rolls
# Maps user_id -> LoadedRoll
_pending_loaded_rolls: dict[int, LoadedRoll] = {}
def set_loaded_roll(user_id: int, d6_1: int, d6_2_total: int, d20: int) -> None:
"""
Set up a loaded roll for a user's next /ab command.
Args:
user_id: Discord user ID
d6_1: Value for the first d6 (1-6)
d6_2_total: Total for the 2d6 roll (2-12)
d20: Value for the d20 roll (1-20)
"""
_pending_loaded_rolls[user_id] = LoadedRoll(
d6_1=d6_1,
d6_2_total=d6_2_total,
d20=d20,
user_id=user_id
)
logger.info(f"Loaded roll set for user {user_id}: d6={d6_1}, 2d6={d6_2_total}, d20={d20}")
def get_and_consume_loaded_roll(user_id: int) -> Optional[LoadedRoll]:
"""
Get and consume a pending loaded roll for a user.
Returns the loaded roll if one exists, then removes it from storage.
Returns None if no loaded roll is pending.
Args:
user_id: Discord user ID
Returns:
LoadedRoll if pending, None otherwise
"""
loaded = _pending_loaded_rolls.pop(user_id, None)
if loaded:
logger.info(f"Consumed loaded roll for user {user_id}: d6={loaded.d6_1}, 2d6={loaded.d6_2_total}, d20={loaded.d20}")
return loaded
def has_pending_loaded_roll(user_id: int) -> bool:
"""Check if a user has a pending loaded roll."""
return user_id in _pending_loaded_rolls
def clear_loaded_roll(user_id: int) -> bool:
"""
Clear a pending loaded roll without consuming it.
Returns True if a roll was cleared, False if none existed.
"""
if user_id in _pending_loaded_rolls:
del _pending_loaded_rolls[user_id]
logger.info(f"Cleared loaded roll for user {user_id}")
return True
return False
class LoadedDiceCommands(commands.Cog):
"""
Developer commands for loaded dice testing.
These are prefix commands (!) to avoid cluttering the slash command list.
Only administrators can use these commands.
"""
def __init__(self, bot: commands.Bot):
self.bot = bot
self.logger = get_contextual_logger(f'{__name__}.LoadedDiceCommands')
async def cog_check(self, ctx: commands.Context) -> bool:
"""Only allow administrators to use dev commands."""
if not ctx.guild:
return False
if not isinstance(ctx.author, discord.Member):
return False
if ctx.author.id != 258104532423147520:
# Silently fail - don't reveal command exists to non-admins
return False
return True
@commands.command(name="loaded", aliases=["load", "ld"])
async def set_loaded_dice(
self,
ctx: commands.Context,
d6_1: int,
d6_2_total: int,
d20: int,
target_user_id: int | None = None
):
"""
Set up a loaded roll for the next /ab command.
Usage:
!loaded <d6> <2d6_total> <d20> [user_id]
Examples:
!loaded 3 7 15 → Load for yourself
!loaded 5 12 1 123456789 → Load for user ID 123456789
The loaded values are consumed after one /ab use.
"""
# Determine target user (default to command author)
target_id = target_user_id or ctx.author.id
self.logger.info(
f"Loaded dice command from {ctx.author}: d6={d6_1}, 2d6={d6_2_total}, d20={d20}, target_id={target_id}"
)
# Validate d6_1 (1-6)
if d6_1 < 1 or d6_1 > 6:
await ctx.send("❌ First d6 must be 1-6", delete_after=10)
return
# Validate 2d6 total (2-12)
if d6_2_total < 2 or d6_2_total > 12:
await ctx.send("❌ 2d6 total must be 2-12", delete_after=10)
return
# Validate d20 (1-20)
if d20 < 1 or d20 > 20:
await ctx.send("❌ d20 must be 1-20", delete_after=10)
return
# Store the loaded roll for target user
set_loaded_roll(target_id, d6_1, d6_2_total, d20)
# Create confirmation embed
card_type = "Batter" if d6_1 <= 3 else "Pitcher"
# Build description based on target
if target_id == ctx.author.id:
description = "Your next `/ab` roll will use these values:"
else:
description = f"Next `/ab` roll for <@{target_id}> will use these values:"
embed = EmbedTemplate.create_base_embed(
title="🎲 Loaded Dice Set",
description=description,
color=EmbedColors.WARNING
)
embed.add_field(
name="Loaded Values",
value=f"```\nd6: {d6_1} ({card_type} card)\n2d6: {d6_2_total}\nd20: {d20}\n```",
inline=False
)
# Special indicators
specials = []
if d20 == 1:
specials.append("⚠️ Wild Pitch check")
elif d20 == 2:
specials.append("⚠️ Passed Ball check")
if d6_1 == 6 and d6_2_total in [7, 8, 9, 10, 11, 12]:
injury_rating = 13 - d6_2_total
specials.append(f"🩹 Injury check (rating {injury_rating})")
if specials:
embed.add_field(
name="Special Plays",
value='\n'.join(specials),
inline=False
)
# Footer with target info
if target_id == ctx.author.id:
footer = "🔧 Dev Mode • Values consumed on next /ab • Use !unload to cancel"
else:
footer = f"🔧 Dev Mode • Target: {target_id} • Use !unload {target_id} to cancel"
embed.set_footer(text=footer)
await ctx.send(embed=embed, delete_after=30)
# Delete the command message to keep it hidden
try:
await ctx.message.delete()
except discord.Forbidden:
pass # Can't delete in DMs or without permissions
@commands.command(name="unload", aliases=["unld", "clearload"])
async def clear_loaded_dice(self, ctx: commands.Context, target_user_id: int | None = None):
"""Clear any pending loaded dice without using them."""
target_id = target_user_id or ctx.author.id
self.logger.info(f"Unload command from {ctx.author}, target_id={target_id}")
if clear_loaded_roll(target_id):
if target_id == ctx.author.id:
await ctx.send("✅ Loaded dice cleared", delete_after=10)
else:
await ctx.send(f"✅ Loaded dice cleared for <@{target_id}>", delete_after=10)
else:
await ctx.send(" No loaded dice pending", delete_after=10)
# Delete the command message
try:
await ctx.message.delete()
except discord.Forbidden:
pass
@commands.command(name="checkload", aliases=["chkld"])
async def check_loaded_dice(self, ctx: commands.Context, target_user_id: int | None = None):
"""Check if loaded dice are pending for a user."""
target_id = target_user_id or ctx.author.id
self.logger.info(f"Check load command from {ctx.author}, target_id={target_id}")
if target_id in _pending_loaded_rolls:
loaded = _pending_loaded_rolls[target_id]
card_type = "Batter" if loaded.d6_1 <= 3 else "Pitcher"
target_text = "" if target_id == ctx.author.id else f" for <@{target_id}>"
await ctx.send(
f"🎲 Loaded{target_text}: d6={loaded.d6_1} ({card_type}), 2d6={loaded.d6_2_total}, d20={loaded.d20}",
delete_after=15
)
else:
await ctx.send(" No loaded dice pending", delete_after=10)
# Delete the command message
try:
await ctx.message.delete()
except discord.Forbidden:
pass
async def setup(bot: commands.Bot):
"""Load the loaded dice commands cog."""
await bot.add_cog(LoadedDiceCommands(bot))
# Export for use by dice commands
__all__ = [
'LoadedDiceCommands',
'LoadedRoll',
'set_loaded_roll',
'get_and_consume_loaded_roll',
'has_pending_loaded_roll',
'clear_loaded_roll',
]