From a98f340e5a135fc0e7867492648c8b7e7e9196d5 Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Sat, 14 Feb 2026 01:16:51 -0500 Subject: [PATCH] Wire target resolution into look command --- src/mudlib/commands/look.py | 50 ++++++++- tests/test_look_targeting.py | 207 +++++++++++++++++++++++++++++++++++ 2 files changed, 253 insertions(+), 4 deletions(-) create mode 100644 tests/test_look_targeting.py diff --git a/src/mudlib/commands/look.py b/src/mudlib/commands/look.py index 0f567e5..fe1556d 100644 --- a/src/mudlib/commands/look.py +++ b/src/mudlib/commands/look.py @@ -1,7 +1,6 @@ """Look command for viewing the world.""" from mudlib.commands import CommandDefinition, register -from mudlib.commands.examine import cmd_examine from mudlib.commands.things import _format_thing_name from mudlib.effects import get_effects_at from mudlib.entity import Entity @@ -27,11 +26,54 @@ async def cmd_look(player: Player, args: str) -> None: Args: player: The player executing the command - args: Command arguments (if provided, route to examine) + args: Command arguments (if provided, use targeting to resolve) """ - # If args provided, route to examine + # If args provided, use targeting to resolve if args.strip(): - await cmd_examine(player, args) + from mudlib.targeting import ( + find_entity_on_tile, + find_in_inventory, + find_thing_on_tile, + ) + + target_name = args.strip() + + # First try to find an entity on the tile + entity = find_entity_on_tile(target_name, player) + if entity: + # Show entity info (name and posture) + if hasattr(entity, "description") and entity.description: + await player.send(f"{entity.description}\r\n") + else: + await player.send(f"{entity.name} is {entity.posture}.\r\n") + return + + # Then try to find a thing on the ground + zone = player.location + if zone is not None and isinstance(zone, Zone): + thing = find_thing_on_tile(target_name, zone, player.x, player.y) + if thing: + # Show thing description + desc = getattr(thing, "description", "") + if desc: + await player.send(f"{desc}\r\n") + else: + await player.send("You see nothing special.\r\n") + return + + # Finally try inventory + thing = find_in_inventory(target_name, player) + if thing: + # Show thing description + desc = getattr(thing, "description", "") + if desc: + await player.send(f"{desc}\r\n") + else: + await player.send("You see nothing special.\r\n") + return + + # Nothing found + await player.send("You don't see that here.\r\n") return zone = player.location diff --git a/tests/test_look_targeting.py b/tests/test_look_targeting.py new file mode 100644 index 0000000..c59e19f --- /dev/null +++ b/tests/test_look_targeting.py @@ -0,0 +1,207 @@ +"""Tests for look command target resolution.""" + +import pytest + +from mudlib.commands.look import cmd_look +from mudlib.entity import Mob +from mudlib.player import Player +from mudlib.thing import Thing +from mudlib.zone import Zone + + +@pytest.fixture +def zone(): + """Create a test zone.""" + terrain = [["." for _ in range(100)] for _ in range(100)] + return Zone( + name="test", + description="Test Zone", + width=100, + height=100, + toroidal=False, + terrain=terrain, + ) + + +@pytest.fixture +def mock_writer(): + """Create a mock writer that captures output.""" + from unittest.mock import MagicMock + + class MockWriter: + def __init__(self): + self.output = [] + # Mock telnet options + self.local_option = MagicMock() + self.remote_option = MagicMock() + self.local_option.enabled = MagicMock(return_value=False) + self.remote_option.enabled = MagicMock(return_value=False) + + def write(self, data): + self.output.append(data) + + async def drain(self): + pass + + def get_output(self): + return "".join(self.output) + + return MockWriter() + + +@pytest.fixture +def player(zone, mock_writer): + """Create a test player.""" + p = Player( + name="TestPlayer", + location=zone, + x=50, + y=50, + writer=mock_writer, + ) + return p + + +@pytest.mark.asyncio +async def test_look_finds_mob_by_exact_name(player, zone, mock_writer): + """look goblin finds mob with exact name.""" + _mob = Mob(name="goblin", location=zone, x=50, y=50) + + await cmd_look(player, "goblin") + + output = mock_writer.get_output() + assert "goblin" in output.lower() + + +@pytest.mark.asyncio +async def test_look_finds_mob_by_prefix(player, zone, mock_writer): + """look gob prefix matches goblin.""" + _mob = Mob(name="goblin", location=zone, x=50, y=50) + + await cmd_look(player, "gob") + + output = mock_writer.get_output() + assert "goblin" in output.lower() + + +@pytest.mark.asyncio +async def test_look_finds_second_mob_with_ordinal(player, zone, mock_writer): + """look 2.goblin finds second goblin.""" + _mob1 = Mob(name="goblin", location=zone, x=50, y=50) + _mob2 = Mob(name="goblin", location=zone, x=50, y=50) + + await cmd_look(player, "2.goblin") + + output = mock_writer.get_output() + # Should find the second goblin + assert "goblin" in output.lower() + + +@pytest.mark.asyncio +async def test_look_finds_thing_on_ground(player, zone, mock_writer): + """look sword finds thing on ground and shows description.""" + _sword = Thing( + name="sword", + description="A sharp blade.", + location=zone, + x=50, + y=50, + ) + + await cmd_look(player, "sword") + + output = mock_writer.get_output() + assert "A sharp blade." in output + + +@pytest.mark.asyncio +async def test_look_shows_error_for_nonexistent(player, zone, mock_writer): + """look nonexistent shows 'You don't see that here.'""" + await cmd_look(player, "nonexistent") + + output = mock_writer.get_output() + assert "You don't see that here." in output + + +@pytest.mark.asyncio +async def test_look_prioritizes_entity_over_thing(player, zone, mock_writer): + """look target finds entity before thing with same name.""" + _mob = Mob(name="target", location=zone, x=50, y=50) + _thing = Thing( + name="target", + description="A thing.", + location=zone, + x=50, + y=50, + ) + + await cmd_look(player, "target") + + output = mock_writer.get_output() + # Should show entity info (name/posture), not thing description + assert "target" in output.lower() + # Should not show thing description + assert "A thing." not in output + + +@pytest.mark.asyncio +async def test_look_skips_dead_mobs(player, zone, mock_writer): + """look goblin skips dead mobs.""" + _mob = Mob(name="goblin", location=zone, x=50, y=50) + _mob.alive = False + + await cmd_look(player, "goblin") + + output = mock_writer.get_output() + assert "You don't see that here." in output + + +@pytest.mark.asyncio +async def test_look_skips_self(player, zone, mock_writer): + """look TestPlayer doesn't target the player themselves.""" + await cmd_look(player, "TestPlayer") + + output = mock_writer.get_output() + assert "You don't see that here." in output + + +@pytest.mark.asyncio +async def test_look_finds_thing_in_inventory(player, zone, mock_writer): + """look sword finds thing in inventory when not on ground.""" + _sword = Thing( + name="sword", + description="A sharp blade.", + location=player, + ) + + await cmd_look(player, "sword") + + output = mock_writer.get_output() + assert "A sharp blade." in output + + +@pytest.mark.asyncio +async def test_look_finds_second_thing_with_ordinal(player, zone, mock_writer): + """look 2.sword finds second sword on ground.""" + _sword1 = Thing( + name="sword", + description="A rusty blade.", + location=zone, + x=50, + y=50, + ) + _sword2 = Thing( + name="sword", + description="A sharp blade.", + location=zone, + x=50, + y=50, + ) + + await cmd_look(player, "2.sword") + + output = mock_writer.get_output() + # Should show the second sword's description + assert "A sharp blade." in output + # Should not show the first sword's description + assert "A rusty blade." not in output