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>
This commit is contained in:
parent
889b3a4e2d
commit
6b35a14066
2
bot.py
2
bot.py
@ -124,6 +124,7 @@ class SBABot(commands.Bot):
|
||||
from commands.spoiler import setup_spoiler
|
||||
from commands.injuries import setup_injuries
|
||||
from commands.gameplay import setup_gameplay
|
||||
from commands.dev import setup_dev
|
||||
|
||||
# Define command packages to load
|
||||
command_packages = [
|
||||
@ -143,6 +144,7 @@ class SBABot(commands.Bot):
|
||||
("spoiler", setup_spoiler),
|
||||
("injuries", setup_injuries),
|
||||
("gameplay", setup_gameplay),
|
||||
("dev", setup_dev), # Dev-only commands (admin restricted)
|
||||
]
|
||||
|
||||
total_successful = 0
|
||||
|
||||
47
commands/dev/__init__.py
Normal file
47
commands/dev/__init__.py
Normal file
@ -0,0 +1,47 @@
|
||||
"""
|
||||
Dev Commands Package
|
||||
|
||||
Developer-only commands for testing. Hidden from regular users.
|
||||
"""
|
||||
import logging
|
||||
from discord.ext import commands
|
||||
|
||||
from .loaded_dice import LoadedDiceCommands
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def setup_dev(bot: commands.Bot) -> tuple[int, int, list[str]]:
|
||||
"""
|
||||
Setup all dev command modules.
|
||||
|
||||
Returns:
|
||||
tuple: (successful_count, failed_count, failed_modules)
|
||||
"""
|
||||
dev_cogs = [
|
||||
("LoadedDiceCommands", LoadedDiceCommands),
|
||||
]
|
||||
|
||||
successful = 0
|
||||
failed = 0
|
||||
failed_modules = []
|
||||
|
||||
for cog_name, cog_class in dev_cogs:
|
||||
try:
|
||||
await bot.add_cog(cog_class(bot))
|
||||
logger.info(f"✅ Loaded {cog_name}")
|
||||
successful += 1
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Failed to load {cog_name}: {e}", exc_info=True)
|
||||
failed += 1
|
||||
failed_modules.append(cog_name)
|
||||
|
||||
if failed == 0:
|
||||
logger.info(f"🎉 All {successful} dev command modules loaded successfully")
|
||||
else:
|
||||
logger.warning(f"⚠️ Dev commands loaded with issues: {successful} successful, {failed} failed")
|
||||
|
||||
return successful, failed, failed_modules
|
||||
|
||||
|
||||
__all__ = ['setup_dev', 'LoadedDiceCommands']
|
||||
281
commands/dev/loaded_dice.py
Normal file
281
commands/dev/loaded_dice.py
Normal file
@ -0,0 +1,281 @@
|
||||
"""
|
||||
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',
|
||||
]
|
||||
@ -18,6 +18,7 @@ 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,
|
||||
@ -130,6 +131,23 @@ class DiceRollCommands(commands.Cog):
|
||||
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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user