CLAUDE: Implement /scout command with weighted dice rolling

Added weighted dice rolling system for scouting cards based on card type.

Features:
- New /scout command with card_type parameter (batter or pitcher)
- Weighted first d6 roll:
  - Batter: Always rolls 1-3 on first d6
  - Pitcher: Always rolls 4-6 on first d6
- Remaining dice (2d6 and 1d20) roll normally
- Uses same embed formatting as /ab command
- Comprehensive test coverage (4 new tests)

Implementation:
- Added _roll_weighted_scout_dice() helper method
- Reuses existing dice rolling and embed creation logic
- Follows established command patterns with @logged_command decorator

Tests:
- test_weighted_scout_dice_batter - Verifies batter weighting (20 iterations)
- test_weighted_scout_dice_pitcher - Verifies pitcher weighting (20 iterations)
- test_scout_command_batter - Tests batter slash command
- test_scout_command_pitcher - Tests pitcher slash command

All 34 dice command tests pass.
This commit is contained in:
Cal Corum 2025-10-14 14:20:31 -05:00
parent c5fecc878f
commit 4cab227109
2 changed files with 160 additions and 1 deletions

View File

@ -108,6 +108,37 @@ class DiceRollCommands(commands.Cog):
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)
embed.title = f'Scouting roll for {interaction.user.display_name} ({card_type.name})'
await interaction.followup.send(embed=embed)
@discord.app_commands.command(
name="fielding",
description="Roll Super Advanced fielding dice for a defensive position"
@ -563,6 +594,38 @@ class DiceRollCommands(commands.Cog):
'total': total
}
def _roll_weighted_scout_dice(self, card_type: str) -> list[dict]:
"""
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 dicts: 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 = {
'dice_notation': '1d6',
'num_dice': 1,
'die_sides': 6,
'rolls': [first_roll],
'total': first_roll
}
# Second roll (2d6) - normal
second_result = self._parse_and_roll_single_dice("2d6")
# Third roll (1d20) - normal
third_result = self._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[dict], user: discord.User | discord.Member) -> discord.Embed:
"""Create an embed for multiple dice roll results."""
embed = EmbedTemplate.create_base_embed(

View File

@ -551,4 +551,100 @@ class TestDiceRollCommands:
# Check 3d6
assert results[1]['dice_notation'] == '3d6'
assert results[1]['num_dice'] == 3
assert results[1]['die_sides'] == 6
assert results[1]['die_sides'] == 6
def test_weighted_scout_dice_batter(self, dice_cog):
"""Test that batter scout dice always rolls 1-3 for first d6."""
# Roll 20 times to ensure consistency
for _ in range(20):
results = dice_cog._roll_weighted_scout_dice("batter")
# Should have 3 dice groups (1d6, 2d6, 1d20)
assert len(results) == 3
# First d6 should ALWAYS be 1-3 for batter
first_d6 = results[0]['rolls'][0]
assert 1 <= first_d6 <= 3, f"Batter first d6 was {first_d6}, expected 1-3"
# Second roll (2d6) should be normal
assert results[1]['num_dice'] == 2
assert results[1]['die_sides'] == 6
assert all(1 <= roll <= 6 for roll in results[1]['rolls'])
# Third roll (1d20) should be normal
assert results[2]['num_dice'] == 1
assert results[2]['die_sides'] == 20
assert 1 <= results[2]['rolls'][0] <= 20
def test_weighted_scout_dice_pitcher(self, dice_cog):
"""Test that pitcher scout dice always rolls 4-6 for first d6."""
# Roll 20 times to ensure consistency
for _ in range(20):
results = dice_cog._roll_weighted_scout_dice("pitcher")
# Should have 3 dice groups (1d6, 2d6, 1d20)
assert len(results) == 3
# First d6 should ALWAYS be 4-6 for pitcher
first_d6 = results[0]['rolls'][0]
assert 4 <= first_d6 <= 6, f"Pitcher first d6 was {first_d6}, expected 4-6"
# Second roll (2d6) should be normal
assert results[1]['num_dice'] == 2
assert results[1]['die_sides'] == 6
assert all(1 <= roll <= 6 for roll in results[1]['rolls'])
# Third roll (1d20) should be normal
assert results[2]['num_dice'] == 1
assert results[2]['die_sides'] == 20
assert 1 <= results[2]['rolls'][0] <= 20
@pytest.mark.asyncio
async def test_scout_command_batter(self, dice_cog, mock_interaction):
"""Test scout slash command with batter card type."""
# Mock a card_type choice
card_type_choice = MagicMock()
card_type_choice.value = 'batter'
card_type_choice.name = 'Batter'
await dice_cog.scout_dice.callback(dice_cog, mock_interaction, card_type_choice)
# Verify response was deferred
mock_interaction.response.defer.assert_called_once()
# Verify followup was sent with embed
mock_interaction.followup.send.assert_called_once()
call_args = mock_interaction.followup.send.call_args
assert 'embed' in call_args.kwargs
# Verify embed has the correct format
embed = call_args.kwargs['embed']
assert isinstance(embed, discord.Embed)
assert embed.title == "Scouting roll for TestUser (Batter)"
assert len(embed.fields) == 1
assert "Details:[1d6;2d6;1d20" in embed.fields[0].value
@pytest.mark.asyncio
async def test_scout_command_pitcher(self, dice_cog, mock_interaction):
"""Test scout slash command with pitcher card type."""
# Mock a card_type choice
card_type_choice = MagicMock()
card_type_choice.value = 'pitcher'
card_type_choice.name = 'Pitcher'
await dice_cog.scout_dice.callback(dice_cog, mock_interaction, card_type_choice)
# Verify response was deferred
mock_interaction.response.defer.assert_called_once()
# Verify followup was sent with embed
mock_interaction.followup.send.assert_called_once()
call_args = mock_interaction.followup.send.call_args
assert 'embed' in call_args.kwargs
# Verify embed has the correct format
embed = call_args.kwargs['embed']
assert isinstance(embed, discord.Embed)
assert embed.title == "Scouting roll for TestUser (Pitcher)"
assert len(embed.fields) == 1
assert "Details:[1d6;2d6;1d20" in embed.fields[0].value