strat-gameplay-webapp/backend/scripts/test_game_flow.py
Cal Corum 0ebe72c09d CLAUDE: Phase 3F - Substitution System Testing Complete
This commit completes all Phase 3 work with comprehensive test coverage:

Test Coverage:
- 31 unit tests for SubstitutionRules (all validation paths)
- 10 integration tests for SubstitutionManager (DB + state sync)
- 679 total tests in test suite (609/609 unit tests passing - 100%)

Testing Scope:
- Pinch hitter validation and execution
- Defensive replacement validation and execution
- Pitching change validation and execution (min batters, force changes)
- Double switch validation
- Multiple substitutions in sequence
- Batting order preservation
- Database persistence verification
- State sync verification
- Lineup cache updates

All substitution system components are now production-ready:
 Core validation logic (SubstitutionRules)
 Orchestration layer (SubstitutionManager)
 Database operations
 WebSocket event handlers
 Comprehensive test coverage
 Complete documentation

Phase 3 Overall: 100% Complete
- Phase 3A-D (X-Check Core): 100%
- Phase 3E (Position Ratings + Redis): 100%
- Phase 3F (Substitutions): 100%

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-06 15:25:53 -06:00

541 lines
16 KiB
Python

"""
Test script to simulate a complete at-bat
Tests the full GameEngine flow:
1. Create game in state manager
2. Start game
3. Submit defensive decision
4. Submit offensive decision
5. Resolve play
6. Check results
Author: Claude
Date: 2025-10-24
"""
import asyncio
import sys
from pathlib import Path
# Add backend to path
sys.path.insert(0, str(Path(__file__).parent.parent))
from uuid import uuid4
from app.core.state_manager import state_manager
from app.core.game_engine import game_engine
from app.database.operations import DatabaseOperations
from app.models.game_models import DefensiveDecision, OffensiveDecision
async def test_single_at_bat():
"""Test a single at-bat flow"""
print("=" * 60)
print("TESTING SINGLE AT-BAT FLOW")
print("=" * 60)
# Create game
game_id = uuid4()
print(f"\n1. Creating game {game_id}...")
# Create in database first
db_ops = DatabaseOperations()
await db_ops.create_game(
game_id=game_id,
league_id="sba",
home_team_id=1,
away_team_id=2,
game_mode="friendly",
visibility="public"
)
# Create dummy lineups for testing
# Away team lineup
positions = ["P", "C", "1B", "2B", "3B", "SS", "LF", "CF", "RF"]
for i in range(1, 10):
await db_ops.add_sba_lineup_player(
game_id=game_id,
team_id=2, # Away team
player_id=100 + i, # Dummy player IDs
position=positions[i-1],
batting_order=i
)
# Home team lineup
for i in range(1, 10):
await db_ops.add_sba_lineup_player(
game_id=game_id,
team_id=1, # Home team
player_id=200 + i, # Dummy player IDs
position=positions[i-1],
batting_order=i
)
# Then create in state manager
state = await state_manager.create_game(
game_id=game_id,
league_id="sba",
home_team_id=1,
away_team_id=2
)
print(f" ✅ Game created with lineups - Status: {state.status}")
# Start game
print("\n2. Starting game...")
state = await game_engine.start_game(game_id)
print(f" ✅ Game started")
print(f" Inning: {state.inning} {state.half}")
print(f" Outs: {state.outs}")
print(f" Status: {state.status}")
# Defensive decision
print("\n3. Submitting defensive decision...")
def_decision = DefensiveDecision(
alignment="normal",
infield_depth="normal",
outfield_depth="normal"
)
state = await game_engine.submit_defensive_decision(game_id, def_decision)
print(f" ✅ Defensive decision submitted")
print(f" Pending: {state.pending_decision}")
# Offensive decision
print("\n4. Submitting offensive decision...")
off_decision = OffensiveDecision(
approach="normal"
)
state = await game_engine.submit_offensive_decision(game_id, off_decision)
print(f" ✅ Offensive decision submitted")
print(f" Pending: {state.pending_decision}")
# Resolve play
print("\n5. Resolving play...")
result = await game_engine.resolve_play(game_id)
print(f" ✅ Play resolved!")
print(f" Outcome: {result.outcome}")
print(f" Description: {result.description}")
print(f" AB Roll: {result.ab_roll}")
print(f" Outs Recorded: {result.outs_recorded}")
print(f" Runs Scored: {result.runs_scored}")
print(f" Is Hit: {result.is_hit}")
print(f" Is Out: {result.is_out}")
# Check final state
print("\n6. Checking final state...")
final_state = await game_engine.get_game_state(game_id)
print(f" 📊 Game State:")
print(f" Inning: {final_state.inning} {final_state.half}")
print(f" Outs: {final_state.outs}")
print(f" Score: Away {final_state.away_score} - Home {final_state.home_score}")
print(f" Runners: {len(final_state.runners)}")
if final_state.runners:
for runner in final_state.runners:
print(f" - Runner on {runner.on_base}B")
print(f" Play Count: {final_state.play_count}")
print(f" Last Result: {final_state.last_play_result}")
print("\n" + "=" * 60)
print("✅ SINGLE AT-BAT TEST PASSED")
print("=" * 60)
return game_id, final_state
async def test_full_inning():
"""Test a complete half-inning (3 outs)"""
print("\n\n")
print("=" * 60)
print("TESTING FULL HALF-INNING (3 OUTS)")
print("=" * 60)
# Create and start game
game_id = uuid4()
print(f"\n1. Creating and starting game {game_id}...")
# Create in database first
db_ops = DatabaseOperations()
await db_ops.create_game(
game_id=game_id,
league_id="sba",
home_team_id=1,
away_team_id=2,
game_mode="friendly",
visibility="public"
)
# Create dummy lineups for testing
positions = ["P", "C", "1B", "2B", "3B", "SS", "LF", "CF", "RF"]
# Away team lineup
for i in range(1, 10):
await db_ops.add_sba_lineup_player(
game_id=game_id,
team_id=2, # Away team
player_id=100 + i,
position=positions[i-1],
batting_order=i
)
# Home team lineup
for i in range(1, 10):
await db_ops.add_sba_lineup_player(
game_id=game_id,
team_id=1, # Home team
player_id=200 + i,
position=positions[i-1],
batting_order=i
)
# Then create in state manager
state = await state_manager.create_game(
game_id=game_id,
league_id="sba",
home_team_id=1,
away_team_id=2
)
await game_engine.start_game(game_id)
print(f" ✅ Game started with lineups")
at_bat_num = 1
# Keep playing until 3 outs
while True:
state = await game_engine.get_game_state(game_id)
if state.outs >= 3:
print(f"\n 🎯 3 outs reached - inning over!")
break
print(f"\n{at_bat_num}. At-Bat #{at_bat_num} (Inning {state.inning} {state.half}, {state.outs} outs)...")
# Submit decisions
def_decision = DefensiveDecision(alignment="normal")
await game_engine.submit_defensive_decision(game_id, def_decision)
off_decision = OffensiveDecision(approach="normal")
await game_engine.submit_offensive_decision(game_id, off_decision)
# Resolve
result = await game_engine.resolve_play(game_id)
print(f" Result: {result.description}")
print(f" Roll: {result.ab_roll.check_d20} (d20)")
print(f" Outs: {result.outs_recorded}, Runs: {result.runs_scored}")
at_bat_num += 1
# Safety check
if at_bat_num > 50:
print(" ⚠️ Safety limit reached - stopping test")
break
# Check final state
final_state = await game_engine.get_game_state(game_id)
print(f"\n📊 Final State:")
print(f" Inning: {final_state.inning} {final_state.half}")
print(f" Total At-Bats: {at_bat_num - 1}")
print(f" Score: Away {final_state.away_score} - Home {final_state.home_score}")
print(f" Total Plays: {final_state.play_count}")
print("\n" + "=" * 60)
print("✅ FULL HALF-INNING TEST PASSED")
print("=" * 60)
async def test_lineup_validation():
"""Test that start_game() fails with incomplete lineups"""
print("\n\n")
print("=" * 60)
print("TESTING LINEUP VALIDATION")
print("=" * 60)
# Test 1: Start game with no lineups
game_id = uuid4()
print(f"\n1. Testing start_game() with NO lineups...")
db_ops = DatabaseOperations()
await db_ops.create_game(
game_id=game_id,
league_id="sba",
home_team_id=1,
away_team_id=2,
game_mode="friendly",
visibility="public"
)
state = await state_manager.create_game(
game_id=game_id,
league_id="sba",
home_team_id=1,
away_team_id=2
)
try:
await game_engine.start_game(game_id)
print(" ❌ FAIL: Should have raised ValidationError")
sys.exit(1)
except Exception as e:
print(f" ✅ Correctly rejected: {e}")
# Test 2: Start game with incomplete lineup (missing positions)
game_id = uuid4()
print(f"\n2. Testing start_game() with INCOMPLETE lineups (only 5 players)...")
await db_ops.create_game(
game_id=game_id,
league_id="sba",
home_team_id=1,
away_team_id=2,
game_mode="friendly",
visibility="public"
)
# Add only 5 players per team
for i in range(1, 6):
await db_ops.add_sba_lineup_player(
game_id=game_id,
team_id=1,
player_id=200 + i,
position=["P", "C", "1B", "2B", "3B"][i-1],
batting_order=i
)
await db_ops.add_sba_lineup_player(
game_id=game_id,
team_id=2,
player_id=100 + i,
position=["P", "C", "1B", "2B", "3B"][i-1],
batting_order=i
)
state = await state_manager.create_game(
game_id=game_id,
league_id="sba",
home_team_id=1,
away_team_id=2
)
try:
await game_engine.start_game(game_id)
print(" ❌ FAIL: Should have raised ValidationError")
sys.exit(1)
except Exception as e:
print(f" ✅ Correctly rejected: {e}")
print("\n" + "=" * 60)
print("✅ LINEUP VALIDATION TEST PASSED")
print("=" * 60)
async def test_snapshot_tracking():
"""Test on_base_code and runner tracking in Play records"""
print("\n\n")
print("=" * 60)
print("TESTING SNAPSHOT TRACKING")
print("=" * 60)
# Create game with lineups
game_id = uuid4()
db_ops = DatabaseOperations()
await db_ops.create_game(
game_id=game_id,
league_id="sba",
home_team_id=1,
away_team_id=2,
game_mode="friendly",
visibility="public"
)
positions = ["P", "C", "1B", "2B", "3B", "SS", "LF", "CF", "RF"]
for i in range(1, 10):
await db_ops.add_sba_lineup_player(
game_id=game_id,
team_id=2,
player_id=100 + i,
position=positions[i-1],
batting_order=i
)
await db_ops.add_sba_lineup_player(
game_id=game_id,
team_id=1,
player_id=200 + i,
position=positions[i-1],
batting_order=i
)
state = await state_manager.create_game(
game_id=game_id,
league_id="sba",
home_team_id=1,
away_team_id=2
)
await game_engine.start_game(game_id)
print(f"\n1. Playing until we get runners on base...")
# Play until we have runners
max_attempts = 20
for attempt in range(max_attempts):
state = await game_engine.get_game_state(game_id)
await game_engine.submit_defensive_decision(game_id, DefensiveDecision())
await game_engine.submit_offensive_decision(game_id, OffensiveDecision())
result = await game_engine.resolve_play(game_id)
if state.runners:
print(f" ✅ Got {len(state.runners)} runner(s) on base after {attempt+1} plays")
break
if state.outs >= 3:
# Reset for new half inning
continue
# Verify snapshot tracking
print(f"\n2. Checking snapshot fields in GameState...")
state = await game_engine.get_game_state(game_id)
print(f" Current batter lineup_id: {state.current_batter.lineup_id if state.current_batter else None}")
print(f" Current pitcher lineup_id: {state.current_pitcher.lineup_id if state.current_pitcher else None}")
print(f" Current catcher lineup_id: {state.current_catcher.lineup_id if state.current_catcher else None}")
print(f" Current on_base_code: {state.current_on_base_code} (binary: {bin(state.current_on_base_code)})")
if state.current_batter and state.current_pitcher:
print(f" ✅ Snapshot fields properly populated")
else:
print(f" ❌ FAIL: Snapshot fields not populated")
sys.exit(1)
# Verify on_base_code matches runners
print(f"\n3. Verifying on_base_code matches runners...")
expected_code = 0
for runner in state.runners:
if runner.on_base == 1:
expected_code |= 1
elif runner.on_base == 2:
expected_code |= 2
elif runner.on_base == 3:
expected_code |= 4
print(f" Runners: {[r.on_base for r in state.runners]}")
print(f" Expected code: {expected_code}, Actual: {state.current_on_base_code}")
if expected_code == state.current_on_base_code:
print(f" ✅ on_base_code correctly calculated")
else:
print(f" ❌ FAIL: on_base_code mismatch")
sys.exit(1)
print("\n" + "=" * 60)
print("✅ SNAPSHOT TRACKING TEST PASSED")
print("=" * 60)
async def test_batting_order_cycling():
"""Test that batting order cycles 0-8 independently per team"""
print("\n\n")
print("=" * 60)
print("TESTING BATTING ORDER CYCLING")
print("=" * 60)
# Create game with lineups
game_id = uuid4()
db_ops = DatabaseOperations()
await db_ops.create_game(
game_id=game_id,
league_id="sba",
home_team_id=1,
away_team_id=2,
game_mode="friendly",
visibility="public"
)
positions = ["P", "C", "1B", "2B", "3B", "SS", "LF", "CF", "RF"]
for i in range(1, 10):
await db_ops.add_sba_lineup_player(
game_id=game_id,
team_id=2,
player_id=100 + i,
position=positions[i-1],
batting_order=i
)
await db_ops.add_sba_lineup_player(
game_id=game_id,
team_id=1,
player_id=200 + i,
position=positions[i-1],
batting_order=i
)
state = await state_manager.create_game(
game_id=game_id,
league_id="sba",
home_team_id=1,
away_team_id=2
)
state = await game_engine.start_game(game_id)
print(f"\n1. Checking initial batter indices...")
print(f" Away team batter idx: {state.away_team_batter_idx}")
print(f" Home team batter idx: {state.home_team_batter_idx}")
# After first play, away_team should be at 1 (started at 0, advanced to 1)
await game_engine.submit_defensive_decision(game_id, DefensiveDecision())
await game_engine.submit_offensive_decision(game_id, OffensiveDecision())
await game_engine.resolve_play(game_id)
state = await game_engine.get_game_state(game_id)
print(f"\n2. After first play...")
print(f" Away team batter idx: {state.away_team_batter_idx} (should be 2)")
print(f" Home team batter idx: {state.home_team_batter_idx} (should be 0)")
print(f" Explanation: Start used idx 0→1, first play used idx 1→2")
if state.away_team_batter_idx != 2:
print(f" ❌ FAIL: Expected away_team_batter_idx=2")
sys.exit(1)
print(f" ✅ Batting indices tracking independently")
print("\n" + "=" * 60)
print("✅ BATTING ORDER CYCLING TEST PASSED")
print("=" * 60)
async def main():
"""Run all tests"""
print("\n🎮 GAME ENGINE FLOW TESTING")
print("Testing complete gameplay flow with GameEngine\n")
try:
# Original tests
# Test 1: Single at-bat
await test_single_at_bat()
# Test 2: Full inning
await test_full_inning()
# New refactor tests
# Test 3: Lineup validation
await test_lineup_validation()
# Test 4: Snapshot tracking
await test_snapshot_tracking()
# Test 5: Batting order cycling
await test_batting_order_cycling()
print("\n\n🎉 ALL TESTS PASSED!")
print("\nGameEngine is working correctly:")
print(" ✅ Game lifecycle management")
print(" ✅ Decision submission")
print(" ✅ Play resolution with AbRoll system")
print(" ✅ State management and persistence")
print(" ✅ Inning advancement")
print(" ✅ Score tracking")
print(" ✅ Lineup validation (refactor)")
print(" ✅ Snapshot tracking with on_base_code (refactor)")
print(" ✅ Independent batting order cycling (refactor)")
except Exception as e:
print(f"\n❌ TEST FAILED: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
asyncio.run(main())