diff --git a/README.md b/README.md index cf4af96..4957627 100644 --- a/README.md +++ b/README.md @@ -410,16 +410,21 @@ Règles modélisées : - Manœuvres de combat : trip, disarm, bull_rush, sunder, grapple, escape, overrun, dirty trick, steal, reposition et drag sont modélisées, chacune 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 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` 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 → - `dazzled` 1 round ; steal → l'arme de mêlée de la cible est volée (exclue de - `usable_attacks()`) ; reposition → déplacement de 5 ft de la cible vers le - premier voisin libre ; drag → traction de 5 ft de la cible, l'attaquant + condition choisie par `Action.trick_condition` (défaut `dazzled` 1 round ; + `blinded`/`deafened`/`entangled`/`nauseated` possibles) ; steal → l'arme de + 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 choisit pas ces manœuvres (utilisables via `Action(kind="maneuver")` ou une 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, `paralysé` 1d4+1 rounds), RD 15/magie+contondant ; son JSON porte un dictionnaire `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 -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. +Non modélisé : la couche `markers` (points nommés de la carte) est validée à +la lecture mais non appliquée dans la résolution. ## Architecture @@ -479,7 +494,9 @@ résolution) : multi-cases, zones menacées en anneau), high ground, actions immédiates opt-in (réactions hors-tour), états de vie, incantation (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 `_modifier_condition_holds` avec le contexte d'action (arme, portée, descripteurs de sauvegarde) ; les conditions inconnues restent exclues. @@ -538,7 +555,7 @@ résolution) : La gate de validation complète (tests + lint + types) : ```bash -uv run pytest -q # 480 tests +uv run pytest -q # 664 tests uv run ruff check src tests 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), 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 - 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 - 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 rédaction du rapport d'équilibrage en français. (Non implémentée.)