"""Tests for unconscious state mechanics.""" import pytest from mudlib.combat.engine import active_encounters, start_encounter from mudlib.combat.moves import CombatMove from mudlib.player import Player, players from mudlib.unconscious import process_unconscious from mudlib.zone import Zone class MockWriter: """Mock writer that captures output.""" def __init__(self): self.written = [] self.closed = False # Mock option negotiation state from unittest.mock import MagicMock self.local_option = MagicMock() self.local_option.enabled = MagicMock(return_value=False) self.remote_option = MagicMock() self.remote_option.enabled = MagicMock(return_value=False) def write(self, data): if isinstance(data, str): self.written.append(data) async def drain(self): pass def close(self): self.closed = True def is_closing(self): return self.closed @pytest.fixture def clear_state(): """Clear global state before each test.""" players.clear() active_encounters.clear() yield players.clear() active_encounters.clear() @pytest.fixture def mock_writer(): """Create a mock writer for testing.""" return MockWriter() def test_unconscious_when_pl_zero(clear_state, mock_writer): """Test that entity becomes unconscious when PL reaches 0.""" player = Player(name="TestPlayer", x=0, y=0, pl=100.0, writer=mock_writer) # Player should start conscious assert player.posture != "unconscious" # Drop PL to 0 player.pl = 0.0 # Player should now be unconscious assert player.posture == "unconscious" def test_unconscious_when_stamina_zero(clear_state, mock_writer): """Test that entity becomes unconscious when stamina reaches 0.""" player = Player(name="TestPlayer", x=0, y=0, stamina=100.0, writer=mock_writer) # Player should start conscious assert player.posture != "unconscious" # Drop stamina to 0 player.stamina = 0.0 # Player should now be unconscious assert player.posture == "unconscious" @pytest.mark.asyncio async def test_unconscious_recovery(clear_state, mock_writer): """Test that unconscious player recovers after ticks.""" player = Player( name="TestPlayer", x=0, y=0, pl=0.0, stamina=0.0, writer=mock_writer ) players["TestPlayer"] = player # Player should start unconscious assert player.posture == "unconscious" # Process recovery ticks until player is conscious # Recovery rate is 0.1 per tick for both PL and stamina # Need 1 tick to get above 0 await process_unconscious() # After one tick, both should be above 0 assert player.pl > 0.0 assert player.stamina > 0.0 assert player.posture != "unconscious" @pytest.mark.asyncio async def test_come_to_message(clear_state, mock_writer): """Test that 'come to' message is sent on recovery.""" player = Player( name="TestPlayer", x=0, y=0, pl=0.0, stamina=0.0, writer=mock_writer ) players["TestPlayer"] = player # Clear any initial messages mock_writer.written.clear() # Process recovery await process_unconscious() # Check for come to message assert any("come to" in msg.lower() for msg in mock_writer.written) @pytest.mark.asyncio async def test_snap_neck_requires_unconscious(clear_state): """Test that snap neck only works on unconscious target.""" from mudlib.commands.snapneck import cmd_snap_neck attacker_writer = MockWriter() defender_writer = MockWriter() attacker = Player(name="Attacker", x=0, y=0, writer=attacker_writer) defender = Player(name="Defender", x=0, y=0, writer=defender_writer, pl=100.0) terrain = [["." for _ in range(3)] for _ in range(3)] zone = Zone(name="test", width=3, height=3, toroidal=True, terrain=terrain) attacker.location = zone defender.location = zone zone._contents.extend([attacker, defender]) players["Attacker"] = attacker players["Defender"] = defender # Start encounter first start_encounter(attacker, defender) # Try to snap neck on conscious target attacker_writer.written.clear() await cmd_snap_neck(attacker, "Defender") # Should fail with "not unconscious" message assert any("unconscious" in msg.lower() for msg in attacker_writer.written) @pytest.mark.asyncio async def test_snap_neck_success(clear_state): """Test that snap neck works on unconscious target.""" from mudlib.commands.snapneck import cmd_snap_neck attacker_writer = MockWriter() defender_writer = MockWriter() attacker = Player(name="Attacker", x=0, y=0, writer=attacker_writer) defender = Player(name="Defender", x=0, y=0, writer=defender_writer, pl=0.0) terrain = [["." for _ in range(3)] for _ in range(3)] zone = Zone(name="test", width=3, height=3, toroidal=True, terrain=terrain) attacker.location = zone defender.location = zone zone._contents.extend([attacker, defender]) players["Attacker"] = attacker players["Defender"] = defender # Start encounter encounter = start_encounter(attacker, defender) # Clear messages attacker_writer.written.clear() defender_writer.written.clear() # Snap neck on unconscious target await cmd_snap_neck(attacker, "Defender") # Should succeed with dramatic message assert any("snap" in msg.lower() for msg in attacker_writer.written) # Encounter should end assert encounter not in active_encounters @pytest.mark.asyncio async def test_snap_neck_message_sent_to_both(clear_state): """Test that snap neck message is sent to both parties.""" from mudlib.commands.snapneck import cmd_snap_neck attacker_writer = MockWriter() defender_writer = MockWriter() attacker = Player(name="Attacker", x=0, y=0, writer=attacker_writer) defender = Player(name="Defender", x=0, y=0, writer=defender_writer, pl=0.0) terrain = [["." for _ in range(3)] for _ in range(3)] zone = Zone(name="test", width=3, height=3, toroidal=True, terrain=terrain) attacker.location = zone defender.location = zone zone._contents.extend([attacker, defender]) players["Attacker"] = attacker players["Defender"] = defender # Start encounter start_encounter(attacker, defender) # Clear messages attacker_writer.written.clear() defender_writer.written.clear() # Snap neck await cmd_snap_neck(attacker, "Defender") # Both should receive messages assert len(attacker_writer.written) > 0 assert len(defender_writer.written) > 0 @pytest.mark.asyncio async def test_knockout_ends_combat(clear_state, mock_writer): """Test that knockout does not auto-end combat.""" from mudlib.combat.encounter import CombatEncounter, CombatState attacker = Player(name="Attacker", x=0, y=0, writer=mock_writer) defender = Player(name="Defender", x=0, y=0, writer=mock_writer, pl=1.0) # Create a simple punch move for testing move = CombatMove( name="test punch", command="testpunch", move_type="attack", damage_pct=0.5, stamina_cost=10.0, timing_window_ms=850, countered_by=[], ) encounter = CombatEncounter(attacker=attacker, defender=defender) encounter.state = CombatState.RESOLVE encounter.current_move = move # Resolve should detect knockout result = encounter.resolve() # Knockout does not end combat by itself assert result.combat_ended is False @pytest.mark.asyncio async def test_defender_stamina_ko_ends_combat(clear_state, mock_writer): """Defender stamina KO should not auto-end combat.""" from mudlib.combat.encounter import CombatEncounter, CombatState attacker = Player(name="Attacker", x=0, y=0, writer=mock_writer) defender = Player(name="Defender", x=0, y=0, writer=mock_writer, stamina=0.0) move = CombatMove( name="test punch", command="testpunch", move_type="attack", damage_pct=0.5, stamina_cost=10.0, timing_window_ms=850, countered_by=[], ) encounter = CombatEncounter(attacker=attacker, defender=defender) encounter.state = CombatState.RESOLVE encounter.current_move = move result = encounter.resolve() assert result.combat_ended is False @pytest.mark.asyncio async def test_partial_recovery(clear_state, mock_writer): """Test that only PL or stamina being above 0 doesn't wake player.""" player = Player( name="TestPlayer", x=0, y=0, pl=5.0, stamina=0.0, writer=mock_writer ) players["TestPlayer"] = player # Player should be unconscious (stamina is 0) assert player.posture == "unconscious" # Process recovery - only stamina should increase await process_unconscious() # Check that PL wasn't affected (started at 5.0, should stay at 5.0) assert player.pl == 5.0 # Stamina should have increased assert player.stamina > 0.0 # Player should now be conscious (both > 0) assert player.posture != "unconscious"