diff --git a/docs/phase-2-llm-architecture.md b/docs/phase-2-llm-architecture.md index 03098c3..e93fbe0 100644 --- a/docs/phase-2-llm-architecture.md +++ b/docs/phase-2-llm-architecture.md @@ -421,17 +421,91 @@ signal le plus net ici est la **queue** (worst-decile HP 1,4 → 6,8 en 3v2 ; 0,0 dans les deux cellules 2v3) et les rounds moyens (6,5 → 5,6 en 3v2). La cartographie du danger à grande n, PJ **et** monstres, reste **P4**. +### Optimiseur (P3 — commit dd64c2e) + +`scenarios.py` : `SideSpec(name, zone, combatant_files, hp_fraction=1.0)` et +`ScenarioSpec(name, map_file, sides, round_cap=100)` — `build_encounter` +compile un scénario en `EncounterSpec` déterministe (aucun chemin de données +en dur dans le src, §15), `scenario_variants(base, hp_fractions=...)` étend +grossier le scénario par fractions de PV des côtés (grille de robustesse, sans +RNG). Test : `tests/test_phase2_scenarios.py`. + +`search.py` : `evaluate(strategy, scenario, *, party, runs, seed, workers=4)` +(`StrategyBenchmark` : métriques de danger du camp `party` + bande 3σ sur le +taux de victoire), `PlaybookEntry(rank, strategy, benchmark)`, +`strategy_name` (nom canonique de `StrategySpec`) et +`optimize(scenario, *, party, runs, seed, workers=4, max_total_runs=None, +candidates=())` — recherche en 3 étages : grille grossière de 24 cellules +(cible × politique de sorts × charges × capacités de classe, repli à 0), +affinage des 3 meilleures cellules sur `retreat_hp_fraction ∈ {0.25, 0.5, +0.75}` (9 cellules ; cellules déjà mesurées dédupliquées), puis les candidats +LLM (`candidates`, P3.3) dédupliqués et évalués. Classement : +`(-P(survive), PV perdus, -pire dcile PV, rounds, nom)`. Le budget +`max_total_runs` est vérifié **avant** tout combat (`ValueError` nommant +`budget`/`max_total_runs`). `format_playbook` rend le classement en colonnes +ASCII avec la bande 3σ. Test : `tests/test_phase2_search.py`. + +**Playbook représentatif (RUNS=200, seed=7, workers=4, camp mesuré = +`players` = 3 gobelins vs 2 orcs)** — 33 cellules (grille 24 + affinage 9 + +candidats 0) ; les rangs non listés sont des variantes à leviers inertes ou +de repli strictement identiques à leur cellule de tête : + +| Rang | Stratégie | Victoire PJ (3σ) | Survivent | ≥1 mort | PV perdus | Pire dcile | Rounds | +|---|---|---|---|---|---|---|---| +| 1 | `lowest_hp` (+ inertes, rangs 2-8) | 97,0 % [93,4 ; 100,0] | 99,0 % | 1,0 % | 3,0 | 5,2 | 5,8 | +| 9 | `weakest_ac` (+ inertes, rangs 10-16) | 97,0 % [93,4 ; 100,0] | 99,0 % | 1,0 % | 3,0 | 5,2 | 5,8 | +| 17 | `lowest_hp, retreat=0.25` (+ inertes) | 96,0 % [91,8 ; 100,0] | 97,5 % | 2,5 % | 3,1 | 3,9 | 6,1 | +| 20 | `lowest_hp, retreat=0.5` (+ inertes) | 95,0 % [90,4 ; 99,6] | 97,5 % | 2,5 % | 3,5 | 3,0 | 6,4 | +| 23 | `default` (nearest, repli 0) | 95,0 % [90,4 ; 99,6] | 97,0 % | 3,0 % | 5,0 | 2,8 | 6,9 | +| 31 | `lowest_hp, retreat=0.75` (+ inertes) | 94,0 % [89,0 ; 99,0] | 95,5 % | 4,5 % | 3,7 | 2,7 | 7,6 | + +TPK 0,0 % dans les 33 cellules. → la sortie P3 (« meilleur candidat du grid + +affinage, playbook classé ») est atteinte : 6600 combats en quelques secondes +sur 4 workers, classement déterministe (rejoué bit-à-bit), budget vérifié +avant exécution. +→ **Le ciblage est le levier dominant** : `lowest_hp` et `weakest_ac` battent +`default` sur toutes les métriques du camp — PV perdus 5,0 → 3,0, pire dcile +2,8 → 5,2, rounds 6,9 → 5,8, survive 97,0 → 99,0 %. Égalité bit-à-bit entre +les deux ordres de cibles : les adversaires adverses étant statistiquement +identiques (gobelins entre eux, orcs entre eux), les deux heuristiques +sélectionnent la même cible au même rang. +→ **La retraite agressive est strictement dominée** : seuil 0,25 → 0,5 → 0,75 +dégrade la survie (99,0 → 97,5 → 97,5 → 95,5 %), le pire dcile (5,2 → 3,9 → +3,0 → 2,7) et allonge le combat (5,8 → 6,1 → 6,4 → 7,6 rounds) — une retraite +est une action à round complet qui paie le départ sans compenser. +→ **Leviers inertes sur cette rencontre** : `spell_policy`, `use_charges` et +`use_class_abilities` ne changent rien (gobelins et orcs n'ont ni sorts, ni +ressources, ni capacités de classe) — leurs variantes font exactement égalité +avec leur cellule de grille (rangs 2-8, 10-16, 24-30). Un scénario avec +lanceur est nécessaire pour discriminer ces leviers (rencontres riches en +P4/P5). +→ **Sémantique de mesure — symétrique** : une politique par cellule, appliquée +aux **deux** camps (`CombatEngine` exécute la politique passée au moteur, sans +routage par côté ; `compile_strategy` est côté-agnostic) ; le classement porte +sur les `DangerMetrics` du camp `party`. Contrairement au spike P2 +(`focus_fire_policy` routait par côté : orcs uniquement), le playbook mesure +donc « la meilleure stratégie quand tout le monde la joue » — l'asymétrie PJ +contre adversaire optimisé arrive en P4 (`AdversarySpec`). +→ Cohérence : la cellule `default` (95,0 % [90,4 ; 99,6]) contient la ligne P2 +défaut (93,0 %) dans sa bande 3σ (mêmes seeds, n = 100 → 200). +→ Limite : P3.3 (candidats LLM) est câblé et testé (déduplication, +0 candidat ici) mais non exercé contre un vrai LLM — aucun fournisseur avant +P5. Bande 3σ ±4,6 points à p = 0,97, n = 200 : au sein d'une famille à +leviers inertes les écarts sont nuls (bit-identiques), mais un départage plus +fin (n plus grand, carte plus riche) reste ouvert. + ### État du plan - **P0** ✅ · **P1** ✅ (`LLMClient` + `FakeLLMClient` scripté + cache composite, garde-fous coût/tokens, `NLPolicy`, `PromptBuilder` versionné) · - **P2** ✅ (`strategy.py` + `SweepRunner`, sortie mesurée) — gate verte - (**2116 tests**). + **P2** ✅ (`strategy.py` + `SweepRunner`, sortie mesurée) · **P3** ✅ + (`scenarios.py` + `search.py`, playbook représentatif mesuré) — gate verte + (**2134 tests**). - Détail P0 : **P0.0 (D1)** ✅ · **P0.1 (`schema_version`)** ✅ · mesures ✅ · `objectives.py` ✅ · `collect_runs` ✅ · **P0.2** (vue compacte) ✅ · **P0.3** (`candidates`) ✅ · **P0.4** (`llm_policy` + `policy_factory`) ✅ · **P0.5** (ReplayLog `call_seq` + `replay()` identique) ✅. - **Frontière P0 atteinte** : gate verte (2022 tests) ; un run enregistré se rejoue à l'identique. -- Suite : **P3** (optimiseur « meilleure stratégie »), P4 (cartographie - complète), P5 (fournisseur réel + rapport). +- Suite : **P4** (adversaire `AdversarySpec` + cartographie complète), P5 + (fournisseur réel + rapport).