feat(phase2): adversary and robustness cartography (P4)
This commit is contained in:
@@ -35,6 +35,7 @@ class SideSpec:
|
||||
zone: str # deployment zone letter, e.g. "A"
|
||||
combatant_files: tuple[Path, ...] # monster/PJ JSON files, loaded via load_combatant
|
||||
hp_fraction: float = 1.0 # initial HP as a fraction of hp_max, in (0, 1]
|
||||
aware: bool | None = None # SRD "Surprise" override; None keeps Combatant.aware (True)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -82,6 +83,19 @@ def _apply_hp_fraction(combatant: Combatant, fraction: float) -> Combatant:
|
||||
)
|
||||
|
||||
|
||||
def _apply_aware(combatant: Combatant, *, aware: bool | None) -> Combatant:
|
||||
"""Apply a side's SRD "Surprise" awareness override via model_copy.
|
||||
|
||||
None keeps the loaded combatant unchanged (Combatant.aware defaults to
|
||||
True, models.py:416-420). SRD "Surprise": only some combatants aware
|
||||
means a surprise round precedes round 1; unaware combatants do not act
|
||||
in it.
|
||||
"""
|
||||
if aware is None:
|
||||
return combatant
|
||||
return combatant.model_copy(update={"aware": aware})
|
||||
|
||||
|
||||
def build_encounter(scenario: ScenarioSpec) -> EncounterSpec:
|
||||
"""Compile a ScenarioSpec into a runner EncounterSpec (deterministic).
|
||||
|
||||
@@ -89,6 +103,12 @@ def build_encounter(scenario: ScenarioSpec) -> EncounterSpec:
|
||||
file with load_combatant, then apply hp_fraction to hp_max via
|
||||
pydantic model_copy(update={"hp_max": max(1, round(hp_max * fraction))})
|
||||
(same model_copy pattern as runner.build_states).
|
||||
- When a side's aware is not None, it is applied to that side's
|
||||
combatants via model_copy(update={"aware": ...}) at the same point
|
||||
where hp_fraction is applied (SRD "Surprise" round semantics: only
|
||||
some aware -> surprise round; Combatant.aware defaults to True, so
|
||||
aware=None keeps the loaded default and is bit-identical to the
|
||||
behaviour without the field).
|
||||
- Duplicate combatant ids are NOT disambiguated here: build_states
|
||||
(called later by run_encounter) handles that. Just list each loaded
|
||||
combatant once per occurrence in combatant_files.
|
||||
@@ -126,7 +146,10 @@ def build_encounter(scenario: ScenarioSpec) -> EncounterSpec:
|
||||
Side(
|
||||
name=side.name,
|
||||
combatants=tuple(
|
||||
_apply_hp_fraction(load_combatant(path), side.hp_fraction)
|
||||
_apply_aware(
|
||||
_apply_hp_fraction(load_combatant(path), side.hp_fraction),
|
||||
aware=side.aware,
|
||||
)
|
||||
for path in side.combatant_files
|
||||
),
|
||||
zone=side.zone,
|
||||
|
||||
Reference in New Issue
Block a user