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.spoiler import setup_spoiler
|
||||||
from commands.injuries import setup_injuries
|
from commands.injuries import setup_injuries
|
||||||
from commands.gameplay import setup_gameplay
|
from commands.gameplay import setup_gameplay
|
||||||
|
from commands.dev import setup_dev
|
||||||
|
|
||||||
# Define command packages to load
|
# Define command packages to load
|
||||||
command_packages = [
|
command_packages = [
|
||||||
@ -143,6 +144,7 @@ class SBABot(commands.Bot):
|
|||||||
("spoiler", setup_spoiler),
|
("spoiler", setup_spoiler),
|
||||||
("injuries", setup_injuries),
|
("injuries", setup_injuries),
|
||||||
("gameplay", setup_gameplay),
|
("gameplay", setup_gameplay),
|
||||||
|
("dev", setup_dev), # Dev-only commands (admin restricted)
|
||||||
]
|
]
|
||||||
|
|
||||||
total_successful = 0
|
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.text_utils import split_text_for_fields
|
||||||
from utils.dice_utils import DiceRoll, parse_and_roll_multiple_dice, parse_and_roll_single_dice
|
from utils.dice_utils import DiceRoll, parse_and_roll_multiple_dice, parse_and_roll_single_dice
|
||||||
from views.embeds import EmbedColors, EmbedTemplate
|
from views.embeds import EmbedColors, EmbedTemplate
|
||||||
|
from commands.dev.loaded_dice import get_and_consume_loaded_roll
|
||||||
from .chart_data import (
|
from .chart_data import (
|
||||||
INFIELD_X_CHART,
|
INFIELD_X_CHART,
|
||||||
OUTFIELD_X_CHART,
|
OUTFIELD_X_CHART,
|
||||||
@ -130,9 +131,26 @@ class DiceRollCommands(commands.Cog):
|
|||||||
await interaction.response.defer()
|
await interaction.response.defer()
|
||||||
embed_color = await self._get_channel_embed_color(interaction)
|
embed_color = await self._get_channel_embed_color(interaction)
|
||||||
|
|
||||||
# Use the standard baseball dice combination
|
# Check for loaded dice (dev testing)
|
||||||
dice_notation = "1d6;2d6;1d20"
|
loaded = get_and_consume_loaded_roll(interaction.user.id)
|
||||||
roll_results = parse_and_roll_multiple_dice(dice_notation)
|
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])
|
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
|
d6_total = roll_results[1].total
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user