Skip to main content

✅ Murmur free march and attack

CategoryAbility
StatusPassing
Testtests/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"