""" 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