strat-gameplay-webapp/backend/app/config/common_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

490 lines
31 KiB
Python
Raw 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.

"""
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
# ============================================================================
# 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
# Note: Infield charts do not use E3 (unlike outfield charts)
# Structure: same as OF but E3 is always empty
# Catcher Error Chart
CATCHER_ERROR_CHART: dict[int, dict[str, List[int]]] = {
0: {'RP': [5], 'E1': [], 'E2': [], 'E3': []},
1: {'RP': [5], 'E1': [17], 'E2': [], 'E3': []},
2: {'RP': [5], 'E1': [3, 17, 18], 'E2': [], 'E3': []},
3: {'RP': [5], 'E1': [3, 16, 10], 'E2': [], 'E3': []},
4: {'RP': [5], 'E1': [16, 17], 'E2': [18], 'E3': []},
5: {'RP': [5], 'E1': [4, 16, 17], 'E2': [18], 'E3': []},
6: {'RP': [5], 'E1': [14], 'E2': [18], 'E3': []},
7: {'RP': [5], 'E1': [3, 15, 16], 'E2': [18], 'E3': []},
8: {'RP': [5], 'E1': [6, 15], 'E2': [18], 'E3': []},
9: {'RP': [5], 'E1': [3, 13], 'E2': [18], 'E3': []},
10: {'RP': [5], 'E1': [12], 'E2': [18], 'E3': []},
11: {'RP': [5], 'E1': [3, 11], 'E2': [18], 'E3': []},
12: {'RP': [5], 'E1': [6, 15, 16, 17], 'E2': [3, 18], 'E3': []},
13: {'RP': [5], 'E1': [4, 6, 15, 16, 17], 'E2': [3, 18], 'E3': []},
14: {'RP': [5], 'E1': [12, 16, 17], 'E2': [3, 18], 'E3': []},
15: {'RP': [5], 'E1': [11, 15], 'E2': [3, 18], 'E3': []},
16: {'RP': [5], 'E1': [7, 14, 16, 17], 'E2': [3, 18], 'E3': []},
}
# First Base Error Chart
FIRST_BASE_ERROR_CHART: dict[int, dict[str, List[int]]] = {
0: {'RP': [5], 'E1': [], 'E2': [], 'E3': []},
1: {'RP': [5], 'E1': [17, 18], 'E2': [], 'E3': []},
2: {'RP': [5], 'E1': [3, 16, 18], 'E2': [], 'E3': []},
3: {'RP': [5], 'E1': [3, 15], 'E2': [18], 'E3': []},
4: {'RP': [5], 'E1': [14], 'E2': [18], 'E3': []},
5: {'RP': [5], 'E1': [14, 17], 'E2': [18], 'E3': []},
6: {'RP': [5], 'E1': [3, 13], 'E2': [18], 'E3': []},
7: {'RP': [5], 'E1': [3, 9], 'E2': [18], 'E3': []},
8: {'RP': [5], 'E1': [6, 15, 16, 17], 'E2': [3, 18], 'E3': []},
9: {'RP': [5], 'E1': [7, 14, 17], 'E2': [3, 18], 'E3': []},
10: {'RP': [5], 'E1': [11, 15], 'E2': [3, 18], 'E3': []},
11: {'RP': [5], 'E1': [6, 8, 15], 'E2': [3, 18], 'E3': []},
12: {'RP': [5], 'E1': [6, 9, 15], 'E2': [3, 18], 'E3': []},
13: {'RP': [5], 'E1': [11, 13], 'E2': [17], 'E3': []},
14: {'RP': [5], 'E1': [3, 9, 12], 'E2': [17], 'E3': []},
15: {'RP': [5], 'E1': [7, 12, 14], 'E2': [17], 'E3': []},
16: {'RP': [5], 'E1': [3, 11, 12, 16], 'E2': [17], 'E3': []},
17: {'RP': [5], 'E1': [3, 6, 11, 12], 'E2': [17], 'E3': []},
18: {'RP': [5], 'E1': [11, 12, 14], 'E2': [17], 'E3': []},
19: {'RP': [5], 'E1': [10, 11, 15, 16], 'E2': [17, 18], 'E3': []},
20: {'RP': [5], 'E1': [6, 10, 11, 15], 'E2': [17, 18], 'E3': []},
21: {'RP': [5], 'E1': [3, 9, 10, 12], 'E2': [17, 18], 'E3': []},
22: {'RP': [5], 'E1': [7, 11, 12, 14], 'E2': [17, 18], 'E3': []},
23: {'RP': [5], 'E1': [10, 11, 12, 16], 'E2': [17, 18], 'E3': []},
24: {'RP': [5], 'E1': [11, 12, 13, 14], 'E2': [3, 17, 18], 'E3': []},
25: {'RP': [5], 'E1': [9, 11, 12, 14], 'E2': [3, 17, 18], 'E3': []},
26: {'RP': [5], 'E1': [9, 12, 13, 14, 15], 'E2': [3, 17, 18], 'E3': []},
27: {'RP': [5], 'E1': [7, 8, 11, 13, 14], 'E2': [3, 17, 18], 'E3': []},
28: {'RP': [5], 'E1': [7, 11, 12, 13, 14], 'E2': [3, 17, 18], 'E3': []},
29: {'RP': [5], 'E1': [9, 10, 11, 12, 17], 'E2': [16], 'E3': []},
30: {'RP': [5], 'E1': [10, 11, 12, 13, 15, 18], 'E2': [16], 'E3': []},
}
# Second Base Error Chart
SECOND_BASE_ERROR_CHART: dict[int, dict[str, List[int]]] = {
0: {'RP': [5], 'E1': [], 'E2': [], 'E3': []},
1: {'RP': [5], 'E1': [18], 'E2': [], 'E3': []},
2: {'RP': [5], 'E1': [3, 18], 'E2': [], 'E3': []},
3: {'RP': [5], 'E1': [3, 17], 'E2': [], 'E3': []},
4: {'RP': [5], 'E1': [3, 17], 'E2': [18], 'E3': []},
5: {'RP': [5], 'E1': [16], 'E2': [18], 'E3': []},
6: {'RP': [5], 'E1': [3, 16], 'E2': [18], 'E3': []},
8: {'RP': [5], 'E1': [16, 17], 'E2': [18], 'E3': []},
10: {'RP': [5], 'E1': [4, 16, 17], 'E2': [18], 'E3': []},
11: {'RP': [5], 'E1': [15, 17], 'E2': [18], 'E3': []},
12: {'RP': [5], 'E1': [15, 17], 'E2': [3, 18], 'E3': []},
13: {'RP': [5], 'E1': [14], 'E2': [3, 18], 'E3': []},
14: {'RP': [5], 'E1': [15, 16], 'E2': [3, 18], 'E3': []},
15: {'RP': [5], 'E1': [14, 17], 'E2': [3, 18], 'E3': []},
16: {'RP': [5], 'E1': [15, 16, 17], 'E2': [3, 18], 'E3': []},
17: {'RP': [5], 'E1': [6, 15], 'E2': [3, 18], 'E3': []},
18: {'RP': [5], 'E1': [13], 'E2': [3, 18], 'E3': []},
19: {'RP': [5], 'E1': [6, 15, 17], 'E2': [3, 18], 'E3': []},
20: {'RP': [5], 'E1': [3, 13, 18], 'E2': [17], 'E3': []},
21: {'RP': [5], 'E1': [4, 13], 'E2': [17], 'E3': []},
22: {'RP': [5], 'E1': [12, 18], 'E2': [17], 'E3': []},
23: {'RP': [5], 'E1': [11], 'E2': [17], 'E3': []},
24: {'RP': [5], 'E1': [11, 18], 'E2': [17], 'E3': []},
25: {'RP': [5], 'E1': [3, 11, 18], 'E2': [17], 'E3': []},
26: {'RP': [5], 'E1': [13, 15], 'E2': [17], 'E3': []},
27: {'RP': [5], 'E1': [13, 15, 18], 'E2': [17], 'E3': []},
28: {'RP': [5], 'E1': [3, 13, 15], 'E2': [17, 18], 'E3': []},
29: {'RP': [5], 'E1': [3, 11, 16], 'E2': [17, 18], 'E3': []},
30: {'RP': [5], 'E1': [12, 15], 'E2': [17, 18], 'E3': []},
32: {'RP': [5], 'E1': [11, 15], 'E2': [17, 18], 'E3': []},
34: {'RP': [5], 'E1': [12, 14], 'E2': [17, 18], 'E3': []},
37: {'RP': [5], 'E1': [11, 15, 18], 'E2': [3, 17, 18], 'E3': []},
39: {'RP': [5], 'E1': [12, 13], 'E2': [3, 17, 18], 'E3': []},
41: {'RP': [5], 'E1': [11, 13], 'E2': [3, 17, 18], 'E3': []},
44: {'RP': [5], 'E1': [9, 12, 18], 'E2': [16], 'E3': []},
47: {'RP': [5], 'E1': [7, 12, 14], 'E2': [16], 'E3': []},
50: {'RP': [5], 'E1': [11, 13, 15, 18], 'E2': [16], 'E3': []},
53: {'RP': [5], 'E1': [11, 12, 15], 'E2': [16, 18], 'E3': []},
56: {'RP': [5], 'E1': [6, 12, 13, 15], 'E2': [16, 18], 'E3': []},
59: {'RP': [5], 'E1': [6, 11, 13, 15], 'E2': [3, 16, 18], 'E3': []},
62: {'RP': [5], 'E1': [6, 11, 12, 15], 'E2': [3, 16, 18], 'E3': []},
65: {'RP': [5], 'E1': [7, 12, 13, 14], 'E2': [3, 16, 18], 'E3': []},
68: {'RP': [5], 'E1': [10, 11, 12], 'E2': [16, 17], 'E3': []},
71: {'RP': [5], 'E1': [11, 12, 13, 15], 'E2': [16, 17], 'E3': []},
}
# Third Base Error Chart
THIRD_BASE_ERROR_CHART: dict[int, dict[str, List[int]]] = {
0: {'RP': [5], 'E1': [], 'E2': [], 'E3': []},
1: {'RP': [5], 'E1': [3], 'E2': [18], 'E3': []},
2: {'RP': [5], 'E1': [3, 4], 'E2': [18], 'E3': []},
3: {'RP': [5], 'E1': [3, 4], 'E2': [17], 'E3': []},
4: {'RP': [5], 'E1': [3, 16], 'E2': [17], 'E3': []},
5: {'RP': [5], 'E1': [15], 'E2': [17], 'E3': []},
6: {'RP': [5], 'E1': [4, 15], 'E2': [17], 'E3': []},
8: {'RP': [5], 'E1': [3, 15, 16], 'E2': [17, 18], 'E3': []},
10: {'RP': [5], 'E1': [13], 'E2': [3, 17, 18], 'E3': []},
11: {'RP': [5], 'E1': [6, 15, 17], 'E2': [16], 'E3': []},
12: {'RP': [5], 'E1': [12], 'E2': [16], 'E3': []},
13: {'RP': [5], 'E1': [11], 'E2': [16, 18], 'E3': []},
14: {'RP': [5], 'E1': [3, 4, 14, 15], 'E2': [16, 18], 'E3': []},
15: {'RP': [5], 'E1': [13, 15], 'E2': [3, 16, 18], 'E3': []},
16: {'RP': [5], 'E1': [4, 7, 14], 'E2': [3, 16, 18], 'E3': []},
17: {'RP': [5], 'E1': [12, 15], 'E2': [16, 17], 'E3': []},
18: {'RP': [5], 'E1': [3, 11, 15], 'E2': [16, 17], 'E3': []},
19: {'RP': [5], 'E1': [7, 14, 16, 17], 'E2': [15], 'E3': []},
20: {'RP': [5], 'E1': [11, 14], 'E2': [15], 'E3': []},
21: {'RP': [5], 'E1': [6, 11, 16], 'E2': [15, 18], 'E3': []},
22: {'RP': [5], 'E1': [12, 14, 16], 'E2': [15, 18], 'E3': []},
23: {'RP': [5], 'E1': [11, 13], 'E2': [3, 15, 18], 'E3': []},
24: {'RP': [5], 'E1': [9, 12], 'E2': [3, 15, 18], 'E3': []},
25: {'RP': [5], 'E1': [6, 8, 13], 'E2': [15, 17], 'E3': []},
26: {'RP': [5], 'E1': [10, 11], 'E2': [15, 17], 'E3': []},
27: {'RP': [5], 'E1': [9, 12, 16], 'E2': [15, 17, 18], 'E3': []},
28: {'RP': [5], 'E1': [11, 13, 15], 'E2': [14], 'E3': []},
29: {'RP': [5], 'E1': [9, 12, 15], 'E2': [14], 'E3': []},
30: {'RP': [5], 'E1': [6, 8, 13, 15], 'E2': [14, 18], 'E3': []},
31: {'RP': [5], 'E1': [10, 11, 15], 'E2': [14, 18], 'E3': []},
32: {'RP': [5], 'E1': [11, 13, 14, 17], 'E2': [15, 16, 18], 'E3': []},
33: {'RP': [5], 'E1': [8, 11, 13], 'E2': [15, 16, 18], 'E3': []},
34: {'RP': [5], 'E1': [6, 9, 12, 15], 'E2': [14, 17], 'E3': []},
35: {'RP': [5], 'E1': [11, 12, 13], 'E2': [14, 17], 'E3': []},
37: {'RP': [5], 'E1': [9, 11, 12], 'E2': [15, 16, 17], 'E3': []},
39: {'RP': [5], 'E1': [7, 9, 12, 14, 18], 'E2': [6, 15], 'E3': []},
41: {'RP': [5], 'E1': [10, 11, 12, 16], 'E2': [13], 'E3': []},
44: {'RP': [5], 'E1': [4, 11, 12, 13, 14], 'E2': [6, 15, 17], 'E3': []},
47: {'RP': [5], 'E1': [8, 9, 11, 12], 'E2': [13, 17], 'E3': []},
50: {'RP': [5], 'E1': [9, 10, 11, 12], 'E2': [14, 15, 18], 'E3': []},
53: {'RP': [5], 'E1': [6, 8, 9, 10, 11], 'E2': [13, 16], 'E3': []},
56: {'RP': [5], 'E1': [8, 9, 10, 15, 14, 17], 'E2': [3, 11, 18], 'E3': []},
59: {'RP': [5], 'E1': [4, 7, 9, 10, 11, 12], 'E2': [13, 15], 'E3': []},
62: {'RP': [5], 'E1': [7, 8, 9, 10, 11, 14], 'E2': [12, 16, 18], 'E3': []},
65: {'RP': [5], 'E1': [7, 8, 9, 10, 12, 13], 'E2': [11, 16, 18], 'E3': []},
}
# Shortstop Error Chart
SHORTSTOP_ERROR_CHART: dict[int, dict[str, List[int]]] = {
0: {'RP': [5], 'E1': [], 'E2': [], 'E3': []},
1: {'RP': [5], 'E1': [18], 'E2': [], 'E3': []},
2: {'RP': [5], 'E1': [3, 18], 'E2': [], 'E3': []},
3: {'RP': [5], 'E1': [17], 'E2': [], 'E3': []},
4: {'RP': [5], 'E1': [17], 'E2': [18], 'E3': []},
5: {'RP': [5], 'E1': [3, 17], 'E2': [18], 'E3': []},
6: {'RP': [5], 'E1': [16], 'E2': [18], 'E3': []},
7: {'RP': [5], 'E1': [3, 16], 'E2': [18], 'E3': []},
8: {'RP': [5], 'E1': [16, 17], 'E2': [18], 'E3': []},
10: {'RP': [5], 'E1': [6, 17], 'E2': [3, 18], 'E3': []},
12: {'RP': [5], 'E1': [15], 'E2': [3, 18], 'E3': []},
14: {'RP': [5], 'E1': [4, 15], 'E2': [17], 'E3': []},
16: {'RP': [5], 'E1': [14], 'E2': [17], 'E3': []},
17: {'RP': [5], 'E1': [15, 16], 'E2': [17], 'E3': []},
18: {'RP': [5], 'E1': [15, 16, 18], 'E2': [17], 'E3': []},
19: {'RP': [5], 'E1': [4, 14], 'E2': [17], 'E3': []},
20: {'RP': [5], 'E1': [4, 15, 16], 'E2': [17], 'E3': []},
21: {'RP': [5], 'E1': [6, 15], 'E2': [17], 'E3': []},
22: {'RP': [5], 'E1': [6, 15], 'E2': [17, 18], 'E3': []},
23: {'RP': [5], 'E1': [3, 13], 'E2': [17, 18], 'E3': []},
24: {'RP': [5], 'E1': [4, 6, 15], 'E2': [17, 18], 'E3': []},
25: {'RP': [5], 'E1': [4, 13], 'E2': [17, 18], 'E3': []},
26: {'RP': [5], 'E1': [12], 'E2': [17, 18], 'E3': []},
27: {'RP': [5], 'E1': [3, 12], 'E2': [17, 18], 'E3': []},
28: {'RP': [5], 'E1': [6, 15, 16], 'E2': [3, 17, 18], 'E3': []},
29: {'RP': [5], 'E1': [11], 'E2': [3, 17, 18], 'E3': []},
30: {'RP': [5], 'E1': [4, 12], 'E2': [3, 17, 18], 'E3': []},
31: {'RP': [5], 'E1': [4, 6, 15, 16], 'E2': [3, 17, 18], 'E3': []},
32: {'RP': [5], 'E1': [13, 15], 'E2': [3, 17, 18], 'E3': []},
33: {'RP': [5], 'E1': [13, 15], 'E2': [16], 'E3': []},
34: {'RP': [5], 'E1': [13, 15, 18], 'E2': [16], 'E3': []},
36: {'RP': [5], 'E1': [13, 15, 17], 'E2': [16], 'E3': []},
38: {'RP': [5], 'E1': [13, 14], 'E2': [16], 'E3': []},
40: {'RP': [5], 'E1': [11, 15], 'E2': [16, 18], 'E3': []},
42: {'RP': [5], 'E1': [12, 14], 'E2': [16, 18], 'E3': []},
44: {'RP': [5], 'E1': [8, 13], 'E2': [16, 18], 'E3': []},
48: {'RP': [5], 'E1': [6, 12, 15], 'E2': [3, 18, 18], 'E3': []},
52: {'RP': [5], 'E1': [11, 13, 18], 'E2': [16, 17], 'E3': []},
56: {'RP': [5], 'E1': [11, 12, 18], 'E2': [16, 17], 'E3': []},
60: {'RP': [5], 'E1': [7, 11, 14], 'E2': [15], 'E3': []},
64: {'RP': [5], 'E1': [6, 9, 12], 'E2': [15, 18], 'E3': []},
68: {'RP': [5], 'E1': [9, 12, 14], 'E2': [15, 18], 'E3': []},
72: {'RP': [5], 'E1': [6, 11, 13, 15], 'E2': [4, 16, 17], 'E3': []},
76: {'RP': [5], 'E1': [9, 12, 13], 'E2': [15, 17], 'E3': []},
80: {'RP': [5], 'E1': [4, 11, 12, 13], 'E2': [15, 17], 'E3': []},
84: {'RP': [5], 'E1': [10, 11, 12], 'E2': [3, 15, 17], 'E3': []},
88: {'RP': [5], 'E1': [9, 11, 12, 16], 'E2': [14], 'E3': []},
}
# Pitcher Error Chart
PITCHER_ERROR_CHART: dict[int, dict[str, List[int]]] = {
0: {'RP': [5], 'E1': [], 'E2': [], 'E3': []},
4: {'RP': [5], 'E1': [14], 'E2': [18], 'E3': []},
6: {'RP': [5], 'E1': [3, 13], 'E2': [18], 'E3': []},
7: {'RP': [5], 'E1': [3, 12], 'E2': [18], 'E3': []},
8: {'RP': [5], 'E1': [6, 15, 16, 17], 'E2': [3, 18], 'E3': []},
10: {'RP': [5], 'E1': [11, 15], 'E2': [3, 18], 'E3': []},
11: {'RP': [5], 'E1': [12, 15, 16], 'E2': [3, 18], 'E3': []},
12: {'RP': [5], 'E1': [6, 12, 15], 'E2': [3, 18], 'E3': []},
13: {'RP': [5], 'E1': [11, 13], 'E2': [17], 'E3': []},
14: {'RP': [5], 'E1': [7, 13, 14], 'E2': [17], 'E3': []},
15: {'RP': [5], 'E1': [4, 11, 12], 'E2': [17], 'E3': []},
16: {'RP': [5], 'E1': [4, 9, 12, 16], 'E2': [17], 'E3': []},
17: {'RP': [5], 'E1': [3, 6, 11, 12], 'E2': [17], 'E3': []},
18: {'RP': [5], 'E1': [11, 12, 14], 'E2': [17], 'E3': []},
19: {'RP': [5], 'E1': [6, 9, 12, 15], 'E2': [17, 18], 'E3': []},
20: {'RP': [5], 'E1': [6, 10, 11, 15], 'E2': [17, 18], 'E3': []},
21: {'RP': [5], 'E1': [7, 11, 13, 14], 'E2': [17, 18], 'E3': []},
22: {'RP': [5], 'E1': [8, 12, 13, 14], 'E2': [17, 18], 'E3': []},
23: {'RP': [5], 'E1': [10, 11, 12, 16], 'E2': [17, 18], 'E3': []},
24: {'RP': [5], 'E1': [10, 11, 12, 15], 'E2': [17, 18], 'E3': []},
26: {'RP': [5], 'E1': [9, 12, 13, 14, 15], 'E2': [3, 17, 18], 'E3': []},
27: {'RP': [5], 'E1': [10, 11, 12, 13], 'E2': [3, 17, 18], 'E3': []},
28: {'RP': [5], 'E1': [9, 10, 11, 12], 'E2': [3, 17, 18], 'E3': []},
30: {'RP': [5], 'E1': [3, 10, 11, 12, 13, 15], 'E2': [16], 'E3': []},
31: {'RP': [5], 'E1': [10, 11, 12, 13, 14], 'E2': [16], 'E3': []},
33: {'RP': [5], 'E1': [3, 8, 10, 11, 12, 13], 'E2': [16], 'E3': []},
34: {'RP': [5], 'E1': [9, 10, 11, 12, 13], 'E2': [16, 18], 'E3': []},
35: {'RP': [5], 'E1': [9, 10, 11, 12, 14, 15], 'E2': [16, 18], 'E3': []},
36: {'RP': [5], 'E1': [3, 4, 6, 7, 9, 10, 11, 12], 'E2': [16, 18], 'E3': []},
38: {'RP': [5], 'E1': [6, 8, 10, 11, 12, 13, 15], 'E2': [16, 18], 'E3': []},
39: {'RP': [5], 'E1': [6, 7, 8, 9, 10, 12, 13], 'E2': [3, 16, 18], 'E3': []},
40: {'RP': [5], 'E1': [4, 6, 9, 10, 11, 12, 13, 15], 'E2': [3, 16, 18], 'E3': []},
42: {'RP': [5], 'E1': [7, 9, 10, 11, 12, 13, 14], 'E2': [3, 16, 18], 'E3': []},
43: {'RP': [5], 'E1': [6, 7, 8, 9, 10, 12, 13, 14], 'E2': [3, 16, 18], 'E3': []},
44: {'RP': [5], 'E1': [3, 7, 8, 9, 10, 11, 12, 13], 'E2': [16, 17], 'E3': []},
46: {'RP': [5], 'E1': [6, 8, 9, 10, 11, 12, 13, 15], 'E2': [16, 17, 18], 'E3': []},
47: {'RP': [5], 'E1': [7, 8, 9, 10, 11, 12, 13, 15], 'E2': [16, 17, 18], 'E3': []},
48: {'RP': [5], 'E1': [7, 8, 9, 10, 11, 12, 13, 14], 'E2': [16, 17, 18], 'E3': []},
50: {'RP': [5], 'E1': [7, 8, 9, 10, 11, 12, 13, 14], 'E2': [15, 17], 'E3': []},
51: {'RP': [5], 'E1': [7, 8, 9, 10, 11, 12, 13, 14], 'E2': [15, 16], 'E3': []},
}
# ============================================================================
# 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'])
"""
if not runner_bases:
return []
holding_positions = []
mif_vs_batter = '2B' if batter_handedness.lower() == 'r' else 'SS'
if 1 in runner_bases:
holding_positions.append('1B')
holding_positions.append(mif_vs_batter)
if 2 in runner_bases and mif_vs_batter not in holding_positions:
holding_positions.append(mif_vs_batter)
if 3 in runner_bases:
holding_positions.append('3B')
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]