# Phase 3B: League Config Tables for X-Check Resolution **Status**: ✅ Complete **Completed**: 2025-11-01 **Actual Effort**: 3 hours **Dependencies**: Phase 3A (Data Models) ## Overview Create defense tables, error charts, and placeholder advancement tables for X-Check resolution. These tables are used to convert dice rolls into play outcomes. Tables are stored in league configs with shared common tables imported by both SBA and PD configs. ## Tasks ### 1. Create Common X-Check Tables Module **File**: `backend/app/config/common_x_check_tables.py` (NEW FILE) ```python """ Common X-Check resolution tables shared across SBA and PD leagues. Tables include: - Defense range tables (20x5) for each position type - Error charts mapping 3d6 rolls to error types - Holding runner responsibility chart Author: Claude Date: 2025-11-01 """ from typing import List, Tuple # ============================================================================ # DEFENSE RANGE TABLES (1d20 × Defense Range 1-5) # ============================================================================ # Row index = d20 roll - 1 (0-indexed) # Column index = defense range - 1 (0-indexed) # Values = base result code (G1, SI2, F2, etc.) INFIELD_DEFENSE_TABLE: List[List[str]] = [ # Range: 1 2 3 4 5 # Best Good Avg Poor Worst ['G3#', 'SI1', 'SI2', 'SI2', 'SI2'], # d20 = 1 ['G2#', 'SI1', 'SI2', 'SI2', 'SI2'], # d20 = 2 ['G2#', 'G3#', 'SI1', 'SI2', 'SI2'], # d20 = 3 ['G2#', 'G3#', 'SI1', 'SI2', 'SI2'], # d20 = 4 ['G1', 'G3#', 'G3#', 'SI1', 'SI2'], # d20 = 5 ['G1', 'G2#', 'G3#', 'SI1', 'SI2'], # d20 = 6 ['G1', 'G2', 'G3#', 'G3#', 'SI1'], # d20 = 7 ['G1', 'G2', 'G3#', 'G3#', 'SI1'], # d20 = 8 ['G1', 'G2', 'G3', 'G3#', 'G3#'], # d20 = 9 ['G1', 'G1', 'G2', 'G3#', 'G3#'], # d20 = 10 ['G1', 'G1', 'G2', 'G3', 'G3#'], # d20 = 11 ['G1', 'G1', 'G2', 'G3', 'G3#'], # d20 = 12 ['G1', 'G1', 'G2', 'G3', 'G3'], # d20 = 13 ['G1', 'G1', 'G2', 'G2', 'G3'], # d20 = 14 ['G1', 'G1', 'G1', 'G2', 'G3'], # d20 = 15 ['G1', 'G1', 'G1', 'G2', 'G3'], # d20 = 16 ['G1', 'G1', 'G1', 'G1', 'G3'], # d20 = 17 ['G1', 'G1', 'G1', 'G1', 'G2'], # d20 = 18 ['G1', 'G1', 'G1', 'G1', 'G2'], # d20 = 19 ['G1', 'G1', 'G1', 'G1', 'G1'], # d20 = 20 ] OUTFIELD_DEFENSE_TABLE: List[List[str]] = [ # Range: 1 2 3 4 5 # Best Good Avg Poor Worst ['TR3', 'DO3', 'DO3', 'DO3', 'DO3'], # d20 = 1 ['DO3', 'DO3', 'DO3', 'DO3', 'DO3'], # d20 = 2 ['DO2', 'DO3', 'DO3', 'DO3', 'DO3'], # d20 = 3 ['DO2', 'DO2', 'DO3', 'DO3', 'DO3'], # d20 = 4 ['SI2', 'DO2', 'DO2', 'DO3', 'DO3'], # d20 = 5 ['SI2', 'SI2', 'DO2', 'DO2', 'DO3'], # d20 = 6 ['F1', 'SI2', 'SI2', 'DO2', 'DO2'], # d20 = 7 ['F1', 'F1', 'SI2', 'SI2', 'DO2'], # d20 = 8 ['F1', 'F1', 'F1', 'SI2', 'SI2'], # d20 = 9 ['F1', 'F1', 'F1', 'SI2', 'SI2'], # d20 = 10 ['F1', 'F1', 'F1', 'F1', 'SI2'], # d20 = 11 ['F1', 'F1', 'F1', 'F1', 'SI2'], # d20 = 12 ['F1', 'F1', 'F1', 'F1', 'F1'], # d20 = 13 ['F2', 'F1', 'F1', 'F1', 'F1'], # d20 = 14 ['F2', 'F2', 'F1', 'F1', 'F1'], # d20 = 15 ['F2', 'F2', 'F2', 'F1', 'F1'], # d20 = 16 ['F2', 'F2', 'F2', 'F2', 'F1'], # d20 = 17 ['F3', 'F2', 'F2', 'F2', 'F2'], # d20 = 18 ['F3', 'F3', 'F2', 'F2', 'F2'], # d20 = 19 ['F3', 'F3', 'F3', 'F2', 'F2'], # d20 = 20 ] CATCHER_DEFENSE_TABLE: List[List[str]] = [ # Range: 1 2 3 4 5 # Best Good Avg Poor Worst ['G3', 'SI1', 'SI1', 'SI1', 'SI1'], # d20 = 1 ['G2', 'G3', 'SI1', 'SI1', 'SI1'], # d20 = 2 ['G2', 'G3', 'SI1', 'SI1', 'SI1'], # d20 = 3 ['G1', 'G2', 'G3', 'SI1', 'SI1'], # d20 = 4 ['G1', 'G2', 'G3', 'SI1', 'SI1'], # d20 = 5 ['G1', 'G1', 'G2', 'G3', 'SI1'], # d20 = 6 ['G1', 'G1', 'G2', 'G3', 'SI1'], # d20 = 7 ['G1', 'G1', 'G1', 'G2', 'G3'], # d20 = 8 ['G1', 'G1', 'G1', 'G2', 'G3'], # d20 = 9 ['SPD', 'G1', 'G1', 'G1', 'G2'], # d20 = 10 ['SPD', 'SPD', 'G1', 'G1', 'G1'], # d20 = 11 ['SPD', 'SPD', 'SPD', 'G1', 'G1'], # d20 = 12 ['FO', 'SPD', 'SPD', 'SPD', 'G1'], # d20 = 13 ['FO', 'FO', 'SPD', 'SPD', 'SPD'], # d20 = 14 ['FO', 'FO', 'FO', 'SPD', 'SPD'], # d20 = 15 ['PO', 'FO', 'FO', 'FO', 'SPD'], # d20 = 16 ['PO', 'PO', 'FO', 'FO', 'FO'], # d20 = 17 ['PO', 'PO', 'PO', 'FO', 'FO'], # d20 = 18 ['PO', 'PO', 'PO', 'PO', 'FO'], # d20 = 19 ['PO', 'PO', 'PO', 'PO', 'PO'], # d20 = 20 ] # ============================================================================ # ERROR CHARTS (3d6 totals by Error Rating and Position Type) # ============================================================================ # Structure: {error_rating: {'RP': [rolls], 'E1': [rolls], 'E2': [rolls], 'E3': [rolls]}} # If 3d6 sum is in the list for that error rating, apply that error type # Otherwise, error_result = 'NO' (no error) # Corner Outfield (LF, RF) Error Chart LF_RF_ERROR_CHART: dict[int, dict[str, List[int]]] = { 0: {'RP': [5], 'E1': [], 'E2': [], 'E3': []}, 1: {'RP': [5], 'E1': [3], 'E2': [], 'E3': [17]}, 2: {'RP': [5], 'E1': [3, 18], 'E2': [], 'E3': [16]}, 3: {'RP': [5], 'E1': [3, 18], 'E2': [], 'E3': [15]}, 4: {'RP': [5], 'E1': [4], 'E2': [3, 15], 'E3': [18]}, 5: {'RP': [5], 'E1': [4], 'E2': [14], 'E3': [18]}, 6: {'RP': [5], 'E1': [3, 4], 'E2': [14, 17], 'E3': [18]}, 7: {'RP': [5], 'E1': [16], 'E2': [6, 15], 'E3': [18]}, 8: {'RP': [5], 'E1': [16], 'E2': [6, 15, 17], 'E3': [3, 18]}, 9: {'RP': [5], 'E1': [16], 'E2': [11], 'E3': [3, 18]}, 10: {'RP': [5], 'E1': [4, 16], 'E2': [14, 15, 17], 'E3': [3, 18]}, 11: {'RP': [5], 'E1': [4, 16], 'E2': [13, 15], 'E3': [3, 18]}, 12: {'RP': [5], 'E1': [4, 16], 'E2': [13, 14], 'E3': [3, 18]}, 13: {'RP': [5], 'E1': [6], 'E2': [4, 12, 15], 'E3': [17]}, 14: {'RP': [5], 'E1': [3, 6], 'E2': [12, 14], 'E3': [17]}, 15: {'RP': [5], 'E1': [3, 6, 18], 'E2': [4, 12, 14], 'E3': [17]}, 16: {'RP': [5], 'E1': [4, 6], 'E2': [12, 13], 'E3': [17]}, 17: {'RP': [5], 'E1': [4, 6], 'E2': [9, 12], 'E3': [17]}, 18: {'RP': [5], 'E1': [3, 4, 6], 'E2': [11, 12, 18], 'E3': [17]}, 19: {'RP': [5], 'E1': [7], 'E2': [3, 10, 11], 'E3': [17, 18]}, 20: {'RP': [5], 'E1': [3, 7], 'E2': [11, 13, 16], 'E3': [17, 18]}, 21: {'RP': [5], 'E1': [3, 7], 'E2': [11, 12, 15], 'E3': [17, 18]}, 22: {'RP': [5], 'E1': [3, 6, 16], 'E2': [9, 12, 14], 'E3': [17, 18]}, 23: {'RP': [5], 'E1': [4, 7], 'E2': [11, 12, 14], 'E3': [17, 18]}, 24: {'RP': [5], 'E1': [4, 6, 16], 'E2': [8, 11, 13], 'E3': [3, 17, 18]}, 25: {'RP': [5], 'E1': [4, 6, 16], 'E2': [11, 12, 13], 'E3': [3, 17, 18]}, } # Center Field Error Chart CF_ERROR_CHART: dict[int, dict[str, List[int]]] = { 0: {'RP': [5], 'E1': [], 'E2': [], 'E3': []}, 1: {'RP': [5], 'E1': [3], 'E2': [], 'E3': [17]}, 2: {'RP': [5], 'E1': [3, 18], 'E2': [], 'E3': [16]}, 3: {'RP': [5], 'E1': [3, 18], 'E2': [], 'E3': [15]}, 4: {'RP': [5], 'E1': [4], 'E2': [3, 15], 'E3': [18]}, 5: {'RP': [5], 'E1': [4], 'E2': [14], 'E3': [18]}, 6: {'RP': [5], 'E1': [3, 4], 'E2': [14, 17], 'E3': [18]}, 7: {'RP': [5], 'E1': [16], 'E2': [6, 15], 'E3': [18]}, 8: {'RP': [5], 'E1': [16], 'E2': [6, 15, 17], 'E3': [3, 18]}, 9: {'RP': [5], 'E1': [16], 'E2': [11], 'E3': [3, 18]}, 10: {'RP': [5], 'E1': [4, 16], 'E2': [14, 15, 17], 'E3': [3, 18]}, 11: {'RP': [5], 'E1': [4, 16], 'E2': [13, 15], 'E3': [3, 18]}, 12: {'RP': [5], 'E1': [4, 16], 'E2': [13, 14], 'E3': [3, 18]}, 13: {'RP': [5], 'E1': [6], 'E2': [4, 12, 15], 'E3': [17]}, 14: {'RP': [5], 'E1': [3, 6], 'E2': [12, 14], 'E3': [17]}, 15: {'RP': [5], 'E1': [3, 6, 18], 'E2': [4, 12, 14], 'E3': [17]}, 16: {'RP': [5], 'E1': [4, 6], 'E2': [12, 13], 'E3': [17]}, 17: {'RP': [5], 'E1': [4, 6], 'E2': [9, 12], 'E3': [17]}, 18: {'RP': [5], 'E1': [3, 4, 6], 'E2': [11, 12, 18], 'E3': [17]}, 19: {'RP': [5], 'E1': [7], 'E2': [3, 10, 11], 'E3': [17, 18]}, 20: {'RP': [5], 'E1': [3, 7], 'E2': [11, 13, 16], 'E3': [17, 18]}, 21: {'RP': [5], 'E1': [3, 7], 'E2': [11, 12, 15], 'E3': [17, 18]}, 22: {'RP': [5], 'E1': [3, 6, 16], 'E2': [9, 12, 14], 'E3': [17, 18]}, 23: {'RP': [5], 'E1': [4, 7], 'E2': [11, 12, 14], 'E3': [17, 18]}, 24: {'RP': [5], 'E1': [4, 6, 16], 'E2': [8, 11, 13], 'E3': [3, 17, 18]}, 25: {'RP': [5], 'E1': [4, 6, 16], 'E2': [11, 12, 13], 'E3': [3, 17, 18]}, } # Infield Error Charts # TODO: Add P, C, 1B, 2B, 3B, SS error charts # Structure same as OF charts above # Placeholder for now - to be filled with actual data PITCHER_ERROR_CHART: dict[int, dict[str, List[int]]] = {} # TODO CATCHER_ERROR_CHART: dict[int, dict[str, List[int]]] = {} # TODO FIRST_BASE_ERROR_CHART: dict[int, dict[str, List[int]]] = {} # TODO SECOND_BASE_ERROR_CHART: dict[int, dict[str, List[int]]] = {} # TODO THIRD_BASE_ERROR_CHART: dict[int, dict[str, List[int]]] = {} # TODO SHORTSTOP_ERROR_CHART: dict[int, dict[str, List[int]]] = {} # TODO # ============================================================================ # HOLDING RUNNER RESPONSIBILITY CHART # ============================================================================ def get_fielders_holding_runners( runner_bases: List[int], batter_handedness: str ) -> List[str]: """ Determine which fielders are responsible for holding runners. Used to determine if G2#/G3# results should convert to SI2. Args: runner_bases: List of bases with runners (e.g., [1, 3] for R1 and R3) batter_handedness: 'L' or 'R' Returns: List of position codes responsible for holds (e.g., ['1B', 'SS']) TODO: Implement full chart logic when chart is provided For now, simple heuristic: - R1 only: 1B holds - R1 + others: 2B or SS holds depending on handedness - R2 only: No holds - R3 only: No holds """ if not runner_bases: return [] holding_positions = [] if 1 in runner_bases: # Runner on first if len(runner_bases) == 1: # Only R1 holding_positions.append('1B') else: # R1 + others - middle infielder holds if batter_handedness == 'R': holding_positions.append('SS') else: holding_positions.append('2B') return holding_positions # ============================================================================ # ERROR CHART LOOKUP HELPER # ============================================================================ def get_error_chart_for_position(position: str) -> dict[int, dict[str, List[int]]]: """ Get error chart for a specific position. Args: position: Position code (P, C, 1B, 2B, 3B, SS, LF, CF, RF) Returns: Error chart dict Raises: ValueError: If position not recognized """ charts = { 'P': PITCHER_ERROR_CHART, 'C': CATCHER_ERROR_CHART, '1B': FIRST_BASE_ERROR_CHART, '2B': SECOND_BASE_ERROR_CHART, '3B': THIRD_BASE_ERROR_CHART, 'SS': SHORTSTOP_ERROR_CHART, 'LF': LF_RF_ERROR_CHART, 'RF': LF_RF_ERROR_CHART, 'CF': CF_ERROR_CHART, } if position not in charts: raise ValueError(f"Unknown position: {position}") return charts[position] ``` ### 2. Import Common Tables in League Configs **File**: `backend/app/config/sba_config.py` **Add imports**: ```python from app.config.common_x_check_tables import ( INFIELD_DEFENSE_TABLE, OUTFIELD_DEFENSE_TABLE, CATCHER_DEFENSE_TABLE, LF_RF_ERROR_CHART, CF_ERROR_CHART, get_fielders_holding_runners, get_error_chart_for_position, ) # Use common tables (no overrides for SBA) X_CHECK_DEFENSE_TABLES = { 'infield': INFIELD_DEFENSE_TABLE, 'outfield': OUTFIELD_DEFENSE_TABLE, 'catcher': CATCHER_DEFENSE_TABLE, } X_CHECK_ERROR_CHARTS = get_error_chart_for_position # Use common function ``` **File**: `backend/app/config/pd_config.py` **Add same imports** (for now, PD uses common tables): ```python from app.config.common_x_check_tables import ( INFIELD_DEFENSE_TABLE, OUTFIELD_DEFENSE_TABLE, CATCHER_DEFENSE_TABLE, LF_RF_ERROR_CHART, CF_ERROR_CHART, get_fielders_holding_runners, get_error_chart_for_position, ) X_CHECK_DEFENSE_TABLES = { 'infield': INFIELD_DEFENSE_TABLE, 'outfield': OUTFIELD_DEFENSE_TABLE, 'catcher': CATCHER_DEFENSE_TABLE, } X_CHECK_ERROR_CHARTS = get_error_chart_for_position ``` ### 3. Add Placeholder Runner Advancement Functions **File**: `backend/app/core/runner_advancement.py` **Add at end of file** (placeholders for Phase 3D): ```python # ============================================================================ # X-CHECK RUNNER ADVANCEMENT (Placeholders - to be implemented in Phase 3D) # ============================================================================ def x_check_g1( on_base_code: int, defender_in: bool, error_result: str ) -> AdvancementResult: """ Runner advancement for X-Check G1 result. TODO: Implement full table lookups in Phase 3D Args: on_base_code: Current base situation code defender_in: Is the defender playing in? error_result: 'NO', 'E1', 'E2', 'E3', 'RP' Returns: AdvancementResult with runner movements """ # Placeholder return AdvancementResult(movements=[], requires_decision=False) def x_check_g2(on_base_code: int, defender_in: bool, error_result: str) -> AdvancementResult: """X-Check G2 advancement (TODO: Phase 3D).""" return AdvancementResult(movements=[], requires_decision=False) def x_check_g3(on_base_code: int, defender_in: bool, error_result: str) -> AdvancementResult: """X-Check G3 advancement (TODO: Phase 3D).""" return AdvancementResult(movements=[], requires_decision=False) def x_check_f1(on_base_code: int, error_result: str) -> AdvancementResult: """X-Check F1 advancement (TODO: Phase 3D).""" return AdvancementResult(movements=[], requires_decision=False) def x_check_f2(on_base_code: int, error_result: str) -> AdvancementResult: """X-Check F2 advancement (TODO: Phase 3D).""" return AdvancementResult(movements=[], requires_decision=False) def x_check_f3(on_base_code: int, error_result: str) -> AdvancementResult: """X-Check F3 advancement (TODO: Phase 3D).""" return AdvancementResult(movements=[], requires_decision=False) # Add more placeholders for SI1, SI2, DO2, DO3, TR3, FO, PO as needed ``` ## Testing Requirements 1. **Unit Tests**: `tests/config/test_x_check_tables.py` - Test defense table dimensions (20 rows × 5 columns) - Test error chart structure - Test get_error_chart_for_position() - Test get_fielders_holding_runners() with various scenarios 2. **Unit Tests**: `tests/core/test_runner_advancement.py` - Test placeholder functions return valid AdvancementResult - Verify function signatures ## Acceptance Criteria - [x] ✅ common_x_check_tables.py created with all defense tables - [x] ✅ LF/RF and CF error charts complete (ratings 0-25) - [x] ✅ Placeholder error charts for P, C, 1B, 2B, 3B, SS (empty dicts ready for data) - [x] ✅ get_fielders_holding_runners() implemented with complete logic - [x] ✅ get_error_chart_for_position() implemented - [x] ✅ SBA and PD configs import common tables (via league_configs.py) - [x] ✅ Placeholder advancement functions added to runner_advancement.py (6 functions) - [x] ✅ All unit tests pass (45/45 tests passing) - [x] ✅ No import errors ## Implementation Notes ### What Was Completed 1. **Defense Range Tables** (100% Complete) - `INFIELD_DEFENSE_TABLE`: 20×5 table with all result codes (G1, G2, G2#, G3, G3#, SI1, SI2) - `OUTFIELD_DEFENSE_TABLE`: 20×5 table with all result codes (F1, F2, F3, SI2, DO2, DO3, TR3) - `CATCHER_DEFENSE_TABLE`: 20×5 table with all result codes (G1, G2, G3, SI1, SPD, FO, PO) 2. **Error Charts** (Partial - Outfield Complete) - `LF_RF_ERROR_CHART`: Complete with all 26 ratings (0-25) and error type distributions - `CF_ERROR_CHART`: Complete with all 26 ratings (0-25) and error type distributions - Infield charts (P, C, 1B, 2B, 3B, SS): Empty dict placeholders ready for data 3. **Helper Functions** (100% Complete) - `get_fielders_holding_runners()`: Full implementation tracking all runners by base - R1: 1B + middle infielder (2B for RHB, SS for LHB) - R2: Middle infielder (2B for RHB, SS for LHB) if not already added - R3: 3B holds - `get_error_chart_for_position()`: Complete with all 9 positions mapped 4. **League Config Integration** (100% Complete) - Tables imported in `league_configs.py` (not separate sba_config.py/pd_config.py as originally planned) - Both SbaConfig and PdConfig have `x_check_defense_tables`, `x_check_error_charts`, `x_check_holding_runners` attributes - Shared common tables for both leagues 5. **Placeholder Advancement Functions** (100% Complete) - 6 functions implemented: `x_check_g1`, `x_check_g2`, `x_check_g3`, `x_check_f1`, `x_check_f2`, `x_check_f3` - All return valid `AdvancementResult` structures - Ready for Phase 3D implementation 6. **Test Coverage** (100% Complete) - 36 tests for X-Check tables (defense tables, error charts, helpers, integration) - 9 tests for placeholder advancement functions - **Total: 45/45 tests passing** ### What Still Needs Data **Infield Error Charts** - 6 positions awaiting actual data: - `PITCHER_ERROR_CHART` - `CATCHER_ERROR_CHART` - `FIRST_BASE_ERROR_CHART` - `SECOND_BASE_ERROR_CHART` - `THIRD_BASE_ERROR_CHART` - `SHORTSTOP_ERROR_CHART` Each needs the same structure as outfield charts: ```python { 0: {'RP': [rolls], 'E1': [rolls], 'E2': [rolls], 'E3': [rolls]}, # ... ratings 1-25 } ``` ### Deviations from Original Plan 1. **Config File Structure**: Used unified `league_configs.py` instead of separate `sba_config.py` and `pd_config.py` files (these don't exist in current architecture) 2. **Holding Runners Implementation**: Completed with full logic instead of placeholder heuristic - tracks all runners by base position 3. **Advancement Function Signatures**: Updated to match actual `AdvancementResult` structure (no `requires_decision` parameter) ## Next Phase After infield error chart data is provided, proceed to **Phase 3C: X-Check Resolution Logic**