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