"""Tests for combat commands.""" from pathlib import Path from unittest.mock import AsyncMock, MagicMock import pytest from mudlib.combat import commands as combat_commands from mudlib.combat.engine import active_encounters, get_encounter from mudlib.combat.moves import load_moves from mudlib.player import Player, players @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 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): p = Player(name="Goku", x=0, y=0, reader=mock_reader, writer=mock_writer) players[p.name] = p return p @pytest.fixture def target(mock_reader, mock_writer): t = Player(name="Vegeta", x=0, y=0, reader=mock_reader, writer=mock_writer) 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.mark.asyncio async def test_attack_starts_combat_with_target(player, target): """Test attack command with target starts combat encounter.""" await combat_commands.cmd_attack(player, "punch right Vegeta") encounter = get_encounter(player) assert encounter is not None assert encounter.attacker is player assert encounter.defender is target assert player.mode == "combat" @pytest.mark.asyncio async def test_attack_without_target_when_not_in_combat(player): """Test attack without target when not in combat gives error.""" await combat_commands.cmd_attack(player, "punch right") player.writer.write.assert_called() message = player.writer.write.call_args[0][0] assert "not in combat" in message.lower() or "need a target" in message.lower() @pytest.mark.asyncio async def test_attack_without_target_when_in_combat(player, target): """Test attack without target when in combat uses implicit target.""" # Start combat first await combat_commands.cmd_attack(player, "punch right Vegeta") # Reset mock to track new calls player.writer.write.reset_mock() # Attack without target should work await combat_commands.cmd_attack(player, "punch left") encounter = get_encounter(player) assert encounter is not None assert encounter.current_move is not None assert encounter.current_move.name == "punch left" @pytest.mark.asyncio async def test_attack_unknown_move(player, target): """Test attack with unknown move name gives error.""" await combat_commands.cmd_attack(player, "kamehameha Vegeta") player.writer.write.assert_called() message = player.writer.write.call_args[0][0] assert "unknown" in message.lower() or "don't know" in message.lower() @pytest.mark.asyncio async def test_attack_insufficient_stamina(player, target): """Test attack with insufficient stamina gives error.""" player.stamina = 1.0 # Not enough for punch (costs 5) await combat_commands.cmd_attack(player, "punch right Vegeta") player.writer.write.assert_called() message = player.writer.write.call_args[0][0] assert "stamina" in message.lower() or "exhausted" in message.lower() @pytest.mark.asyncio async def test_attack_sends_telegraph_to_defender(player, target): """Test attack sends telegraph message to defender.""" await combat_commands.cmd_attack(player, "punch right Vegeta") # Check that encounter has the move encounter = get_encounter(player) assert encounter is not None assert encounter.current_move is not None assert encounter.current_move.name == "punch right" @pytest.mark.asyncio async def test_defense_only_in_combat(player): """Test defense command only works in combat mode.""" await combat_commands.cmd_defend(player, "dodge left") player.writer.write.assert_called() message = player.writer.write.call_args[0][0] assert "not in combat" in message.lower() @pytest.mark.asyncio async def test_defense_records_pending_defense(player, target): """Test defense command records the defense move.""" # Start combat await combat_commands.cmd_attack(player, "punch right Vegeta") player.writer.write.reset_mock() # Switch to defender's perspective target.writer = player.writer target.mode_stack = ["combat"] # Defend await combat_commands.cmd_defend(target, "dodge left") encounter = get_encounter(target) assert encounter is not None assert encounter.pending_defense is not None assert encounter.pending_defense.name == "dodge left" @pytest.mark.asyncio async def test_defense_unknown_move(player, target): """Test defense with unknown move gives error.""" # Start combat await combat_commands.cmd_attack(player, "punch right Vegeta") target.writer = player.writer target.mode_stack = ["combat"] player.writer.write.reset_mock() await combat_commands.cmd_defend(target, "teleport") target.writer.write.assert_called() message = target.writer.write.call_args[0][0] assert "unknown" in message.lower() or "don't know" in message.lower() @pytest.mark.asyncio async def test_defense_insufficient_stamina(player, target): """Test defense with insufficient stamina gives error.""" # Start combat await combat_commands.cmd_attack(player, "punch right Vegeta") target.writer = player.writer target.mode_stack = ["combat"] target.stamina = 1.0 # Not enough for dodge (costs 3) player.writer.write.reset_mock() await combat_commands.cmd_defend(target, "dodge left") target.writer.write.assert_called() message = target.writer.write.call_args[0][0] assert "stamina" in message.lower() or "exhausted" in message.lower() @pytest.mark.asyncio async def test_attack_alias_works(player, target): """Test attack using alias (pr for punch right).""" await combat_commands.cmd_attack(player, "pr Vegeta") encounter = get_encounter(player) assert encounter is not None assert encounter.current_move is not None assert encounter.current_move.name == "punch right" @pytest.mark.asyncio async def test_defense_alias_works(player, target): """Test defense using alias (dl for dodge left).""" # Start combat await combat_commands.cmd_attack(player, "punch right Vegeta") target.writer = player.writer target.mode_stack = ["combat"] await combat_commands.cmd_defend(target, "dl") encounter = get_encounter(target) assert encounter is not None assert encounter.pending_defense is not None assert encounter.pending_defense.name == "dodge left"