CLAUDE: Add HBP (Hit By Pitch) handler to PlayResolver

Fixed missing PlayOutcome.HIT_BY_PITCH handling in resolve_outcome().
HBP was being submitted via WebSocket but rejected with error:
"Unhandled outcome: PlayOutcome.HIT_BY_PITCH"

Changes:
- Added HBP case to play_resolver.py (lines 430-446)
- HBP uses same forced runner advancement logic as WALK
- HBP is NOT classified as is_walk=True (correct for stats)
- Added 2 unit tests: test_hit_by_pitch, test_hit_by_pitch_bases_loaded

Test results: 981/981 passing (100%)

🤖 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-27 22:53:41 -06:00
parent bda98b1efe
commit b12905a71b
2 changed files with 83 additions and 0 deletions

View File

@ -427,6 +427,24 @@ class PlayResolver:
is_walk=True,
)
if outcome == PlayOutcome.HIT_BY_PITCH:
# HBP - identical to walk: batter to first, runners advance if forced
runners_advanced = self._advance_on_walk(state)
runs_scored = sum(
1 for (from_base, to_base) in runners_advanced if to_base == 4
)
return PlayResult(
outcome=outcome,
outs_recorded=0,
runs_scored=runs_scored,
batter_result=1,
runners_advanced=runners_advanced,
description="Hit by pitch",
ab_roll=ab_roll,
# Note: HBP is NOT classified as a walk for statistics purposes
)
# ==================== Singles ====================
if outcome == PlayOutcome.SINGLE_1:
# Single with standard advancement

View File

@ -148,6 +148,71 @@ class TestResolveOutcome:
assert result.runs_scored == 1 # Runner on 3rd forced home
assert (3, 4) in result.runners_advanced
def test_hit_by_pitch(self):
"""
Test HBP (Hit By Pitch) resolution.
HBP works identically to walk for runner advancement, but is NOT
classified as a walk for baseball statistics purposes.
"""
resolver = PlayResolver(league_id="sba", auto_mode=False)
state = GameState(
game_id=uuid4(),
league_id="sba",
home_team_id=1,
away_team_id=2,
current_batter=LineupPlayerState(lineup_id=1, card_id=100, position="CF", batting_order=1)
)
ab_roll = create_mock_ab_roll(state.game_id)
result = resolver.resolve_outcome(
outcome=PlayOutcome.HIT_BY_PITCH,
hit_location=None,
state=state,
defensive_decision=DefensiveDecision(),
offensive_decision=OffensiveDecision(),
ab_roll=ab_roll
)
assert result.outcome == PlayOutcome.HIT_BY_PITCH
assert result.outs_recorded == 0
assert result.runs_scored == 0
assert result.batter_result == 1 # Batter to first
assert result.is_walk is False # HBP is NOT classified as walk
assert result.is_hit is False
assert result.description == "Hit by pitch"
def test_hit_by_pitch_bases_loaded(self):
"""
Test HBP with bases loaded forces run home.
Same forced runner advancement logic as walk.
"""
resolver = PlayResolver(league_id="sba", auto_mode=False)
state = GameState(
game_id=uuid4(),
league_id="sba",
home_team_id=1,
away_team_id=2,
current_batter=LineupPlayerState(lineup_id=1, card_id=100, position="CF", batting_order=1),
on_first=LineupPlayerState(lineup_id=2, card_id=101, position="CF", batting_order=2),
on_second=LineupPlayerState(lineup_id=3, card_id=102, position="RF", batting_order=3),
on_third=LineupPlayerState(lineup_id=4, card_id=103, position="SS", batting_order=4)
)
ab_roll = create_mock_ab_roll(state.game_id)
result = resolver.resolve_outcome(
outcome=PlayOutcome.HIT_BY_PITCH,
hit_location=None,
state=state,
defensive_decision=DefensiveDecision(),
offensive_decision=OffensiveDecision(),
ab_roll=ab_roll
)
assert result.runs_scored == 1 # Runner on 3rd forced home
assert (3, 4) in result.runners_advanced
def test_groundball_uses_runner_advancement(self):
"""Test that groundballs delegate to RunnerAdvancement"""
resolver = PlayResolver(league_id="sba", auto_mode=False)