197 lines
6.7 KiB
Python
197 lines
6.7 KiB
Python
"""
|
|
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()
|