mud/tests/test_combat_zaxis.py

271 lines
7.3 KiB
Python

"""Tests for z-axis altitude checks in combat."""
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock
import pytest
from mudlib.combat import commands as combat_commands
from mudlib.combat.encounter import CombatState
from mudlib.combat.engine import active_encounters, get_encounter, start_encounter
from mudlib.combat.moves import load_moves
from mudlib.player import Player, players
from mudlib.zone import Zone
@pytest.fixture(autouse=True)
def clear_state():
"""Clear encounters and players before and after each test."""
active_encounters.clear()
players.clear()
yield
active_encounters.clear()
players.clear()
@pytest.fixture
def test_zone():
"""Create a test zone for players."""
terrain = [["." for _ in range(256)] for _ in range(256)]
zone = Zone(
name="testzone",
width=256,
height=256,
toroidal=True,
terrain=terrain,
impassable=set(),
)
return zone
@pytest.fixture
def mock_writer():
writer = MagicMock()
writer.write = MagicMock()
writer.drain = AsyncMock()
return writer
@pytest.fixture
def mock_reader():
return MagicMock()
@pytest.fixture
def player(mock_reader, mock_writer, test_zone):
p = Player(name="Goku", x=0, y=0, reader=mock_reader, writer=mock_writer)
p.location = test_zone
test_zone._contents.append(p)
players[p.name] = p
return p
@pytest.fixture
def target(mock_reader, mock_writer, test_zone):
t = Player(name="Vegeta", x=0, y=0, reader=mock_reader, writer=mock_writer)
t.location = test_zone
test_zone._contents.append(t)
players[t.name] = t
return t
@pytest.fixture
def moves():
"""Load combat moves from content directory."""
content_dir = Path(__file__).parent.parent / "content" / "combat"
return load_moves(content_dir)
@pytest.fixture(autouse=True)
def inject_moves(moves):
"""Inject loaded moves into combat commands module."""
combat_commands.combat_moves = moves
yield
combat_commands.combat_moves = {}
@pytest.fixture
def punch_right(moves):
"""Get the punch right move."""
return moves["punch right"]
@pytest.fixture
def dodge_left(moves):
"""Get the dodge left move."""
return moves["dodge left"]
# --- Z-axis altitude check tests for starting combat ---
@pytest.mark.asyncio
async def test_attack_fails_when_attacker_flying_defender_grounded(
player, target, punch_right
):
"""Test attack fails when attacker is flying and defender is grounded."""
player.flying = True
target.flying = False
await combat_commands.do_attack(player, "Vegeta", punch_right)
# Combat should not start
encounter = get_encounter(player)
assert encounter is None
# Player should get error message
player.writer.write.assert_called()
messages = [call[0][0] for call in player.writer.write.call_args_list]
assert any("can't reach" in msg.lower() for msg in messages)
@pytest.mark.asyncio
async def test_attack_fails_when_attacker_grounded_defender_flying(
player, target, punch_right
):
"""Test attack fails when attacker is grounded and defender is flying."""
player.flying = False
target.flying = True
await combat_commands.do_attack(player, "Vegeta", punch_right)
# Combat should not start
encounter = get_encounter(player)
assert encounter is None
# Player should get error message
player.writer.write.assert_called()
messages = [call[0][0] for call in player.writer.write.call_args_list]
assert any("can't reach" in msg.lower() for msg in messages)
@pytest.mark.asyncio
async def test_attack_succeeds_when_both_flying(player, target, punch_right):
"""Test attack succeeds when both are flying."""
player.flying = True
target.flying = True
await combat_commands.do_attack(player, "Vegeta", punch_right)
# Combat should start
encounter = get_encounter(player)
assert encounter is not None
assert encounter.attacker is player
assert encounter.defender is target
@pytest.mark.asyncio
async def test_attack_succeeds_when_both_grounded(player, target, punch_right):
"""Test attack succeeds when both are grounded."""
player.flying = False
target.flying = False
await combat_commands.do_attack(player, "Vegeta", punch_right)
# Combat should start
encounter = get_encounter(player)
assert encounter is not None
assert encounter.attacker is player
assert encounter.defender is target
# --- Flying dodge tests (altitude mismatch at resolve time) ---
@pytest.mark.asyncio
async def test_flying_during_window_causes_miss(player, target, punch_right):
"""Test flying during window phase causes attack to miss at resolve time."""
# Both grounded, start combat
player.flying = False
target.flying = False
encounter = start_encounter(player, target)
encounter.attack(punch_right)
# Advance to WINDOW phase
encounter.state = CombatState.WINDOW
# Defender flies during window
target.flying = True
# Resolve
result = encounter.resolve()
# Attack should miss (countered or zero damage)
assert result.countered is True or result.damage == 0.0
@pytest.mark.asyncio
async def test_both_flying_at_resolve_attack_lands(player, target, punch_right):
"""Test both flying at resolve time — attack lands normally."""
# Both flying, start combat
player.flying = True
target.flying = True
encounter = start_encounter(player, target)
encounter.attack(punch_right)
# Advance to WINDOW phase (no altitude change)
encounter.state = CombatState.WINDOW
# Resolve
result = encounter.resolve()
# Attack should land (damage > 0 unless defended)
# Since there's no defense, damage should be > 0
assert result.damage > 0.0
@pytest.mark.asyncio
async def test_attacker_flies_during_window_causes_miss(player, target, punch_right):
"""Test attacker flying during window phase causes attack to miss."""
# Both grounded, start combat
player.flying = False
target.flying = False
encounter = start_encounter(player, target)
encounter.attack(punch_right)
# Advance to WINDOW phase
encounter.state = CombatState.WINDOW
# Attacker flies during window
player.flying = True
# Resolve
result = encounter.resolve()
# Attack should miss
assert result.countered is True or result.damage == 0.0
@pytest.mark.asyncio
async def test_flying_dodge_messages_correct_grammar(player, target, punch_right):
"""Test flying dodge produces grammatically correct messages from both POVs."""
from mudlib.render.pov import render_pov
# Both grounded, start combat
player.flying = False
target.flying = False
encounter = start_encounter(player, target)
encounter.attack(punch_right)
# Advance to WINDOW phase
encounter.state = CombatState.WINDOW
# Defender flies during window
target.flying = True
# Resolve
result = encounter.resolve()
# Render from both POVs
attacker_msg = render_pov(result.resolve_template, player, player, target)
defender_msg = render_pov(result.resolve_template, target, player, target)
# Attacker POV should say "You miss — Vegeta is out of reach!"
assert attacker_msg == "You miss — Vegeta is out of reach!"
# Defender POV should say "Goku misses — You are out of reach!"
assert defender_msg == "Goku misses — You are out of reach!"