"""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