Add full multi-step decision workflow for SINGLE_UNCAPPED and DOUBLE_UNCAPPED outcomes, replacing the previous stub that fell through to basic single/double advancement. The decision tree follows the same interactive pattern as X-Check resolution with 5 phases: lead runner advance, defensive throw, trail runner advance, throw target selection, and safe/out speed check. - game_models.py: PendingUncappedHit model, 5 new decision phases - game_engine.py: initiate_uncapped_hit(), 5 submit methods, 3 result builders - handlers.py: 5 new WebSocket event handlers - ai_opponent.py: 5 AI decision stubs (conservative defaults) - play_resolver.py: Updated TODO comments for fallback paths - 80 new backend tests (2481 total): workflow (49), handlers (23), truth tables (8) - Fix GameplayPanel.spec.ts: add missing Pinia setup, fix component references Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
213 lines
6.8 KiB
Python
213 lines
6.8 KiB
Python
"""
|
|
AI Opponent - Automated decision-making for AI-controlled teams.
|
|
|
|
Provides strategic decision generation for AI opponents in single-player
|
|
and AI vs AI game modes.
|
|
|
|
This is a stub implementation for Week 7. Full AI logic will be developed
|
|
in Week 9 as part of Phase 3 AI opponent integration.
|
|
|
|
Author: Claude
|
|
Date: 2025-10-29
|
|
"""
|
|
|
|
import logging
|
|
|
|
from app.models.game_models import DefensiveDecision, GameState, OffensiveDecision
|
|
|
|
logger = logging.getLogger(f"{__name__}.AIOpponent")
|
|
|
|
|
|
class AIOpponent:
|
|
"""
|
|
AI opponent decision-making engine.
|
|
|
|
Generates defensive and offensive decisions for AI-controlled teams.
|
|
Current implementation uses simple heuristics. Full AI logic will be
|
|
implemented in Week 9.
|
|
"""
|
|
|
|
def __init__(self, difficulty: str = "balanced"):
|
|
"""
|
|
Initialize AI opponent.
|
|
|
|
Args:
|
|
difficulty: AI difficulty level
|
|
- "balanced": Standard decision-making
|
|
- "yolo": Aggressive playstyle (more risks)
|
|
- "safe": Conservative playstyle (fewer risks)
|
|
"""
|
|
self.difficulty = difficulty
|
|
logger.info(f"AIOpponent initialized with difficulty: {difficulty}")
|
|
|
|
async def generate_defensive_decision(self, state: GameState) -> DefensiveDecision:
|
|
"""
|
|
Generate defensive decision for AI-controlled fielding team.
|
|
|
|
Week 7 stub: Returns default "normal" positioning.
|
|
Week 9: Implement full decision logic based on game situation.
|
|
|
|
Args:
|
|
state: Current game state
|
|
|
|
Returns:
|
|
DefensiveDecision with AI-generated strategy
|
|
|
|
TODO (Week 9):
|
|
- Analyze batter tendencies (pull/opposite field)
|
|
- Consider runner speed for hold decisions
|
|
- Evaluate double play opportunities
|
|
- Adjust positioning based on inning/score
|
|
"""
|
|
logger.debug(f"Generating defensive decision for game {state.game_id}")
|
|
|
|
# Week 7 stub: Simple default decision
|
|
decision = DefensiveDecision(
|
|
alignment="normal",
|
|
infield_depth="normal",
|
|
outfield_depth="normal",
|
|
hold_runners=[],
|
|
)
|
|
|
|
# TODO Week 9: Add actual AI logic
|
|
# if state.outs < 2 and state.is_runner_on_first():
|
|
# decision.infield_depth = "double_play"
|
|
# if state.is_runner_on_third() and state.outs < 2:
|
|
# decision.infield_depth = "in"
|
|
|
|
logger.info(
|
|
f"AI defensive decision: IF: {decision.infield_depth}, OF: {decision.outfield_depth}"
|
|
)
|
|
return decision
|
|
|
|
async def generate_offensive_decision(self, state: GameState) -> OffensiveDecision:
|
|
"""
|
|
Generate offensive decision for AI-controlled batting team.
|
|
|
|
Week 7 stub: Returns default "normal" approach.
|
|
Week 9: Implement full decision logic based on game situation.
|
|
|
|
Args:
|
|
state: Current game state
|
|
|
|
Returns:
|
|
OffensiveDecision with AI-generated strategy
|
|
|
|
TODO (Week 9):
|
|
- Evaluate stealing opportunities (runner speed, pitcher, catcher)
|
|
- Consider bunting in appropriate situations
|
|
- Adjust batting approach based on score/inning
|
|
- Implement hit-and-run logic
|
|
"""
|
|
logger.debug(f"Generating offensive decision for game {state.game_id}")
|
|
|
|
# Week 7 stub: Simple default decision
|
|
decision = OffensiveDecision(
|
|
approach="normal", steal_attempts=[], hit_and_run=False, bunt_attempt=False
|
|
)
|
|
|
|
# TODO Week 9: Add actual AI logic
|
|
# if state.is_runner_on_first() and not state.is_runner_on_second():
|
|
# # Consider stealing second
|
|
# if self._should_attempt_steal(state):
|
|
# decision.steal_attempts = [2]
|
|
|
|
logger.info(
|
|
f"AI offensive decision: steal={decision.steal_attempts}, hr={decision.hit_and_run}"
|
|
)
|
|
return decision
|
|
|
|
# ========================================================================
|
|
# UNCAPPED HIT DECISIONS
|
|
# ========================================================================
|
|
|
|
async def decide_uncapped_lead_advance(
|
|
self, state: GameState, pending: "PendingUncappedHit"
|
|
) -> bool:
|
|
"""
|
|
AI decision: should lead runner attempt advance on uncapped hit?
|
|
|
|
Conservative default: don't risk the runner.
|
|
"""
|
|
logger.debug(f"AI uncapped lead advance decision for game {state.game_id}")
|
|
return False
|
|
|
|
async def decide_uncapped_defensive_throw(
|
|
self, state: GameState, pending: "PendingUncappedHit"
|
|
) -> bool:
|
|
"""
|
|
AI decision: should defense throw to the base?
|
|
|
|
Aggressive default: always challenge the runner.
|
|
"""
|
|
logger.debug(f"AI uncapped defensive throw decision for game {state.game_id}")
|
|
return True
|
|
|
|
async def decide_uncapped_trail_advance(
|
|
self, state: GameState, pending: "PendingUncappedHit"
|
|
) -> bool:
|
|
"""
|
|
AI decision: should trail runner attempt advance on uncapped hit?
|
|
|
|
Conservative default: don't risk the trail runner.
|
|
"""
|
|
logger.debug(f"AI uncapped trail advance decision for game {state.game_id}")
|
|
return False
|
|
|
|
async def decide_uncapped_throw_target(
|
|
self, state: GameState, pending: "PendingUncappedHit"
|
|
) -> str:
|
|
"""
|
|
AI decision: throw at lead or trail runner?
|
|
|
|
Default: target the lead runner (higher-value out).
|
|
"""
|
|
logger.debug(f"AI uncapped throw target decision for game {state.game_id}")
|
|
return "lead"
|
|
|
|
async def decide_uncapped_safe_out(
|
|
self, state: GameState, pending: "PendingUncappedHit"
|
|
) -> str:
|
|
"""
|
|
AI decision: declare runner safe or out?
|
|
|
|
Offensive AI always wants the runner safe.
|
|
"""
|
|
logger.debug(f"AI uncapped safe/out decision for game {state.game_id}")
|
|
return "safe"
|
|
|
|
def _should_attempt_steal(self, state: GameState) -> bool:
|
|
"""
|
|
Determine if AI should attempt a steal (Week 9).
|
|
|
|
TODO: Implement steal decision logic
|
|
- Evaluate runner speed
|
|
- Evaluate pitcher hold rating
|
|
- Evaluate catcher arm
|
|
- Consider game situation (score, inning, outs)
|
|
|
|
Returns:
|
|
True if steal should be attempted
|
|
"""
|
|
# Week 7 stub: Never steal
|
|
return False
|
|
|
|
def _should_attempt_bunt(self, state: GameState) -> bool:
|
|
"""
|
|
Determine if AI should attempt a bunt (Week 9).
|
|
|
|
TODO: Implement bunt decision logic
|
|
- Consider sacrifice bunt situations
|
|
- Evaluate batter bunting ability
|
|
- Assess defensive positioning
|
|
|
|
Returns:
|
|
True if bunt should be attempted
|
|
"""
|
|
# Week 7 stub: Never bunt
|
|
return False
|
|
|
|
|
|
# Singleton instance for global access
|
|
ai_opponent = AIOpponent()
|