Complete X-Check resolution table system for defensive play outcomes. Components: - Defense range tables (20×5) for infield, outfield, catcher - Error charts for LF/RF and CF (ratings 0-25) - Placeholder error charts for P, C, 1B, 2B, 3B, SS (awaiting data) - get_fielders_holding_runners() - Complete implementation - get_error_chart_for_position() - Maps all 9 positions - 6 X-Check placeholder advancement functions (g1-g3, f1-f3) League Config Integration: - Both SbaConfig and PdConfig include X-Check tables - Shared common tables via league_configs.py - Attributes: x_check_defense_tables, x_check_error_charts, x_check_holding_runners Testing: - 36 tests for X-Check tables (all passing) - 9 tests for X-Check placeholders (all passing) - Total: 45/45 tests passing Documentation: - Updated backend/CLAUDE.md with Phase 3B section - Updated app/config/CLAUDE.md with X-Check tables documentation - Updated app/core/CLAUDE.md with X-Check placeholder functions - Updated tests/CLAUDE.md with new test counts (519 unit tests) - Updated phase-3b-league-config-tables.md (marked complete) - Updated NEXT_SESSION.md with Phase 3B completion What's Pending: - 6 infield error charts need actual data (P, C, 1B, 2B, 3B, SS) - Phase 3C will implement full X-Check resolution logic 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
478 lines
20 KiB
Markdown
478 lines
20 KiB
Markdown
# 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**
|