301 lines
8.9 KiB
Python
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,
|
|
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"
|