Add foundational data structures for X-Check play resolution system: Models Added: - PositionRating: Defensive ratings (range 1-5, error 0-88) for X-Check resolution - XCheckResult: Dataclass tracking complete X-Check resolution flow with dice rolls, conversions (SPD test, G2#/G3#→SI2), error results, and final outcomes - BasePlayer.active_position_rating: Optional field for current defensive position Enums Extended: - PlayOutcome.X_CHECK: New outcome type requiring special resolution - PlayOutcome.is_x_check(): Helper method for type checking Documentation Enhanced: - Play.check_pos: Documented as X-Check position identifier - Play.hit_type: Documented with examples (single_2_plus_error_1, etc.) Utilities Added: - app/core/cache.py: Redis cache key helpers for player positions and game state Implementation Planning: - Complete 6-phase implementation plan (3A-3F) documented in .claude/implementation/ - Phase 3A complete with all acceptance criteria met - Zero breaking changes, all existing tests passing Next: Phase 3B will add defense tables, error charts, and advancement logic 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
583 lines
19 KiB
Markdown
583 lines
19 KiB
Markdown
# Phase 3D: X-Check Runner Advancement Tables
|
|
|
|
**Status**: Not Started
|
|
**Estimated Effort**: 6-8 hours (table-heavy)
|
|
**Dependencies**: Phase 3C (Resolution Logic)
|
|
|
|
## Overview
|
|
|
|
Implement complete runner advancement tables for all X-Check result types. Each combination of (base_result, error_result, on_base_code, defender_in) has specific advancement rules.
|
|
|
|
This phase involves:
|
|
- Groundball advancement (G1, G2, G3) with defender_in and error variations
|
|
- Flyball advancement (F1, F2, F3) with error variations
|
|
- Hit advancement (SI1, SI2, DO2, DO3, TR3) with error bonuses
|
|
- Out advancement (FO, PO) with error overrides
|
|
|
|
## Tasks
|
|
|
|
### 1. Create X-Check Advancement Tables Module
|
|
|
|
**File**: `backend/app/core/x_check_advancement_tables.py` (NEW FILE)
|
|
|
|
```python
|
|
"""
|
|
X-Check runner advancement tables.
|
|
|
|
Each X-Check result type has specific advancement rules based on:
|
|
- on_base_code: Current runner configuration
|
|
- defender_in: Whether defender was playing in
|
|
- error_result: NO, E1, E2, E3, RP
|
|
|
|
Author: Claude
|
|
Date: 2025-11-01
|
|
"""
|
|
import logging
|
|
from typing import List, Dict, Tuple
|
|
from app.models.game_models import RunnerMovement, AdvancementResult
|
|
from app.core.runner_advancement import GroundballResultType
|
|
|
|
logger = logging.getLogger(f'{__name__}')
|
|
|
|
# ============================================================================
|
|
# GROUNDBALL ADVANCEMENT TABLES
|
|
# ============================================================================
|
|
# Structure: {on_base_code: {(defender_in, error_result): GroundballResultType}}
|
|
#
|
|
# These tables cross-reference:
|
|
# - on_base_code (0-7)
|
|
# - defender_in (True/False)
|
|
# - error_result ('NO', 'E1', 'E2', 'E3', 'RP')
|
|
#
|
|
# Result is a GroundballResultType which feeds into existing groundball_X() functions
|
|
|
|
# TODO: Fill these tables with actual data from rulebook
|
|
# For now, placeholders with basic logic
|
|
|
|
G1_ADVANCEMENT_TABLE: Dict[int, Dict[Tuple[bool, str], GroundballResultType]] = {
|
|
# on_base_code 0 (bases empty)
|
|
0: {
|
|
(False, 'NO'): GroundballResultType.GROUNDOUT_ROUTINE,
|
|
(False, 'E1'): GroundballResultType.SAFE_ALL_ADVANCE_ONE,
|
|
(False, 'E2'): GroundballResultType.SAFE_ALL_ADVANCE_TWO,
|
|
(False, 'E3'): GroundballResultType.SAFE_ALL_ADVANCE_THREE,
|
|
(False, 'RP'): GroundballResultType.RARE_PLAY,
|
|
(True, 'NO'): GroundballResultType.GROUNDOUT_ROUTINE,
|
|
(True, 'E1'): GroundballResultType.SAFE_ALL_ADVANCE_ONE,
|
|
(True, 'E2'): GroundballResultType.SAFE_ALL_ADVANCE_TWO,
|
|
(True, 'E3'): GroundballResultType.SAFE_ALL_ADVANCE_THREE,
|
|
(True, 'RP'): GroundballResultType.RARE_PLAY,
|
|
},
|
|
# on_base_code 1 (R1 only)
|
|
1: {
|
|
(False, 'NO'): GroundballResultType.GROUNDOUT_DP_ATTEMPT,
|
|
(False, 'E1'): GroundballResultType.SAFE_ALL_ADVANCE_ONE,
|
|
(False, 'E2'): GroundballResultType.SAFE_ALL_ADVANCE_TWO,
|
|
(False, 'E3'): GroundballResultType.SAFE_ALL_ADVANCE_THREE,
|
|
(False, 'RP'): GroundballResultType.RARE_PLAY,
|
|
(True, 'NO'): GroundballResultType.FORCE_AT_THIRD, # Infield in
|
|
(True, 'E1'): GroundballResultType.SAFE_ALL_ADVANCE_ONE,
|
|
(True, 'E2'): GroundballResultType.SAFE_ALL_ADVANCE_TWO,
|
|
(True, 'E3'): GroundballResultType.SAFE_ALL_ADVANCE_THREE,
|
|
(True, 'RP'): GroundballResultType.RARE_PLAY,
|
|
},
|
|
# TODO: Add codes 2-7
|
|
}
|
|
|
|
G2_ADVANCEMENT_TABLE: Dict[int, Dict[Tuple[bool, str], GroundballResultType]] = {
|
|
# Similar structure to G1
|
|
# TODO: Fill with actual data
|
|
}
|
|
|
|
G3_ADVANCEMENT_TABLE: Dict[int, Dict[Tuple[bool, str], GroundballResultType]] = {
|
|
# Similar structure to G1
|
|
# TODO: Fill with actual data
|
|
}
|
|
|
|
|
|
def get_groundball_advancement(
|
|
result_type: str, # 'G1', 'G2', or 'G3'
|
|
on_base_code: int,
|
|
defender_in: bool,
|
|
error_result: str
|
|
) -> GroundballResultType:
|
|
"""
|
|
Get GroundballResultType for X-Check groundball.
|
|
|
|
Args:
|
|
result_type: 'G1', 'G2', or 'G3'
|
|
on_base_code: Current base situation (0-7)
|
|
defender_in: Is defender playing in?
|
|
error_result: 'NO', 'E1', 'E2', 'E3', 'RP'
|
|
|
|
Returns:
|
|
GroundballResultType to pass to existing groundball functions
|
|
|
|
Raises:
|
|
ValueError: If parameters invalid
|
|
"""
|
|
# Select table
|
|
tables = {
|
|
'G1': G1_ADVANCEMENT_TABLE,
|
|
'G2': G2_ADVANCEMENT_TABLE,
|
|
'G3': G3_ADVANCEMENT_TABLE,
|
|
}
|
|
|
|
if result_type not in tables:
|
|
raise ValueError(f"Unknown groundball type: {result_type}")
|
|
|
|
table = tables[result_type]
|
|
|
|
# Lookup
|
|
key = (defender_in, error_result)
|
|
|
|
if on_base_code not in table:
|
|
raise ValueError(f"on_base_code {on_base_code} not in {result_type} table")
|
|
|
|
if key not in table[on_base_code]:
|
|
raise ValueError(
|
|
f"Key {key} not in {result_type} table for on_base_code {on_base_code}"
|
|
)
|
|
|
|
return table[on_base_code][key]
|
|
|
|
|
|
# ============================================================================
|
|
# FLYBALL ADVANCEMENT TABLES
|
|
# ============================================================================
|
|
# Flyballs are simpler - only cross-reference on_base_code and error_result
|
|
# (No defender_in parameter)
|
|
|
|
# Structure: {on_base_code: {error_result: List[RunnerMovement]}}
|
|
|
|
F1_ADVANCEMENT_TABLE: Dict[int, Dict[str, List[RunnerMovement]]] = {
|
|
# on_base_code 0 (bases empty)
|
|
0: {
|
|
'NO': [], # Out, no runners
|
|
'E1': [RunnerMovement(from_base=0, to_base=1, is_out=False)], # Batter to 1B
|
|
'E2': [RunnerMovement(from_base=0, to_base=2, is_out=False)], # Batter to 2B
|
|
'E3': [RunnerMovement(from_base=0, to_base=3, is_out=False)], # Batter to 3B
|
|
'RP': [], # Rare play - TODO: specific advancement
|
|
},
|
|
# on_base_code 1 (R1 only)
|
|
1: {
|
|
'NO': [
|
|
# F1 = deep fly, R1 advances
|
|
RunnerMovement(from_base=1, to_base=2, is_out=False)
|
|
],
|
|
'E1': [
|
|
RunnerMovement(from_base=1, to_base=2, is_out=False),
|
|
RunnerMovement(from_base=0, to_base=1, is_out=False),
|
|
],
|
|
'E2': [
|
|
RunnerMovement(from_base=1, to_base=3, is_out=False),
|
|
RunnerMovement(from_base=0, to_base=2, is_out=False),
|
|
],
|
|
'E3': [
|
|
RunnerMovement(from_base=1, to_base=4, is_out=False), # R1 scores
|
|
RunnerMovement(from_base=0, to_base=3, is_out=False),
|
|
],
|
|
'RP': [], # TODO
|
|
},
|
|
# TODO: Add codes 2-7
|
|
}
|
|
|
|
F2_ADVANCEMENT_TABLE: Dict[int, Dict[str, List[RunnerMovement]]] = {
|
|
# Similar structure
|
|
# TODO: Fill with actual data
|
|
}
|
|
|
|
F3_ADVANCEMENT_TABLE: Dict[int, Dict[str, List[RunnerMovement]]] = {
|
|
# Similar structure
|
|
# TODO: Fill with actual data
|
|
}
|
|
|
|
|
|
def get_flyball_advancement(
|
|
result_type: str, # 'F1', 'F2', or 'F3'
|
|
on_base_code: int,
|
|
error_result: str
|
|
) -> List[RunnerMovement]:
|
|
"""
|
|
Get runner movements for X-Check flyball.
|
|
|
|
Args:
|
|
result_type: 'F1', 'F2', or 'F3'
|
|
on_base_code: Current base situation (0-7)
|
|
error_result: 'NO', 'E1', 'E2', 'E3', 'RP'
|
|
|
|
Returns:
|
|
List of RunnerMovements
|
|
|
|
Raises:
|
|
ValueError: If parameters invalid
|
|
"""
|
|
# Select table
|
|
tables = {
|
|
'F1': F1_ADVANCEMENT_TABLE,
|
|
'F2': F2_ADVANCEMENT_TABLE,
|
|
'F3': F3_ADVANCEMENT_TABLE,
|
|
}
|
|
|
|
if result_type not in tables:
|
|
raise ValueError(f"Unknown flyball type: {result_type}")
|
|
|
|
table = tables[result_type]
|
|
|
|
# Lookup
|
|
if on_base_code not in table:
|
|
raise ValueError(f"on_base_code {on_base_code} not in {result_type} table")
|
|
|
|
if error_result not in table[on_base_code]:
|
|
raise ValueError(
|
|
f"error_result {error_result} not in {result_type} table for on_base_code {on_base_code}"
|
|
)
|
|
|
|
return table[on_base_code][error_result]
|
|
|
|
|
|
# ============================================================================
|
|
# HIT ADVANCEMENT (SI1, SI2, DO2, DO3, TR3)
|
|
# ============================================================================
|
|
# Hits with errors: base advancement + error bonus
|
|
|
|
def get_hit_advancement(
|
|
result_type: str, # 'SI1', 'SI2', 'DO2', 'DO3', 'TR3'
|
|
on_base_code: int,
|
|
error_result: str
|
|
) -> List[RunnerMovement]:
|
|
"""
|
|
Get runner movements for X-Check hit + error.
|
|
|
|
For hits, we combine:
|
|
- Base hit advancement (use existing single/double advancement)
|
|
- Error bonus (all runners advance N additional bases)
|
|
|
|
Args:
|
|
result_type: Hit type
|
|
on_base_code: Current base situation
|
|
error_result: 'NO', 'E1', 'E2', 'E3', 'RP'
|
|
|
|
Returns:
|
|
List of RunnerMovements
|
|
|
|
TODO: Implement proper hit advancement with error bonuses
|
|
For now, placeholder
|
|
"""
|
|
movements = []
|
|
|
|
# Base advancement for hit type
|
|
base_advances = {
|
|
'SI1': 1,
|
|
'SI2': 1,
|
|
'DO2': 2,
|
|
'DO3': 2,
|
|
'TR3': 3,
|
|
}
|
|
|
|
batter_advances = base_advances.get(result_type, 1)
|
|
|
|
# Error bonus
|
|
error_bonus = {
|
|
'NO': 0,
|
|
'E1': 1,
|
|
'E2': 2,
|
|
'E3': 3,
|
|
'RP': 0, # Rare play handled separately
|
|
}
|
|
|
|
bonus = error_bonus.get(error_result, 0)
|
|
|
|
# Batter advancement
|
|
batter_final = min(batter_advances + bonus, 4)
|
|
movements.append(RunnerMovement(from_base=0, to_base=batter_final, is_out=False))
|
|
|
|
# TODO: Advance existing runners based on hit type + error
|
|
# This requires knowing current runner positions
|
|
|
|
return movements
|
|
|
|
|
|
# ============================================================================
|
|
# OUT ADVANCEMENT (FO, PO)
|
|
# ============================================================================
|
|
|
|
def get_out_advancement(
|
|
result_type: str, # 'FO' or 'PO'
|
|
on_base_code: int,
|
|
error_result: str
|
|
) -> List[RunnerMovement]:
|
|
"""
|
|
Get runner movements for X-Check out (foul out or popout).
|
|
|
|
If error: all runners advance N bases (error overrides out)
|
|
If no error: batter out, runners hold (or tag if deep enough)
|
|
|
|
Args:
|
|
result_type: 'FO' or 'PO'
|
|
on_base_code: Current base situation
|
|
error_result: 'NO', 'E1', 'E2', 'E3', 'RP'
|
|
|
|
Returns:
|
|
List of RunnerMovements
|
|
"""
|
|
if error_result == 'NO':
|
|
# Simple out, no advancement
|
|
return []
|
|
|
|
# Error on out - all runners advance
|
|
error_advances = {
|
|
'E1': 1,
|
|
'E2': 2,
|
|
'E3': 3,
|
|
'RP': 0, # Rare play - TODO
|
|
}
|
|
|
|
advances = error_advances.get(error_result, 0)
|
|
|
|
movements = [
|
|
RunnerMovement(from_base=0, to_base=advances, is_out=False)
|
|
]
|
|
|
|
# TODO: Advance existing runners
|
|
# Need to know which bases are occupied
|
|
|
|
return movements
|
|
```
|
|
|
|
### 2. Update Runner Advancement Functions
|
|
|
|
**File**: `backend/app/core/runner_advancement.py`
|
|
|
|
**Replace placeholder functions** with full implementations:
|
|
|
|
```python
|
|
from app.core.x_check_advancement_tables import (
|
|
get_groundball_advancement,
|
|
get_flyball_advancement,
|
|
get_hit_advancement,
|
|
get_out_advancement,
|
|
)
|
|
|
|
# ============================================================================
|
|
# X-CHECK RUNNER ADVANCEMENT
|
|
# ============================================================================
|
|
|
|
def x_check_g1(
|
|
on_base_code: int,
|
|
defender_in: bool,
|
|
error_result: str
|
|
) -> AdvancementResult:
|
|
"""
|
|
Runner advancement for X-Check G1 result.
|
|
|
|
Uses G1 advancement table to get GroundballResultType,
|
|
then calls appropriate groundball_X() function.
|
|
|
|
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
|
|
"""
|
|
gb_type = get_groundball_advancement('G1', on_base_code, defender_in, error_result)
|
|
|
|
# Map GroundballResultType to existing function
|
|
# These functions already exist: groundball_1 through groundball_13
|
|
gb_func_map = {
|
|
GroundballResultType.GROUNDOUT_ROUTINE: groundball_1,
|
|
GroundballResultType.GROUNDOUT_DP_ATTEMPT: groundball_2,
|
|
GroundballResultType.FORCE_AT_THIRD: groundball_3,
|
|
# ... add full mapping based on existing GroundballResultType enum
|
|
}
|
|
|
|
if gb_type in gb_func_map:
|
|
return gb_func_map[gb_type](on_base_code)
|
|
|
|
# Fallback
|
|
logger.warning(f"Unknown GroundballResultType: {gb_type}, using groundball_1")
|
|
return groundball_1(on_base_code)
|
|
|
|
|
|
def x_check_g2(on_base_code: int, defender_in: bool, error_result: str) -> AdvancementResult:
|
|
"""Runner advancement for X-Check G2 result."""
|
|
gb_type = get_groundball_advancement('G2', on_base_code, defender_in, error_result)
|
|
# Similar logic to x_check_g1
|
|
# TODO: Implement full mapping
|
|
return AdvancementResult(movements=[], requires_decision=False)
|
|
|
|
|
|
def x_check_g3(on_base_code: int, defender_in: bool, error_result: str) -> AdvancementResult:
|
|
"""Runner advancement for X-Check G3 result."""
|
|
gb_type = get_groundball_advancement('G3', on_base_code, defender_in, error_result)
|
|
# Similar logic to x_check_g1
|
|
# TODO: Implement full mapping
|
|
return AdvancementResult(movements=[], requires_decision=False)
|
|
|
|
|
|
def x_check_f1(on_base_code: int, error_result: str) -> AdvancementResult:
|
|
"""Runner advancement for X-Check F1 result."""
|
|
movements = get_flyball_advancement('F1', on_base_code, error_result)
|
|
return AdvancementResult(movements=movements, requires_decision=False)
|
|
|
|
|
|
def x_check_f2(on_base_code: int, error_result: str) -> AdvancementResult:
|
|
"""Runner advancement for X-Check F2 result."""
|
|
movements = get_flyball_advancement('F2', on_base_code, error_result)
|
|
return AdvancementResult(movements=movements, requires_decision=False)
|
|
|
|
|
|
def x_check_f3(on_base_code: int, error_result: str) -> AdvancementResult:
|
|
"""Runner advancement for X-Check F3 result."""
|
|
movements = get_flyball_advancement('F3', on_base_code, error_result)
|
|
return AdvancementResult(movements=movements, requires_decision=False)
|
|
|
|
|
|
def x_check_si1(on_base_code: int, error_result: str) -> AdvancementResult:
|
|
"""Runner advancement for X-Check SI1 + error."""
|
|
movements = get_hit_advancement('SI1', on_base_code, error_result)
|
|
return AdvancementResult(movements=movements, requires_decision=False)
|
|
|
|
|
|
def x_check_si2(on_base_code: int, error_result: str) -> AdvancementResult:
|
|
"""Runner advancement for X-Check SI2 + error."""
|
|
movements = get_hit_advancement('SI2', on_base_code, error_result)
|
|
return AdvancementResult(movements=movements, requires_decision=False)
|
|
|
|
|
|
def x_check_do2(on_base_code: int, error_result: str) -> AdvancementResult:
|
|
"""Runner advancement for X-Check DO2 + error."""
|
|
movements = get_hit_advancement('DO2', on_base_code, error_result)
|
|
return AdvancementResult(movements=movements, requires_decision=False)
|
|
|
|
|
|
def x_check_do3(on_base_code: int, error_result: str) -> AdvancementResult:
|
|
"""Runner advancement for X-Check DO3 + error."""
|
|
movements = get_hit_advancement('DO3', on_base_code, error_result)
|
|
return AdvancementResult(movements=movements, requires_decision=False)
|
|
|
|
|
|
def x_check_tr3(on_base_code: int, error_result: str) -> AdvancementResult:
|
|
"""Runner advancement for X-Check TR3 + error."""
|
|
movements = get_hit_advancement('TR3', on_base_code, error_result)
|
|
return AdvancementResult(movements=movements, requires_decision=False)
|
|
|
|
|
|
def x_check_fo(on_base_code: int, error_result: str) -> AdvancementResult:
|
|
"""Runner advancement for X-Check FO (foul out)."""
|
|
movements = get_out_advancement('FO', on_base_code, error_result)
|
|
outs = 0 if error_result != 'NO' else 1
|
|
return AdvancementResult(movements=movements, requires_decision=False)
|
|
|
|
|
|
def x_check_po(on_base_code: int, error_result: str) -> AdvancementResult:
|
|
"""Runner advancement for X-Check PO (popout)."""
|
|
movements = get_out_advancement('PO', on_base_code, error_result)
|
|
outs = 0 if error_result != 'NO' else 1
|
|
return AdvancementResult(movements=movements, requires_decision=False)
|
|
```
|
|
|
|
### 3. Update PlayResolver to Call Correct Functions
|
|
|
|
**File**: `backend/app/core/play_resolver.py`
|
|
|
|
**Update _get_x_check_advancement** to handle all result types:
|
|
|
|
```python
|
|
def _get_x_check_advancement(
|
|
self,
|
|
converted_result: str,
|
|
error_result: str,
|
|
on_base_code: int,
|
|
defender_in: bool
|
|
) -> AdvancementResult:
|
|
"""
|
|
Get runner advancement for X-Check result.
|
|
|
|
Calls appropriate advancement function based on result type.
|
|
|
|
Args:
|
|
converted_result: Base result after conversions (G1, F2, SI2, etc.)
|
|
error_result: 'NO', 'E1', 'E2', 'E3', 'RP'
|
|
on_base_code: Current base situation
|
|
defender_in: Was defender playing in?
|
|
|
|
Returns:
|
|
AdvancementResult
|
|
"""
|
|
from app.core.runner_advancement import (
|
|
x_check_g1, x_check_g2, x_check_g3,
|
|
x_check_f1, x_check_f2, x_check_f3,
|
|
x_check_si1, x_check_si2,
|
|
x_check_do2, x_check_do3, x_check_tr3,
|
|
x_check_fo, x_check_po,
|
|
)
|
|
|
|
# Map result to function
|
|
advancement_funcs = {
|
|
# Groundballs (need defender_in)
|
|
'G1': lambda: x_check_g1(on_base_code, defender_in, error_result),
|
|
'G2': lambda: x_check_g2(on_base_code, defender_in, error_result),
|
|
'G3': lambda: x_check_g3(on_base_code, defender_in, error_result),
|
|
# Flyballs (no defender_in)
|
|
'F1': lambda: x_check_f1(on_base_code, error_result),
|
|
'F2': lambda: x_check_f2(on_base_code, error_result),
|
|
'F3': lambda: x_check_f3(on_base_code, error_result),
|
|
# Hits
|
|
'SI1': lambda: x_check_si1(on_base_code, error_result),
|
|
'SI2': lambda: x_check_si2(on_base_code, error_result),
|
|
'DO2': lambda: x_check_do2(on_base_code, error_result),
|
|
'DO3': lambda: x_check_do3(on_base_code, error_result),
|
|
'TR3': lambda: x_check_tr3(on_base_code, error_result),
|
|
# Outs
|
|
'FO': lambda: x_check_fo(on_base_code, error_result),
|
|
'PO': lambda: x_check_po(on_base_code, error_result),
|
|
}
|
|
|
|
if converted_result in advancement_funcs:
|
|
return advancement_funcs[converted_result]()
|
|
|
|
# Fallback
|
|
logger.warning(f"Unknown X-Check result: {converted_result}, no advancement")
|
|
return AdvancementResult(movements=[], requires_decision=False)
|
|
```
|
|
|
|
## Testing Requirements
|
|
|
|
1. **Unit Tests**: `tests/core/test_x_check_advancement_tables.py`
|
|
- Test get_groundball_advancement() for all combinations
|
|
- Test get_flyball_advancement() for all combinations
|
|
- Test get_hit_advancement() with errors
|
|
- Test get_out_advancement() with errors
|
|
|
|
2. **Integration Tests**: `tests/integration/test_x_check_advancement.py`
|
|
- Test complete advancement for each result type
|
|
- Test error bonuses applied correctly
|
|
- Test defender_in affects groundball results
|
|
|
|
## Acceptance Criteria
|
|
|
|
- [ ] x_check_advancement_tables.py created
|
|
- [ ] All groundball tables complete (G1, G2, G3)
|
|
- [ ] All flyball tables complete (F1, F2, F3)
|
|
- [ ] Hit advancement with errors working
|
|
- [ ] Out advancement with errors working
|
|
- [ ] All x_check_* functions implemented in runner_advancement.py
|
|
- [ ] PlayResolver._get_x_check_advancement() updated
|
|
- [ ] All unit tests pass
|
|
- [ ] All integration tests pass
|
|
|
|
## Notes
|
|
|
|
- This phase requires rulebook data for all advancement tables
|
|
- Tables marked TODO need actual values filled in
|
|
- GroundballResultType enum may need new values for X-Check specific results
|
|
- Error bonuses on hits need careful testing (batter advances + runners advance)
|
|
- Rare Play (RP) advancement needs special handling per result type
|
|
|
|
## Next Phase
|
|
|
|
After completion, proceed to **Phase 3E: WebSocket Events & UI Integration**
|