mud/tests/test_unconscious.py
Jared Miller b4dea1d349
Add unconscious state with automatic recovery
Players become unconscious when PL or stamina drops to 0. While unconscious, both stats slowly recover at 0.1 per tick (1.0 per second). When both reach above 0, player regains consciousness with a message. Recovery runs in the main game loop via process_unconscious.
2026-02-14 01:00:37 -05:00

259 lines
7.4 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
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)
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)
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)
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 combat ends when a player is knocked out."""
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()
# Combat should end
assert result.combat_ended
@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"