Add put and take-from commands for containers
This commit is contained in:
parent
68161fd025
commit
557fffe5fa
3 changed files with 496 additions and 1 deletions
|
|
@ -90,5 +90,59 @@ async def cmd_close(player: Player, args: str) -> None:
|
|||
await player.send(f"You close the {thing.name}.\r\n")
|
||||
|
||||
|
||||
async def cmd_put(player: Player, args: str) -> None:
|
||||
"""Put an item into a container."""
|
||||
if not args.strip() or " in " not in args:
|
||||
await player.send("Put what where? (put <item> in <container>)\r\n")
|
||||
return
|
||||
|
||||
# Parse "thing in container"
|
||||
parts = args.strip().split(" in ", 1)
|
||||
if len(parts) != 2:
|
||||
await player.send("Put what where? (put <item> in <container>)\r\n")
|
||||
return
|
||||
|
||||
thing_name = parts[0].strip()
|
||||
container_name = parts[1].strip()
|
||||
|
||||
if not thing_name or not container_name:
|
||||
await player.send("Put what where? (put <item> in <container>)\r\n")
|
||||
return
|
||||
|
||||
# Find thing in player's inventory
|
||||
from mudlib.commands.things import _find_thing_in_inventory
|
||||
|
||||
thing = _find_thing_in_inventory(thing_name, player)
|
||||
if thing is None:
|
||||
await player.send("You're not carrying that.\r\n")
|
||||
return
|
||||
|
||||
# Find container (on ground or in inventory)
|
||||
container_obj = _find_container(container_name, player)
|
||||
if container_obj is None:
|
||||
await player.send("You don't see that here.\r\n")
|
||||
return
|
||||
|
||||
if not isinstance(container_obj, Container):
|
||||
await player.send("That's not a container.\r\n")
|
||||
return
|
||||
|
||||
if thing is container_obj:
|
||||
await player.send("You can't put something inside itself.\r\n")
|
||||
return
|
||||
|
||||
if container_obj.closed:
|
||||
await player.send(f"The {container_obj.name} is closed.\r\n")
|
||||
return
|
||||
|
||||
if not container_obj.can_accept(thing):
|
||||
await player.send(f"The {container_obj.name} is full.\r\n")
|
||||
return
|
||||
|
||||
thing.move_to(container_obj)
|
||||
await player.send(f"You put the {thing.name} in the {container_obj.name}.\r\n")
|
||||
|
||||
|
||||
register(CommandDefinition("open", cmd_open))
|
||||
register(CommandDefinition("close", cmd_close))
|
||||
register(CommandDefinition("put", cmd_put))
|
||||
|
|
|
|||
|
|
@ -51,11 +51,23 @@ def _format_thing_name(thing: Thing) -> str:
|
|||
|
||||
|
||||
async def cmd_get(player: Player, args: str) -> None:
|
||||
"""Pick up an item from the ground."""
|
||||
"""Pick up an item from the ground or take from a container."""
|
||||
if not args.strip():
|
||||
await player.send("Get what?\r\n")
|
||||
return
|
||||
|
||||
# Check if this is "take/get X from Y" syntax
|
||||
# Match " from " or "from " (at start) or " from" (at end)
|
||||
args_lower = args.lower()
|
||||
if (
|
||||
" from " in args_lower
|
||||
or args_lower.startswith("from ")
|
||||
or args_lower.endswith(" from")
|
||||
or args_lower == "from"
|
||||
):
|
||||
await _handle_take_from(player, args)
|
||||
return
|
||||
|
||||
zone = player.location
|
||||
if zone is None or not isinstance(zone, Zone):
|
||||
await player.send("You are nowhere.\r\n")
|
||||
|
|
@ -74,6 +86,54 @@ async def cmd_get(player: Player, args: str) -> None:
|
|||
await player.send(f"You pick up {thing.name}.\r\n")
|
||||
|
||||
|
||||
async def _handle_take_from(player: Player, args: str) -> None:
|
||||
"""Handle 'take/get X from Y' to remove items from containers."""
|
||||
# Parse "thing from container"
|
||||
parts = args.strip().split(" from ", 1)
|
||||
thing_name = parts[0].strip() if len(parts) > 0 else ""
|
||||
container_name = parts[1].strip() if len(parts) > 1 else ""
|
||||
|
||||
if not thing_name or not container_name:
|
||||
await player.send("Take what from where? (take <item> from <container>)\r\n")
|
||||
return
|
||||
|
||||
# Find container (on ground or in inventory)
|
||||
from mudlib.commands.containers import _find_container
|
||||
|
||||
container_obj = _find_container(container_name, player)
|
||||
if container_obj is None:
|
||||
await player.send("You don't see that here.\r\n")
|
||||
return
|
||||
|
||||
if not isinstance(container_obj, Container):
|
||||
await player.send("That's not a container.\r\n")
|
||||
return
|
||||
|
||||
if container_obj.closed:
|
||||
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
|
||||
|
||||
if thing is None:
|
||||
await player.send(f"The {container_obj.name} doesn't contain that.\r\n")
|
||||
return
|
||||
|
||||
thing.move_to(player)
|
||||
await player.send(f"You take the {thing.name} from the {container_obj.name}.\r\n")
|
||||
|
||||
|
||||
async def cmd_drop(player: Player, args: str) -> None:
|
||||
"""Drop an item from inventory onto the ground."""
|
||||
if not args.strip():
|
||||
|
|
|
|||
381
tests/test_put_take.py
Normal file
381
tests/test_put_take.py
Normal file
|
|
@ -0,0 +1,381 @@
|
|||
"""Tests for put and take-from commands."""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from mudlib.commands import _registry
|
||||
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,
|
||||
toroidal=True,
|
||||
terrain=terrain,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def player(mock_reader, mock_writer, test_zone):
|
||||
p = Player(
|
||||
name="TestPlayer",
|
||||
x=5,
|
||||
y=5,
|
||||
reader=mock_reader,
|
||||
writer=mock_writer,
|
||||
location=test_zone,
|
||||
)
|
||||
return p
|
||||
|
||||
|
||||
# --- cmd_put ---
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_put_thing_in_container(player, test_zone, mock_writer):
|
||||
"""put moves thing from inventory to container."""
|
||||
from mudlib.commands.containers import cmd_put
|
||||
|
||||
chest = Container(
|
||||
name="chest", location=test_zone, x=5, y=5, closed=False, capacity=10
|
||||
)
|
||||
rock = Thing(name="rock", location=player)
|
||||
|
||||
await cmd_put(player, "rock in chest")
|
||||
|
||||
assert rock.location == chest
|
||||
assert rock in chest.contents
|
||||
assert rock not in player.contents
|
||||
output = mock_writer.write.call_args_list[-1][0][0]
|
||||
assert "put" in output.lower() and "rock" in output.lower()
|
||||
assert "chest" in output.lower()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_put_no_args(player, mock_writer):
|
||||
"""put with no args gives usage message."""
|
||||
from mudlib.commands.containers import cmd_put
|
||||
|
||||
await cmd_put(player, "")
|
||||
output = mock_writer.write.call_args_list[-1][0][0]
|
||||
assert "put" in output.lower() and "container" in output.lower()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_put_missing_container_arg(player, mock_writer):
|
||||
"""put with missing container argument gives error."""
|
||||
from mudlib.commands.containers import cmd_put
|
||||
|
||||
_rock = Thing(name="rock", location=player)
|
||||
await cmd_put(player, "rock in")
|
||||
output = mock_writer.write.call_args_list[-1][0][0]
|
||||
assert "put" in output.lower() and "container" in output.lower()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_put_thing_not_in_inventory(player, test_zone, mock_writer):
|
||||
"""put on thing not carried gives error."""
|
||||
from mudlib.commands.containers import cmd_put
|
||||
|
||||
_chest = Container(
|
||||
name="chest", location=test_zone, x=5, y=5, closed=False, capacity=10
|
||||
)
|
||||
_rock = Thing(name="rock", location=test_zone, x=5, y=5)
|
||||
|
||||
await cmd_put(player, "rock in chest")
|
||||
output = mock_writer.write.call_args_list[-1][0][0]
|
||||
assert "not carrying" in output.lower() or "aren't carrying" in output.lower()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_put_container_not_found(player, mock_writer):
|
||||
"""put into non-existent container gives error."""
|
||||
from mudlib.commands.containers import cmd_put
|
||||
|
||||
_rock = Thing(name="rock", location=player)
|
||||
|
||||
await cmd_put(player, "rock in chest")
|
||||
output = mock_writer.write.call_args_list[-1][0][0]
|
||||
assert "don't see" in output.lower()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_put_container_closed(player, test_zone, mock_writer):
|
||||
"""put into closed container gives error."""
|
||||
from mudlib.commands.containers import cmd_put
|
||||
|
||||
_chest = Container(
|
||||
name="chest", location=test_zone, x=5, y=5, closed=True, capacity=10
|
||||
)
|
||||
rock = Thing(name="rock", location=player)
|
||||
|
||||
await cmd_put(player, "rock in chest")
|
||||
assert rock.location == player # Should not have moved
|
||||
output = mock_writer.write.call_args_list[-1][0][0]
|
||||
assert "closed" in output.lower()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_put_container_full(player, test_zone, mock_writer):
|
||||
"""put into full container gives error."""
|
||||
from mudlib.commands.containers import cmd_put
|
||||
|
||||
chest = Container(
|
||||
name="chest", location=test_zone, x=5, y=5, closed=False, capacity=1
|
||||
)
|
||||
# Fill the container
|
||||
_other = Thing(name="other", location=chest)
|
||||
rock = Thing(name="rock", location=player)
|
||||
|
||||
await cmd_put(player, "rock in chest")
|
||||
assert rock.location == player # Should not have moved
|
||||
output = mock_writer.write.call_args_list[-1][0][0]
|
||||
assert "full" in output.lower()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_put_target_not_container(player, test_zone, mock_writer):
|
||||
"""put into non-container thing gives error."""
|
||||
from mudlib.commands.containers import cmd_put
|
||||
|
||||
_stick = Thing(name="stick", location=test_zone, x=5, y=5)
|
||||
rock = Thing(name="rock", location=player)
|
||||
|
||||
await cmd_put(player, "rock in stick")
|
||||
assert rock.location == player # Should not have moved
|
||||
output = mock_writer.write.call_args_list[-1][0][0]
|
||||
assert "not a container" in output.lower() or "can't" in output.lower()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_put_matches_thing_alias(player, test_zone):
|
||||
"""put matches thing by alias."""
|
||||
from mudlib.commands.containers import cmd_put
|
||||
|
||||
chest = Container(
|
||||
name="chest", location=test_zone, x=5, y=5, closed=False, capacity=10
|
||||
)
|
||||
rock = Thing(name="small rock", aliases=["rock", "pebble"], location=player)
|
||||
|
||||
await cmd_put(player, "pebble in chest")
|
||||
assert rock.location == chest
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_put_matches_container_alias(player, test_zone):
|
||||
"""put matches container by alias."""
|
||||
from mudlib.commands.containers import cmd_put
|
||||
|
||||
chest = Container(
|
||||
name="wooden chest",
|
||||
aliases=["chest", "box"],
|
||||
location=test_zone,
|
||||
x=5,
|
||||
y=5,
|
||||
closed=False,
|
||||
capacity=10,
|
||||
)
|
||||
rock = Thing(name="rock", location=player)
|
||||
|
||||
await cmd_put(player, "rock in box")
|
||||
assert rock.location == chest
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_put_container_in_inventory(player, test_zone):
|
||||
"""put works with container in player inventory."""
|
||||
from mudlib.commands.containers import cmd_put
|
||||
|
||||
box = Container(name="box", location=player, closed=False, capacity=10)
|
||||
rock = Thing(name="rock", location=player)
|
||||
|
||||
await cmd_put(player, "rock in box")
|
||||
assert rock.location == box
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_put_container_in_itself(player, test_zone, mock_writer):
|
||||
"""put container in itself gives error."""
|
||||
from mudlib.commands.containers import cmd_put
|
||||
|
||||
box = Container(name="box", location=player, closed=False, capacity=10)
|
||||
|
||||
await cmd_put(player, "box in box")
|
||||
assert box.location == player # Should not have moved
|
||||
output = mock_writer.write.call_args_list[-1][0][0]
|
||||
assert "can't put" in output.lower() and "itself" in output.lower()
|
||||
|
||||
|
||||
# --- cmd_get with "from" (take-from) ---
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_take_from_container(player, test_zone, mock_writer):
|
||||
"""take from container moves thing to inventory."""
|
||||
from mudlib.commands.things import cmd_get
|
||||
|
||||
chest = Container(
|
||||
name="chest", location=test_zone, x=5, y=5, closed=False, capacity=10
|
||||
)
|
||||
rock = Thing(name="rock", location=chest)
|
||||
|
||||
await cmd_get(player, "rock from chest")
|
||||
|
||||
assert rock.location == player
|
||||
assert rock in player.contents
|
||||
assert rock not in chest.contents
|
||||
output = mock_writer.write.call_args_list[-1][0][0]
|
||||
assert "take" in output.lower() and "rock" in output.lower()
|
||||
assert "chest" in output.lower()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_take_from_no_args(player, mock_writer):
|
||||
"""take from with missing args gives usage message."""
|
||||
from mudlib.commands.things import cmd_get
|
||||
|
||||
await cmd_get(player, "from")
|
||||
output = mock_writer.write.call_args_list[-1][0][0]
|
||||
assert "take" in output.lower() and "container" in output.lower()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_take_from_missing_container(player, test_zone, mock_writer):
|
||||
"""take from with missing container gives error."""
|
||||
from mudlib.commands.things import cmd_get
|
||||
|
||||
_chest = Container(
|
||||
name="chest", location=test_zone, x=5, y=5, closed=False, capacity=10
|
||||
)
|
||||
|
||||
await cmd_get(player, "rock from")
|
||||
output = mock_writer.write.call_args_list[-1][0][0]
|
||||
assert "take" in output.lower() and "container" in output.lower()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_take_from_container_not_found(player, test_zone, mock_writer):
|
||||
"""take from non-existent container gives error."""
|
||||
from mudlib.commands.things import cmd_get
|
||||
|
||||
await cmd_get(player, "rock from chest")
|
||||
output = mock_writer.write.call_args_list[-1][0][0]
|
||||
assert "don't see" in output.lower()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_take_from_closed_container(player, test_zone, mock_writer):
|
||||
"""take from closed container gives error."""
|
||||
from mudlib.commands.things import cmd_get
|
||||
|
||||
chest = Container(
|
||||
name="chest", location=test_zone, x=5, y=5, closed=True, capacity=10
|
||||
)
|
||||
_rock = Thing(name="rock", location=chest)
|
||||
|
||||
await cmd_get(player, "rock from chest")
|
||||
output = mock_writer.write.call_args_list[-1][0][0]
|
||||
assert "closed" in output.lower()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_take_from_thing_not_in_container(player, test_zone, mock_writer):
|
||||
"""take from container when thing not inside gives error."""
|
||||
from mudlib.commands.things import cmd_get
|
||||
|
||||
_chest = Container(
|
||||
name="chest", location=test_zone, x=5, y=5, closed=False, capacity=10
|
||||
)
|
||||
|
||||
await cmd_get(player, "rock from chest")
|
||||
output = mock_writer.write.call_args_list[-1][0][0]
|
||||
assert "doesn't contain" in output.lower() or "not in" in output.lower()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_take_from_non_container(player, test_zone, mock_writer):
|
||||
"""take from non-container thing gives error."""
|
||||
from mudlib.commands.things import cmd_get
|
||||
|
||||
_stick = Thing(name="stick", location=test_zone, x=5, y=5)
|
||||
|
||||
await cmd_get(player, "rock from stick")
|
||||
output = mock_writer.write.call_args_list[-1][0][0]
|
||||
assert "not a container" in output.lower() or "can't" in output.lower()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_take_from_matches_thing_alias(player, test_zone):
|
||||
"""take from matches thing by alias."""
|
||||
from mudlib.commands.things import cmd_get
|
||||
|
||||
chest = Container(
|
||||
name="chest", location=test_zone, x=5, y=5, closed=False, capacity=10
|
||||
)
|
||||
rock = Thing(name="small rock", aliases=["rock", "pebble"], location=chest)
|
||||
|
||||
await cmd_get(player, "pebble from chest")
|
||||
assert rock.location == player
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_take_from_matches_container_alias(player, test_zone):
|
||||
"""take from matches container by alias."""
|
||||
from mudlib.commands.things import cmd_get
|
||||
|
||||
chest = Container(
|
||||
name="wooden chest",
|
||||
aliases=["chest", "box"],
|
||||
location=test_zone,
|
||||
x=5,
|
||||
y=5,
|
||||
closed=False,
|
||||
capacity=10,
|
||||
)
|
||||
rock = Thing(name="rock", location=chest)
|
||||
|
||||
await cmd_get(player, "rock from box")
|
||||
assert rock.location == player
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_take_from_container_in_inventory(player, test_zone):
|
||||
"""take from works with container in player inventory."""
|
||||
from mudlib.commands.things import cmd_get
|
||||
|
||||
box = Container(name="box", location=player, closed=False, capacity=10)
|
||||
rock = Thing(name="rock", location=box)
|
||||
|
||||
await cmd_get(player, "rock from box")
|
||||
assert rock.location == player
|
||||
|
||||
|
||||
# --- command registration ---
|
||||
|
||||
|
||||
def test_put_command_registered():
|
||||
"""put command is registered."""
|
||||
import mudlib.commands.containers # noqa: F401
|
||||
|
||||
assert "put" in _registry
|
||||
Loading…
Reference in a new issue