fix(combat): summon spawns only on passable cells

This commit is contained in:
2026-09-04 22:56:05 +02:00
parent 114ec20866
commit fc62a19c9e
2 changed files with 70 additions and 2 deletions
+1 -2
View File
@@ -2040,8 +2040,7 @@ class CombatEngine:
if dr == 0 and dc == 0: if dr == 0 and dc == 0:
continue continue
pos = (center[0] + dr, center[1] + dc) pos = (center[0] + dr, center[1] + dc)
if (0 <= pos[0] < self._grid.height and if (self._grid.passable(pos) and
0 <= pos[1] < self._grid.width and
not any(s.pos == pos for s in self._states if s.active)): not any(s.pos == pos for s in self._states if s.active)):
spawn_pos = pos spawn_pos = pos
break break
+69
View File
@@ -163,6 +163,75 @@ class TestSummonedCreatureActs:
) )
def _build_engine_with_walls(seed: int = 42, duration_rounds: int = 2) -> CombatEngine:
"""Caster at (1,1) under a walled row 0, like sample_arena's border walls.
The spawn search must skip wall cells and pick the first passable one.
"""
caster = _make_caster()
enemy = _make_enemy()
c = CombatantState(
combatant=caster, side="players", pos=(1, 1), hp=caster.hp_max,
)
e = CombatantState(
combatant=enemy, side="monsters", pos=(9, 1), hp=enemy.hp_max,
)
legend = {
".": TerrainType(type="floor", move_cost=1),
"#": TerrainType(type="wall", move_cost=None),
}
terrain = tuple(
"#" * 12 if r == 0 else "#..........." if r == 1 else "." * 12
for r in range(12)
)
spec = MapSpec(name="test", terrain=terrain, legend=legend)
grid = Grid.from_spec(spec)
reg = {"Summon Monster II": _make_summon_spell(duration_rounds)}
return CombatEngine(
SeededRng(seed), grid, [c, e],
policy=_summon_policy,
spell_registry=reg,
)
class TestSummonSpawnsOnPassableCell:
"""Given: a caster whose adjacent ring contains wall cells (arena border)
When: a summons resolves
Then: the creature spawns on a passable cell and acts (not just waits)."""
def test_summon_spawns_on_passable_cell_and_acts(self) -> None:
engine = _build_engine_with_walls(seed=42)
result = engine.run()
wolf_lines = [
line for line in result.transcript if "celestial_wolf" in line
]
spawn_line = next(
(ln for ln in wolf_lines if "summon celestial_wolf at" in ln),
None,
)
assert spawn_line is not None, (
f"Wolf was never summoned. Wolf lines: {wolf_lines}"
)
# "summon celestial_wolf at (r, c) (2r)" — parse the spawn position.
pos_str = spawn_line.split("at (")[1].split(")")[0]
spawn_pos = tuple(int(part) for part in pos_str.split(", "))
assert engine._grid.passable(spawn_pos), (
f"Wolf spawned on impassable cell {spawn_pos}: {spawn_line!r}"
)
action_keywords = (
"charge", "attack", "move", "full_attack", "bite", "5-foot",
)
acted_lines = [
line for line in wolf_lines
if any(kw in line for kw in action_keywords)
]
assert len(acted_lines) >= 1, (
f"Wolf never acted (only waited). Wolf lines: {wolf_lines}"
)
class TestSummonedCreatureExpires: class TestSummonedCreatureExpires:
"""Given: a caster who summons with duration 2 """Given: a caster who summons with duration 2
When: enough rounds pass When: enough rounds pass