mud/tests/test_power.py
Jared Miller 292557e5fd
Add power up/down commands
Implements power level management system with tick-based power-up loop.
Players can raise PL toward max_pl (costs stamina per tick), lower PL
instantly, set exact PL targets, and cancel ongoing power-ups.
2026-02-13 23:01:33 -05:00

309 lines
8.3 KiB
Python

"""Tests for power commands."""
import asyncio
import time
from unittest.mock import AsyncMock, MagicMock
import pytest
from mudlib.combat.encounter import CombatEncounter
from mudlib.combat.engine import active_encounters
from mudlib.commands.power import cmd_power
from mudlib.player import Player, players
from mudlib.zone import Zone
@pytest.fixture(autouse=True)
def clear_state():
"""Clear players and encounters before and after each test."""
players.clear()
active_encounters.clear()
yield
players.clear()
active_encounters.clear()
@pytest.fixture
def test_zone():
"""Create a test zone for players."""
terrain = [["." for _ in range(256)] for _ in range(256)]
zone = Zone(
name="testzone",
width=256,
height=256,
toroidal=True,
terrain=terrain,
impassable=set(),
)
return zone
@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, test_zone):
p = Player(name="Goku", x=0, y=0, reader=mock_reader, writer=mock_writer)
p.location = test_zone
test_zone._contents.append(p)
players[p.name] = p
return p
@pytest.fixture
def nearby_player(mock_reader, mock_writer, test_zone):
p = Player(name="Vegeta", x=0, y=0, reader=mock_reader, writer=mock_writer)
p.location = test_zone
test_zone._contents.append(p)
players[p.name] = p
return p
@pytest.mark.asyncio
async def test_power_down_instantly_lowers_pl(player):
"""Test power down instantly lowers PL to minimum."""
player.pl = 100.0
player.max_pl = 100.0
await cmd_power(player, "down")
assert player.pl == 1.0
messages = [call[0][0] for call in player.writer.write.call_args_list]
assert any("power down" in msg.lower() for msg in messages)
@pytest.mark.asyncio
async def test_power_to_number_lowers_instantly(player):
"""Test power <number> instantly lowers when target < current."""
player.pl = 100.0
player.max_pl = 100.0
await cmd_power(player, "50")
assert player.pl == 50.0
messages = [call[0][0] for call in player.writer.write.call_args_list]
assert any("50" in msg for msg in messages)
@pytest.mark.asyncio
async def test_power_to_number_rejects_zero(player):
"""Test power <number> rejects 0 or negative values."""
player.pl = 100.0
await cmd_power(player, "0")
assert player.pl == 100.0 # unchanged
messages = [call[0][0] for call in player.writer.write.call_args_list]
assert any("must be greater than 0" in msg.lower() for msg in messages)
@pytest.mark.asyncio
async def test_power_to_number_rejects_above_max(player):
"""Test power <number> rejects values above max_pl."""
player.pl = 100.0
player.max_pl = 100.0
await cmd_power(player, "150")
assert player.pl == 100.0 # unchanged
messages = [call[0][0] for call in player.writer.write.call_args_list]
assert any("maximum" in msg.lower() for msg in messages)
@pytest.mark.asyncio
async def test_power_up_starts_increasing_pl(player):
"""Test power up starts a loop that increases PL over time."""
player.pl = 10.0
player.max_pl = 100.0
player.stamina = 100.0
await cmd_power(player, "up")
# Give it a moment to run a few ticks
await asyncio.sleep(0.15)
assert player.pl > 10.0
assert player.pl < player.max_pl
@pytest.mark.asyncio
async def test_power_up_deducts_stamina(player):
"""Test power up deducts stamina per tick."""
player.pl = 10.0
player.max_pl = 100.0
player.stamina = 100.0
await cmd_power(player, "up")
# Give it a moment to run a few ticks
await asyncio.sleep(0.15)
assert player.stamina < 100.0
@pytest.mark.asyncio
async def test_power_up_stops_at_max_pl(player):
"""Test power up stops when PL reaches max_pl."""
player.pl = 95.0
player.max_pl = 100.0
player.stamina = 100.0
await cmd_power(player, "up")
# Wait for it to complete
await asyncio.sleep(0.3)
assert player.pl == 100.0
assert player._power_task is None or player._power_task.done()
@pytest.mark.asyncio
async def test_power_up_stops_when_stamina_depleted(player):
"""Test power up stops when stamina runs out."""
player.pl = 10.0
player.max_pl = 100.0
player.stamina = 5.0 # Very low stamina
await cmd_power(player, "up")
# Wait for stamina to deplete
await asyncio.sleep(0.3)
# PL should have increased a little, but stopped
assert player.pl > 10.0
assert player.pl < 100.0
assert player.stamina <= 0.0
assert player._power_task is None or player._power_task.done()
@pytest.mark.asyncio
async def test_power_stop_cancels_power_up(player):
"""Test power stop cancels an ongoing power-up."""
player.pl = 10.0
player.max_pl = 100.0
player.stamina = 100.0
await cmd_power(player, "up")
await asyncio.sleep(0.05) # Let it start
await cmd_power(player, "stop")
pl_at_stop = player.pl
await asyncio.sleep(0.1) # Wait a bit more
# PL should not have increased after stop
assert player.pl == pl_at_stop
assert player._power_task is None or player._power_task.done()
@pytest.mark.asyncio
async def test_power_up_rejects_during_combat(player, nearby_player):
"""Test power up is rejected when player is in combat."""
player.pl = 50.0
player.stamina = 100.0
# Put player in combat
encounter = CombatEncounter(
attacker=player, defender=nearby_player, last_action_at=time.monotonic()
)
active_encounters.append(encounter)
await cmd_power(player, "up")
messages = [call[0][0] for call in player.writer.write.call_args_list]
assert any("combat" in msg.lower() for msg in messages)
assert player.pl == 50.0 # unchanged
@pytest.mark.asyncio
async def test_power_up_rejects_with_no_stamina(player):
"""Test power up is rejected when stamina is 0."""
player.pl = 50.0
player.stamina = 0.0
await cmd_power(player, "up")
messages = [call[0][0] for call in player.writer.write.call_args_list]
assert any("stamina" in msg.lower() for msg in messages)
assert player.pl == 50.0 # unchanged
@pytest.mark.asyncio
async def test_power_up_sends_feedback_messages(player):
"""Test power up sends feedback to player."""
player.pl = 10.0
player.max_pl = 100.0
player.stamina = 100.0
await cmd_power(player, "up")
messages = [call[0][0] for call in player.writer.write.call_args_list]
assert any("powering up" in msg.lower() for msg in messages)
@pytest.mark.asyncio
async def test_power_up_broadcasts_to_nearby(player, nearby_player):
"""Test power up broadcasts to nearby players."""
player.pl = 10.0
player.max_pl = 100.0
player.stamina = 100.0
await cmd_power(player, "up")
nearby_messages = [call[0][0] for call in nearby_player.writer.write.call_args_list]
assert any("Goku" in msg and "power" in msg.lower() for msg in nearby_messages)
@pytest.mark.asyncio
async def test_power_up_when_already_powering_up(player):
"""Test power up when already powering up sends message."""
player.pl = 10.0
player.max_pl = 100.0
player.stamina = 100.0
await cmd_power(player, "up")
await asyncio.sleep(0.05)
# Try to power up again
await cmd_power(player, "up")
messages = [call[0][0] for call in player.writer.write.call_args_list]
assert any("already" in msg.lower() for msg in messages)
@pytest.mark.asyncio
async def test_power_down_broadcasts_to_nearby(player, nearby_player):
"""Test power down broadcasts to nearby players."""
player.pl = 100.0
await cmd_power(player, "down")
nearby_messages = [call[0][0] for call in nearby_player.writer.write.call_args_list]
assert any("Goku" in msg and "power" in msg.lower() for msg in nearby_messages)
@pytest.mark.asyncio
async def test_power_to_number_raises_with_loop(player):
"""Test power <number> starts power-up loop when target > current."""
player.pl = 10.0
player.max_pl = 100.0
player.stamina = 100.0
await cmd_power(player, "80")
# Give it a moment to run
await asyncio.sleep(0.15)
# Should be increasing toward 80
assert player.pl > 10.0
assert player.stamina < 100.0 # deducting stamina