# Architecture technique ## Modules principaux | Module | Responsabilité | |--------|----------------| | `combat.py` | `CombatEngine` : initiative, actions, résolution attaques, états de vie, incantation, invocation, manœuvres, transcript, politique par défaut, `Observation`/`LLMPolicy` | | `dice.py` | Parseur notation dés (`2d4+1`) + lancer | | `rng.py` | `SeededRng` (flux reproductible par graine), `ScriptedRng` (tests) | | `models.py` | Schémas Pydantic v2 : `Combatant`, `AbilityScores`, `ACProfile`, `AttackSpec`, `DamageComponent`, `Saves`, `DamageReduction` | | `map.py` | `MapSpec`/`TerrainType`, chargement YAML, validation (dimensions, chars, zones, déploiement, markers) | | `grid.py` | Grille 5-10-5 : `distance`, `step_cost`, `diagonal_allowed`, Dijkstra `reachable`, `airborne_view` | | `los.py` | Ligne d'effet, couvert par règle des coins : gate dans `weapon_for`, bonus dans `resolve_attack` | | `areas.py` | Géométrie gabarits zone : `cone_cells` (quart cercle 90°), `line_cells` (Bresenham 5ft), bursts via `_area_creatures` | | `combat.py` | `CombatEngine` déterministe : initiative, actions, résolution attaques, tailles/allonges, high ground, actions immédiates opt-in, états de vie, incantation, invocation, manœuvres, capacités de classe, transcripts, politique par défaut, `Observation`/`LLMPolicy` | | `effects.py` | `StatModifier` + `resolve_modifiers` (empilement PF1e : dodge/racial/trait/sans-type s'additionnent ; moral/sacré/profane/enhancement gardent le max) | | `conditions.py` | 29 conditions PF1e : `Condition` (modificateurs + flags comportementaux + `miss_chance`) | | `abilities.py` | Dons : `AbilitySpec` + fabriques (Weapon Focus, Toughness, Power Attack actif −X/+2X, X=BAB//4+1) | | `spells.py` | `SpellSpec` (niveau, école, portée, save, DC, `requires_ranged_touch`, `harmless`, `cast_time`, `components`) + effets (`DamageEffect`, `ConditionEffect`, `HealEffect`, `BuffEffect`, `AreaEffect`, `OneShotEffect`) + scalage (`scaling` dés, `scaling_flat` bonus plat) | | `metrics.py` | `win_rate`, `win_rate_sigma`, `win_rate_band` (bande 3σ bornée [0,1]) | | `runner.py` | `EncounterSpec`/`Side`, `build_states` (placement + désambiguïsation ids), `run_encounter` (1 `SeededRng`/run), `EncounterReport` + attrition | | `objectives.py` | Primitives Phase 2 : P(survie), P(TPK), P(≥1 mort), PV perdus, rounds, dcile PV restants | | `phase2_spike.py` | Politique "focus fire" + cartographie danger | | `cli.py` | `argparse` `pf1e-sim`, rapport FR, codes sortie, flags `--llm-report`/`--output` | | `loaders/combatant.py` | Chargeur unifié PJ + monstres : résolution dons → `AbilitySpec`, sorts → `Combatant.spells` | | `loaders/foundry.py` | Migration Foundry one-shot (incomplet — ne pas régénérer `data/pcs/*.json`) | | `report.py` | LLM report writer P5.3 : `ReportContext`, `LLMReportPolicy`, `ReportBudget`, `format_llm_report` | ## Flux de données principal ``` CLI (args) → _build_spec() → EncounterSpec ↓ load_spell_registry() → spell_registry ↓ run_encounter(spec, runs, seed, spell_registry, ...) ↓ Pour chaque run: SeededRng(seed+i) → CombatEngine → CombatResult ↓ Agrégation → EncounterReport (wins, draws, avg_rounds, attrition) ↓ format_report() → sortie FR ``` ## Frontière LLM (Phase 2/3) ``` CombatEngine(llm_policy=LLMPolicy) → avant chaque appel politique: Observation snapshot (to_dict() → JSON isolé) LLMPolicy(observation) → tuple[Action, ...] ``` - LLM ne reçoit **que** le snapshot, jamais `engine.*` - `PromptPack` externe (`PF1E_PROMPT_PACK_DIR`) : `version.txt` + `.md` + `.examples.json` - `prompt_version = version:name:hash12` (clé cache composite D6) - Cache `ResponseCache` (clé = modèle|params|prompt_version|schema|prompt_hash) - Budget `CostBudget` + `CircuitBreaker` + retry/backoff ## Tests & qualité - Transcripts épinglés (combats 1v1/2v2 scriptés via `ScriptedRng`) - Assertions statistiques forme fermée (matchup symétrique dans bande 3σ) - Gate : `uv run pytest -q && uv run ruff check src tests && uv run basedpyright src` - Ruff `select = ["ALL"]`, ligne 100 ; basedpyright strict sur `src` + `tests`