""" 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}") print(f" Current pitcher lineup_id: {state.current_pitcher_lineup_id}") print(f" Current catcher lineup_id: {state.current_catcher_lineup_id}") print(f" Current on_base_code: {state.current_on_base_code} (binary: {bin(state.current_on_base_code)})") if state.current_batter_lineup_id and state.current_pitcher_lineup_id: 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())