strat-gameplay-webapp/backend/tests/unit/config/test_x_check_tables.py
Cal Corum cc5bf43e84 CLAUDE: Complete Phase 3B - Add all 6 infield error charts
Added complete error chart data for all infield positions to finalize
Phase 3B X-Check league config tables implementation.

Changes:
- Added CATCHER_ERROR_CHART (17 ratings: 0-16)
- Added FIRST_BASE_ERROR_CHART (31 ratings: 0-30)
- Added SECOND_BASE_ERROR_CHART (40 ratings: 0-71, sparse)
- Added THIRD_BASE_ERROR_CHART (45 ratings: 0-65, sparse)
- Added SHORTSTOP_ERROR_CHART (43 ratings: 0-88, sparse)
- Added PITCHER_ERROR_CHART (40 ratings: 0-51, sparse)

All charts follow same structure as outfield charts with E3 field
(empty for infield positions). Total ~250 lines of error chart data.

Testing:
- Updated test_infield_error_charts_complete() to verify charts populated
- All 36 X-Check table tests passing
- Verified chart structure matches outfield charts

Phase 3B Status: 100% COMPLETE 
All defense tables complete, all error charts complete, ready for Phase 3C

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 14:33:59 -06:00

381 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Unit tests for X-Check resolution tables.
Tests defense range tables, error charts, and helper functions
for X-Check play resolution.
Author: Claude
Date: 2025-11-01
"""
import pytest
from app.config.common_x_check_tables import (
INFIELD_DEFENSE_TABLE,
OUTFIELD_DEFENSE_TABLE,
CATCHER_DEFENSE_TABLE,
LF_RF_ERROR_CHART,
CF_ERROR_CHART,
PITCHER_ERROR_CHART,
CATCHER_ERROR_CHART,
FIRST_BASE_ERROR_CHART,
SECOND_BASE_ERROR_CHART,
THIRD_BASE_ERROR_CHART,
SHORTSTOP_ERROR_CHART,
get_fielders_holding_runners,
get_error_chart_for_position,
)
# ============================================================================
# DEFENSE TABLE TESTS
# ============================================================================
class TestDefenseTables:
"""Test defense range table structure and content."""
def test_infield_defense_table_dimensions(self):
"""Infield table should be 20 rows × 5 columns."""
assert len(INFIELD_DEFENSE_TABLE) == 20
for row in INFIELD_DEFENSE_TABLE:
assert len(row) == 5
def test_outfield_defense_table_dimensions(self):
"""Outfield table should be 20 rows × 5 columns."""
assert len(OUTFIELD_DEFENSE_TABLE) == 20
for row in OUTFIELD_DEFENSE_TABLE:
assert len(row) == 5
def test_catcher_defense_table_dimensions(self):
"""Catcher table should be 20 rows × 5 columns."""
assert len(CATCHER_DEFENSE_TABLE) == 20
for row in CATCHER_DEFENSE_TABLE:
assert len(row) == 5
def test_infield_defense_table_valid_results(self):
"""All infield results should be valid codes."""
valid_codes = {'G1', 'G2', 'G2#', 'G3', 'G3#', 'SI1', 'SI2'}
for row_idx, row in enumerate(INFIELD_DEFENSE_TABLE):
for col_idx, result in enumerate(row):
assert result in valid_codes, (
f"Invalid infield result '{result}' at row {row_idx + 1}, "
f"col {col_idx + 1}"
)
def test_outfield_defense_table_valid_results(self):
"""All outfield results should be valid codes."""
valid_codes = {'F1', 'F2', 'F3', 'SI2', 'DO2', 'DO3', 'TR3'}
for row_idx, row in enumerate(OUTFIELD_DEFENSE_TABLE):
for col_idx, result in enumerate(row):
assert result in valid_codes, (
f"Invalid outfield result '{result}' at row {row_idx + 1}, "
f"col {col_idx + 1}"
)
def test_catcher_defense_table_valid_results(self):
"""All catcher results should be valid codes."""
valid_codes = {'G1', 'G2', 'G3', 'SI1', 'SPD', 'FO', 'PO'}
for row_idx, row in enumerate(CATCHER_DEFENSE_TABLE):
for col_idx, result in enumerate(row):
assert result in valid_codes, (
f"Invalid catcher result '{result}' at row {row_idx + 1}, "
f"col {col_idx + 1}"
)
def test_best_range_always_best(self):
"""Range 1 (best) should always be equal or better than range 5."""
# Infield: Lower code number = better (G1 > G2 > G3 > SI)
assert INFIELD_DEFENSE_TABLE[0][0] in {'G3#', 'G2#', 'G1'}
assert INFIELD_DEFENSE_TABLE[0][4] in {'SI2', 'SI1'}
# Outfield: Different codes
assert OUTFIELD_DEFENSE_TABLE[0][0] == 'TR3' # Best range
assert OUTFIELD_DEFENSE_TABLE[0][4] == 'DO3' # Worst range
def test_worst_range_consistent(self):
"""Range 5 (worst) should show worst outcomes consistently."""
# Last row (d20=20) with worst range should still be makeable
# but harder than best range
assert INFIELD_DEFENSE_TABLE[19][4] in {'G1', 'G2'}
assert OUTFIELD_DEFENSE_TABLE[19][4] in {'F2'}
# ============================================================================
# ERROR CHART TESTS
# ============================================================================
class TestErrorCharts:
"""Test error chart structure and content."""
def test_lf_rf_error_chart_structure(self):
"""LF/RF error chart should have ratings 0-25."""
assert len(LF_RF_ERROR_CHART) == 26 # 0 through 25
# Each rating should have 4 error types
for rating, chart in LF_RF_ERROR_CHART.items():
assert 'RP' in chart
assert 'E1' in chart
assert 'E2' in chart
assert 'E3' in chart
# Each type should be a list of 3d6 rolls (3-18)
for error_type, rolls in chart.items():
assert isinstance(rolls, list)
for roll in rolls:
assert 3 <= roll <= 18, f"Invalid 3d6 roll: {roll}"
def test_cf_error_chart_structure(self):
"""CF error chart should have ratings 0-25."""
assert len(CF_ERROR_CHART) == 26 # 0 through 25
for rating, chart in CF_ERROR_CHART.items():
assert 'RP' in chart
assert 'E1' in chart
assert 'E2' in chart
assert 'E3' in chart
def test_infield_error_charts_complete(self):
"""Infield error charts should now have data (Phase 3B completed)."""
# Verify all infield charts now have data (not empty placeholders)
assert len(PITCHER_ERROR_CHART) > 0
assert len(CATCHER_ERROR_CHART) > 0
assert len(FIRST_BASE_ERROR_CHART) > 0
assert len(SECOND_BASE_ERROR_CHART) > 0
assert len(THIRD_BASE_ERROR_CHART) > 0
assert len(SHORTSTOP_ERROR_CHART) > 0
# Verify structure matches outfield charts (has E3 even if empty)
for rating_dict in CATCHER_ERROR_CHART.values():
assert 'RP' in rating_dict
assert 'E1' in rating_dict
assert 'E2' in rating_dict
assert 'E3' in rating_dict
def test_error_rating_0_has_minimal_errors(self):
"""Error rating 0 should have fewest error opportunities."""
# RP should always be on 5 (snake eyes + 3)
assert LF_RF_ERROR_CHART[0]['RP'] == [5]
assert CF_ERROR_CHART[0]['RP'] == [5]
# E1 should be empty
assert LF_RF_ERROR_CHART[0]['E1'] == []
assert CF_ERROR_CHART[0]['E1'] == []
def test_error_rating_25_has_most_errors(self):
"""Error rating 25 should have most error opportunities."""
# Should have multiple rolls for each error type
assert len(LF_RF_ERROR_CHART[25]['E1']) > 0
assert len(LF_RF_ERROR_CHART[25]['E2']) > 0
assert len(LF_RF_ERROR_CHART[25]['E3']) > 0
def test_error_rolls_unique_within_rating(self):
"""Same 3d6 roll shouldn't appear in multiple error types for one rating."""
for rating, chart in LF_RF_ERROR_CHART.items():
all_rolls = []
all_rolls.extend(chart['RP'])
all_rolls.extend(chart['E1'])
all_rolls.extend(chart['E2'])
all_rolls.extend(chart['E3'])
# Check for duplicates
assert len(all_rolls) == len(set(all_rolls)), (
f"Duplicate 3d6 roll in error rating {rating}"
)
# ============================================================================
# HELPER FUNCTION TESTS
# ============================================================================
class TestGetErrorChartForPosition:
"""Test get_error_chart_for_position() function."""
def test_get_chart_for_lf(self):
"""LF should return LF/RF chart."""
chart = get_error_chart_for_position('LF')
assert chart == LF_RF_ERROR_CHART
def test_get_chart_for_rf(self):
"""RF should return LF/RF chart (same as LF)."""
chart = get_error_chart_for_position('RF')
assert chart == LF_RF_ERROR_CHART
def test_get_chart_for_cf(self):
"""CF should return CF chart."""
chart = get_error_chart_for_position('CF')
assert chart == CF_ERROR_CHART
def test_get_chart_for_infield_positions(self):
"""Infield positions should return empty placeholders."""
assert get_error_chart_for_position('P') == PITCHER_ERROR_CHART
assert get_error_chart_for_position('C') == CATCHER_ERROR_CHART
assert get_error_chart_for_position('1B') == FIRST_BASE_ERROR_CHART
assert get_error_chart_for_position('2B') == SECOND_BASE_ERROR_CHART
assert get_error_chart_for_position('3B') == THIRD_BASE_ERROR_CHART
assert get_error_chart_for_position('SS') == SHORTSTOP_ERROR_CHART
def test_invalid_position_raises_error(self):
"""Invalid position should raise ValueError."""
with pytest.raises(ValueError, match="Unknown position"):
get_error_chart_for_position('DH')
with pytest.raises(ValueError, match="Unknown position"):
get_error_chart_for_position('XX')
class TestGetFieldersHoldingRunners:
"""Test get_fielders_holding_runners() function."""
def test_empty_bases_no_holds(self):
"""No runners means no fielders holding."""
result = get_fielders_holding_runners([], 'R')
assert result == []
def test_runner_on_first_only_rhb(self):
"""R1 only with RHB: 1B and 2B hold."""
result = get_fielders_holding_runners([1], 'R')
assert '1B' in result
assert '2B' in result
def test_runner_on_first_only_lhb(self):
"""R1 only with LHB: 1B and SS hold."""
result = get_fielders_holding_runners([1], 'L')
assert '1B' in result
assert 'SS' in result
def test_runner_on_second_only_rhb(self):
"""R2 only with RHB: 2B holds."""
result = get_fielders_holding_runners([2], 'R')
assert result == ['2B']
def test_runner_on_second_only_lhb(self):
"""R2 only with LHB: SS holds."""
result = get_fielders_holding_runners([2], 'L')
assert result == ['SS']
def test_runner_on_third_only(self):
"""R3 only: 3B holds."""
result = get_fielders_holding_runners([3], 'R')
assert result == ['3B']
def test_first_and_third_rhb(self):
"""R1 and R3 with RHB: 1B, 2B, and 3B hold."""
result = get_fielders_holding_runners([1, 3], 'R')
assert '1B' in result
assert '2B' in result
assert '3B' in result
def test_first_and_third_lhb(self):
"""R1 and R3 with LHB: 1B, SS, and 3B hold."""
result = get_fielders_holding_runners([1, 3], 'L')
assert '1B' in result
assert 'SS' in result
assert '3B' in result
def test_first_and_second_rhb(self):
"""R1 and R2 with RHB: 1B and 2B hold (2B already added for R1)."""
result = get_fielders_holding_runners([1, 2], 'R')
assert '1B' in result
assert '2B' in result
def test_first_and_second_lhb(self):
"""R1 and R2 with LHB: 1B and SS hold (SS already added for R1)."""
result = get_fielders_holding_runners([1, 2], 'L')
assert '1B' in result
assert 'SS' in result
def test_bases_loaded_rhb(self):
"""Bases loaded with RHB: 1B, 2B, and 3B hold."""
result = get_fielders_holding_runners([1, 2, 3], 'R')
assert '1B' in result
assert '2B' in result
assert '3B' in result
def test_bases_loaded_lhb(self):
"""Bases loaded with LHB: 1B, SS, and 3B hold."""
result = get_fielders_holding_runners([1, 2, 3], 'L')
assert '1B' in result
assert 'SS' in result
assert '3B' in result
def test_second_and_third_rhb(self):
"""R2 and R3 with RHB: 2B and 3B hold (no runner on first)."""
result = get_fielders_holding_runners([2, 3], 'R')
assert '2B' in result
assert '3B' in result
def test_second_and_third_lhb(self):
"""R2 and R3 with LHB: SS and 3B hold (no runner on first)."""
result = get_fielders_holding_runners([2, 3], 'L')
assert 'SS' in result
assert '3B' in result
# ============================================================================
# INTEGRATION TESTS
# ============================================================================
class TestXCheckTablesIntegration:
"""Integration tests combining tables and helper functions."""
def test_defense_table_lookup(self):
"""Test looking up a defense result."""
# d20 roll = 10, defense range = 3 (average)
d20_roll = 10
defense_range = 3
# Row index = d20 - 1, col index = range - 1
result = INFIELD_DEFENSE_TABLE[d20_roll - 1][defense_range - 1]
# d20=10, range=3 should be G2
assert result == 'G2'
def test_error_check_workflow(self):
"""Test complete error check workflow."""
# Scenario: LF with error rating 10, 3d6 roll = 16
position = 'LF'
error_rating = 10
three_d6_roll = 16
# Get error chart for position
chart = get_error_chart_for_position(position)
# Get error chances for this rating
error_chances = chart[error_rating]
# Check each error type
if three_d6_roll in error_chances['RP']:
error_result = 'RP'
elif three_d6_roll in error_chances['E1']:
error_result = 'E1'
elif three_d6_roll in error_chances['E2']:
error_result = 'E2'
elif three_d6_roll in error_chances['E3']:
error_result = 'E3'
else:
error_result = 'NO'
# For rating 10, roll 16: E1 = [4, 16]
assert error_result == 'E1'
def test_holding_runner_affects_result(self):
"""Test how holding runners might affect play resolution."""
# Scenario: G2# result with runner on 1st
# Check if fielders are holding
fielders_holding = get_fielders_holding_runners([1], 'R')
# If fielder involved in play is holding, G2# → SI2
# This logic will be implemented in Phase 3C
assert '1B' in fielders_holding
assert '2B' in fielders_holding # RHB means 2B holds
# For now, just verify the function returns expected values
# Full integration will happen in Phase 3C