feat(combat): add action economy framework (6 action types, sequence policy)
This commit is contained in:
@@ -1,7 +1,12 @@
|
||||
"""Deterministic combat engine: attacks, crits, DR, hp states, initiative, rounds.
|
||||
|
||||
Phase 0 documented deviations from PF1e (conventions):
|
||||
- One action per turn: move OR attack (no move+attack, no AoO, no flanking).
|
||||
- Action economy framework: `Action.kind` covers the six PF1e action types
|
||||
(attack=standard, move, full_round, swift, free, immediate); the engine
|
||||
executes a policy-returned sequence of actions per turn. The default policy
|
||||
currently emits one action per turn (attack OR move); standard+move
|
||||
activation and special actions (AoO, flanking, full attack, charge,
|
||||
withdraw) are deferred.
|
||||
- A weapon with `count` > 1 ("2x Talons") resolves `count` independent attacks
|
||||
in one attack action; remaining swings are lost once the target drops
|
||||
(down or dead) mid-routine. BAB iterative attacks are not modeled.
|
||||
@@ -106,9 +111,17 @@ class AttackResult:
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Action:
|
||||
"""What a policy wants a combatant to do this turn."""
|
||||
"""What a policy wants a combatant to do this turn.
|
||||
|
||||
kind: Literal["attack", "move", "wait"]
|
||||
Kinds map to the six PF1e action types: ``attack`` is a standard action
|
||||
(single attack), ``move`` is a move action, ``full_round`` consumes the
|
||||
whole turn (full attack, charge, withdraw — deferred), ``swift`` and
|
||||
``free`` are minor actions, ``immediate`` is an off-turn reaction
|
||||
(deferred). ``wait`` is a no-op. The engine executes a policy-returned
|
||||
sequence per turn; the default policy currently emits one action.
|
||||
"""
|
||||
|
||||
kind: Literal["attack", "move", "wait", "swift", "free", "immediate", "full_round"]
|
||||
target_id: str | None = None
|
||||
|
||||
|
||||
@@ -158,8 +171,13 @@ class CombatEngine:
|
||||
for state in order:
|
||||
if not state.active:
|
||||
continue
|
||||
action = self._policy(self, state)
|
||||
self._execute(state, action, round_no)
|
||||
actions = self._policy(self, state)
|
||||
for action in actions:
|
||||
if not state.active:
|
||||
break
|
||||
self._execute(state, action, round_no)
|
||||
if self._winner_side() is not None:
|
||||
break
|
||||
if self._winner_side() is not None:
|
||||
break
|
||||
if self._winner_side() is not None:
|
||||
@@ -379,14 +397,14 @@ class CombatEngine:
|
||||
self._transcript.append(line)
|
||||
|
||||
|
||||
Policy = Callable[[CombatEngine, CombatantState], Action]
|
||||
Policy = Callable[[CombatEngine, CombatantState], tuple[Action, ...]]
|
||||
|
||||
|
||||
def default_policy(engine: CombatEngine, state: CombatantState) -> Action:
|
||||
def default_policy(engine: CombatEngine, state: CombatantState) -> tuple[Action, ...]:
|
||||
"""Attack the nearest active enemy when a weapon is in range, else approach."""
|
||||
target = engine.nearest_enemy(state)
|
||||
if target is None:
|
||||
return Action(kind="wait")
|
||||
return (Action(kind="wait"),)
|
||||
if engine.weapon_for(state, target) is not None:
|
||||
return Action(kind="attack", target_id=target.combatant.id)
|
||||
return Action(kind="move", target_id=target.combatant.id)
|
||||
return (Action(kind="attack", target_id=target.combatant.id),)
|
||||
return (Action(kind="move", target_id=target.combatant.id),)
|
||||
|
||||
Reference in New Issue
Block a user