"""Tests for the command system.""" from unittest.mock import AsyncMock, MagicMock import pytest from mudlib import commands from mudlib.commands import look, movement from mudlib.player import Player @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.is_passable = MagicMock(return_value=True) # Create a 21x11 viewport filled with "." 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="TestPlayer", x=5, y=5, reader=mock_reader, writer=mock_writer) # Test command registration def test_register_command(): """Test that commands can be registered.""" async def test_handler(player, args): pass commands.register("test", test_handler) assert "test" in commands._registry def test_register_command_with_aliases(): """Test that command aliases work.""" async def test_handler(player, args): pass commands.register("testcmd", test_handler, aliases=["tc", "t"]) assert "testcmd" in commands._registry assert "tc" in commands._registry assert "t" in commands._registry assert commands._registry["testcmd"] == commands._registry["tc"] assert commands._registry["testcmd"] == commands._registry["t"] @pytest.mark.asyncio async def test_dispatch_routes_to_handler(player): """Test that dispatch routes input to the correct handler.""" called = False received_args = None async def test_handler(p, args): nonlocal called, received_args called = True received_args = args commands.register("testcmd", test_handler) await commands.dispatch(player, "testcmd arg1 arg2") assert called assert received_args == "arg1 arg2" @pytest.mark.asyncio async def test_dispatch_handles_unknown_command(player, mock_writer): """Test that unknown commands give feedback.""" await commands.dispatch(player, "unknowncommand") # Should have written some kind of error message assert mock_writer.write.called error_msg = mock_writer.write.call_args[0][0] assert "unknown" in error_msg.lower() or "not found" in error_msg.lower() @pytest.mark.asyncio async def test_dispatch_handles_empty_input(player): """Test that empty input doesn't crash.""" await commands.dispatch(player, "") await commands.dispatch(player, " ") # Test movement direction parsing @pytest.mark.parametrize( "direction,expected_delta", [ ("n", (0, -1)), ("north", (0, -1)), ("s", (0, 1)), ("south", (0, 1)), ("e", (1, 0)), ("east", (1, 0)), ("w", (-1, 0)), ("west", (-1, 0)), ("ne", (1, -1)), ("northeast", (1, -1)), ("nw", (-1, -1)), ("northwest", (-1, -1)), ("se", (1, 1)), ("southeast", (1, 1)), ("sw", (-1, 1)), ("southwest", (-1, 1)), ], ) def test_direction_deltas(direction, expected_delta): """Test that all direction commands map to correct deltas.""" assert movement.DIRECTIONS[direction] == expected_delta @pytest.mark.asyncio async def test_movement_updates_position(player, mock_world): """Test that movement updates player position when passable.""" # Inject mock world into both movement and look modules movement.world = mock_world look.world = mock_world original_x, original_y = player.x, player.y await movement.move_north(player, "") assert player.x == original_x assert player.y == original_y - 1 assert mock_world.is_passable.called @pytest.mark.asyncio async def test_movement_blocked_by_impassable_terrain(player, mock_world, mock_writer): """Test that movement is blocked by impassable terrain.""" mock_world.is_passable.return_value = False movement.world = mock_world original_x, original_y = player.x, player.y await movement.move_north(player, "") # Position should not change assert player.x == original_x assert player.y == original_y # Should send a message to the player assert mock_writer.write.called error_msg = mock_writer.write.call_args[0][0] assert "can't" in error_msg.lower() or "cannot" in error_msg.lower() @pytest.mark.asyncio async def test_movement_sends_departure_message(player, mock_world): """Test that movement sends departure message to nearby players.""" movement.world = mock_world look.world = mock_world # Create another player in the area other_writer = MagicMock() other_writer.write = MagicMock() other_writer.drain = AsyncMock() other_player = Player( name="OtherPlayer", x=5, y=4, reader=MagicMock(), writer=other_writer ) # Register both players from mudlib.player import players players.clear() players[player.name] = player players[other_player.name] = other_player await movement.move_north(player, "") # Other player should have received a departure message # (We'll check this is called, exact message format is implementation detail) assert other_player.writer.write.called @pytest.mark.asyncio async def test_arrival_message_uses_opposite_direction(player, mock_world): """Test that arrival messages use the opposite direction.""" movement.world = mock_world look.world = mock_world # Create another player at the destination other_writer = MagicMock() other_writer.write = MagicMock() other_writer.drain = AsyncMock() other_player = Player( name="OtherPlayer", x=5, y=3, reader=MagicMock(), writer=other_writer ) from mudlib.player import players players.clear() players[player.name] = player players[other_player.name] = other_player # Player at (5, 5) moves north to (5, 4) await movement.move_north(player, "") # Other player at (5, 3) should see arrival "from the south" # (Implementation will determine exact message format) assert other_player.writer.write.called # Test look command @pytest.mark.asyncio async def test_look_command_sends_viewport(player, mock_world): """Test that look command sends the viewport to the player.""" look.world = mock_world await look.cmd_look(player, "") assert mock_world.get_viewport.called assert player.writer.write.called @pytest.mark.asyncio async def test_look_command_shows_player_at_center(player, mock_world): """Test that look command shows player @ at center.""" look.world = mock_world await look.cmd_look(player, "") # Check that the output contains the @ symbol for the player output = "".join([call[0][0] for call in player.writer.write.call_args_list]) assert "@" in output @pytest.mark.asyncio async def test_look_command_shows_other_players(player, mock_world): """Test that look command shows other players as *.""" look.world = mock_world # Create another player in the viewport other_player = Player( name="OtherPlayer", x=6, y=5, reader=MagicMock(), writer=MagicMock(), ) from mudlib.player import players players.clear() players[player.name] = player players[other_player.name] = other_player await look.cmd_look(player, "") # Check that the output contains * for other players output = "".join([call[0][0] for call in player.writer.write.call_args_list]) assert "*" in output