docs(phase2): record the P4 robustness measurements and plan status

This commit is contained in:
2026-09-25 21:34:13 +02:00
parent cd69312ff4
commit 790df17623
+117 -4
View File
@@ -494,18 +494,131 @@ 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 leviers inertes les écarts sont nuls (bit-identiques), mais un départage plus
fin (n plus grand, carte plus riche) reste ouvert. fin (n plus grand, carte plus riche) reste ouvert.
### Adversaire et robustesse (P4)
`adversary.py` : `AdversarySpec(target_priority="nearest" | "lowest_hp" |
"weakest_ac" | "healer", spell_policy="default" | "none",
retreat_hp_fraction=0.0 | 0.25 | 0.5, use_charges=True | False)` — les
leviers adverses (D12 : l'adversaire joue pour gagner) ; un seul
`AdversarySpec()` (défaut = `nearest`, la politique focus fire du spike P2)
est composé avec la stratégie du camp mesuré en une seule politique de
combat par `compile_matchup(party, adversary, party_side=)`. `adversary_name`
: nom canonique d'un levier. Test : `tests/test_phase2_adversary.py`.
`robustness.py` : `matchup_benchmark(scenario, party, adversary, *,
party_side, runs, seed) -> MatchupBenchmark` (mesure le danger des DEUX
camps — même mesure, les deux sens du §3.2), puis les trois couches :
- `adversarial_analysis` (P4.2, min-max alterné) : itérations successives —
la partie maximise sa survie parmi `party_candidates`, puis l'adversaire
minimise le danger de la partie parmi `adversary_candidates` ; chaque
itération est rendue par `format_robustness`. Le budget `max_total_runs=`
est vérifié **avant** tout combat (`ValueError` nommant
`budget`/`max_total_runs`).
- `danger_map` (P4.3) : grille `SideSpec.hp_fraction` PJ × adversaire, une
cellule par paire de fractions = un `MatchupBenchmark` ; `format_danger_map`
rend la grille (survie des deux camps par cellule).
- `frontier` (P4.3) : bissection de la fraction de PV PJ où `criterion`
croise `target` (convention §3.2), adversaire fixé — le retour est plafonné
au pas `precision` (`ceil`, jamais arrondi, pour rester du côté croisant du
seuil) ; None quand même `max_fraction` ne croise pas. `format_frontier`
rend « no frontier ».
- `sensitivity` (P4.3) : classe les 7 leviers adverses par |delta| du
critère de la partie vs baseline `AdversarySpec()` (budget exact 8×runs,
vérifié avant combat) ; `format_sensitivity` affiche les probabilités en %,
`expected_hp_lost` à 1 décimale.
Tests : `tests/test_phase2_robustness.py`.
**Mesures représentatives (RUNS=100, seed=7, camp mesuré = `players` =
3 gobelins vs 2 orcs, `sample_arena.yaml`)** :
P4.1 — équilibre du min-max alterné (2 itérations ; l'adversaire best-
response converge vers `AdversarySpec()` dès la première itération, les deux
itérations sont bit-identiques) :
| Stratégie PJ | P(survie PJ) | PV perdus PJ | P(survie adv) | PV perdus adv | TPK |
|---|---|---|---|---|---|
| `default` (nearest) | 96 % | 5,0 | 98 % | 11,6 | 0 % |
| `lowest_hp` | 95 % | 6,3 | 99 % | 11,5 | 0 % |
| `retreat=0.5` | 97 % | 5,3 | 99 % | 11,4 | 0 % |
→ la meilleure réponse adverse est le ciblage `nearest` par défaut (le plus
dangereux pour la partie, voir sensitivity) ; face à cet équilibre les trois
stratégies PJ sont dans le bruit (bande 3σ ±6 points à n=100). Le camp
adverse encaisse ~11,5 PV moyens par combat (il perd presque toutes les
runs) mais ne **meurt** (hp ≤ −CON) que dans ~2 % des runs (98-99 % de
survie adv) : la rencontre bat l'adversaire en le mettant à ≤ 0 pv sans
souvent le tuer — cohérent avec la règle de mort `hp ≤ −CON` du moteur.
P4.2 — danger_map (PJ `default` × adversaire `weakest_ac`, fractions
0.5/0.7/1.0, 9 cellules ; grille `Party\Adv` : survie partie/survie
adversaire par cellule) :
| | adv=0.5 | adv=0.7 | adv=1.0 |
|---|---|---|---|
| party=0.5 | 100 %/100 % | 100 %/99 % | 99 %/99 % |
| party=0.7 | 99 %/100 % | 100 %/99 % | 100 %/99 % |
| party=1.0 | 100 %/100 % | 100 %/99 % | 100 %/99 % |
→ la grille est plate (99-100 %) et le TPK nul partout : la mort est un
événement rare sur cette rencontre (au plus 1 run sur 100 par cellule) et
les écarts observés restent sous la bande 3σ (±6 points) — même décimée à
50 % de PV, la partie survit. Une carte discriminante exige une rencontre
plus équilibrée (le 2 gobelins vs 3 orcs de P2, où ≥ 1 mort monte à 4 %).
P4.3 — frontier (`p_survives`, target=0.5, fractions [0.10, 1.00],
adversaire `healer`) : **`no frontier`** — la survie de la partie ne
descend jamais sous 50 %, même à 10 % de PV initiaux : le plancher de
robustesse de la partie est sous la barre des 10 %.
P4.3 — sensitivity (criterion=`expected_hp_lost`, baseline `default` :
PV perdus 4,97, survie 96 %) :
| Rang | Levier | Value | Delta |
|---|---|---|---|
| 1 | `healer` | 2,4 | −2,6 |
| 2 | `lowest_hp` | 2,4 | −2,6 |
| 3 | `weakest_ac` | 2,4 | −2,6 |
| 4 | `retreat=0.5` | 3,1 | −1,9 |
| 5 | `retreat=0.25` | 4,0 | −1,0 |
| 6 | `spell_policy=none` | 5,0 | +0,0 |
| 7 | `use_charges=false` | 5,0 | +0,0 |
→ le ciblage est le levier adverse le plus sensible : quitter `nearest`
pour n'importe quel ciblage explicite (healer/lowest_hp/weakest_ac, ex æquo
à −2,6) fait tomber l'attrition de la partie de 5,0 à 2,4 PV et remonte sa
survie à 100 % — le ciblage de la cible la plus proche (la plus engagée en
mêlée) est donc le plus dangereux pour la partie ; des adversaires
statistiquement identiques rendent les trois ciblages ex æquo (même cible au
même rang, déjà observé en P3). La retraite orc (combat écourté) réduit
aussi le danger (−1,9 / −1,0). Leviers inertes : `spell_policy` et
`use_charges` (les orcs n'ont ni sorts ni ressources) — delta 0,000.
Budget : `adversarial_analysis(max_total_runs=1)` → `ValueError`
(« robustness budget exceeded: 800 runs > 1 max_total_runs ») avant tout
combat. Déterminisme : deux passes de `matchup_benchmark` bit-identiques
(p_survives 1,000 / p_tpk 0,000 / hp_lost 2,4).
→ la sortie P4 (« adversaire `AdversarySpec` + cartographie de danger
complète, bornée en coût ») est atteinte : min-max alterné convergeant,
grille de danger 3×3, frontière de survie, classement des leviers adverses,
déterminisme et budget vérifiés. Les mesures placent la rencontre de
référence 3v2 du côté sûr de l'équilibre : TPK nul, plancher de survie sous
10 % de PV, et le seul levier adverse qui y compte est le ciblage par
défaut.
### É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) · **P3** ✅ **P2** ✅ (`strategy.py` + `SweepRunner`, sortie mesurée) · **P3** ✅
(`scenarios.py` + `search.py`, playbook représentatif mesuré) — gate verte (`scenarios.py` + `search.py`, playbook représentatif mesuré) · **P4** ✅
(**2134 tests**). (`adversary.py` + `robustness.py`, adversaire `AdversarySpec` + carte de
danger complète) — gate verte (**2169 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 : **P4** (adversaire `AdversarySpec` + cartographie complète), P5 - Suite : **P5** (fournisseur réel + rapport).
(fournisseur réel + rapport).