""" Moteur de décision IA principal - Architecture hybride. Flux: StateAnalyzer → HeuristicEngine + LLMAdvisor → Fusion → FullAdvice """ import time from dataclasses import dataclass, field import structlog log = structlog.get_logger() @dataclass class GameState: """État complet d'un tour HSBG.""" turn: int = 0 tavern_tier: int = 1 gold: int = 3 hero_id: str = "" hero_hp: int = 40 tavern_minions: list = field(default_factory=list) board_minions: list = field(default_factory=list) hand_minions: list = field(default_factory=list) freeze: bool = False can_upgrade: bool = True upgrade_cost: int = 5 available_spells: list = field(default_factory=list) opponent_boards: list = field(default_factory=list) current_placement: int = 5 player_count: int = 8 phase: str = "recruit" @dataclass class Decision: """Une décision IA avec justification.""" action: str # buy|sell|freeze|upgrade|reposition|hero_power|wait target: dict = None # Carte ou minion ciblé priority: int = 5 # 1-10 confidence: float = 0.5 # 0.0-1.0 reasoning: str = "" # Explication textuelle alternatives: list = field(default_factory=list) synergies_highlighted: list = field(default_factory=list) warnings: list = field(default_factory=list) @dataclass class FullAdvice: """Conseil complet pour un tour entier.""" main_decision: Decision secondary_decisions: list = field(default_factory=list) board_analysis: str = "" strategy_long_term: str = "" threat_assessment: str = "" processing_ms: int = 0 model_used: str = "heuristic" confidence_overall: float = 0.5 class DecisionEngine: """ Moteur de décision hybride HSBG. Architecture: 1. StateAnalyzer → Parse l'état brut en GameState typé 2. HeuristicEngine → Règles métier rapides et déterministes 3. LLMAdvisor → Raisonnement LLM pour enrichir (si disponible) 4. Fusion → Combine les deux avec pondération par confiance """ def __init__(self, settings): self.settings = settings self._initialized = False # Import lazy pour éviter les imports circulaires from backend.ai.engine.heuristics import HeuristicEngine from backend.ai.engine.llm_advisor import LLMAdvisor from backend.ai.engine.state_analyzer import StateAnalyzer self.heuristic = HeuristicEngine() self.llm = LLMAdvisor(settings) self.analyzer = StateAnalyzer() async def initialize(self): """Initialise les composants asynchrones (LLM).""" await self.llm.initialize() self._initialized = True log.info("decision_engine.ready", model=self.settings.llm_model) async def get_advice(self, raw_state: dict) -> FullAdvice: """ Point d'entrée principal. Args: raw_state: Dict brut (depuis API ou OCR) Returns: FullAdvice avec conseil principal + alternatives """ start = time.perf_counter() # 1. Parser l'état brut state = await self.analyzer.parse(raw_state) # 2. Heuristiques (toujours disponibles, < 5ms) heuristic_decisions = self.heuristic.evaluate(state) # 3. LLM (si disponible, ~500-2000ms) llm_advice = None if self._initialized: try: llm_advice = await self.llm.advise(state, heuristic_decisions) except Exception as e: log.warning("llm.failed_gracefully", error=str(e)) # 4. Fusion avec pondération final = self._fuse(state, heuristic_decisions, llm_advice) final.processing_ms = int((time.perf_counter() - start) * 1000) log.info( "advice.generated", action=final.main_decision.action, confidence=round(final.main_decision.confidence, 2), ms=final.processing_ms, model=final.model_used, ) return final def _fuse(self, state: GameState, heuristics: list, llm: "FullAdvice | None") -> FullAdvice: """ Fusionne heuristiques + LLM. Règle: LLM prioritaire si confiance > 0.7, sinon heuristiques. """ if not heuristics: return FullAdvice( main_decision=Decision(action="wait", reasoning="Aucune action identifiée"), model_used="fallback", ) # LLM haute confiance → priorité totale if llm and llm.confidence_overall > 0.7: llm.model_used = f"{self.settings.llm_model}+heuristic" return llm # Heuristiques comme base main = max(heuristics, key=lambda d: d.priority * d.confidence) secondary = sorted( [d for d in heuristics if d != main], key=lambda d: d.priority, reverse=True, )[:3] result = FullAdvice( main_decision=main, secondary_decisions=secondary, board_analysis=self._analyze_board(state), model_used="heuristic", confidence_overall=main.confidence, ) # Enrichissement LLM partiel (stratégie + menaces même si confiance faible) if llm: result.strategy_long_term = llm.strategy_long_term result.threat_assessment = llm.threat_assessment result.model_used = f"heuristic+{self.settings.llm_model}" return result def _analyze_board(self, state: GameState) -> str: """Génère une analyse textuelle rapide du board.""" parts = [] if len(state.board_minions) == 7: parts.append("⚠️ Board plein — vendre avant d'acheter") if state.gold >= 10 and state.tavern_tier < 6: parts.append(f"💰 Or abondant — envisager tier {state.tavern_tier + 1}") if state.current_placement > 5 and state.turn > 8: parts.append("🚨 Position critique — trouver synergie forte rapidement") # Détecter synergie dominante from collections import Counter races = [] for m in state.board_minions: r = m.get("race", []) races.extend(r if isinstance(r, list) else [r]) if races: top_race, top_count = Counter(races).most_common(1)[0] if top_count >= 3: parts.append(f"✨ Synergie {top_race} détectée ({top_count}/7)") if state.freeze: parts.append("❄️ Taverne gelée — les cartes sont réservées") return " | ".join(parts) if parts else "Board standard — continuer normalement" async def shutdown(self): """Arrête proprement les composants.""" await self.llm.shutdown()