docs(phase2): record the P4 robustness measurements and plan status
This commit is contained in:
@@ -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).
|
|
||||||
|
|||||||
Reference in New Issue
Block a user