feat(rules): add the surprise round

SRD Surprise: when some but not all combatants are aware of their
opponents, a surprise round precedes round 1. Aware combatants take a
single standard OR move action plus free actions (swift and full-round
are dropped, full_attack collapses to one standard attack, withdraw
survives as the Restricted Withdraw standard); unaware combatants skip
their turn. New Combatant.aware flag (default true, parsed at the JSON
boundary) keeps every existing encounter bit-identical, exposed in the
observation snapshot. Unaware combatants are not flagged flat-footed
(documented simplification).

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-09-13 13:20:37 +02:00
parent 7a8351e3b2
commit 3cc0502228
5 changed files with 301 additions and 6 deletions
+23 -5
View File
@@ -364,6 +364,26 @@ Règles modélisées :
appliqué.
- `hp < 0` = inactive (mourante ou morte) ; `hp = 0` = active mais invalide.
- Initiative : triée sur (total, modificateur, ordre de liste), sans re-jet.
- Round de surprise (SRD « Surprise ») : le champ booléen `aware` du JSON de
combattant (défaut `true`) encode la conscience des adversaires au début du
combat. Si certains combattants actifs sont `aware` et d'autres non, un
round de surprise (round 0) précède le round 1 : « In initiative order
(highest to lowest), combatants who started the battle aware of their
opponents each take a standard or move action during the surprise round.
You can also take free actions during the surprise round. » Le filtre
`_surprise_filter` applique cette allowance : une seule action standard
**ou** de mouvement (la première de la séquence gagne), les actions libres
conservées, les actions swift et à round complet retirées
(`downgraded:surprise`) ; un `full_attack` se dégrade en attaque standard
unique (même convention que le filtre accablé) et `withdraw` survit comme
retraite restreinte standard (SRD « Full-Round Actions »). « Combatants who
are unaware at the start of battle don't get to act in the surprise round »
— les non-`aware` sautent leur tour (journalisé). « If no one or everyone is
surprised, no surprise round occurs » — avec le défaut `aware: true`, le
comportement est bit-identique à l'absence de la règle. Simplification
documentée : les combattants non avertis ne sont pas marqués `flat_footed`
pendant le round de surprise (le moteur n'injecte jamais cette condition
lui-même — voir « Actions immédiates »).
- Économie d'action : un tour normal donne une action standard + une action de
mouvement (ou une action à round complet), plus swift/free/immediate. La
politique renvoie une séquence ordonnée d'actions exécutées dans l'ordre ;
@@ -1321,7 +1341,8 @@ Règles modélisées :
- 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_stolen, grappled_with, moved_this_turn, power_attack,
name, level, size, dr {amount, bypass} ou None (C1), creature_type (C3),
name, level, size, aware (SRD « Surprise » : agit au round de surprise),
dr {amount, bypass} ou None (C1), creature_type (C3),
bab (E1), initiative_mod (E2) et initiative_total (U2 : total du jet
d'initiative courant = d20 + mod, 0 si pas encore de clé — invocation
fraîche), casting_ability ou None pour non-jeteur
@@ -1432,9 +1453,6 @@ la lecture mais non appliquée dans la résolution.
Limites hors périmètre (arrêtées lors de la conformance L1-L10, documentées
plutôt que bricolées) :
- **Pas de round de surprise** : le modèle de combattant n'a pas de champ de
perception/attention (conscience du combat), donc le SRD « Surprise » ne
peut pas être résolu — le combat démarre toujours au round 1 complet.
- **Incantation `one_round` bornée à un round complet** : le moteur ne tient
pas d'incantation en cours entre les tours ; un sort d'1 round se résout à
la fin du tour courant au lieu du suivant (approximation documentée dans
@@ -1561,7 +1579,7 @@ Limites de sorts (conformance D6-D9, documentées plutôt que bricolées) :
La gate de validation complète (tests + lint + types) :
```bash
uv run pytest -q # 1393 tests
uv run pytest -q # 1402 tests
uv run ruff check src tests
uv run basedpyright src # mode strict
```