From 6ce57ad970e63678c4932258b34d833f44c4bcb7 Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Wed, 11 Feb 2026 21:31:21 -0500 Subject: [PATCH] Add key-based unlock as first verb interaction Implements unlock_handler that checks for a key in player inventory and unlocks containers. Tests cover error cases (non-container, not locked, no key), success case, key aliasing, and state preservation. --- src/mudlib/verb_handlers.py | 38 +++++++ tests/test_key_unlock.py | 201 ++++++++++++++++++++++++++++++++++++ 2 files changed, 239 insertions(+) create mode 100644 src/mudlib/verb_handlers.py create mode 100644 tests/test_key_unlock.py diff --git a/src/mudlib/verb_handlers.py b/src/mudlib/verb_handlers.py new file mode 100644 index 0000000..0783e51 --- /dev/null +++ b/src/mudlib/verb_handlers.py @@ -0,0 +1,38 @@ +"""Verb handlers for object interactions.""" + + +async def unlock_handler(obj, player, args): + """ + Handle unlocking a container. + + Args: + obj: The object being unlocked (should be a Container) + player: The player attempting to unlock + args: Additional arguments (unused) + """ + from mudlib.container import Container + + if not isinstance(obj, Container): + await player.send("That can't be unlocked.\r\n") + return + + if not obj.locked: + await player.send("That's not locked.\r\n") + return + + # Check for key in player inventory + key = None + for item in player.contents: + if item.name.lower() == "key" or "key" in [ + a.lower() for a in getattr(item, "aliases", []) + ]: + key = item + break + + if key is None: + await player.send("You don't have a key.\r\n") + return + + obj.locked = False + obj.closed = False + await player.send(f"You unlock the {obj.name} with the {key.name}.\r\n") diff --git a/tests/test_key_unlock.py b/tests/test_key_unlock.py new file mode 100644 index 0000000..a6cb402 --- /dev/null +++ b/tests/test_key_unlock.py @@ -0,0 +1,201 @@ +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from mudlib.container import Container +from mudlib.player import Player +from mudlib.thing import Thing +from mudlib.zone import Zone + + +@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 test_zone(): + terrain = [["." for _ in range(10)] for _ in range(10)] + return Zone( + name="testzone", + width=10, + height=10, + terrain=terrain, + ) + + +@pytest.fixture +def player(mock_reader, mock_writer, test_zone): + return Player( + name="TestPlayer", + x=5, + y=5, + reader=mock_reader, + writer=mock_writer, + location=test_zone, + ) + + +@pytest.mark.asyncio +async def test_unlock_handler_on_non_container(player, test_zone, mock_writer): + """unlock_handler on non-container gives appropriate error""" + from mudlib.verb_handlers import unlock_handler + + thing = Thing( + name="rock", + description="a rock", + location=test_zone, + x=5, + y=5, + ) + + await unlock_handler(thing, player, "") + + output = "".join(call[0][0] for call in mock_writer.write.call_args_list) + assert "can't be unlocked" in output + + +@pytest.mark.asyncio +async def test_unlock_handler_on_unlocked_container(player, test_zone, mock_writer): + """unlock_handler on unlocked container says it's not locked""" + from mudlib.verb_handlers import unlock_handler + + chest = Container( + name="chest", + description="a wooden chest", + location=test_zone, + x=5, + y=5, + closed=True, + locked=False, + ) + + await unlock_handler(chest, player, "") + + output = "".join(call[0][0] for call in mock_writer.write.call_args_list) + assert "not locked" in output + + +@pytest.mark.asyncio +async def test_unlock_handler_without_key(player, test_zone, mock_writer): + """unlock_handler on locked container without key in inventory""" + from mudlib.verb_handlers import unlock_handler + + chest = Container( + name="chest", + description="a wooden chest", + location=test_zone, + x=5, + y=5, + closed=True, + locked=True, + ) + + await unlock_handler(chest, player, "") + + output = "".join(call[0][0] for call in mock_writer.write.call_args_list) + assert "don't have a key" in output + + +@pytest.mark.asyncio +async def test_unlock_handler_with_key(player, test_zone, mock_writer): + """unlock_handler unlocks and opens chest with key in inventory""" + from mudlib.verb_handlers import unlock_handler + + chest = Container( + name="chest", + description="a wooden chest", + location=test_zone, + x=5, + y=5, + closed=True, + locked=True, + ) + + key = Thing( + name="key", + description="a brass key", + location=player, + ) + assert key in player.contents + + await unlock_handler(chest, player, "") + + output = "".join(call[0][0] for call in mock_writer.write.call_args_list) + assert "You unlock the chest with the key" in output + assert not chest.locked + assert not chest.closed + + +@pytest.mark.asyncio +async def test_unlock_handler_key_found_by_alias(player, test_zone, mock_writer): + """unlock_handler finds key even if it has a different name""" + from mudlib.verb_handlers import unlock_handler + + chest = Container( + name="chest", + description="a wooden chest", + location=test_zone, + x=5, + y=5, + closed=True, + locked=True, + ) + + key = Thing( + name="brass key", + description="a brass key", + location=player, + ) + key.aliases = ["key"] + + await unlock_handler(chest, player, "") + + output = "".join(call[0][0] for call in mock_writer.write.call_args_list) + assert "You unlock the chest with the brass key" in output + assert not chest.locked + assert not chest.closed + + +@pytest.mark.asyncio +async def test_unlock_handler_preserves_chest_state(player, test_zone, mock_writer): + """unlock_handler changes locked and closed but preserves other state""" + from mudlib.verb_handlers import unlock_handler + + chest = Container( + name="chest", + description="a wooden chest", + location=test_zone, + x=5, + y=5, + closed=True, + locked=True, + ) + + inner_thing = Thing( + name="coin", + description="a gold coin", + location=chest, + ) + + key = Thing( + name="key", + description="a brass key", + location=player, + ) + assert key in player.contents + + await unlock_handler(chest, player, "") + + assert not chest.locked + assert not chest.closed + assert inner_thing in chest.contents + assert chest.location == test_zone