CLAUDE: Integrate X-Check advancement with full GameState support

Updated X-Check runner advancement functions to properly delegate to
existing result handlers for non-error cases.

Changes:
- Updated x_check_g1/g2/g3 signatures to accept GameState, hit_location,
  and defensive_decision parameters
- Updated x_check_f1/f2/f3 signatures to accept GameState and hit_location
- Implemented delegation logic: error cases use simple tables, non-error
  cases delegate to existing tested result handlers (_execute_result,
  _fb_result_*)
- Updated PlayResolver._get_x_check_advancement() to pass new parameters
- Updated all tests to provide required GameState fixtures

Benefits:
- Reuses 13 existing groundball + 4 flyball result handlers (DRY)
- No DP probability needed - X-Check d20 already tested defender
- Full game context: real lineup IDs, outs count, conditional logic
- Error cases remain simple and efficient

Test Results: 264/265 core tests passing (1 pre-existing dice failure)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2025-11-03 00:21:52 -06:00
parent 5f42576694
commit fc0e2f100c
4 changed files with 664 additions and 75 deletions

View File

@ -18,7 +18,7 @@ import pendulum
from app.core.dice import dice_system
from app.core.roll_types import AbRoll, RollType
from app.core.runner_advancement import RunnerAdvancement
from app.core.runner_advancement import AdvancementResult, RunnerAdvancement
from app.models.game_models import GameState, DefensiveDecision, OffensiveDecision, ManualOutcomeSubmission, XCheckResult
from app.config import PlayOutcome, get_league_config
from app.config.result_charts import calculate_hit_location, PdAutoResultChart, ManualResultChart
@ -712,13 +712,35 @@ class PlayResolver:
spd_test_passed=spd_test_passed,
)
# Step 10: Get runner advancement (placeholder)
# Step 10: Get runner advancement
defender_in = (adjusted_range > defender_range)
# TODO: Will use _get_x_check_advancement when advancement tables are ready
runners_advanced = []
runs_scored = 0
outs_recorded = 1 if final_outcome.is_out() and error_result == 'NO' else 0
batter_result = None if outs_recorded > 0 else 1 # Simplified
# Call appropriate x_check function based on converted_result
advancement = self._get_x_check_advancement(
converted_result=converted_result,
error_result=error_result,
state=state,
defender_in=defender_in,
hit_location=position,
defensive_decision=defensive_decision
)
# Convert AdvancementResult to PlayResult format
runners_advanced = [
(movement.from_base, movement.to_base)
for movement in advancement.movements
if not movement.is_out and movement.from_base > 0 # Exclude batter, include only runners
]
# Extract batter result from movements
batter_movement = next(
(m for m in advancement.movements if m.from_base == 0),
None
)
batter_result = batter_movement.to_base if batter_movement and not batter_movement.is_out else None
runs_scored = advancement.runs_scored
outs_recorded = advancement.outs_recorded
# Step 11: Create PlayResult
return PlayResult(
@ -885,6 +907,230 @@ class PlayResolver:
logger.debug(f"Error chart: 3d6={d6_roll} → NO")
return 'NO'
def _get_x_check_advancement(
self,
converted_result: str,
error_result: str,
state: 'GameState',
defender_in: bool,
hit_location: str,
defensive_decision: 'DefensiveDecision'
) -> 'AdvancementResult':
"""
Get runner advancement for X-Check result.
Calls appropriate x_check function based on result type:
- G1, G2, G3: Groundball advancement (uses x_check tables)
- F1, F2, F3: Flyball advancement (uses x_check tables)
- SI1, SI2, DO2, DO3, TR3: Hit advancement (uses existing methods + error bonuses)
- FO, PO: Out advancement (error overrides out, so just error advancement)
Args:
converted_result: Result after SPD test and hash conversion
error_result: Error type (NO, E1, E2, E3, RP)
state: Current game state (for runner positions)
defender_in: Whether defender was playing in
hit_location: Position where ball was hit (fielder's position)
defensive_decision: Defensive positioning decision
Returns:
AdvancementResult with runner movements
Raises:
ValueError: If result type is not recognized
"""
from app.core.runner_advancement import (
x_check_g1, x_check_g2, x_check_g3,
x_check_f1, x_check_f2, x_check_f3,
AdvancementResult, RunnerMovement
)
on_base_code = state.current_on_base_code
# Groundball results
if converted_result == 'G1':
return x_check_g1(on_base_code, defender_in, error_result, state, hit_location, defensive_decision)
elif converted_result == 'G2':
return x_check_g2(on_base_code, defender_in, error_result, state, hit_location, defensive_decision)
elif converted_result == 'G3':
return x_check_g3(on_base_code, defender_in, error_result, state, hit_location, defensive_decision)
# Flyball results
elif converted_result == 'F1':
return x_check_f1(on_base_code, error_result, state, hit_location)
elif converted_result == 'F2':
return x_check_f2(on_base_code, error_result, state, hit_location)
elif converted_result == 'F3':
return x_check_f3(on_base_code, error_result, state, hit_location)
# Hit results - use existing advancement methods + error bonuses
elif converted_result in ['SI1', 'SI2', 'DO2', 'DO3', 'TR3']:
return self._get_hit_advancement_with_error(converted_result, error_result, state)
# Out results - error overrides out, so just error advancement
elif converted_result in ['FO', 'PO']:
return self._get_out_advancement_with_error(error_result, state)
else:
raise ValueError(f"Unknown X-Check result type: {converted_result}")
def _get_hit_advancement_with_error(
self,
hit_type: str,
error_result: str,
state: 'GameState'
) -> 'AdvancementResult':
"""
Get runner advancement for X-Check hit with error.
Uses existing advancement methods and adds error bonuses:
- NO: No bonus
- E1: +1 base
- E2: +2 bases
- E3: +3 bases
- RP: Treat as E3
Args:
hit_type: SI1, SI2, DO2, DO3, or TR3
error_result: Error type
state: Current game state (for runner positions)
Returns:
AdvancementResult with movements
"""
from app.core.runner_advancement import AdvancementResult, RunnerMovement
# Get base advancement (without error)
if hit_type == 'SI1':
base_advances = self._advance_on_single_1(state)
batter_reaches = 1
elif hit_type == 'SI2':
base_advances = self._advance_on_single_2(state)
batter_reaches = 1
elif hit_type == 'DO2':
base_advances = self._advance_on_double_2(state)
batter_reaches = 2
elif hit_type == 'DO3':
base_advances = self._advance_on_double_3(state)
batter_reaches = 3
elif hit_type == 'TR3':
base_advances = self._advance_on_triple(state)
batter_reaches = 3
else:
raise ValueError(f"Unknown hit type: {hit_type}")
# Apply error bonus
error_bonus = {'NO': 0, 'E1': 1, 'E2': 2, 'E3': 3, 'RP': 3}[error_result]
movements = []
runs_scored = 0
# Add batter movement (with error bonus)
batter_final = min(batter_reaches + error_bonus, 4)
if batter_final == 4:
runs_scored += 1
movements.append(RunnerMovement(
lineup_id=0, # Placeholder - will be set by game engine
from_base=0,
to_base=batter_final,
is_out=False
))
# Add runner movements (with error bonus)
for from_base, to_base in base_advances:
final_base = min(to_base + error_bonus, 4)
if final_base == 4:
runs_scored += 1
movements.append(RunnerMovement(
lineup_id=0, # Placeholder
from_base=from_base,
to_base=final_base,
is_out=False
))
return AdvancementResult(
movements=movements,
outs_recorded=0,
runs_scored=runs_scored,
result_type=None,
description=f"X-Check {hit_type} + {error_result}"
)
def _get_out_advancement_with_error(
self,
error_result: str,
state: 'GameState'
) -> 'AdvancementResult':
"""
Get runner advancement for X-Check out with error.
When an out has an error, the out is negated and it becomes an error play.
Runners advance based on error severity:
- E1: All advance 1 base
- E2: All advance 2 bases
- E3: All advance 3 bases
- RP: All advance 3 bases
Args:
error_result: Error type (should not be 'NO' for outs)
state: Current game state (for runner positions)
Returns:
AdvancementResult with movements
"""
from app.core.runner_advancement import AdvancementResult, RunnerMovement
if error_result == 'NO':
# No error on out - just record out
return AdvancementResult(
movements=[RunnerMovement(lineup_id=0, from_base=0, to_base=0, is_out=True)],
outs_recorded=1,
runs_scored=0,
result_type=None,
description="X-Check out (no error)"
)
# Error prevents out - batter and runners advance
error_bonus = {'E1': 1, 'E2': 2, 'E3': 3, 'RP': 3}[error_result]
movements = []
runs_scored = 0
# Batter reaches base based on error severity
batter_final = min(error_bonus, 4)
if batter_final == 4:
runs_scored += 1
movements.append(RunnerMovement(
lineup_id=0,
from_base=0,
to_base=batter_final,
is_out=False
))
# All runners advance by error bonus
for base, _ in state.get_all_runners():
final_base = min(base + error_bonus, 4)
if final_base == 4:
runs_scored += 1
movements.append(RunnerMovement(
lineup_id=0,
from_base=base,
to_base=final_base,
is_out=False
))
return AdvancementResult(
movements=movements,
outs_recorded=0,
runs_scored=runs_scored,
result_type=None,
description=f"X-Check out + {error_result} (error overrides out)"
)
def _advance_on_triple(self, state: 'GameState') -> List[tuple[int, int]]:
"""Calculate runner advancement on triple (all runners score)."""
return [(base, 4) for base, _ in state.get_all_runners()]
def _determine_final_x_check_outcome(
self,
converted_result: str,

View File

@ -1441,7 +1441,10 @@ class RunnerAdvancement:
def x_check_g1(
on_base_code: int,
defender_in: bool,
error_result: str
error_result: str,
state: GameState,
hit_location: str,
defensive_decision: DefensiveDecision
) -> AdvancementResult:
"""
Runner advancement for X-Check G1 result.
@ -1453,6 +1456,9 @@ def x_check_g1(
on_base_code: Current base situation code (0-7 bit field)
defender_in: Is the defender playing in?
error_result: 'NO', 'E1', 'E2', 'E3', 'RP'
state: Current game state (for lineup IDs, outs, etc.)
hit_location: Where the ball was hit (1B, 2B, SS, 3B, P, C)
defensive_decision: Defensive positioning decision
Returns:
AdvancementResult with runner movements
@ -1465,11 +1471,29 @@ def x_check_g1(
# Lookup groundball result type from table
gb_type = get_groundball_advancement('G1', on_base_code, defender_in, error_result)
# Build advancement result
return build_advancement_from_code(on_base_code, gb_type, result_name="G1")
# If error result: use simple error advancement (doesn't need GameState details)
if error_result in ['E1', 'E2', 'E3', 'RP']:
return build_advancement_from_code(on_base_code, gb_type, result_name="G1")
# If no error: delegate to existing result handler (needs full GameState)
else:
runner_adv = RunnerAdvancement()
return runner_adv._execute_result(
result_type=gb_type,
state=state,
hit_location=hit_location,
defensive_decision=defensive_decision
)
def x_check_g2(on_base_code: int, defender_in: bool, error_result: str) -> AdvancementResult:
def x_check_g2(
on_base_code: int,
defender_in: bool,
error_result: str,
state: GameState,
hit_location: str,
defensive_decision: DefensiveDecision
) -> AdvancementResult:
"""
Runner advancement for X-Check G2 result.
@ -1480,6 +1504,9 @@ def x_check_g2(on_base_code: int, defender_in: bool, error_result: str) -> Advan
on_base_code: Current base situation code (0-7 bit field)
defender_in: Is the defender playing in?
error_result: 'NO', 'E1', 'E2', 'E3', 'RP'
state: Current game state (for lineup IDs, outs, etc.)
hit_location: Where the ball was hit (1B, 2B, SS, 3B, P, C)
defensive_decision: Defensive positioning decision
Returns:
AdvancementResult with runner movements
@ -1490,10 +1517,30 @@ def x_check_g2(on_base_code: int, defender_in: bool, error_result: str) -> Advan
)
gb_type = get_groundball_advancement('G2', on_base_code, defender_in, error_result)
return build_advancement_from_code(on_base_code, gb_type, result_name="G2")
# If error result: use simple error advancement (doesn't need GameState details)
if error_result in ['E1', 'E2', 'E3', 'RP']:
return build_advancement_from_code(on_base_code, gb_type, result_name="G2")
# If no error: delegate to existing result handler (needs full GameState)
else:
runner_adv = RunnerAdvancement()
return runner_adv._execute_result(
result_type=gb_type,
state=state,
hit_location=hit_location,
defensive_decision=defensive_decision
)
def x_check_g3(on_base_code: int, defender_in: bool, error_result: str) -> AdvancementResult:
def x_check_g3(
on_base_code: int,
defender_in: bool,
error_result: str,
state: GameState,
hit_location: str,
defensive_decision: DefensiveDecision
) -> AdvancementResult:
"""
Runner advancement for X-Check G3 result.
@ -1504,6 +1551,9 @@ def x_check_g3(on_base_code: int, defender_in: bool, error_result: str) -> Advan
on_base_code: Current base situation code (0-7 bit field)
defender_in: Is the defender playing in?
error_result: 'NO', 'E1', 'E2', 'E3', 'RP'
state: Current game state (for lineup IDs, outs, etc.)
hit_location: Where the ball was hit (1B, 2B, SS, 3B, P, C)
defensive_decision: Defensive positioning decision
Returns:
AdvancementResult with runner movements
@ -1514,73 +1564,96 @@ def x_check_g3(on_base_code: int, defender_in: bool, error_result: str) -> Advan
)
gb_type = get_groundball_advancement('G3', on_base_code, defender_in, error_result)
return build_advancement_from_code(on_base_code, gb_type, result_name="G3")
# If error result: use simple error advancement (doesn't need GameState details)
if error_result in ['E1', 'E2', 'E3', 'RP']:
return build_advancement_from_code(on_base_code, gb_type, result_name="G3")
# If no error: delegate to existing result handler (needs full GameState)
else:
runner_adv = RunnerAdvancement()
return runner_adv._execute_result(
result_type=gb_type,
state=state,
hit_location=hit_location,
defensive_decision=defensive_decision
)
def x_check_f1(on_base_code: int, error_result: str) -> AdvancementResult:
def x_check_f1(
on_base_code: int,
error_result: str,
state: GameState,
hit_location: str
) -> AdvancementResult:
"""
Runner advancement for X-Check F1 (deep flyball) result.
F1 maps to FLYOUT_A behavior:
- If error: all runners advance E# bases (out negated)
- If no error: delegate to existing FLYOUT_A logic (requires GameState)
- If no error: delegate to existing FLYOUT_A logic
Args:
on_base_code: Current base situation code (0-7 bit field)
error_result: 'NO', 'E1', 'E2', 'E3', 'RP'
state: Current game state (for lineup IDs, outs, etc.)
hit_location: Where the ball was hit (LF, CF, RF)
Returns:
AdvancementResult with runner movements
"""
from app.core.x_check_advancement_tables import build_flyball_advancement_with_error
# If error result: use simple error advancement (doesn't need GameState details)
if error_result != 'NO':
# Error case: all advance E# bases
return build_flyball_advancement_with_error(on_base_code, error_result, flyball_type="F1")
# If no error: delegate to existing FLYOUT_A logic
else:
# No error: batter out, runners would tag (requires GameState for FLYOUT_A logic)
# TODO: Delegate to FLYOUT_A logic when GameState available
logger.warning("X-Check F1 with no error requires GameState for proper FLYOUT_A resolution")
return AdvancementResult(
movements=[RunnerMovement(lineup_id=0, from_base=0, to_base=0, is_out=True)],
outs_recorded=1,
runs_scored=0,
result_type=None,
description="F1: Deep flyball out (requires GameState for runner advancement)"
)
runner_adv = RunnerAdvancement()
return runner_adv._fb_result_deep(state)
def x_check_f2(on_base_code: int, error_result: str) -> AdvancementResult:
def x_check_f2(
on_base_code: int,
error_result: str,
state: GameState,
hit_location: str
) -> AdvancementResult:
"""
Runner advancement for X-Check F2 (medium flyball) result.
F2 maps to FLYOUT_B behavior:
- If error: all runners advance E# bases (out negated)
- If no error: delegate to existing FLYOUT_B logic (requires GameState)
- If no error: delegate to existing FLYOUT_B logic
Args:
on_base_code: Current base situation code (0-7 bit field)
error_result: 'NO', 'E1', 'E2', 'E3', 'RP'
state: Current game state (for lineup IDs, outs, etc.)
hit_location: Where the ball was hit (LF, CF, RF)
Returns:
AdvancementResult with runner movements
"""
from app.core.x_check_advancement_tables import build_flyball_advancement_with_error
# If error result: use simple error advancement (doesn't need GameState details)
if error_result != 'NO':
return build_flyball_advancement_with_error(on_base_code, error_result, flyball_type="F2")
# If no error: delegate to existing FLYOUT_B logic
else:
logger.warning("X-Check F2 with no error requires GameState for proper FLYOUT_B resolution")
return AdvancementResult(
movements=[RunnerMovement(lineup_id=0, from_base=0, to_base=0, is_out=True)],
outs_recorded=1,
runs_scored=0,
result_type=None,
description="F2: Medium flyball out (requires GameState for runner advancement)"
)
runner_adv = RunnerAdvancement()
return runner_adv._fb_result_medium(state, hit_location)
def x_check_f3(on_base_code: int, error_result: str) -> AdvancementResult:
def x_check_f3(
on_base_code: int,
error_result: str,
state: GameState,
hit_location: str
) -> AdvancementResult:
"""
Runner advancement for X-Check F3 (shallow flyball) result.
@ -1591,20 +1664,19 @@ def x_check_f3(on_base_code: int, error_result: str) -> AdvancementResult:
Args:
on_base_code: Current base situation code (0-7 bit field)
error_result: 'NO', 'E1', 'E2', 'E3', 'RP'
state: Current game state (for lineup IDs, outs, etc.)
hit_location: Where the ball was hit (LF, CF, RF)
Returns:
AdvancementResult with runner movements
"""
from app.core.x_check_advancement_tables import build_flyball_advancement_with_error
# If error result: use simple error advancement (doesn't need GameState details)
if error_result != 'NO':
return build_flyball_advancement_with_error(on_base_code, error_result, flyball_type="F3")
# If no error: delegate to existing FLYOUT_C logic
else:
# F3 with no error: batter out, all runners hold (no tag-up on shallow fly)
return AdvancementResult(
movements=[RunnerMovement(lineup_id=0, from_base=0, to_base=0, is_out=True)],
outs_recorded=1,
runs_scored=0,
result_type=None,
description="F3: Shallow flyball out, all runners hold"
)
runner_adv = RunnerAdvancement()
return runner_adv._fb_result_shallow(state)

View File

@ -646,28 +646,66 @@ class TestXCheckPlaceholders:
def test_x_check_g1_returns_valid_result(self):
"""x_check_g1 should return valid AdvancementResult (Phase 3D: Now implemented)."""
from app.core.runner_advancement import x_check_g1, GroundballResultType
from uuid import uuid4
# Create GameState (bases empty, 0 outs)
state = GameState(
game_id=uuid4(),
league_id='sba',
home_team_id=1,
away_team_id=2,
current_batter_lineup_id=1
)
state.outs = 0
defensive_decision = DefensiveDecision(
alignment='normal',
infield_depth='normal',
outfield_depth='normal'
)
result = x_check_g1(
on_base_code=0,
defender_in=False,
error_result='NO'
error_result='NO',
state=state,
hit_location='SS',
defensive_decision=defensive_decision
)
assert isinstance(result, AdvancementResult)
# With no error on bases empty, should be batter out
assert result.result_type == GroundballResultType.BATTER_OUT_RUNNERS_HOLD
# NOTE: Non-error results require GameState - returns placeholder
assert len(result.movements) == 1 # Batter out placeholder
assert len(result.movements) == 1 # Batter out
assert result.outs_recorded == 1
def test_x_check_g2_returns_valid_result(self):
"""x_check_g2 should return valid AdvancementResult (Phase 3D: Now implemented)."""
from app.core.runner_advancement import x_check_g2, GroundballResultType
from uuid import uuid4
# Create GameState (not used for error case, but required by signature)
state = GameState(
game_id=uuid4(),
league_id='sba',
home_team_id=1,
away_team_id=2,
current_batter_lineup_id=1
)
defensive_decision = DefensiveDecision(
alignment='normal',
infield_depth='normal',
outfield_depth='normal'
)
result = x_check_g2(
on_base_code=5,
on_base_code=5, # R1 + R3
defender_in=True,
error_result='E1'
error_result='E1',
state=state,
hit_location='2B',
defensive_decision=defensive_decision
)
assert isinstance(result, AdvancementResult)
@ -680,11 +718,30 @@ class TestXCheckPlaceholders:
def test_x_check_g3_returns_valid_result(self):
"""x_check_g3 should return valid AdvancementResult (Phase 3D: Now implemented)."""
from app.core.runner_advancement import x_check_g3, GroundballResultType
from uuid import uuid4
# Create GameState (not used for error case, but required by signature)
state = GameState(
game_id=uuid4(),
league_id='sba',
home_team_id=1,
away_team_id=2,
current_batter_lineup_id=1
)
defensive_decision = DefensiveDecision(
alignment='normal',
infield_depth='normal',
outfield_depth='normal'
)
result = x_check_g3(
on_base_code=7,
on_base_code=7, # Bases loaded
defender_in=False,
error_result='E2'
error_result='E2',
state=state,
hit_location='3B',
defensive_decision=defensive_decision
)
assert isinstance(result, AdvancementResult)
@ -697,25 +754,50 @@ class TestXCheckPlaceholders:
def test_x_check_f1_returns_valid_result(self):
"""x_check_f1 should return valid AdvancementResult (Phase 3D: Now implemented)."""
from app.core.runner_advancement import x_check_f1
from uuid import uuid4
# Create GameState (bases empty, 0 outs)
state = GameState(
game_id=uuid4(),
league_id='sba',
home_team_id=1,
away_team_id=2,
current_batter_lineup_id=1
)
state.outs = 0
result = x_check_f1(
on_base_code=0,
error_result='NO'
error_result='NO',
state=state,
hit_location='CF'
)
assert isinstance(result, AdvancementResult)
assert result.outs_recorded == 1 # F1 is a flyout, should record out
assert result.runs_scored == 0
# NOTE: F1 with no error requires GameState for FLYOUT_A logic
assert 'GameState' in result.description or 'flyball' in result.description.lower()
# F1 with no error delegates to FLYOUT_A logic
assert 'flyball' in result.description.lower() or 'Deep' in result.description
def test_x_check_f2_returns_valid_result(self):
"""x_check_f2 should return valid AdvancementResult (Phase 3D: Now implemented)."""
from app.core.runner_advancement import x_check_f2
from uuid import uuid4
# Create GameState (not used for error case, but required by signature)
state = GameState(
game_id=uuid4(),
league_id='sba',
home_team_id=1,
away_team_id=2,
current_batter_lineup_id=1
)
result = x_check_f2(
on_base_code=3, # Code 3 = R3 only (sequential mapping, not bit field)
error_result='E3'
error_result='E3',
state=state,
hit_location='RF'
)
assert isinstance(result, AdvancementResult)
@ -727,10 +809,22 @@ class TestXCheckPlaceholders:
def test_x_check_f3_returns_valid_result(self):
"""x_check_f3 should return valid AdvancementResult (Phase 3D: Now implemented)."""
from app.core.runner_advancement import x_check_f3
from uuid import uuid4
# Create GameState (not used for error case, but required by signature)
state = GameState(
game_id=uuid4(),
league_id='sba',
home_team_id=1,
away_team_id=2,
current_batter_lineup_id=1
)
result = x_check_f3(
on_base_code=5,
error_result='RP'
on_base_code=5, # R1 + R3
error_result='RP',
state=state,
hit_location='LF'
)
assert isinstance(result, AdvancementResult)
@ -742,6 +836,23 @@ class TestXCheckPlaceholders:
def test_x_check_functions_accept_all_error_types(self):
"""X-Check functions should accept all error result types."""
from app.core.runner_advancement import x_check_g1, x_check_f1
from uuid import uuid4
# Create GameState
state = GameState(
game_id=uuid4(),
league_id='sba',
home_team_id=1,
away_team_id=2,
current_batter_lineup_id=1
)
state.outs = 0
defensive_decision = DefensiveDecision(
alignment='normal',
infield_depth='normal',
outfield_depth='normal'
)
error_types = ['NO', 'E1', 'E2', 'E3', 'RP']
@ -750,37 +861,82 @@ class TestXCheckPlaceholders:
result_g = x_check_g1(
on_base_code=0,
defender_in=False,
error_result=error_type
error_result=error_type,
state=state,
hit_location='SS',
defensive_decision=defensive_decision
)
assert isinstance(result_g, AdvancementResult)
# Test flyball function
result_f = x_check_f1(
on_base_code=0,
error_result=error_type
error_result=error_type,
state=state,
hit_location='CF'
)
assert isinstance(result_f, AdvancementResult)
def test_x_check_g_functions_accept_all_on_base_codes(self):
"""X-Check G functions should accept all on-base codes 0-7."""
from app.core.runner_advancement import x_check_g1
from uuid import uuid4
# Create GameState
state = GameState(
game_id=uuid4(),
league_id='sba',
home_team_id=1,
away_team_id=2,
current_batter_lineup_id=1
)
state.outs = 0
defensive_decision = DefensiveDecision(
alignment='normal',
infield_depth='normal',
outfield_depth='normal'
)
for on_base_code in range(8):
result = x_check_g1(
on_base_code=on_base_code,
defender_in=False,
error_result='NO'
error_result='NO',
state=state,
hit_location='2B',
defensive_decision=defensive_decision
)
assert isinstance(result, AdvancementResult)
def test_x_check_g_functions_accept_defender_in_flags(self):
"""X-Check G functions should accept both defender_in values."""
from app.core.runner_advancement import x_check_g1
from uuid import uuid4
# Create GameState
state = GameState(
game_id=uuid4(),
league_id='sba',
home_team_id=1,
away_team_id=2,
current_batter_lineup_id=1
)
state.outs = 0
defensive_decision = DefensiveDecision(
alignment='normal',
infield_depth='normal',
outfield_depth='normal'
)
for defender_in in [True, False]:
result = x_check_g1(
on_base_code=0,
defender_in=defender_in,
error_result='NO'
error_result='NO',
state=state,
hit_location='1B',
defensive_decision=defensive_decision
)
assert isinstance(result, AdvancementResult)

View File

@ -19,6 +19,7 @@ Date: 2025-11-02
"""
import pytest
from uuid import uuid4
from app.core.x_check_advancement_tables import (
get_groundball_advancement,
build_advancement_from_code,
@ -38,6 +39,29 @@ from app.core.runner_advancement import (
x_check_f2,
x_check_f3,
)
from app.models.game_models import GameState, DefensiveDecision
# Helper function to create test GameState
def create_test_state():
"""Create a minimal GameState for testing."""
return GameState(
game_id=uuid4(),
league_id='sba',
home_team_id=1,
away_team_id=2,
current_batter_lineup_id=1
)
# Helper function to create test DefensiveDecision
def create_test_defensive_decision():
"""Create a default DefensiveDecision for testing."""
return DefensiveDecision(
alignment='normal',
infield_depth='normal',
outfield_depth='normal'
)
# ============================================================================
@ -544,7 +568,17 @@ class TestXCheckFunctionIntegration:
Scenario: x_check_g1 with runners on 1st and 2nd, infield in, E1
Expected: Uses G1 table SAFE_ALL_ADVANCE_ONE proper advancement
"""
result = x_check_g1(on_base_code=4, defender_in=True, error_result='E1')
state = create_test_state()
defensive_decision = create_test_defensive_decision()
result = x_check_g1(
on_base_code=4,
defender_in=True,
error_result='E1',
state=state,
hit_location='SS',
defensive_decision=defensive_decision
)
assert result.outs_recorded == 0
assert result.runs_scored == 0
@ -559,7 +593,17 @@ class TestXCheckFunctionIntegration:
Scenario: x_check_g2 with bases loaded, normal, E2
Expected: Uses G2 table SAFE_ALL_ADVANCE_TWO 2 runs score
"""
result = x_check_g2(on_base_code=7, defender_in=False, error_result='E2')
state = create_test_state()
defensive_decision = create_test_defensive_decision()
result = x_check_g2(
on_base_code=7,
defender_in=False,
error_result='E2',
state=state,
hit_location='2B',
defensive_decision=defensive_decision
)
assert result.outs_recorded == 0
assert result.runs_scored == 2
@ -572,7 +616,17 @@ class TestXCheckFunctionIntegration:
Scenario: x_check_g3 with runner on 3rd, normal, E3
Expected: Uses G3 table SAFE_ALL_ADVANCE_THREE 1 run scores
"""
result = x_check_g3(on_base_code=3, defender_in=False, error_result='E3')
state = create_test_state()
defensive_decision = create_test_defensive_decision()
result = x_check_g3(
on_base_code=3,
defender_in=False,
error_result='E3',
state=state,
hit_location='3B',
defensive_decision=defensive_decision
)
assert result.outs_recorded == 0
assert result.runs_scored == 1
@ -586,7 +640,14 @@ class TestXCheckFunctionIntegration:
Scenario: x_check_f1 with runner on 2nd, E1 error
Expected: Flyball + E1 batter to 1st, R2 to 3rd
"""
result = x_check_f1(on_base_code=2, error_result='E1')
state = create_test_state()
result = x_check_f1(
on_base_code=2,
error_result='E1',
state=state,
hit_location='CF'
)
assert result.outs_recorded == 0 # Error negates out
assert result.runs_scored == 0
@ -599,7 +660,14 @@ class TestXCheckFunctionIntegration:
Scenario: x_check_f2 with runners on 1st and 3rd, E2 error
Expected: Batter to 2nd, R1 to 3rd, R3 scores (1 run)
"""
result = x_check_f2(on_base_code=5, error_result='E2')
state = create_test_state()
result = x_check_f2(
on_base_code=5,
error_result='E2',
state=state,
hit_location='RF'
)
assert result.outs_recorded == 0
assert result.runs_scored == 1
@ -610,7 +678,14 @@ class TestXCheckFunctionIntegration:
Scenario: x_check_f3 with bases empty, no error
Expected: Shallow flyball, batter out, no advancement
"""
result = x_check_f3(on_base_code=0, error_result='NO')
state = create_test_state()
result = x_check_f3(
on_base_code=0,
error_result='NO',
state=state,
hit_location='LF'
)
assert result.outs_recorded == 1
assert result.runs_scored == 0
@ -740,7 +815,17 @@ class TestComprehensiveScenarios:
Play: G1 groundball to shortstop, E2 error
Expected: Error negates DP attempt, batter to 2nd, 2 runs score
"""
result = x_check_g1(on_base_code=7, defender_in=True, error_result='E2')
state = create_test_state()
defensive_decision = create_test_defensive_decision()
result = x_check_g1(
on_base_code=7,
defender_in=True,
error_result='E2',
state=state,
hit_location='SS',
defensive_decision=defensive_decision
)
# Verify result
assert result.outs_recorded == 0 # Error prevents outs
@ -752,12 +837,25 @@ class TestComprehensiveScenarios:
"""
Real-world scenario: Runner on 3rd, 2 outs, infield in
Play: G3 slow grounder, no error
Expected: Batter out (inning over), but would be DECIDE if < 2 outs
Expected: Table returns DECIDE_OPPORTUNITY, conservative default is batter out
"""
result = x_check_g3(on_base_code=3, defender_in=True, error_result='NO')
state = create_test_state()
state.outs = 2 # Set to 2 outs for this scenario
defensive_decision = create_test_defensive_decision()
# Table says DECIDE_OPPORTUNITY
result = x_check_g3(
on_base_code=3,
defender_in=True,
error_result='NO',
state=state,
hit_location='P',
defensive_decision=defensive_decision
)
# Table returns DECIDE_OPPORTUNITY, conservative handling returns batter out
assert result.result_type == GroundballResultType.DECIDE_OPPORTUNITY
assert result.outs_recorded == 1 # Conservative: batter out
assert result.runs_scored == 0 # Conservative: runner holds
def test_scenario_flyball_to_outfield_runner_tags(self):
"""
@ -765,7 +863,14 @@ class TestComprehensiveScenarios:
Play: F1 to left field, E1 error by outfielder
Expected: Error allows batter to 1st, runner scores
"""
result = x_check_f1(on_base_code=3, error_result='E1')
state = create_test_state()
result = x_check_f1(
on_base_code=3,
error_result='E1',
state=state,
hit_location='LF'
)
assert result.outs_recorded == 0 # Error negates out
assert result.runs_scored == 1 # R3 scores
@ -778,7 +883,17 @@ class TestComprehensiveScenarios:
Play: G1 grounder, E1 error
Expected: Error prevents DP, all safe
"""
result = x_check_g1(on_base_code=1, defender_in=False, error_result='E1')
state = create_test_state()
defensive_decision = create_test_defensive_decision()
result = x_check_g1(
on_base_code=1,
defender_in=False,
error_result='E1',
state=state,
hit_location='2B',
defensive_decision=defensive_decision
)
assert result.outs_recorded == 0 # No DP, error
assert result.runs_scored == 0