mud/tests/test_unconscious.py
Jared Miller edbad4666f
Rework combat state machine
PENDING phase, defense active/recovery windows
2026-02-16 12:17:34 -05:00

301 lines
8.9 KiB
Python

"""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,
hit_time_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,
hit_time_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"