- Created utils/dice_utils.py with reusable dice rolling functions - DiceRoll dataclass for roll results - parse_and_roll_multiple_dice() for multiple dice notation - parse_and_roll_single_dice() for single dice notation - Graceful error handling with empty list returns - Refactored commands/dice/rolls.py to use new utility module - Removed duplicate DiceRoll class and parsing methods - Updated all method calls to use standalone functions - Added new /d20 command for quick d20 rolls - Fixed fielding prefix command to include d100 roll - Updated tests/test_commands_dice.py - Updated imports to use utils.dice_utils - Fixed all test calls to use standalone functions - Added comprehensive test for /d20 command - All 35 tests passing - Updated utils/CLAUDE.md documentation - Added Dice Utilities section with full API reference - Documented functions, usage patterns, and design benefits - Listed all commands using dice utilities Benefits: - Reusability: Dice functions can be imported by any command file - Maintainability: Centralized dice logic in one place - Testability: Functions testable independent of command cogs - Consistency: All dice commands use same underlying logic 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
672 lines
25 KiB
Python
672 lines
25 KiB
Python
"""
|
|
Tests for dice rolling commands
|
|
|
|
Validates dice rolling functionality, parsing, and embed creation.
|
|
"""
|
|
import pytest
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
import discord
|
|
from discord.ext import commands
|
|
|
|
from commands.dice.rolls import DiceRollCommands
|
|
from utils.dice_utils import DiceRoll, parse_and_roll_multiple_dice, parse_and_roll_single_dice
|
|
|
|
|
|
class TestDiceRollCommands:
|
|
"""Test dice rolling command functionality."""
|
|
|
|
@pytest.fixture
|
|
def bot(self):
|
|
"""Create a mock bot instance."""
|
|
bot = AsyncMock(spec=commands.Bot)
|
|
return bot
|
|
|
|
@pytest.fixture
|
|
def dice_cog(self, bot):
|
|
"""Create DiceRollCommands cog instance."""
|
|
return DiceRollCommands(bot)
|
|
|
|
@pytest.fixture
|
|
def mock_interaction(self):
|
|
"""Create a mock Discord interaction."""
|
|
interaction = AsyncMock(spec=discord.Interaction)
|
|
|
|
# Mock the user
|
|
user = MagicMock(spec=discord.User)
|
|
user.name = "TestUser"
|
|
user.display_name = "TestUser"
|
|
user.display_avatar.url = "https://example.com/avatar.png"
|
|
interaction.user = user
|
|
|
|
# Mock response methods
|
|
interaction.response.defer = AsyncMock()
|
|
interaction.followup.send = AsyncMock()
|
|
|
|
return interaction
|
|
|
|
@pytest.fixture
|
|
def mock_context(self):
|
|
"""Create a mock Discord context for prefix commands."""
|
|
ctx = AsyncMock(spec=commands.Context)
|
|
|
|
# Mock the author (user)
|
|
author = MagicMock(spec=discord.User)
|
|
author.display_name = "TestUser"
|
|
author.display_avatar.url = "https://example.com/avatar.png"
|
|
author.id = 12345 # Add user ID
|
|
ctx.author = author
|
|
|
|
# Mock send method
|
|
ctx.send = AsyncMock()
|
|
|
|
return ctx
|
|
|
|
def test_parse_valid_dice_notation(self, dice_cog):
|
|
"""Test parsing valid dice notation."""
|
|
# Test basic notation
|
|
results = parse_and_roll_multiple_dice("2d6")
|
|
assert len(results) == 1
|
|
result = results[0]
|
|
assert result.num_dice == 2
|
|
assert result.die_sides == 6
|
|
assert len(result.rolls) == 2
|
|
assert all(1 <= roll <= 6 for roll in result.rolls)
|
|
assert result.total == sum(result.rolls)
|
|
|
|
# Test single die
|
|
results = parse_and_roll_multiple_dice("1d20")
|
|
assert len(results) == 1
|
|
result = results[0]
|
|
assert result.num_dice == 1
|
|
assert result.die_sides == 20
|
|
assert len(result.rolls) == 1
|
|
assert 1 <= result.rolls[0] <= 20
|
|
|
|
def test_parse_invalid_dice_notation(self, dice_cog):
|
|
"""Test parsing invalid dice notation."""
|
|
# Invalid formats
|
|
assert parse_and_roll_multiple_dice("invalid") == []
|
|
assert parse_and_roll_multiple_dice("2d") == []
|
|
assert parse_and_roll_multiple_dice("d6") == []
|
|
assert parse_and_roll_multiple_dice("2d6+5") == [] # No modifiers in simple version
|
|
assert parse_and_roll_multiple_dice("") == []
|
|
|
|
# Out of bounds values
|
|
assert parse_and_roll_multiple_dice("0d6") == [] # num_dice < 1
|
|
assert parse_and_roll_multiple_dice("2d1") == [] # die_sides < 2
|
|
assert parse_and_roll_multiple_dice("101d6") == [] # num_dice > 100
|
|
assert parse_and_roll_multiple_dice("1d1001") == [] # die_sides > 1000
|
|
|
|
def test_parse_multiple_dice(self, dice_cog):
|
|
"""Test parsing multiple dice rolls."""
|
|
# Test multiple rolls
|
|
results = parse_and_roll_multiple_dice("1d6;2d8;1d20")
|
|
assert len(results) == 3
|
|
|
|
assert results[0].dice_notation == '1d6'
|
|
assert results[0].num_dice == 1
|
|
assert results[0].die_sides == 6
|
|
|
|
assert results[1].dice_notation == '2d8'
|
|
assert results[1].num_dice == 2
|
|
assert results[1].die_sides == 8
|
|
|
|
assert results[2].dice_notation == '1d20'
|
|
assert results[2].num_dice == 1
|
|
assert results[2].die_sides == 20
|
|
|
|
def test_parse_case_insensitive(self, dice_cog):
|
|
"""Test that dice notation parsing is case insensitive."""
|
|
result_lower = parse_and_roll_multiple_dice("2d6")
|
|
result_upper = parse_and_roll_multiple_dice("2D6")
|
|
|
|
assert len(result_lower) == 1
|
|
assert len(result_upper) == 1
|
|
assert result_lower[0].num_dice == result_upper[0].num_dice
|
|
assert result_lower[0].die_sides == result_upper[0].die_sides
|
|
|
|
def test_parse_whitespace_handling(self, dice_cog):
|
|
"""Test that whitespace is handled properly."""
|
|
results = parse_and_roll_multiple_dice(" 2d6 ")
|
|
assert len(results) == 1
|
|
assert results[0].num_dice == 2
|
|
assert results[0].die_sides == 6
|
|
|
|
results = parse_and_roll_multiple_dice("2 d 6")
|
|
assert len(results) == 1
|
|
assert results[0].num_dice == 2
|
|
assert results[0].die_sides == 6
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_roll_dice_valid_input(self, dice_cog, mock_interaction):
|
|
"""Test roll_dice command with valid input."""
|
|
await dice_cog.roll_dice.callback(dice_cog, mock_interaction, "2d6")
|
|
|
|
# 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 is a Discord embed
|
|
embed = call_args.kwargs['embed']
|
|
assert isinstance(embed, discord.Embed)
|
|
assert embed.title == "🎲 Dice Roll"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_roll_dice_invalid_input(self, dice_cog, mock_interaction):
|
|
"""Test roll_dice command with invalid input."""
|
|
await dice_cog.roll_dice.callback(dice_cog, mock_interaction, "invalid")
|
|
|
|
# Verify response was deferred
|
|
mock_interaction.response.defer.assert_called_once()
|
|
|
|
# Verify error message was sent
|
|
mock_interaction.followup.send.assert_called_once()
|
|
call_args = mock_interaction.followup.send.call_args
|
|
assert "Invalid dice notation" in call_args.args[0]
|
|
assert call_args.kwargs['ephemeral'] is True
|
|
|
|
def test_create_multi_roll_embed_single_die(self, dice_cog, mock_interaction):
|
|
"""Test embed creation for single die roll."""
|
|
roll_results = [
|
|
DiceRoll(
|
|
dice_notation='1d20',
|
|
num_dice=1,
|
|
die_sides=20,
|
|
rolls=[15],
|
|
total=15
|
|
)
|
|
]
|
|
|
|
embed = dice_cog._create_multi_roll_embed("1d20", roll_results, mock_interaction.user)
|
|
|
|
assert embed.title == "🎲 Dice Roll"
|
|
assert embed.author.name == "TestUser"
|
|
assert embed.author.icon_url == "https://example.com/avatar.png"
|
|
|
|
# Check the formatted field content
|
|
assert len(embed.fields) == 1
|
|
assert embed.fields[0].name == 'Result'
|
|
expected_value = "```md\n# 15\nDetails:[1d20 (15)]```"
|
|
assert embed.fields[0].value == expected_value
|
|
|
|
def test_create_multi_roll_embed_multiple_dice(self, dice_cog, mock_interaction):
|
|
"""Test embed creation for multiple dice rolls."""
|
|
roll_results = [
|
|
DiceRoll(
|
|
dice_notation='1d6',
|
|
num_dice=1,
|
|
die_sides=6,
|
|
rolls=[5],
|
|
total=5
|
|
),
|
|
DiceRoll(
|
|
dice_notation='2d6',
|
|
num_dice=2,
|
|
die_sides=6,
|
|
rolls=[5, 6],
|
|
total=11
|
|
),
|
|
DiceRoll(
|
|
dice_notation='1d20',
|
|
num_dice=1,
|
|
die_sides=20,
|
|
rolls=[13],
|
|
total=13
|
|
)
|
|
]
|
|
|
|
embed = dice_cog._create_multi_roll_embed("1d6;2d6;1d20", roll_results, mock_interaction.user)
|
|
|
|
assert embed.title == "🎲 Dice Roll"
|
|
assert embed.author.name == "TestUser"
|
|
|
|
# Check the formatted field content matches the expected format
|
|
assert len(embed.fields) == 1
|
|
assert embed.fields[0].name == 'Result'
|
|
expected_value = "```md\n# 5,11,13\nDetails:[1d6;2d6;1d20 (5 - 5 6 - 13)]```"
|
|
assert embed.fields[0].value == expected_value
|
|
|
|
def test_dice_roll_randomness(self, dice_cog):
|
|
"""Test that dice rolls produce different results."""
|
|
results = []
|
|
for _ in range(20): # Roll 20 times
|
|
result = parse_and_roll_multiple_dice("1d20")
|
|
results.append(result[0].rolls[0])
|
|
|
|
# Should have some variation in results (very unlikely all 20 rolls are the same)
|
|
unique_results = set(results)
|
|
assert len(unique_results) > 1, f"All rolls were the same: {results}"
|
|
|
|
def test_dice_boundaries(self, dice_cog):
|
|
"""Test dice rolling at boundaries."""
|
|
# Test maximum allowed dice
|
|
results = parse_and_roll_multiple_dice("100d2")
|
|
assert len(results) == 1
|
|
result = results[0]
|
|
assert len(result.rolls) == 100
|
|
assert all(roll in [1, 2] for roll in result.rolls)
|
|
|
|
# Test maximum die size
|
|
results = parse_and_roll_multiple_dice("1d1000")
|
|
assert len(results) == 1
|
|
result = results[0]
|
|
assert 1 <= result.rolls[0] <= 1000
|
|
|
|
# Test minimum valid values
|
|
results = parse_and_roll_multiple_dice("1d2")
|
|
assert len(results) == 1
|
|
result = results[0]
|
|
assert result.rolls[0] in [1, 2]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_prefix_command_valid_input(self, dice_cog, mock_context):
|
|
"""Test prefix command with valid input."""
|
|
await dice_cog.roll_dice_prefix.callback(dice_cog, mock_context, dice="2d6")
|
|
|
|
# Verify send was called with embed
|
|
mock_context.send.assert_called_once()
|
|
call_args = mock_context.send.call_args
|
|
# Check if embed was passed as positional or keyword argument
|
|
if call_args.args:
|
|
embed = call_args.args[0]
|
|
else:
|
|
embed = call_args.kwargs.get('embed')
|
|
assert isinstance(embed, discord.Embed)
|
|
assert embed.title == "🎲 Dice Roll"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_prefix_command_invalid_input(self, dice_cog, mock_context):
|
|
"""Test prefix command with invalid input."""
|
|
await dice_cog.roll_dice_prefix.callback(dice_cog, mock_context, dice="invalid")
|
|
|
|
# Verify error message was sent
|
|
mock_context.send.assert_called_once()
|
|
call_args = mock_context.send.call_args
|
|
error_msg = call_args[0][0]
|
|
assert "Invalid dice notation" in error_msg
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_prefix_command_no_input(self, dice_cog, mock_context):
|
|
"""Test prefix command with no input."""
|
|
await dice_cog.roll_dice_prefix.callback(dice_cog, mock_context, dice=None)
|
|
|
|
# Verify usage message was sent
|
|
mock_context.send.assert_called_once()
|
|
call_args = mock_context.send.call_args
|
|
usage_msg = call_args[0][0]
|
|
assert "Please provide dice notation" in usage_msg
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_prefix_command_multiple_dice(self, dice_cog, mock_context):
|
|
"""Test prefix command with multiple dice rolls."""
|
|
await dice_cog.roll_dice_prefix.callback(dice_cog, mock_context, dice="1d6;2d8;1d20")
|
|
|
|
# Verify send was called with embed
|
|
mock_context.send.assert_called_once()
|
|
call_args = mock_context.send.call_args
|
|
# Check if embed was passed as positional or keyword argument
|
|
if call_args.args:
|
|
embed = call_args.args[0]
|
|
else:
|
|
embed = call_args.kwargs.get('embed')
|
|
|
|
assert isinstance(embed, discord.Embed)
|
|
assert embed.title == "🎲 Dice Roll"
|
|
# Should have summary format with 3 totals in field
|
|
assert len(embed.fields) == 1
|
|
assert embed.fields[0].name == 'Result'
|
|
assert embed.fields[0].value.startswith("```md\n#")
|
|
assert "Details:[1d6;2d8;1d20" in embed.fields[0].value
|
|
|
|
def test_prefix_command_attributes(self, dice_cog):
|
|
"""Test that prefix command has correct attributes."""
|
|
# Check command exists and has correct name
|
|
assert hasattr(dice_cog, 'roll_dice_prefix')
|
|
command = dice_cog.roll_dice_prefix
|
|
assert command.name == "roll"
|
|
assert command.aliases == ["r", "dice"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_d20_command_slash(self, dice_cog, mock_interaction):
|
|
"""Test d20 slash command."""
|
|
await dice_cog.d20_dice.callback(dice_cog, mock_interaction)
|
|
|
|
# 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 == "d20 roll for TestUser"
|
|
assert len(embed.fields) == 1
|
|
assert "Details:[1d20" in embed.fields[0].value
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ab_command_slash(self, dice_cog, mock_interaction):
|
|
"""Test ab slash command."""
|
|
await dice_cog.ab_dice.callback(dice_cog, mock_interaction)
|
|
|
|
# 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 == "At bat roll for TestUser"
|
|
assert len(embed.fields) == 1
|
|
assert "Details:[1d6;2d6;1d20" in embed.fields[0].value
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ab_command_prefix(self, dice_cog, mock_context):
|
|
"""Test ab prefix command."""
|
|
await dice_cog.ab_dice_prefix.callback(dice_cog, mock_context)
|
|
|
|
# Verify send was called with embed
|
|
mock_context.send.assert_called_once()
|
|
call_args = mock_context.send.call_args
|
|
|
|
# Check if embed was passed as positional or keyword argument
|
|
if call_args.args:
|
|
embed = call_args.args[0]
|
|
else:
|
|
embed = call_args.kwargs.get('embed')
|
|
|
|
assert isinstance(embed, discord.Embed)
|
|
assert embed.title == "At bat roll for TestUser"
|
|
assert len(embed.fields) == 1
|
|
assert "Details:[1d6;2d6;1d20" in embed.fields[0].value
|
|
|
|
def test_ab_command_attributes(self, dice_cog):
|
|
"""Test that ab prefix command has correct attributes."""
|
|
# Check command exists and has correct name
|
|
assert hasattr(dice_cog, 'ab_dice_prefix')
|
|
command = dice_cog.ab_dice_prefix
|
|
assert command.name == "ab"
|
|
assert command.aliases == ["atbat"]
|
|
|
|
def test_ab_command_dice_combination(self, dice_cog):
|
|
"""Test that ab command uses the correct dice combination."""
|
|
dice_notation = "1d6;2d6;1d20"
|
|
results = parse_and_roll_multiple_dice(dice_notation)
|
|
|
|
# Should have 3 dice groups
|
|
assert len(results) == 3
|
|
|
|
# Check each dice type
|
|
assert results[0].dice_notation == '1d6'
|
|
assert results[0].num_dice == 1
|
|
assert results[0].die_sides == 6
|
|
|
|
assert results[1].dice_notation == '2d6'
|
|
assert results[1].num_dice == 2
|
|
assert results[1].die_sides == 6
|
|
|
|
assert results[2].dice_notation == '1d20'
|
|
assert results[2].num_dice == 1
|
|
assert results[2].die_sides == 20
|
|
|
|
# Fielding command tests
|
|
@pytest.mark.asyncio
|
|
async def test_fielding_command_slash(self, dice_cog, mock_interaction):
|
|
"""Test fielding slash command with valid position."""
|
|
# Mock a position choice
|
|
position_choice = MagicMock()
|
|
position_choice.value = '3B'
|
|
|
|
await dice_cog.fielding_roll.callback(dice_cog, mock_interaction, position_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 == "SA Fielding roll for TestUser"
|
|
assert len(embed.fields) >= 2 # Range and Error fields
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fielding_command_prefix_valid(self, dice_cog, mock_context):
|
|
"""Test fielding prefix command with valid position."""
|
|
await dice_cog.fielding_roll_prefix.callback(dice_cog, mock_context, "SS")
|
|
|
|
# Verify send was called with embed
|
|
mock_context.send.assert_called_once()
|
|
call_args = mock_context.send.call_args
|
|
|
|
# Check if embed was passed as positional or keyword argument
|
|
if call_args.args:
|
|
embed = call_args.args[0]
|
|
else:
|
|
embed = call_args.kwargs.get('embed')
|
|
|
|
assert isinstance(embed, discord.Embed)
|
|
assert embed.title == "SA Fielding roll for TestUser"
|
|
assert len(embed.fields) >= 2 # Range and Error fields
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fielding_command_prefix_no_position(self, dice_cog, mock_context):
|
|
"""Test fielding prefix command with no position."""
|
|
await dice_cog.fielding_roll_prefix.callback(dice_cog, mock_context, None)
|
|
|
|
# Verify error message was sent
|
|
mock_context.send.assert_called_once()
|
|
call_args = mock_context.send.call_args
|
|
error_msg = call_args[0][0]
|
|
assert "Please specify a position" in error_msg
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fielding_command_prefix_invalid_position(self, dice_cog, mock_context):
|
|
"""Test fielding prefix command with invalid position."""
|
|
await dice_cog.fielding_roll_prefix.callback(dice_cog, mock_context, "INVALID")
|
|
|
|
# Verify error message was sent
|
|
mock_context.send.assert_called_once()
|
|
call_args = mock_context.send.call_args
|
|
error_msg = call_args[0][0]
|
|
assert "Invalid position" in error_msg
|
|
|
|
def test_fielding_command_attributes(self, dice_cog):
|
|
"""Test that fielding prefix command has correct attributes."""
|
|
# Check command exists and has correct name
|
|
assert hasattr(dice_cog, 'fielding_roll_prefix')
|
|
command = dice_cog.fielding_roll_prefix
|
|
assert command.name == "f"
|
|
assert command.aliases == ["fielding", "saf"]
|
|
|
|
def test_fielding_range_charts(self, dice_cog):
|
|
"""Test that fielding range charts work for all positions."""
|
|
# Test infield range (applies to 1B, 2B, 3B, SS)
|
|
infield_result = dice_cog._get_infield_range(10)
|
|
assert isinstance(infield_result, str)
|
|
assert len(infield_result) > 0
|
|
|
|
# Test outfield range (applies to LF, CF, RF)
|
|
outfield_result = dice_cog._get_outfield_range(10)
|
|
assert isinstance(outfield_result, str)
|
|
assert len(outfield_result) > 0
|
|
|
|
# Test catcher range
|
|
catcher_result = dice_cog._get_catcher_range(10)
|
|
assert isinstance(catcher_result, str)
|
|
assert len(catcher_result) > 0
|
|
|
|
def test_fielding_error_charts(self, dice_cog):
|
|
"""Test that error charts work for all positions."""
|
|
# Test all position error methods
|
|
test_total = 10
|
|
|
|
# Test 1B error
|
|
error_1b = dice_cog._get_1b_error(test_total)
|
|
assert isinstance(error_1b, str)
|
|
|
|
# Test 2B error
|
|
error_2b = dice_cog._get_2b_error(test_total)
|
|
assert isinstance(error_2b, str)
|
|
|
|
# Test 3B error
|
|
error_3b = dice_cog._get_3b_error(test_total)
|
|
assert isinstance(error_3b, str)
|
|
|
|
# Test SS error
|
|
error_ss = dice_cog._get_ss_error(test_total)
|
|
assert isinstance(error_ss, str)
|
|
|
|
# Test corner OF error
|
|
error_corner = dice_cog._get_corner_of_error(test_total)
|
|
assert isinstance(error_corner, str)
|
|
|
|
# Test CF error
|
|
error_cf = dice_cog._get_cf_error(test_total)
|
|
assert isinstance(error_cf, str)
|
|
|
|
# Test catcher error
|
|
error_catcher = dice_cog._get_catcher_error(test_total)
|
|
assert isinstance(error_catcher, str)
|
|
|
|
def test_get_error_result_all_positions(self, dice_cog):
|
|
"""Test _get_error_result for all valid positions."""
|
|
test_total = 12
|
|
positions = ['1B', '2B', '3B', 'SS', 'LF', 'RF', 'CF', 'C']
|
|
|
|
for position in positions:
|
|
result = dice_cog._get_error_result(position, test_total)
|
|
assert isinstance(result, str)
|
|
assert len(result) > 0
|
|
|
|
def test_get_error_result_invalid_position(self, dice_cog):
|
|
"""Test _get_error_result with invalid position raises error."""
|
|
with pytest.raises(ValueError, match="Unknown position"):
|
|
dice_cog._get_error_result("INVALID", 10)
|
|
|
|
def test_fielding_dice_combination(self, dice_cog):
|
|
"""Test that fielding uses correct dice combination (1d20;3d6)."""
|
|
dice_notation = "1d20;3d6"
|
|
results = parse_and_roll_multiple_dice(dice_notation)
|
|
|
|
# Should have 2 dice groups
|
|
assert len(results) == 2
|
|
|
|
# Check 1d20
|
|
assert results[0].dice_notation == '1d20'
|
|
assert results[0].num_dice == 1
|
|
assert results[0].die_sides == 20
|
|
|
|
# Check 3d6
|
|
assert results[1].dice_notation == '3d6'
|
|
assert results[1].num_dice == 3
|
|
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"
|
|
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"
|
|
assert len(embed.fields) == 1
|
|
assert "Details:[1d6;2d6;1d20" in embed.fields[0].value |