feat(phase2): observation-only LLMPolicy entry (D1) + pinned schema_version (P0.1)

- LLMPolicy is now Callable[[Observation], tuple[Action, ...]]: the snapshot
  is the policy's sole information source; the live CombatantState is no
  longer passed to new-style policies.
- CombatEngine._llm_adapter keeps a compatibility path for pre-D1
  two-argument callables: the signature is inspected ONCE at wrap time
  (_accepts_state) and legacy policies still receive (observation, state).
- Observation.to_dict() carries schema_version (OBSERVATION_SCHEMA_VERSION=1)
  as its first pinned key; bump only on removal/meaning change.
- Pinned keyset tests updated (test_observation, test_observation_enriched);
  new tests: observation-only e2e, legacy 2-arg e2e through the adapter,
  schema_version pin + JSON-serializability.
- README: documents the new entry signature and the schema_version key.
This commit is contained in:
2026-09-21 11:10:28 +02:00
parent 3fd9860b5c
commit 3c3e30634e
6 changed files with 142 additions and 21 deletions
+9 -3
View File
@@ -1951,8 +1951,11 @@ Règles modélisées :
retour : la politique peut les muter sans affecter le combat ; l'objet est un
dataclass figé au premier niveau, les dictionnaires internes sont des copies
mutables, non deep-freezés). Une politique LLM
se branche via `CombatEngine(..., llm_policy=...)` (`LLMPolicy(observation,
state) -> tuple[Action, ...]`). Schéma enrichi (E1-E5) :
se branche via `CombatEngine(..., llm_policy=...)` (`LLMPolicy(observation)
-> tuple[Action, ...]` : la politique ne reçoit QUE le snapshot, jamais
l'état vivant ; un callable hérité à deux arguments `(observation, state)`
reste accepté via l'adaptateur de compatibilité D1, signature inspectée
une fois à la construction). Schéma enrichi (E1-E5) :
- Par combattant : id, side, pos, occupied_cells, hp/hp_max, active/dead,
dying/stabilized, conditions (noms + rounds_left), resources_left (+ `spell_slot_{level}`),
weapon_broken/weapon_destroyed/weapon_stolen/weapon_dropped, carried_items
@@ -2050,7 +2053,10 @@ Règles modélisées :
charge_reason (str | None — None si `can_charge` est vrai ; sinon la
raison de `ChargeResult` verbatim (`no_line_of_effect` / `no_charge_path`)
ou `no_weapon` si aucune arme de mêlée utilisable).
- Top-level : round, acting_combatant, combatants, pairs, terrain, elevation,
- Top-level : schema_version (P0.1 : version épinglée du schéma d'observation,
constante `OBSERVATION_SCHEMA_VERSION` = 1 ; incrémentée seulement si une
clé est retirée ou change de sens), round, acting_combatant, combatants,
pairs, terrain, elevation,
threatened (grille bool des cases menacées au contact par les ennemis —
AoO), ranged_threatened (U6d : grille bool des cases sous tir ennemi à
distance — champ de tir des armes à distance utilisables des ennemis