Removed the unused alignment field from DefensiveDecision model and all related code across backend and frontend. Backend changes: - models/game_models.py: Removed alignment field and validator - terminal_client/display.py: Removed alignment from display - core/ai_opponent.py: Updated log message - tests/unit/models/test_game_models.py: Removed alignment tests - tests/unit/core/test_validators.py: Removed alignment validation test Frontend changes: - types/game.ts: Removed alignment from DefensiveDecision interface - components/Decisions/DefensiveSetup.vue: * Removed alignment section from template * Removed alignment from localSetup initialization * Removed alignmentOptions array * Removed alignmentDisplay computed property * Removed alignment from hasChanges comparison * Removed alignment from visual preview (reorganized to col-span-2) Rationale: Defensive alignment is not active in the game and will not be used. Per Cal's decision, remove completely rather than keep as dead code. Tests: All 728 backend unit tests passing (100%) Session 1 Part 3 - Change #6 complete Part of cleanup work from demo review 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
796 lines
27 KiB
Python
796 lines
27 KiB
Python
"""
|
|
Unit tests for Pydantic game state models.
|
|
|
|
Tests validation, helper methods, and business logic for all game_models.py classes.
|
|
|
|
Author: Claude
|
|
Date: 2025-10-22
|
|
"""
|
|
|
|
import pytest
|
|
from uuid import uuid4
|
|
from pydantic import ValidationError
|
|
|
|
from app.models.game_models import (
|
|
LineupPlayerState,
|
|
TeamLineupState,
|
|
DefensiveDecision,
|
|
OffensiveDecision,
|
|
GameState,
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# TEST FIXTURES
|
|
# ============================================================================
|
|
|
|
@pytest.fixture
|
|
def sample_batter():
|
|
"""Create a sample batter for testing GameState"""
|
|
return LineupPlayerState(
|
|
lineup_id=1,
|
|
card_id=100,
|
|
position="RF",
|
|
batting_order=3,
|
|
is_active=True
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_pitcher():
|
|
"""Create a sample pitcher for testing GameState"""
|
|
return LineupPlayerState(
|
|
lineup_id=10,
|
|
card_id=200,
|
|
position="P",
|
|
batting_order=9,
|
|
is_active=True
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_catcher():
|
|
"""Create a sample catcher for testing GameState"""
|
|
return LineupPlayerState(
|
|
lineup_id=2,
|
|
card_id=101,
|
|
position="C",
|
|
batting_order=2,
|
|
is_active=True
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# LINEUP TESTS
|
|
# ============================================================================
|
|
|
|
class TestLineupPlayerState:
|
|
"""Tests for LineupPlayerState model"""
|
|
|
|
def test_create_lineup_player_valid(self):
|
|
"""Test creating a valid lineup player"""
|
|
player = LineupPlayerState(
|
|
lineup_id=1,
|
|
card_id=100,
|
|
position="CF",
|
|
batting_order=1,
|
|
is_active=True
|
|
)
|
|
assert player.lineup_id == 1
|
|
assert player.card_id == 100
|
|
assert player.position == "CF"
|
|
assert player.batting_order == 1
|
|
assert player.is_active is True
|
|
|
|
def test_lineup_player_valid_positions(self):
|
|
"""Test all valid positions"""
|
|
valid_positions = ['P', 'C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF', 'DH']
|
|
for pos in valid_positions:
|
|
player = LineupPlayerState(lineup_id=1, card_id=100, position=pos)
|
|
assert player.position == pos
|
|
|
|
def test_lineup_player_invalid_position(self):
|
|
"""Test that invalid positions raise error"""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
LineupPlayerState(lineup_id=1, card_id=100, position="XX")
|
|
assert "Position must be one of" in str(exc_info.value)
|
|
|
|
def test_lineup_player_batting_order_range(self):
|
|
"""Test valid batting order range (1-9)"""
|
|
for order in range(1, 10):
|
|
player = LineupPlayerState(lineup_id=1, card_id=100, position="CF", batting_order=order)
|
|
assert player.batting_order == order
|
|
|
|
def test_lineup_player_batting_order_invalid_zero(self):
|
|
"""Test that batting order 0 is invalid"""
|
|
with pytest.raises(ValidationError):
|
|
LineupPlayerState(lineup_id=1, card_id=100, position="CF", batting_order=0)
|
|
|
|
def test_lineup_player_batting_order_invalid_ten(self):
|
|
"""Test that batting order 10 is invalid"""
|
|
with pytest.raises(ValidationError):
|
|
LineupPlayerState(lineup_id=1, card_id=100, position="CF", batting_order=10)
|
|
|
|
def test_lineup_player_no_batting_order(self):
|
|
"""Test that batting order can be None (for pitchers in AL)"""
|
|
player = LineupPlayerState(lineup_id=1, card_id=100, position="P", batting_order=None)
|
|
assert player.batting_order is None
|
|
|
|
|
|
class TestTeamLineupState:
|
|
"""Tests for TeamLineupState model"""
|
|
|
|
def test_create_empty_lineup(self):
|
|
"""Test creating an empty lineup"""
|
|
lineup = TeamLineupState(team_id=1, players=[])
|
|
assert lineup.team_id == 1
|
|
assert len(lineup.players) == 0
|
|
|
|
def test_get_batting_order(self):
|
|
"""Test getting players in batting order"""
|
|
players = [
|
|
LineupPlayerState(lineup_id=3, card_id=103, position="CF", batting_order=3),
|
|
LineupPlayerState(lineup_id=1, card_id=101, position="LF", batting_order=1),
|
|
LineupPlayerState(lineup_id=2, card_id=102, position="RF", batting_order=2),
|
|
LineupPlayerState(lineup_id=10, card_id=110, position="P", batting_order=None),
|
|
]
|
|
lineup = TeamLineupState(team_id=1, players=players)
|
|
|
|
ordered = lineup.get_batting_order()
|
|
assert len(ordered) == 3 # Pitcher excluded
|
|
assert ordered[0].batting_order == 1
|
|
assert ordered[1].batting_order == 2
|
|
assert ordered[2].batting_order == 3
|
|
|
|
def test_get_pitcher(self):
|
|
"""Test getting the active pitcher"""
|
|
players = [
|
|
LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1),
|
|
LineupPlayerState(lineup_id=10, card_id=110, position="P", is_active=True),
|
|
]
|
|
lineup = TeamLineupState(team_id=1, players=players)
|
|
|
|
pitcher = lineup.get_pitcher()
|
|
assert pitcher is not None
|
|
assert pitcher.position == "P"
|
|
assert pitcher.card_id == 110
|
|
|
|
def test_get_pitcher_none_when_inactive(self):
|
|
"""Test that inactive pitcher is not returned"""
|
|
players = [
|
|
LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1),
|
|
LineupPlayerState(lineup_id=10, card_id=110, position="P", is_active=False),
|
|
]
|
|
lineup = TeamLineupState(team_id=1, players=players)
|
|
|
|
pitcher = lineup.get_pitcher()
|
|
assert pitcher is None
|
|
|
|
def test_get_player_by_lineup_id(self):
|
|
"""Test getting player by lineup ID"""
|
|
players = [
|
|
LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1),
|
|
LineupPlayerState(lineup_id=5, card_id=105, position="SS", batting_order=5),
|
|
]
|
|
lineup = TeamLineupState(team_id=1, players=players)
|
|
|
|
player = lineup.get_player_by_lineup_id(5)
|
|
assert player is not None
|
|
assert player.card_id == 105
|
|
assert player.position == "SS"
|
|
|
|
def test_get_player_by_lineup_id_not_found(self):
|
|
"""Test that None is returned for non-existent lineup ID"""
|
|
players = [
|
|
LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1),
|
|
]
|
|
lineup = TeamLineupState(team_id=1, players=players)
|
|
|
|
player = lineup.get_player_by_lineup_id(99)
|
|
assert player is None
|
|
|
|
def test_get_batter(self):
|
|
"""Test getting batter by batting order index"""
|
|
players = [
|
|
LineupPlayerState(lineup_id=1, card_id=101, position="LF", batting_order=1),
|
|
LineupPlayerState(lineup_id=2, card_id=102, position="CF", batting_order=2),
|
|
LineupPlayerState(lineup_id=3, card_id=103, position="RF", batting_order=3),
|
|
]
|
|
lineup = TeamLineupState(team_id=1, players=players)
|
|
|
|
batter = lineup.get_batter(0) # First batter (index 0)
|
|
assert batter is not None
|
|
assert batter.batting_order == 1
|
|
|
|
batter = lineup.get_batter(2) # Third batter (index 2)
|
|
assert batter is not None
|
|
assert batter.batting_order == 3
|
|
|
|
def test_get_batter_out_of_range(self):
|
|
"""Test that None is returned for out-of-range index"""
|
|
players = [
|
|
LineupPlayerState(lineup_id=1, card_id=101, position="LF", batting_order=1),
|
|
]
|
|
lineup = TeamLineupState(team_id=1, players=players)
|
|
|
|
batter = lineup.get_batter(5)
|
|
assert batter is None
|
|
|
|
|
|
# ============================================================================
|
|
# DECISION TESTS
|
|
# ============================================================================
|
|
|
|
class TestDefensiveDecision:
|
|
"""Tests for DefensiveDecision model"""
|
|
|
|
def test_create_defensive_decision_defaults(self):
|
|
"""Test creating defensive decision with defaults"""
|
|
decision = DefensiveDecision()
|
|
assert decision.infield_depth == "normal"
|
|
assert decision.outfield_depth == "normal"
|
|
assert decision.hold_runners == []
|
|
|
|
def test_defensive_decision_valid_infield_depths(self):
|
|
"""Test all valid infield depths"""
|
|
valid = ['infield_in', 'normal', 'corners_in']
|
|
for depth in valid:
|
|
decision = DefensiveDecision(infield_depth=depth)
|
|
assert decision.infield_depth == depth
|
|
|
|
def test_defensive_decision_invalid_infield_depth(self):
|
|
"""Test that invalid infield depth raises error"""
|
|
with pytest.raises(ValidationError):
|
|
DefensiveDecision(infield_depth="invalid")
|
|
|
|
def test_defensive_decision_hold_runners(self):
|
|
"""Test holding runners"""
|
|
decision = DefensiveDecision(hold_runners=[1, 3])
|
|
assert decision.hold_runners == [1, 3]
|
|
|
|
|
|
class TestOffensiveDecision:
|
|
"""Tests for OffensiveDecision model"""
|
|
|
|
def test_create_offensive_decision_defaults(self):
|
|
"""Test creating offensive decision with defaults"""
|
|
decision = OffensiveDecision()
|
|
assert decision.approach == "normal"
|
|
assert decision.steal_attempts == []
|
|
assert decision.hit_and_run is False
|
|
assert decision.bunt_attempt is False
|
|
|
|
def test_offensive_decision_valid_approaches(self):
|
|
"""Test all valid batting approaches"""
|
|
valid = ['normal', 'contact', 'power', 'patient']
|
|
for approach in valid:
|
|
decision = OffensiveDecision(approach=approach)
|
|
assert decision.approach == approach
|
|
|
|
def test_offensive_decision_invalid_approach(self):
|
|
"""Test that invalid approach raises error"""
|
|
with pytest.raises(ValidationError):
|
|
OffensiveDecision(approach="invalid")
|
|
|
|
def test_offensive_decision_steal_attempts(self):
|
|
"""Test steal attempts"""
|
|
decision = OffensiveDecision(steal_attempts=[2])
|
|
assert decision.steal_attempts == [2]
|
|
|
|
decision = OffensiveDecision(steal_attempts=[2, 3]) # Double steal
|
|
assert decision.steal_attempts == [2, 3]
|
|
|
|
def test_offensive_decision_invalid_steal_base(self):
|
|
"""Test that invalid steal base raises error"""
|
|
with pytest.raises(ValidationError):
|
|
OffensiveDecision(steal_attempts=[1]) # Can't steal first
|
|
|
|
def test_offensive_decision_hit_and_run(self):
|
|
"""Test hit and run"""
|
|
decision = OffensiveDecision(hit_and_run=True)
|
|
assert decision.hit_and_run is True
|
|
|
|
def test_offensive_decision_bunt(self):
|
|
"""Test bunt attempt"""
|
|
decision = OffensiveDecision(bunt_attempt=True)
|
|
assert decision.bunt_attempt is True
|
|
|
|
|
|
# ============================================================================
|
|
# GAMESTATE TESTS
|
|
# ============================================================================
|
|
|
|
class TestGameState:
|
|
"""Tests for GameState model"""
|
|
|
|
def test_create_game_state_minimal(self, sample_batter):
|
|
"""Test creating game state with minimal fields"""
|
|
game_id = uuid4()
|
|
state = GameState(
|
|
game_id=game_id,
|
|
league_id="sba",
|
|
home_team_id=1,
|
|
away_team_id=2,
|
|
current_batter=sample_batter
|
|
)
|
|
assert state.game_id == game_id
|
|
assert state.league_id == "sba"
|
|
assert state.home_team_id == 1
|
|
assert state.away_team_id == 2
|
|
assert state.status == "pending"
|
|
assert state.inning == 1
|
|
assert state.half == "top"
|
|
assert state.outs == 0
|
|
assert state.home_score == 0
|
|
assert state.away_score == 0
|
|
|
|
def test_game_state_valid_league_ids(self, sample_batter):
|
|
"""Test valid league IDs"""
|
|
game_id = uuid4()
|
|
for league in ['sba', 'pd']:
|
|
state = GameState(
|
|
game_id=game_id,
|
|
league_id=league,
|
|
home_team_id=1,
|
|
away_team_id=2,
|
|
current_batter=sample_batter
|
|
)
|
|
assert state.league_id == league
|
|
|
|
def test_game_state_invalid_league_id(self, sample_batter):
|
|
"""Test that invalid league ID raises error"""
|
|
game_id = uuid4()
|
|
with pytest.raises(ValidationError):
|
|
GameState(
|
|
game_id=game_id,
|
|
league_id="invalid",
|
|
home_team_id=1,
|
|
away_team_id=2,
|
|
current_batter=sample_batter
|
|
)
|
|
|
|
def test_game_state_valid_statuses(self, sample_batter):
|
|
"""Test valid game statuses"""
|
|
game_id = uuid4()
|
|
for status in ['pending', 'active', 'paused', 'completed']:
|
|
state = GameState(
|
|
game_id=game_id,
|
|
league_id="sba",
|
|
home_team_id=1,
|
|
away_team_id=2,
|
|
current_batter=sample_batter,
|
|
status=status
|
|
)
|
|
assert state.status == status
|
|
|
|
def test_game_state_invalid_status(self, sample_batter):
|
|
"""Test that invalid status raises error"""
|
|
game_id = uuid4()
|
|
with pytest.raises(ValidationError):
|
|
GameState(
|
|
game_id=game_id,
|
|
league_id="sba",
|
|
home_team_id=1,
|
|
away_team_id=2,
|
|
current_batter=sample_batter,
|
|
status="invalid"
|
|
)
|
|
|
|
def test_game_state_valid_halves(self, sample_batter):
|
|
"""Test valid inning halves"""
|
|
game_id = uuid4()
|
|
for half in ['top', 'bottom']:
|
|
state = GameState(
|
|
game_id=game_id,
|
|
league_id="sba",
|
|
home_team_id=1,
|
|
away_team_id=2,
|
|
current_batter=sample_batter,
|
|
half=half
|
|
)
|
|
assert state.half == half
|
|
|
|
def test_game_state_invalid_half(self, sample_batter):
|
|
"""Test that invalid half raises error"""
|
|
game_id = uuid4()
|
|
with pytest.raises(ValidationError):
|
|
GameState(
|
|
game_id=game_id,
|
|
league_id="sba",
|
|
home_team_id=1,
|
|
away_team_id=2,
|
|
current_batter=sample_batter,
|
|
half="middle"
|
|
)
|
|
|
|
def test_game_state_outs_validation(self, sample_batter):
|
|
"""Test outs validation (0-2)"""
|
|
game_id = uuid4()
|
|
|
|
# Valid outs
|
|
for outs in [0, 1, 2]:
|
|
state = GameState(
|
|
game_id=game_id,
|
|
league_id="sba",
|
|
home_team_id=1,
|
|
away_team_id=2,
|
|
current_batter=sample_batter,
|
|
outs=outs
|
|
)
|
|
assert state.outs == outs
|
|
|
|
# Invalid outs
|
|
with pytest.raises(ValidationError):
|
|
GameState(
|
|
game_id=game_id,
|
|
league_id="sba",
|
|
home_team_id=1,
|
|
away_team_id=2,
|
|
current_batter=sample_batter,
|
|
outs=3
|
|
)
|
|
|
|
def test_get_batting_team_id(self, sample_batter):
|
|
"""Test getting batting team ID"""
|
|
game_id = uuid4()
|
|
|
|
# Top of inning - away team bats
|
|
state = GameState(
|
|
game_id=game_id,
|
|
league_id="sba",
|
|
home_team_id=1,
|
|
away_team_id=2,
|
|
current_batter=sample_batter,
|
|
half="top"
|
|
)
|
|
assert state.get_batting_team_id() == 2
|
|
|
|
# Bottom of inning - home team bats
|
|
state.half = "bottom"
|
|
assert state.get_batting_team_id() == 1
|
|
|
|
def test_get_fielding_team_id(self, sample_batter):
|
|
"""Test getting fielding team ID"""
|
|
game_id = uuid4()
|
|
|
|
# Top of inning - home team fields
|
|
state = GameState(
|
|
game_id=game_id,
|
|
league_id="sba",
|
|
home_team_id=1,
|
|
away_team_id=2,
|
|
current_batter=sample_batter,
|
|
half="top"
|
|
)
|
|
assert state.get_fielding_team_id() == 1
|
|
|
|
# Bottom of inning - away team fields
|
|
state.half = "bottom"
|
|
assert state.get_fielding_team_id() == 2
|
|
|
|
def test_is_runner_on_base(self, sample_batter):
|
|
"""Test checking for runners on specific bases"""
|
|
game_id = uuid4()
|
|
state = GameState(
|
|
game_id=game_id,
|
|
league_id="sba",
|
|
home_team_id=1,
|
|
away_team_id=2,
|
|
current_batter=sample_batter,
|
|
on_first=LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1),
|
|
on_third=LineupPlayerState(lineup_id=3, card_id=103, position="SS", batting_order=3)
|
|
)
|
|
|
|
assert state.is_runner_on_first() is True
|
|
assert state.is_runner_on_second() is False
|
|
assert state.is_runner_on_third() is True
|
|
|
|
def test_get_runner_at_base(self, sample_batter):
|
|
"""Test getting runner at specific base"""
|
|
game_id = uuid4()
|
|
state = GameState(
|
|
game_id=game_id,
|
|
league_id="sba",
|
|
home_team_id=1,
|
|
away_team_id=2,
|
|
current_batter=sample_batter,
|
|
on_first=LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1),
|
|
on_second=LineupPlayerState(lineup_id=2, card_id=102, position="RF", batting_order=2)
|
|
)
|
|
|
|
runner = state.get_runner_at_base(1)
|
|
assert runner is not None
|
|
assert runner.lineup_id == 1
|
|
|
|
runner = state.get_runner_at_base(3)
|
|
assert runner is None
|
|
|
|
def test_bases_occupied(self, sample_batter):
|
|
"""Test getting list of occupied bases"""
|
|
game_id = uuid4()
|
|
state = GameState(
|
|
game_id=game_id,
|
|
league_id="sba",
|
|
home_team_id=1,
|
|
away_team_id=2,
|
|
current_batter=sample_batter,
|
|
on_first=LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1),
|
|
on_third=LineupPlayerState(lineup_id=3, card_id=103, position="SS", batting_order=3)
|
|
)
|
|
|
|
bases = state.bases_occupied()
|
|
assert bases == [1, 3]
|
|
|
|
def test_clear_bases(self, sample_batter):
|
|
"""Test clearing all runners"""
|
|
game_id = uuid4()
|
|
state = GameState(
|
|
game_id=game_id,
|
|
league_id="sba",
|
|
home_team_id=1,
|
|
away_team_id=2,
|
|
current_batter=sample_batter,
|
|
on_first=LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1),
|
|
on_second=LineupPlayerState(lineup_id=2, card_id=102, position="RF", batting_order=2)
|
|
)
|
|
|
|
assert len(state.get_all_runners()) == 2
|
|
state.clear_bases()
|
|
assert len(state.get_all_runners()) == 0
|
|
assert state.on_first is None
|
|
assert state.on_second is None
|
|
assert state.on_third is None
|
|
|
|
def test_add_runner(self, sample_batter):
|
|
"""Test adding a runner to a base"""
|
|
game_id = uuid4()
|
|
state = GameState(
|
|
game_id=game_id,
|
|
league_id="sba",
|
|
home_team_id=1,
|
|
away_team_id=2,
|
|
current_batter=sample_batter
|
|
)
|
|
|
|
player = LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1)
|
|
state.add_runner(player=player, base=1)
|
|
assert len(state.get_all_runners()) == 1
|
|
assert state.is_runner_on_first() is True
|
|
|
|
def test_add_runner_replaces_existing(self, sample_batter):
|
|
"""Test that adding runner to occupied base replaces existing"""
|
|
game_id = uuid4()
|
|
state = GameState(
|
|
game_id=game_id,
|
|
league_id="sba",
|
|
home_team_id=1,
|
|
away_team_id=2,
|
|
current_batter=sample_batter,
|
|
on_first=LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1)
|
|
)
|
|
|
|
new_player = LineupPlayerState(lineup_id=2, card_id=102, position="RF", batting_order=2)
|
|
state.add_runner(player=new_player, base=1)
|
|
assert len(state.get_all_runners()) == 1 # Still only 1 runner
|
|
runner = state.get_runner_at_base(1)
|
|
assert runner.lineup_id == 2 # New runner replaced old
|
|
|
|
def test_advance_runner_to_base(self, sample_batter):
|
|
"""Test advancing runner to another base"""
|
|
game_id = uuid4()
|
|
state = GameState(
|
|
game_id=game_id,
|
|
league_id="sba",
|
|
home_team_id=1,
|
|
away_team_id=2,
|
|
current_batter=sample_batter,
|
|
on_first=LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1)
|
|
)
|
|
|
|
state.advance_runner(from_base=1, to_base=2)
|
|
assert state.is_runner_on_first() is False
|
|
assert state.is_runner_on_second() is True
|
|
|
|
def test_advance_runner_to_home(self, sample_batter):
|
|
"""Test advancing runner to home (scoring)"""
|
|
game_id = uuid4()
|
|
state = GameState(
|
|
game_id=game_id,
|
|
league_id="sba",
|
|
home_team_id=1,
|
|
away_team_id=2,
|
|
current_batter=sample_batter,
|
|
half="top",
|
|
on_third=LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1)
|
|
)
|
|
|
|
initial_score = state.away_score
|
|
state.advance_runner(from_base=3, to_base=4)
|
|
assert len(state.get_all_runners()) == 0 # Runner removed from bases
|
|
assert state.away_score == initial_score + 1 # Score increased
|
|
|
|
def test_advance_runner_scores_correct_team(self, sample_batter):
|
|
"""Test that scoring increments correct team's score"""
|
|
game_id = uuid4()
|
|
|
|
# Top of inning - away team batting
|
|
state = GameState(
|
|
game_id=game_id,
|
|
league_id="sba",
|
|
home_team_id=1,
|
|
away_team_id=2,
|
|
current_batter=sample_batter,
|
|
half="top",
|
|
on_third=LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1)
|
|
)
|
|
state.advance_runner(from_base=3, to_base=4)
|
|
assert state.away_score == 1
|
|
assert state.home_score == 0
|
|
|
|
# Bottom of inning - home team batting
|
|
state = GameState(
|
|
game_id=game_id,
|
|
league_id="sba",
|
|
home_team_id=1,
|
|
away_team_id=2,
|
|
current_batter=sample_batter,
|
|
half="bottom",
|
|
on_third=LineupPlayerState(lineup_id=5, card_id=105, position="RF", batting_order=5)
|
|
)
|
|
state.advance_runner(from_base=3, to_base=4)
|
|
assert state.home_score == 1
|
|
assert state.away_score == 0
|
|
|
|
def test_increment_outs(self, sample_batter):
|
|
"""Test incrementing outs"""
|
|
game_id = uuid4()
|
|
state = GameState(
|
|
game_id=game_id,
|
|
league_id="sba",
|
|
home_team_id=1,
|
|
away_team_id=2,
|
|
current_batter=sample_batter,
|
|
outs=0
|
|
)
|
|
|
|
assert state.increment_outs() is False # 1 out - not end of inning
|
|
assert state.outs == 1
|
|
|
|
assert state.increment_outs() is False # 2 outs - not end of inning
|
|
assert state.outs == 2
|
|
|
|
assert state.increment_outs() is True # 3 outs - end of inning
|
|
assert state.outs == 3
|
|
|
|
def test_end_half_inning_top_to_bottom(self, sample_batter):
|
|
"""Test ending top of inning goes to bottom"""
|
|
game_id = uuid4()
|
|
state = GameState(
|
|
game_id=game_id,
|
|
league_id="sba",
|
|
home_team_id=1,
|
|
away_team_id=2,
|
|
current_batter=sample_batter,
|
|
inning=3,
|
|
half="top",
|
|
outs=2,
|
|
on_first=LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1)
|
|
)
|
|
|
|
state.end_half_inning()
|
|
assert state.inning == 3 # Same inning
|
|
assert state.half == "bottom" # Now bottom
|
|
assert state.outs == 0 # Outs reset
|
|
assert len(state.get_all_runners()) == 0 # Bases cleared
|
|
|
|
def test_end_half_inning_bottom_to_next_top(self, sample_batter):
|
|
"""Test ending bottom of inning goes to next inning top"""
|
|
game_id = uuid4()
|
|
state = GameState(
|
|
game_id=game_id,
|
|
league_id="sba",
|
|
home_team_id=1,
|
|
away_team_id=2,
|
|
current_batter=sample_batter,
|
|
inning=3,
|
|
half="bottom",
|
|
outs=2,
|
|
on_second=LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1)
|
|
)
|
|
|
|
state.end_half_inning()
|
|
assert state.inning == 4 # Next inning
|
|
assert state.half == "top" # Top of next inning
|
|
assert state.outs == 0 # Outs reset
|
|
assert len(state.get_all_runners()) == 0 # Bases cleared
|
|
|
|
def test_is_game_over_early_innings(self, sample_batter):
|
|
"""Test game is not over in early innings"""
|
|
game_id = uuid4()
|
|
state = GameState(
|
|
game_id=game_id,
|
|
league_id="sba",
|
|
home_team_id=1,
|
|
away_team_id=2,
|
|
current_batter=sample_batter,
|
|
inning=5,
|
|
half="bottom",
|
|
home_score=5,
|
|
away_score=2
|
|
)
|
|
|
|
assert state.is_game_over() is False
|
|
|
|
def test_is_game_over_bottom_ninth_home_ahead(self, sample_batter):
|
|
"""Test game over when home team ahead in bottom 9th"""
|
|
game_id = uuid4()
|
|
state = GameState(
|
|
game_id=game_id,
|
|
league_id="sba",
|
|
home_team_id=1,
|
|
away_team_id=2,
|
|
current_batter=sample_batter,
|
|
inning=9,
|
|
half="bottom",
|
|
home_score=5,
|
|
away_score=2
|
|
)
|
|
|
|
assert state.is_game_over() is True
|
|
|
|
def test_is_game_over_bottom_ninth_tied(self, sample_batter):
|
|
"""Test game continues when tied in bottom 9th"""
|
|
game_id = uuid4()
|
|
state = GameState(
|
|
game_id=game_id,
|
|
league_id="sba",
|
|
home_team_id=1,
|
|
away_team_id=2,
|
|
current_batter=sample_batter,
|
|
inning=9,
|
|
half="bottom",
|
|
home_score=2,
|
|
away_score=2
|
|
)
|
|
|
|
assert state.is_game_over() is False
|
|
|
|
def test_is_game_over_extra_innings_walkoff(self, sample_batter):
|
|
"""Test game over on walk-off in extra innings"""
|
|
game_id = uuid4()
|
|
state = GameState(
|
|
game_id=game_id,
|
|
league_id="sba",
|
|
home_team_id=1,
|
|
away_team_id=2,
|
|
current_batter=sample_batter,
|
|
inning=10,
|
|
half="bottom",
|
|
home_score=5,
|
|
away_score=4
|
|
)
|
|
|
|
assert state.is_game_over() is True
|
|
|
|
def test_is_game_over_after_top_ninth_home_ahead(self, sample_batter):
|
|
"""Test game over at start of bottom 9th when away team ahead"""
|
|
game_id = uuid4()
|
|
# Bottom of 9th, away team ahead - home team can't catch up
|
|
# Note: This would only happen at START of bottom 9th
|
|
# In reality, game wouldn't start bottom 9th if home is losing
|
|
state = GameState(
|
|
game_id=game_id,
|
|
league_id="sba",
|
|
home_team_id=1,
|
|
away_team_id=2,
|
|
current_batter=sample_batter,
|
|
inning=9,
|
|
half="bottom",
|
|
outs=0,
|
|
home_score=2,
|
|
away_score=5
|
|
)
|
|
|
|
# Game continues - home team gets chance to catch up
|
|
assert state.is_game_over() is False
|