✅ Murmur free march and attack
| Category | Ability |
| Status | Passing |
| Test | tests/test_abilities_passive.py::test_murmur_free_march_and_attack |
Tests interaction between Murmur, Sabnock.
Preconditions
-
P1 Main Phase, P1 has 3 AP
-
Lane 0: P1's Murmur (#002) — 9 HP, 0 damage, READIED
-
Lane 1: P2's enemy demon — 12 HP, 0 damage, READIED
-
Murmur passive: -2 AP Cost on all actions
Action
-
Murmur marches lane 0 → 1 (cost: max(0, 1-2) = 0 AP)
-
Murmur marches lane 1 → 0 (cost: 0 AP)
-
Repeat 4 more times (10 marches total)
-
Murmur marches lane 0 → 1 (to be in range of enemy)
-
Murmur attacks enemy (cost: max(0, 2-2) = 0 AP, exhausts Murmur)
from engine.operations import march_demon, deal_damage, exhaust_demon, get_effective_pwr
murmur = make_demon("002", lane=0, owner=Side.PLAYER_1)
enemy = make_demon("003", lane=1, owner=Side.PLAYER_2) # Sabnock, 15 HP
state = make_game_state(phase=Phase.MAIN, current_player=Side.PLAYER_1)
state.players[Side.PLAYER_1].ap = 3
state = place_demon(state, murmur)
state = place_demon(state, enemy)
murmur_p = state.demons[0]
enemy_p = state.demons[1]
# March back and forth 10 times (5 round trips)
for i in range(5):
state = march_demon(state, next(d for d in state.demons if d.unit_id == "002"), 1)
state = march_demon(state, next(d for d in state.demons if d.unit_id == "002"), 0)
# AP should still be 3 — all marches cost 0
Expected Postconditions
-
P1 AP: 3 (unchanged — all actions cost 0)
-
Murmur: lane 1, EXHAUSTED (from attack)
-
Enemy: took Murmur's PWR (2) damage
-
Total marches: 11, total attacks: 1, total AP spent: 0
Assertions
assert state.players[Side.PLAYER_1].ap == 3, (
f"Murmur's -2 AP Cost should make March free. AP={state.players[Side.PLAYER_1].ap}"
)
# March to lane 1 for the attack
state = march_demon(state, next(d for d in state.demons if d.unit_id == "002"), 1)
assert state.players[Side.PLAYER_1].ap == 3, "11th march should still be free"
# Attack the enemy — cost should be max(0, 2-2) = 0 AP
murmur_now = next(d for d in state.demons if d.unit_id == "002")
enemy_now = next(d for d in state.demons if d.unit_id == "003")
pwr = get_effective_pwr(state, murmur_now)
import copy
new_state = copy.deepcopy(state)
new_state = deal_damage(new_state, murmur_now, enemy_now, pwr)
murmur_ref = next(d for d in new_state.demons if d.unit_id == "002")
new_state = exhaust_demon(new_state, murmur_ref)
# Compute effective attack cost (same as game_loop.py)
from engine.abilities import get_passive_modifiers as gpm
from engine.status_effects import get_stat_modifier as gsm
from engine.constants import ATTACK_AP_COST
attack_cost = max(0, ATTACK_AP_COST + gpm(new_state, murmur_ref, "ap_cost") + gsm(new_state, murmur_ref, "ap_cost"))
assert attack_cost == 0, f"Murmur attack should cost 0 AP (2-2=0). Got {attack_cost}"
new_state.players[Side.PLAYER_1].ap = max(0, new_state.players[Side.PLAYER_1].ap - attack_cost)
# Verify final state
assert new_state.players[Side.PLAYER_1].ap == 3, (
f"AP should still be 3 after 11 marches + 1 attack at 0 cost each. Got {new_state.players[Side.PLAYER_1].ap}"
)
murmur_final = next(d for d in new_state.demons if d.unit_id == "002")
assert murmur_final.state == DemonState.EXHAUSTED, "Murmur should be EXHAUSTED after attack"
assert murmur_final.lane == 1, "Murmur should be in lane 1"
enemy_final = next(d for d in new_state.demons if d.unit_id == "003")
assert enemy_final.damage == pwr, f"Enemy should have {pwr} damage from Murmur's attack"