mud/tests/test_fly.py
Jared Miller 93ad4523e2
Make flying a toggle state
fly with no args toggles flying on/off. Movement commands (fly east,
etc) only work while airborne. "You aren't flying." if you try to
move without toggling on first. Player.flying field tracks the state.
2026-02-07 14:48:42 -05:00

293 lines
7.2 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()
# --- toggle ---
@pytest.mark.asyncio
async def test_fly_toggles_on(player, mock_writer):
"""fly with no args sets flying=True."""
players[player.name] = player
assert not player.flying
await fly.cmd_fly(player, "")
assert player.flying
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_toggles_off(player, mock_writer):
"""fly again sets flying=False."""
players[player.name] = player
player.flying = True
await fly.cmd_fly(player, "")
assert not player.flying
calls = [str(c) for c in mock_writer.write.call_args_list]
assert any("land" in c.lower() for c in calls)
@pytest.mark.asyncio
async def test_fly_toggle_on_notifies_nearby(player):
"""Others see liftoff message."""
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, "")
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_toggle_off_notifies_nearby(player):
"""Others see landing message."""
players[player.name] = player
player.flying = True
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, "")
calls = [str(c) for c in other_writer.write.call_args_list]
assert any("shmup" in c.lower() and "lands" in c.lower() for c in calls)
# --- must be flying to move ---
@pytest.mark.asyncio
async def test_fly_direction_without_flying_errors(player, mock_writer):
"""fly east when not flying gives an error."""
players[player.name] = player
assert not player.flying
await fly.cmd_fly(player, "east")
calls = [str(c) for c in mock_writer.write.call_args_list]
assert any("aren't flying" in c.lower() for c in calls)
assert player.x == 50
assert player.y == 50
# --- movement while flying ---
@pytest.mark.asyncio
async def test_fly_east_moves_5_tiles(player):
"""fly east should move player 5 tiles east."""
players[player.name] = player
player.flying = True
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
player.flying = True
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
player.flying = True
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
player.flying = True
players[player.name] = player
await fly.cmd_fly(player, "east")
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
player.flying = True
await fly.cmd_fly(player, "east")
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
player.flying = True
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
player.flying = True
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
player.flying = True
await fly.cmd_fly(player, "se")
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
# --- no movement without flying leaves no trail ---
@pytest.mark.asyncio
async def test_no_trail_when_not_flying(player):
"""Trying to fly a direction while grounded creates no effects."""
players[player.name] = player
await fly.cmd_fly(player, "east")
assert len(active_effects) == 0
# --- error cases ---
@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
player.flying = True
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
player.flying = True
await fly.cmd_fly(player, "east")
assert mock_world.get_viewport.called
@pytest.mark.asyncio
async def test_stays_flying_after_move(player):
"""Moving while flying doesn't turn off flying."""
players[player.name] = player
player.flying = True
await fly.cmd_fly(player, "east")
assert player.flying