docs: document LLM observation contract and expanded action options

This commit is contained in:
2026-09-06 11:14:18 +02:00
parent e75ab58055
commit cd796ecffb
+35 -16
View File
@@ -410,16 +410,21 @@ Règles modélisées :
- Manœuvres de combat : trip, disarm, bull_rush, sunder, grapple, escape, - Manœuvres de combat : trip, disarm, bull_rush, sunder, grapple, escape,
overrun, dirty trick, steal, reposition et drag sont modélisées, chacune overrun, dirty trick, steal, reposition et drag sont modélisées, chacune
comme une tentative unique (1d20 + CMB contre CMD) : comme une tentative unique (1d20 + CMB contre CMD) :
trip → `prone` ; disarm → `disarmed` (−4 attaque) ; bull_rush → pousse la cible ; trip → `prone` ; disarm → `disarmed` (−4 attaque) ; bull_rush → pousse la
cible (direction sélectionnable via `Action.direction`) ;
grapple → `grappled`/`pinned` ; escape → **une** sauvegarde (bonus statique de la grapple → `grappled`/`pinned` ; escape → **une** sauvegarde (bonus statique de la
compétence `escape_artist` ou CMB, + un d20) contre CMD — une seule tentative, compétence `escape_artist` ou CMB, + un d20) contre CMD — une seule tentative,
sans boucle multi-tour. Un sunder endommage l'**arme**, pas les PV : `str_mod + 2` sans boucle multi-tour. Un sunder endommage l'**arme**, pas les PV : `str_mod + 2`
de dégâts à l'arme, qui se brise à 10 d'usure cumulée (`_WEAPON_DURABILITY`) → de dégâts à l'arme, qui se brise à 10 d'usure cumulée (`_WEAPON_DURABILITY`) →
`disarmed` ; une arme brisée est exclue de `usable_attacks()`. overrun → `disarmed` ; une arme brisée est exclue de `usable_attacks()` et réparable via
`Action(kind="repair_weapon")`. overrun →
`prone` 1 round + passage au travers si la case au-delà est libre ; dirty trick → `prone` 1 round + passage au travers si la case au-delà est libre ; dirty trick →
`dazzled` 1 round ; steal → l'arme de mêlée de la cible est volée (exclue de condition choisie par `Action.trick_condition` (défaut `dazzled` 1 round ;
`usable_attacks()`) ; reposition → déplacement de 5 ft de la cible vers le `blinded`/`deafened`/`entangled`/`nauseated` possibles) ; steal → l'arme de
premier voisin libre ; drag → traction de 5 ft de la cible, l'attaquant mêlée de la cible est volée (exclue de `usable_attacks()`, récupérable via
`Action(kind="retrieve_weapon")`) ; reposition → déplacement de 5 ft de la
cible vers le premier voisin libre (direction sélectionnable via
`Action.direction`) ; drag → traction de 5 ft de la cible, l'attaquant
occupant sa case d'origine. `default_policy` ne occupant sa case d'origine. `default_policy` ne
choisit pas ces manœuvres (utilisables via `Action(kind="maneuver")` ou une choisit pas ces manœuvres (utilisables via `Action(kind="maneuver")` ou une
politique personnalisée). politique personnalisée).
@@ -448,14 +453,24 @@ Règles modélisées :
`shaken` 1 round, au début de son tour) et contact paralysant (au toucher, `shaken` 1 round, au début de son tour) et contact paralysant (au toucher,
`paralysé` 1d4+1 rounds), RD 15/magie+contondant ; son JSON porte un dictionnaire `paralysé` 1d4+1 rounds), RD 15/magie+contondant ; son JSON porte un dictionnaire
`spell_slots` sans clés dupliquées. `spell_slots` sans clés dupliquées.
- Snapshot d'observation (contrat LLM, Phase 2) : avant chaque appel de
politique, le moteur construit une `Observation` sérialisable (`to_dict()` →
JSON) figée — champs par combattant (id, side, pos, occupied_cells,
hp/hp_max, active/dead, conditions noms+durées, resources_left,
weapon_broken/weapon_stolen, grappled_with, moved_this_turn, power_attack),
par paire combattant actif × cible (distance_ft, weapon_for, range_penalty,
can_charge, can_step5_to_attack, has_line_of_effect, flanking) et couches
terrain/élévation de la grille. Les API de décision exposées (items L1-L8)
s'y retrouvent : `direction`/`trick_condition`/`weapon_id` sur `Action`,
`can_charge` → `ChargeResult`, zones menacées `threatened_squares`/
`enemies_threatening`, coûts `reachable_ft`/`movement_cost_ft`, kinds
`retrieve_weapon`/`repair_weapon`. Une politique LLM se branche via
`CombatEngine(..., llm_policy=...)` (`LLMPolicy(observation, state) ->
tuple[Action, ...]`) ; le snapshot est la SEULE source d'info de la LLM
(aucune référence `engine.*` exposée, collections copiées/figées).
Non modélisé (couche `markers` présente mais non appliquée dans la Non modélisé : la couche `markers` (points nommés de la carte) est validée à
résolution) : la lecture mais non appliquée dans la résolution.
- Manœuvres : dirty trick applique toujours `dazzled` (le choix
aveuglé/assourdi/entravé/malade n'est pas modélisé) ; reposition et drag
utilisent une direction déterministe (premier voisin libre / au-delà de la
cible) ; un objet volé n'est pas récupérable en cours de combat. Une arme
brisée ne peut pas être réparée en cours de combat.
## Architecture ## Architecture
@@ -479,7 +494,9 @@ résolution) :
multi-cases, zones menacées en anneau), high ground, actions immédiates multi-cases, zones menacées en anneau), high ground, actions immédiates
opt-in (réactions hors-tour), états de vie, incantation opt-in (réactions hors-tour), états de vie, incantation
(emplacements, concentration par situation, AoO d'incantation), invocation, (emplacements, concentration par situation, AoO d'incantation), invocation,
manœuvres, capacités de classe, transcripts, politique par défaut. Les manœuvres (avec sélection directionnelle et choix de condition d'état),
capacités de classe, transcripts, politique par défaut, snapshot
d'observation sérialisable `Observation`/`LLMPolicy` (frontière LLM). Les
modificateurs conditionnés (`StatModifier.condition`) sont évalués via modificateurs conditionnés (`StatModifier.condition`) sont évalués via
`_modifier_condition_holds` avec le contexte d'action (arme, portée, `_modifier_condition_holds` avec le contexte d'action (arme, portée,
descripteurs de sauvegarde) ; les conditions inconnues restent exclues. descripteurs de sauvegarde) ; les conditions inconnues restent exclues.
@@ -538,7 +555,7 @@ résolution) :
La gate de validation complète (tests + lint + types) : La gate de validation complète (tests + lint + types) :
```bash ```bash
uv run pytest -q # 480 tests uv run pytest -q # 664 tests
uv run ruff check src tests uv run ruff check src tests
uv run basedpyright src # mode strict uv run basedpyright src # mode strict
``` ```
@@ -561,8 +578,10 @@ uv run basedpyright src # mode strict
(coût de déplacement + high ground), tailles Large+ (occupation multi-cases), (coût de déplacement + high ground), tailles Large+ (occupation multi-cases),
allonges > 5 ft (allonge naturelle + `reach_ft`, zones menacées en anneau) et allonges > 5 ft (allonge naturelle + `reach_ft`, zones menacées en anneau) et
actions immédiates hors-tour (politique de réaction opt-in). Restent hors du actions immédiates hors-tour (politique de réaction opt-in). Restent hors du
socle (voir « Non modélisé ») : les markers et les manœuvres avancées. socle (voir « Non modélisé ») : les markers.
- **Phase 2** — couche tactique LLM : stratégies en langage naturel traduites - **Phase 2** — couche tactique LLM : stratégies en langage naturel traduites
en politiques, balayage de matrices de positionnement. (Non implémentée.) en politiques, balayage de matrices de positionnement. Le contrat
d'observation (snapshot sérialisable `Observation` + wrapper `LLMPolicy`)
est en place ; la couche LLM elle-même reste à implémenter.
- **Phase 3** — rapporteur LLM local : agrégation des statistiques et - **Phase 3** — rapporteur LLM local : agrégation des statistiques et
rédaction du rapport d'équilibrage en français. (Non implémentée.) rédaction du rapport d'équilibrage en français. (Non implémentée.)