From aca98648815d3c57f241cb22ad684096cd1e5c4b Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Sat, 14 Feb 2026 01:20:45 -0500 Subject: [PATCH] Wire target resolution into thing commands Replace local exact-match helpers with targeting module calls for prefix matching and ordinal disambiguation. Works in get, drop, and container extraction (get X from Y). --- src/mudlib/commands/things.py | 48 +++++-------- tests/test_things_targeting.py | 125 +++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 32 deletions(-) create mode 100644 tests/test_things_targeting.py diff --git a/src/mudlib/commands/things.py b/src/mudlib/commands/things.py index 945f4c0..2749598 100644 --- a/src/mudlib/commands/things.py +++ b/src/mudlib/commands/things.py @@ -3,34 +3,25 @@ from mudlib.commands import CommandDefinition, register from mudlib.container import Container from mudlib.player import Player +from mudlib.targeting import find_in_inventory, find_thing_on_tile from mudlib.thing import Thing from mudlib.zone import Zone def _find_thing_at(name: str, zone: Zone, x: int, y: int) -> Thing | None: - """Find a thing on the ground matching name or alias.""" - name_lower = name.lower() - for obj in zone.contents_at(x, y): - if not isinstance(obj, Thing): - continue - if obj.name.lower() == name_lower: - return obj - if name_lower in (a.lower() for a in obj.aliases): - return obj - return None + """Find a thing on the ground matching name or alias. + + Deprecated: Use find_thing_on_tile from mudlib.targeting instead. + """ + return find_thing_on_tile(name, zone, x, y) def _find_thing_in_inventory(name: str, player: Player) -> Thing | None: - """Find a thing in the player's inventory matching name or alias.""" - name_lower = name.lower() - for obj in player.contents: - if not isinstance(obj, Thing): - continue - if obj.name.lower() == name_lower: - return obj - if name_lower in (a.lower() for a in obj.aliases): - return obj - return None + """Find a thing in the player's inventory matching name or alias. + + Deprecated: Use find_in_inventory from mudlib.targeting instead. + """ + return find_in_inventory(name, player) def _format_thing_name(thing: Thing) -> str: @@ -113,18 +104,11 @@ async def _handle_take_from(player: Player, args: str) -> None: await player.send(f"The {container_obj.name} is closed.\r\n") return - # Find thing in container - thing = None - thing_name_lower = thing_name.lower() - for obj in container_obj.contents: - if not isinstance(obj, Thing): - continue - if obj.name.lower() == thing_name_lower: - thing = obj - break - if thing_name_lower in (a.lower() for a in obj.aliases): - thing = obj - break + # Find thing in container using targeting (supports prefix and ordinals) + from mudlib.targeting import resolve_target + + container_things = [obj for obj in container_obj.contents if isinstance(obj, Thing)] + thing = resolve_target(thing_name, container_things) if thing is None: await player.send(f"The {container_obj.name} doesn't contain that.\r\n") diff --git a/tests/test_things_targeting.py b/tests/test_things_targeting.py new file mode 100644 index 0000000..ce9a4b8 --- /dev/null +++ b/tests/test_things_targeting.py @@ -0,0 +1,125 @@ +"""Tests for targeting integration in thing commands.""" + +import pytest + +from mudlib.commands.things import cmd_drop, cmd_get +from mudlib.container import Container +from mudlib.thing import Thing + + +@pytest.mark.asyncio +async def test_get_prefix_match(player, test_zone): + """Test that 'get gob' prefix matches 'goblin figurine' on ground.""" + figurine = Thing(name="goblin figurine", x=0, y=0) + figurine.location = test_zone + test_zone._contents.append(figurine) + + await cmd_get(player, "gob") + + # Check thing moved to player inventory + assert figurine in player.contents + assert figurine not in test_zone._contents + + +@pytest.mark.asyncio +async def test_get_ordinal_from_ground(player, test_zone): + """Test that 'get 2.sword' gets second sword from ground.""" + sword1 = Thing(name="sword", x=0, y=0) + sword1.location = test_zone + test_zone._contents.append(sword1) + + sword2 = Thing(name="sword", x=0, y=0) + sword2.location = test_zone + test_zone._contents.append(sword2) + + await cmd_get(player, "2.sword") + + # Check second sword moved to player inventory + assert sword2 in player.contents + assert sword1 not in player.contents + + +@pytest.mark.asyncio +async def test_drop_prefix_match(player, test_zone): + """Test that 'drop sw' prefix matches sword in inventory.""" + sword = Thing(name="sword", x=0, y=0) + sword.move_to(player) + + await cmd_drop(player, "sw") + + # Check sword moved to ground + assert sword not in player.contents + assert sword in test_zone._contents + assert sword.x == 0 + assert sword.y == 0 + + +@pytest.mark.asyncio +async def test_get_ordinal_from_container(player, test_zone): + """Test that 'get 2.sword from chest' gets second sword from container.""" + chest = Container(name="chest", x=0, y=0, capacity=100) + chest.location = test_zone + chest.closed = False + test_zone._contents.append(chest) + + sword1 = Thing(name="sword", x=0, y=0) + sword1.move_to(chest) + + sword2 = Thing(name="sword", x=0, y=0) + sword2.move_to(chest) + + await cmd_get(player, "2.sword from chest") + + # Check second sword moved to player inventory + assert sword2 in player.contents + assert sword1 not in player.contents + assert sword1 in chest.contents + + +@pytest.mark.asyncio +async def test_get_no_match(player, test_zone): + """Test that get fails gracefully when no match is found.""" + await cmd_get(player, "nonexistent") + + # Check for error message + written = "".join(call.args[0] for call in player.writer.write.call_args_list) + assert "don't see" in written.lower() + + +@pytest.mark.asyncio +async def test_drop_no_match(player, test_zone): + """Test that drop fails gracefully when no match is found.""" + await cmd_drop(player, "nonexistent") + + # Check for error message + written = "".join(call.args[0] for call in player.writer.write.call_args_list) + assert "not carrying" in written.lower() + + +@pytest.mark.asyncio +async def test_get_alias_match(player, test_zone): + """Test that aliases work with targeting.""" + figurine = Thing(name="goblin figurine", x=0, y=0, aliases=["fig", "goblin"]) + figurine.location = test_zone + test_zone._contents.append(figurine) + + await cmd_get(player, "fig") + + assert figurine in player.contents + + +@pytest.mark.asyncio +async def test_drop_ordinal(player, test_zone): + """Test that drop works with ordinal disambiguation.""" + sword1 = Thing(name="sword", x=0, y=0) + sword1.move_to(player) + + sword2 = Thing(name="sword", x=0, y=0) + sword2.move_to(player) + + await cmd_drop(player, "2.sword") + + # Check second sword moved to ground + assert sword2 not in player.contents + assert sword1 in player.contents + assert sword2 in test_zone._contents