paper-dynasty-discord/tests/in_game/test_simulations.py
Cal Corum 3debfd6e82 Catchup commit
Includes discord_ui refactor, testing overhaul, addition of
2025-07-22 09:22:19 -05:00

425 lines
15 KiB
Python

import pytest
import random
from unittest.mock import Mock, patch, call
import discord
from in_game import simulations, data_cache
from tests.factory import session_fixture
@pytest.fixture
def mock_batting_wrapper():
"""Create a mock BattingWrapper with test data."""
batting_card = data_cache.BattingCard(
player_id=123,
variant=0,
steal_low=9,
steal_high=12,
steal_auto=True,
steal_jump=0.25,
bunting='B',
hit_and_run='A',
running=13,
offense_col=1,
hand='R'
)
ratings_vl = data_cache.BattingRatings(
homerun=5.0,
bp_homerun=7.0,
triple=2.0,
double_three=1.0,
double_two=8.0,
double_pull=6.0,
single_two=4.0,
single_one=3.0,
single_center=12.0,
bp_single=5.0,
hbp=2.0,
walk=10.0,
strikeout=15.0,
lineout=3.0,
popout=6.0,
flyout_a=1.0,
flyout_bq=0.5,
flyout_lf_b=4.0,
flyout_rf_b=8.0,
groundout_a=2.0,
groundout_b=20.0,
groundout_c=15.0,
avg=0.285,
obp=0.375,
slg=0.455,
pull_rate=0.42,
center_rate=0.33,
slap_rate=0.25
)
ratings_vr = data_cache.BattingRatings(
homerun=3.0,
bp_homerun=6.0,
triple=1.0,
double_three=0.5,
double_two=6.0,
double_pull=5.0,
single_two=3.5,
single_one=2.5,
single_center=10.0,
bp_single=4.0,
hbp=1.5,
walk=8.0,
strikeout=18.0,
lineout=4.0,
popout=7.0,
flyout_a=1.5,
flyout_bq=0.75,
flyout_lf_b=5.0,
flyout_rf_b=10.0,
groundout_a=2.5,
groundout_b=22.0,
groundout_c=16.0,
avg=0.265,
obp=0.345,
slg=0.425,
pull_rate=0.45,
center_rate=0.30,
slap_rate=0.25
)
return data_cache.BattingWrapper(
card=batting_card,
ratings_vl=ratings_vl,
ratings_vr=ratings_vr
)
@pytest.fixture
def mock_pitching_wrapper():
"""Create a mock PitchingWrapper with test data."""
pitching_card = data_cache.PitchingCard(
player_id=456,
variant=0,
balk=1,
wild_pitch=2,
hold=8,
starter_rating=7,
relief_rating=5,
batting='B',
offense_col=1,
hand='L',
closer_rating=6
)
ratings_vl = data_cache.PitchingRatings(
homerun=3.0,
bp_homerun=5.0,
triple=1.0,
double_three=0.5,
double_two=5.0,
double_cf=4.0,
single_two=3.0,
single_one=2.0,
single_center=8.0,
bp_single=4.0,
hbp=2.0,
walk=12.0,
strikeout=25.0,
flyout_lf_b=5.0,
flyout_cf_b=6.0,
flyout_rf_b=6.0,
groundout_a=3.0,
groundout_b=18.0,
xcheck_p=1.0,
xcheck_c=1.0,
xcheck_1b=1.0,
xcheck_2b=1.0,
xcheck_3b=1.0,
xcheck_ss=1.0,
xcheck_lf=1.0,
xcheck_cf=1.0,
xcheck_rf=1.0,
avg=0.245,
obp=0.335,
slg=0.385
)
ratings_vr = data_cache.PitchingRatings(
homerun=4.0,
bp_homerun=6.0,
triple=1.5,
double_three=0.75,
double_two=7.0,
double_cf=5.0,
single_two=4.0,
single_one=3.0,
single_center=10.0,
bp_single=5.0,
hbp=1.5,
walk=10.0,
strikeout=22.0,
flyout_lf_b=6.0,
flyout_cf_b=7.0,
flyout_rf_b=8.0,
groundout_a=4.0,
groundout_b=20.0,
xcheck_p=1.5,
xcheck_c=1.5,
xcheck_1b=1.5,
xcheck_2b=1.5,
xcheck_3b=1.5,
xcheck_ss=1.5,
xcheck_lf=1.5,
xcheck_cf=1.5,
xcheck_rf=1.5,
avg=0.255,
obp=0.345,
slg=0.405
)
return data_cache.PitchingWrapper(
card=pitching_card,
ratings_vl=ratings_vl,
ratings_vr=ratings_vr
)
@pytest.fixture
def mock_player_data():
"""Create mock player data for helper function calls."""
return {
'player_id': 123,
'p_name': 'Test Player',
'description': '2024',
'image': 'test_player_pitchingcard',
'image2': 'test_player_battingcard'
}
class TestGetResult:
"""Test the get_result function which simulates plate appearance outcomes."""
@patch('random.choice')
@patch('random.choices')
def test_get_result_pitcher_chosen_left_handed_batter(self, mock_choices, mock_choice, mock_pitching_wrapper, mock_batting_wrapper):
"""Test get_result when pitcher is chosen and batter is left-handed."""
# Make the batter left-handed
mock_batting_wrapper.card.hand = 'L'
mock_choice.return_value = 'pitcher'
mock_choices.return_value = ['strikeout']
result = simulations.get_result(mock_pitching_wrapper, mock_batting_wrapper)
mock_choice.assert_called_once_with(['pitcher', 'batter'])
assert result == 'strikeout'
# Verify the correct ratings were used (pitcher vs left-handed batter)
call_args = mock_choices.call_args
assert call_args is not None
# Should use pitcher's ratings_vl when facing left-handed batter
@patch('random.choice')
@patch('random.choices')
def test_get_result_pitcher_chosen_right_handed_batter(self, mock_choices, mock_choice, mock_pitching_wrapper, mock_batting_wrapper):
"""Test get_result when pitcher is chosen and batter is right-handed."""
# Batter is already right-handed in fixture
mock_choice.return_value = 'pitcher'
mock_choices.return_value = ['groundout_b']
result = simulations.get_result(mock_pitching_wrapper, mock_batting_wrapper)
mock_choice.assert_called_once_with(['pitcher', 'batter'])
assert result == 'groundout_b'
@patch('random.choice')
@patch('random.choices')
def test_get_result_batter_chosen_left_handed_pitcher(self, mock_choices, mock_choice, mock_pitching_wrapper, mock_batting_wrapper):
"""Test get_result when batter is chosen and pitcher is left-handed."""
# Pitcher is already left-handed in fixture
mock_choice.return_value = 'batter'
mock_choices.return_value = ['homerun']
result = simulations.get_result(mock_pitching_wrapper, mock_batting_wrapper)
mock_choice.assert_called_once_with(['pitcher', 'batter'])
assert result == 'homerun'
@patch('random.choice')
@patch('random.choices')
def test_get_result_batter_chosen_right_handed_pitcher(self, mock_choices, mock_choice, mock_pitching_wrapper, mock_batting_wrapper):
"""Test get_result when batter is chosen and pitcher is right-handed."""
# Make the pitcher right-handed
mock_pitching_wrapper.card.hand = 'R'
mock_choice.return_value = 'batter'
mock_choices.return_value = ['single_center']
result = simulations.get_result(mock_pitching_wrapper, mock_batting_wrapper)
mock_choice.assert_called_once_with(['pitcher', 'batter'])
assert result == 'single_center'
@patch('random.choice')
@patch('random.choices')
def test_get_result_unused_fields_filtered_out(self, mock_choices, mock_choice, mock_pitching_wrapper, mock_batting_wrapper):
"""Test that unused statistical fields are filtered out from the random selection."""
mock_choice.return_value = 'batter'
mock_choices.return_value = ['walk']
result = simulations.get_result(mock_pitching_wrapper, mock_batting_wrapper)
# Get the call arguments to verify unused fields were removed
call_args = mock_choices.call_args
results_list = call_args[0][0] # First positional argument (results)
probs_list = call_args[0][1] # Second positional argument (probabilities)
# Verify unused fields are not in the results
unused_fields = ['avg', 'obp', 'slg', 'pull_rate', 'center_rate', 'slap_rate']
for field in unused_fields:
assert field not in results_list
# Verify we have valid baseball outcomes
assert len(results_list) == len(probs_list)
assert all(isinstance(prob, (int, float)) for prob in probs_list)
assert result == 'walk'
class TestGetPosEmbeds:
"""Test the get_pos_embeds function which creates Discord embeds for matchups."""
@patch('in_game.simulations.get_result')
@patch('helpers.player_desc')
@patch('helpers.player_pcard')
@patch('helpers.player_bcard')
@patch('helpers.SBA_COLOR', 'a6ce39')
def test_get_pos_embeds_structure(self, mock_bcard, mock_pcard, mock_desc, mock_get_result,
mock_pitching_wrapper, mock_batting_wrapper, mock_player_data):
"""Test that get_pos_embeds returns the correct embed structure."""
# Setup mocks
mock_desc.side_effect = ['2024 Test Pitcher', '2024 Test Batter']
mock_pcard.return_value = 'pitcher_card_url'
mock_bcard.return_value = 'batter_card_url'
mock_get_result.return_value = 'strikeout'
pitcher_data = {'p_name': 'Test Pitcher', 'description': '2024'}
batter_data = {'p_name': 'Test Batter', 'description': '2024'}
embeds = simulations.get_pos_embeds(pitcher_data, batter_data, mock_pitching_wrapper, mock_batting_wrapper)
# Verify we get 3 embeds
assert len(embeds) == 3
assert all(isinstance(embed, discord.Embed) for embed in embeds)
# Check first embed (pitcher)
assert embeds[0].title == '2024 Test Pitcher vs. 2024 Test Batter'
assert embeds[0].image.url == 'pitcher_card_url'
assert embeds[0].color.value == int('a6ce39', 16)
# Check second embed (batter)
assert embeds[1].image.url == 'batter_card_url'
assert embeds[1].color.value == int('a6ce39', 16)
# Check third embed (result)
assert len(embeds[2].fields) == 3
assert embeds[2].fields[0].name == 'Pitcher'
assert embeds[2].fields[0].value == '2024 Test Pitcher'
assert embeds[2].fields[1].name == 'Batter'
assert embeds[2].fields[1].value == '2024 Test Batter'
assert embeds[2].fields[2].name == 'Result'
assert embeds[2].fields[2].value == 'strikeout'
assert embeds[2].fields[2].inline is False
# Verify helper functions were called correctly
mock_desc.assert_has_calls([call(pitcher_data), call(batter_data)])
mock_pcard.assert_called_once_with(pitcher_data)
mock_bcard.assert_called_once_with(batter_data)
mock_get_result.assert_called_once_with(mock_pitching_wrapper, mock_batting_wrapper)
@patch('in_game.simulations.get_result')
@patch('helpers.player_desc')
@patch('helpers.player_pcard')
@patch('helpers.player_bcard')
@patch('helpers.SBA_COLOR', 'a6ce39')
def test_get_pos_embeds_different_results(self, mock_bcard, mock_pcard, mock_desc, mock_get_result,
mock_pitching_wrapper, mock_batting_wrapper, mock_player_data):
"""Test get_pos_embeds with different simulation results."""
mock_desc.side_effect = ['Live Ace Pitcher', 'Live Power Hitter']
mock_pcard.return_value = 'ace_pitcher_card'
mock_bcard.return_value = 'power_hitter_card'
mock_get_result.return_value = 'homerun'
pitcher_data = {'p_name': 'Ace Pitcher', 'description': 'Live'}
batter_data = {'p_name': 'Power Hitter', 'description': 'Live'}
embeds = simulations.get_pos_embeds(pitcher_data, batter_data, mock_pitching_wrapper, mock_batting_wrapper)
assert len(embeds) == 3
assert embeds[0].title == 'Live Ace Pitcher vs. Live Power Hitter'
assert embeds[2].fields[2].value == 'homerun'
class TestIntegration:
"""Integration tests that test the functions together with minimal mocking."""
def test_get_result_randomness_distribution(self, mock_pitching_wrapper, mock_batting_wrapper):
"""Test that get_result produces a reasonable distribution of outcomes over many iterations."""
# Run many simulations to test randomness
results = []
for _ in range(1000):
result = simulations.get_result(mock_pitching_wrapper, mock_batting_wrapper)
results.append(result)
# Verify we got various outcomes (not just one result)
unique_results = set(results)
assert len(unique_results) > 1
# Verify all results are valid baseball outcomes
valid_outcomes = {
'homerun', 'triple', 'double_two', 'double_three', 'double_pull', 'double_cf',
'single_center', 'single_one', 'single_two', 'bp_single', 'bp_homerun',
'walk', 'hbp', 'strikeout', 'lineout', 'popout',
'flyout_a', 'flyout_bq', 'flyout_lf_b', 'flyout_cf_b', 'flyout_rf_b',
'groundout_a', 'groundout_b', 'groundout_c',
'xcheck_p', 'xcheck_c', 'xcheck_1b', 'xcheck_2b', 'xcheck_3b', 'xcheck_ss',
'xcheck_lf', 'xcheck_cf', 'xcheck_rf'
}
for result in unique_results:
assert result in valid_outcomes
@patch('helpers.SBA_COLOR', 'a6ce39')
def test_full_simulation_flow(self, mock_pitching_wrapper, mock_batting_wrapper):
"""Test the complete flow from player data to Discord embeds."""
pitcher_data = {
'p_name': 'Test Pitcher',
'description': '2024',
'image': 'test_pitcher_pitchingcard',
'image2': None
}
batter_data = {
'p_name': 'Test Batter',
'description': '2024',
'image': None,
'image2': 'test_batter_battingcard'
}
# This will call the actual helper functions, so we need valid player data
with patch('helpers.player_desc') as mock_desc, \
patch('helpers.player_pcard') as mock_pcard, \
patch('helpers.player_bcard') as mock_bcard:
mock_desc.side_effect = ['2024 Test Pitcher', '2024 Test Batter']
mock_pcard.return_value = 'test_pitcher_pitchingcard'
mock_bcard.return_value = 'test_batter_battingcard'
embeds = simulations.get_pos_embeds(pitcher_data, batter_data, mock_pitching_wrapper, mock_batting_wrapper)
# Verify the complete embed structure
assert len(embeds) == 3
assert embeds[0].title == '2024 Test Pitcher vs. 2024 Test Batter'
assert embeds[0].image.url == 'test_pitcher_pitchingcard'
assert embeds[1].image.url == 'test_batter_battingcard'
# The result should be one of many possible baseball outcomes
result_field = embeds[2].fields[2]
assert result_field.name == 'Result'
assert isinstance(result_field.value, str)
assert len(result_field.value) > 0