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.
This commit is contained in:
parent
d2de6bdc16
commit
6ce57ad970
2 changed files with 239 additions and 0 deletions
38
src/mudlib/verb_handlers.py
Normal file
38
src/mudlib/verb_handlers.py
Normal file
|
|
@ -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")
|
||||||
201
tests/test_key_unlock.py
Normal file
201
tests/test_key_unlock.py
Normal file
|
|
@ -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
|
||||||
Loading…
Reference in a new issue