fix(runner): disambiguation suffix skips ids already taken by explicit combatants
This commit is contained in:
@@ -109,12 +109,16 @@ def _first_fit_anchor(
|
|||||||
def build_states(spec: EncounterSpec) -> list[CombatantState]:
|
def build_states(spec: EncounterSpec) -> list[CombatantState]:
|
||||||
"""Fresh states for one battle; duplicate combatant ids get -N suffixes.
|
"""Fresh states for one battle; duplicate combatant ids get -N suffixes.
|
||||||
|
|
||||||
|
The suffix keeps incrementing until the candidate id is not already
|
||||||
|
taken, so an explicit id like ``goblin-2`` never collides with a
|
||||||
|
disambiguated duplicate of ``goblin`` (the engine keys stats by id).
|
||||||
|
|
||||||
Combatants of a side fill the zone's cells in (row, col) order. A side with
|
Combatants of a side fill the zone's cells in (row, col) order. A side with
|
||||||
more combatants than zone cells, or a zone with no cells, is a spec error.
|
more combatants than zone cells, or a zone with no cells, is a spec error.
|
||||||
Creatures larger than 1x1 occupy an n x n footprint anchored at their cell.
|
Creatures larger than 1x1 occupy an n x n footprint anchored at their cell.
|
||||||
"""
|
"""
|
||||||
states: list[CombatantState] = []
|
states: list[CombatantState] = []
|
||||||
seen: dict[str, int] = {}
|
taken: set[str] = set()
|
||||||
grid = Grid.from_spec(spec.map)
|
grid = Grid.from_spec(spec.map)
|
||||||
for side in spec.sides:
|
for side in spec.sides:
|
||||||
cells = zone_cells(spec.map, side.zone)
|
cells = zone_cells(spec.map, side.zone)
|
||||||
@@ -137,9 +141,12 @@ def build_states(spec: EncounterSpec) -> list[CombatantState]:
|
|||||||
f"{combatant.id!r} ({combatant.size}, {n}x{n})"
|
f"{combatant.id!r} ({combatant.size}, {n}x{n})"
|
||||||
)
|
)
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
count = seen.get(combatant.id, 0) + 1
|
cid = combatant.id
|
||||||
seen[combatant.id] = count
|
suffix = 2
|
||||||
cid = combatant.id if count == 1 else f"{combatant.id}-{count}"
|
while cid in taken:
|
||||||
|
cid = f"{combatant.id}-{suffix}"
|
||||||
|
suffix += 1
|
||||||
|
taken.add(cid)
|
||||||
effective = (
|
effective = (
|
||||||
combatant
|
combatant
|
||||||
if cid == combatant.id
|
if cid == combatant.id
|
||||||
|
|||||||
@@ -178,6 +178,30 @@ def test_build_states_disambiguates_duplicate_ids() -> None:
|
|||||||
assert all(s.hp == s.combatant.hp_max for s in states)
|
assert all(s.hp == s.combatant.hp_max for s in states)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_states_suffix_skips_explicitly_taken_ids() -> None:
|
||||||
|
"""A -N suffix must never land on an id another combatant already owns.
|
||||||
|
|
||||||
|
Two "gob" docs plus an explicit "gob-2" used to yield two states named
|
||||||
|
"gob-2" (the per-base counter ignored explicit ids), merging their
|
||||||
|
attrition in the engine's by-id stats keys.
|
||||||
|
"""
|
||||||
|
spec = EncounterSpec(
|
||||||
|
map=make_map(),
|
||||||
|
sides=(
|
||||||
|
Side(
|
||||||
|
name="players",
|
||||||
|
combatants=(make_combatant("gob"), make_combatant("gob-2")),
|
||||||
|
zone="A",
|
||||||
|
),
|
||||||
|
Side(name="monsters", combatants=(make_combatant("gob"),), zone="B"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
states = build_states(spec)
|
||||||
|
ids = [s.combatant.id for s in states]
|
||||||
|
assert ids == ["gob", "gob-2", "gob-3"]
|
||||||
|
assert len(set(ids)) == 3
|
||||||
|
|
||||||
|
|
||||||
def test_build_states_rejects_too_many_combatants_for_zone() -> None:
|
def test_build_states_rejects_too_many_combatants_for_zone() -> None:
|
||||||
"""A zone with fewer cells than combatants is a spec error, not a runtime surprise."""
|
"""A zone with fewer cells than combatants is a spec error, not a runtime surprise."""
|
||||||
spec = EncounterSpec(
|
spec = EncounterSpec(
|
||||||
|
|||||||
Reference in New Issue
Block a user