Initial commit

This commit is contained in:
2026-03-31 13:10:46 +02:00
commit f60d9628e0
52 changed files with 3383 additions and 0 deletions

View File

@@ -0,0 +1,196 @@
"""
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()