fix(rules): classify run as a full-round action

PF1e CRB: running is a full-round action, not a move action. The budget
filter now classifies run as full-round (consumes the standard AND the
move slot), _FULL_ROUND_KINDS includes it for the staggered filter, and
the nauseated filter drops it (run is no longer move-like). The
moved_this_turn/running flags are unchanged; the _execute_run
moved_this_turn early return stays as defense in depth. default_policy
never emits run, so the golden transcripts are untouched.

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-09-11 13:48:58 +02:00
parent 9c6f5e277e
commit e44ec75f2b
4 changed files with 28 additions and 19 deletions
+12 -7
View File
@@ -3393,6 +3393,10 @@ class CombatEngine:
"""
kind = "run"
if state.moved_this_turn:
# Defense in depth, mirroring the _move/_5foot_step no-op-ok
# convention. Unreachable through the filtered path: run is a
# full-round action, so no move-slot action can precede it in a
# kept sequence (the budget filter would have dropped one).
return ActionResult(kind=kind, ok=True, reason=_REASON_OK)
if self.has_condition_flag(state, "cant_move"):
self._log(f"round {self._current_round} {state.combatant.id}: wait (cannot move)")
@@ -5781,7 +5785,9 @@ def _choose_spell_action(
return None
_FULL_ROUND_KINDS: frozenset[str] = frozenset({"full_attack", "charge", "withdraw", "full_round"})
_FULL_ROUND_KINDS: frozenset[str] = frozenset(
{"full_attack", "charge", "withdraw", "full_round", "run"}
)
_SWIFT_BONUS_KINDS: frozenset[str] = frozenset({"deed", "inspire_courage", "swift"})
# PF1e (CRB): a nauseated creature can take only a single move action per
# turn — no standard, no full-round, no attacks, no spellcasting.
@@ -5843,12 +5849,11 @@ def _nauseated_filter(actions: tuple[Action, ...]) -> tuple[Action, ...]:
# SpellSpec has no cast_time field, so every spell casts as a standard),
# maneuver, channel_energy, hex, delay, ready, total_defense, aid_another
# - move: move, stand_up, retrieve_weapon (drawing a weapon is a move action),
# repair_weapon, run (a full-round action in RAW, but this engine models it
# as move machinery that sets moved_this_turn and already excludes it from
# _FULL_ROUND_KINDS — kept consistent, see README)
# repair_weapon
# - swift: swift, deed, inspire_courage (the engine's swift bonus kinds)
# - full-round: full_attack, charge, withdraw, full_round, spell_combat
# (spell + full attack in one round); consumes the standard AND the move slot
# (spell + full attack in one round), run (4x-speed movement, PF1e full-round);
# consumes the standard AND the move slot
# - free/conditional (everything else, no budget): wait, free, immediate
# (off-turn; it consumes the NEXT swift via immediate_used, not this turn's
# slots), 5foot_step (free step, mutually exclusive with other movement via
@@ -5860,13 +5865,13 @@ _ACTION_ECONOMY_STANDARD_KINDS: frozenset[str] = frozenset({
"delay", "ready", "total_defense", "aid_another",
})
_ACTION_ECONOMY_MOVE_KINDS: frozenset[str] = frozenset({
"move", "stand_up", "retrieve_weapon", "repair_weapon", "run",
"move", "stand_up", "retrieve_weapon", "repair_weapon",
})
_ACTION_ECONOMY_SWIFT_KINDS: frozenset[str] = frozenset(
{"swift", "deed", "inspire_courage"}
)
_ACTION_ECONOMY_FULL_ROUND_KINDS: frozenset[str] = frozenset({
"full_attack", "charge", "withdraw", "full_round", "spell_combat",
"full_attack", "charge", "withdraw", "full_round", "spell_combat", "run",
})
_ACTION_ECONOMY_FREE_KINDS: frozenset[str] = frozenset({
"wait", "free", "immediate", "5foot_step", "drop_prone",