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.
|
"""Deterministic combat engine: attacks, crits, DR, hp states, initiative, rounds.
|
||||||
|
|
||||||
Phase 0 documented deviations from PF1e (conventions):
|
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
|
- A weapon with `count` > 1 ("2x Talons") resolves `count` independent attacks
|
||||||
in one attack action; remaining swings are lost once the target drops
|
in one attack action; remaining swings are lost once the target drops
|
||||||
(down or dead) mid-routine. BAB iterative attacks are not modeled.
|
(down or dead) mid-routine. BAB iterative attacks are not modeled.
|
||||||
@@ -106,9 +111,17 @@ class AttackResult:
|
|||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class Action:
|
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
|
target_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@@ -158,12 +171,17 @@ class CombatEngine:
|
|||||||
for state in order:
|
for state in order:
|
||||||
if not state.active:
|
if not state.active:
|
||||||
continue
|
continue
|
||||||
action = self._policy(self, state)
|
actions = self._policy(self, state)
|
||||||
|
for action in actions:
|
||||||
|
if not state.active:
|
||||||
|
break
|
||||||
self._execute(state, action, round_no)
|
self._execute(state, action, round_no)
|
||||||
if self._winner_side() is not None:
|
if self._winner_side() is not None:
|
||||||
break
|
break
|
||||||
if self._winner_side() is not None:
|
if self._winner_side() is not None:
|
||||||
break
|
break
|
||||||
|
if self._winner_side() is not None:
|
||||||
|
break
|
||||||
round_no += 1
|
round_no += 1
|
||||||
winner = self._winner_side()
|
winner = self._winner_side()
|
||||||
if winner is not None:
|
if winner is not None:
|
||||||
@@ -379,14 +397,14 @@ class CombatEngine:
|
|||||||
self._transcript.append(line)
|
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."""
|
"""Attack the nearest active enemy when a weapon is in range, else approach."""
|
||||||
target = engine.nearest_enemy(state)
|
target = engine.nearest_enemy(state)
|
||||||
if target is None:
|
if target is None:
|
||||||
return Action(kind="wait")
|
return (Action(kind="wait"),)
|
||||||
if engine.weapon_for(state, target) is not None:
|
if engine.weapon_for(state, target) is not None:
|
||||||
return Action(kind="attack", target_id=target.combatant.id)
|
return (Action(kind="attack", target_id=target.combatant.id),)
|
||||||
return Action(kind="move", target_id=target.combatant.id)
|
return (Action(kind="move", target_id=target.combatant.id),)
|
||||||
|
|||||||
Reference in New Issue
Block a user