docs(phase2): record the P3 optimizer playbook and plan status

This commit is contained in:
2026-09-22 15:44:06 +02:00
parent dd64c2ebd6
commit 016d6e1459
+78 -4
View File
@@ -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 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**. 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 ### État du plan
- **P0** ✅ · **P1** ✅ (`LLMClient` + `FakeLLMClient` scripté + cache - **P0** ✅ · **P1** ✅ (`LLMClient` + `FakeLLMClient` scripté + cache
composite, garde-fous coût/tokens, `NLPolicy`, `PromptBuilder` versionné) · composite, garde-fous coût/tokens, `NLPolicy`, `PromptBuilder` versionné) ·
**P2** ✅ (`strategy.py` + `SweepRunner`, sortie mesurée) — gate verte **P2** ✅ (`strategy.py` + `SweepRunner`, sortie mesurée) · **P3** ✅
(**2116 tests**). (`scenarios.py` + `search.py`, playbook représentatif mesuré) — gate verte
(**2134 tests**).
- Détail P0 : **P0.0 (D1)** ✅ · **P0.1 (`schema_version`)** ✅ · mesures ✅ · - Détail P0 : **P0.0 (D1)** ✅ · **P0.1 (`schema_version`)** ✅ · mesures ✅ ·
`objectives.py` ✅ · `collect_runs` ✅ · **P0.2** (vue compacte) ✅ · `objectives.py` ✅ · `collect_runs` ✅ · **P0.2** (vue compacte) ✅ ·
**P0.3** (`candidates`) ✅ · **P0.4** (`llm_policy` + `policy_factory`) ✅ · **P0.3** (`candidates`) ✅ · **P0.4** (`llm_policy` + `policy_factory`) ✅ ·
**P0.5** (ReplayLog `call_seq` + `replay()` identique) ✅. **P0.5** (ReplayLog `call_seq` + `replay()` identique) ✅.
- **Frontière P0 atteinte** : gate verte (2022 tests) ; un run enregistré se - **Frontière P0 atteinte** : gate verte (2022 tests) ; un run enregistré se
rejoue à l'identique. rejoue à l'identique.
- Suite : **P3** (optimiseur « meilleure stratégie »), P4 (cartographie - Suite : **P4** (adversaire `AdversarySpec` + cartographie complète), P5
complète), P5 (fournisseur réel + rapport). (fournisseur réel + rapport).