fly <direction> moves the player 5 tiles, ignoring terrain. Leaves a trail of bright white ~ clouds that fade after 2 seconds. Effects system supports arbitrary timed visual overlays on the viewport. TinTin aliases: fn/fs/fe/fw/fne/fnw/fse/fsw.
216 lines
5.7 KiB
Python
216 lines
5.7 KiB
Python
"""Tests for the fly command."""
|
|
|
|
import time
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
|
|
from mudlib.commands import fly, look, movement
|
|
from mudlib.effects import active_effects
|
|
from mudlib.player import Player, players
|
|
|
|
|
|
@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 mock_world():
|
|
world = MagicMock()
|
|
world.width = 100
|
|
world.height = 100
|
|
world.wrap = MagicMock(side_effect=lambda x, y: (x % 100, y % 100))
|
|
viewport = [["." for _ in range(21)] for _ in range(11)]
|
|
world.get_viewport = MagicMock(return_value=viewport)
|
|
return world
|
|
|
|
|
|
@pytest.fixture
|
|
def player(mock_reader, mock_writer):
|
|
return Player(name="shmup", x=50, y=50, reader=mock_reader, writer=mock_writer)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def clean_state(mock_world):
|
|
"""Clean global state before/after each test."""
|
|
fly.world = mock_world
|
|
look.world = mock_world
|
|
movement.world = mock_world
|
|
players.clear()
|
|
active_effects.clear()
|
|
yield
|
|
players.clear()
|
|
active_effects.clear()
|
|
|
|
|
|
# --- direction parsing ---
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fly_east_moves_5_tiles(player):
|
|
"""fly east should move player 5 tiles east."""
|
|
players[player.name] = player
|
|
await fly.cmd_fly(player, "east")
|
|
|
|
assert player.x == 55
|
|
assert player.y == 50
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fly_north_moves_5_tiles(player):
|
|
players[player.name] = player
|
|
await fly.cmd_fly(player, "north")
|
|
|
|
assert player.x == 50
|
|
assert player.y == 45
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fly_northwest_moves_5_diagonal(player):
|
|
players[player.name] = player
|
|
await fly.cmd_fly(player, "nw")
|
|
|
|
assert player.x == 45
|
|
assert player.y == 45
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fly_wraps_around_world(player):
|
|
"""Flying near an edge should wrap toroidally."""
|
|
player.x = 98
|
|
player.y = 50
|
|
players[player.name] = player
|
|
await fly.cmd_fly(player, "east")
|
|
|
|
# 98 + 5 = 103, wraps to 3
|
|
assert player.x == 3
|
|
assert player.y == 50
|
|
|
|
|
|
# --- cloud trail ---
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fly_creates_cloud_trail(player):
|
|
"""Flying should leave ~ effects along the path."""
|
|
players[player.name] = player
|
|
await fly.cmd_fly(player, "east")
|
|
|
|
# trail at positions 50,51,52,53,54 (start through second-to-last)
|
|
# player ends at 55
|
|
assert len(active_effects) == 5
|
|
trail_positions = [(e.x, e.y) for e in active_effects]
|
|
for i in range(5):
|
|
assert (50 + i, 50) in trail_positions
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cloud_trail_chars_are_tilde(player):
|
|
players[player.name] = player
|
|
await fly.cmd_fly(player, "east")
|
|
|
|
for e in active_effects:
|
|
assert e.char == "~"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cloud_trail_has_ttl(player):
|
|
"""Cloud effects should expire after roughly 2 seconds."""
|
|
players[player.name] = player
|
|
|
|
before = time.monotonic()
|
|
await fly.cmd_fly(player, "east")
|
|
|
|
for e in active_effects:
|
|
remaining = e.expires_at - before
|
|
assert 1.5 <= remaining <= 2.5
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fly_diagonal_trail(player):
|
|
"""Diagonal flight should leave trail at each intermediate step."""
|
|
players[player.name] = player
|
|
await fly.cmd_fly(player, "se")
|
|
|
|
# southeast: dx=1, dy=1, 5 steps
|
|
# trail at (50,50), (51,51), (52,52), (53,53), (54,54)
|
|
# player ends at (55, 55)
|
|
assert len(active_effects) == 5
|
|
trail_positions = [(e.x, e.y) for e in active_effects]
|
|
for i in range(5):
|
|
assert (50 + i, 50 + i) in trail_positions
|
|
|
|
|
|
# --- messages ---
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fly_sends_self_message(player, mock_writer):
|
|
"""Player should see 'You fly into the air'."""
|
|
players[player.name] = player
|
|
await fly.cmd_fly(player, "east")
|
|
|
|
calls = [str(c) for c in mock_writer.write.call_args_list]
|
|
assert any("fly" in c.lower() and "air" in c.lower() for c in calls)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fly_sends_nearby_message(player):
|
|
"""Other players should see '{name} lifts into the air'."""
|
|
players[player.name] = player
|
|
|
|
other_writer = MagicMock()
|
|
other_writer.write = MagicMock()
|
|
other_writer.drain = AsyncMock()
|
|
other = Player(
|
|
name="bystander", x=52, y=50, reader=MagicMock(), writer=other_writer
|
|
)
|
|
players[other.name] = other
|
|
|
|
await fly.cmd_fly(player, "east")
|
|
|
|
calls = [str(c) for c in other_writer.write.call_args_list]
|
|
assert any("shmup" in c.lower() and "lifts" in c.lower() for c in calls)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fly_no_direction_gives_error(player, mock_writer):
|
|
"""fly with no args should tell the player to specify a direction."""
|
|
players[player.name] = player
|
|
await fly.cmd_fly(player, "")
|
|
|
|
calls = [str(c) for c in mock_writer.write.call_args_list]
|
|
assert any("direction" in c.lower() for c in calls)
|
|
assert player.x == 50
|
|
assert player.y == 50
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fly_bad_direction_gives_error(player, mock_writer):
|
|
"""fly with an invalid direction should give feedback."""
|
|
players[player.name] = player
|
|
await fly.cmd_fly(player, "sideways")
|
|
|
|
calls = [str(c) for c in mock_writer.write.call_args_list]
|
|
assert any("direction" in c.lower() for c in calls)
|
|
assert player.x == 50
|
|
assert player.y == 50
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fly_triggers_look(player, mock_world):
|
|
"""Flying should auto-look at the destination."""
|
|
players[player.name] = player
|
|
await fly.cmd_fly(player, "east")
|
|
|
|
# get_viewport is called by look, which fly triggers
|
|
assert mock_world.get_viewport.called
|