# 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**
