Add container grammar with get-all and targeting support
- Update _find_container to use targeting module (prefix + ordinal) - Update cmd_put to use find_in_inventory directly - Add 'get all from <container>' support with portable item filtering - Add comprehensive tests for all container grammar features
This commit is contained in:
parent
aca9864881
commit
4878f39124
3 changed files with 168 additions and 21 deletions
|
|
@ -3,6 +3,7 @@
|
|||
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
|
||||
|
||||
|
|
@ -10,34 +11,21 @@ from mudlib.zone import Zone
|
|||
def _find_container(name: str, player: Player) -> Container | Thing | None:
|
||||
"""Find a thing by name in inventory first, then on ground.
|
||||
|
||||
Uses targeting module for prefix matching and ordinal support.
|
||||
Returns Thing if found (caller must check if it's a Container).
|
||||
Returns None if not found.
|
||||
"""
|
||||
name_lower = name.lower()
|
||||
|
||||
# Check inventory first
|
||||
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
|
||||
result = find_in_inventory(name, player)
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
# Check ground at player's position
|
||||
zone = player.location
|
||||
if zone is None or not isinstance(zone, Zone):
|
||||
return None
|
||||
|
||||
for obj in zone.contents_at(player.x, player.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
|
||||
return find_thing_on_tile(name, zone, player.x, player.y)
|
||||
|
||||
|
||||
async def cmd_open(player: Player, args: str) -> None:
|
||||
|
|
@ -110,9 +98,7 @@ async def cmd_put(player: Player, args: str) -> None:
|
|||
return
|
||||
|
||||
# Find thing in player's inventory
|
||||
from mudlib.commands.things import _find_thing_in_inventory
|
||||
|
||||
thing = _find_thing_in_inventory(thing_name, player)
|
||||
thing = find_in_inventory(thing_name, player)
|
||||
if thing is None:
|
||||
await player.send("You're not carrying that.\r\n")
|
||||
return
|
||||
|
|
|
|||
|
|
@ -104,6 +104,24 @@ async def _handle_take_from(player: Player, args: str) -> None:
|
|||
await player.send(f"The {container_obj.name} is closed.\r\n")
|
||||
return
|
||||
|
||||
# Handle "get all from container"
|
||||
if thing_name.lower() == "all":
|
||||
container_things = [
|
||||
obj for obj in container_obj.contents if isinstance(obj, Thing)
|
||||
]
|
||||
portable_things = [t for t in container_things if player.can_accept(t)]
|
||||
|
||||
if not portable_things:
|
||||
msg = f"There's nothing in the {container_obj.name} to take.\r\n"
|
||||
await player.send(msg)
|
||||
return
|
||||
|
||||
for thing in portable_things:
|
||||
thing.move_to(player)
|
||||
msg = f"You take the {thing.name} from the {container_obj.name}.\r\n"
|
||||
await player.send(msg)
|
||||
return
|
||||
|
||||
# Find thing in container using targeting (supports prefix and ordinals)
|
||||
from mudlib.targeting import resolve_target
|
||||
|
||||
|
|
|
|||
143
tests/test_container_grammar.py
Normal file
143
tests/test_container_grammar.py
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
"""Tests for container grammar with targeting and get-all support."""
|
||||
|
||||
import pytest
|
||||
|
||||
from mudlib.commands.containers import cmd_open, cmd_put
|
||||
from mudlib.commands.things import cmd_get
|
||||
from mudlib.container import Container
|
||||
from mudlib.player import Player
|
||||
from mudlib.thing import Thing
|
||||
from mudlib.zone import Zone
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_from_container_basic(mock_writer):
|
||||
"""Test basic 'get item from container' command."""
|
||||
zone = Zone(name="test", width=10, height=10)
|
||||
player = Player(name="test", x=5, y=5, location=zone, writer=mock_writer)
|
||||
chest = Container(name="chest", x=5, y=5, location=zone)
|
||||
sword = Thing(name="sword", location=chest)
|
||||
|
||||
await cmd_open(player, "chest")
|
||||
await cmd_get(player, "sword from chest")
|
||||
|
||||
assert sword.location is player
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_from_container_prefix_match_item(mock_writer):
|
||||
"""Test 'get sw from chest' — prefix match item in container."""
|
||||
zone = Zone(name="test", width=10, height=10)
|
||||
player = Player(name="test", x=5, y=5, location=zone, writer=mock_writer)
|
||||
chest = Container(name="chest", x=5, y=5, location=zone)
|
||||
sword = Thing(name="sword", location=chest)
|
||||
|
||||
await cmd_open(player, "chest")
|
||||
await cmd_get(player, "sw from chest")
|
||||
|
||||
assert sword.location is player
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_from_container_ordinal(mock_writer):
|
||||
"""Test 'get 2.sword from chest' — ordinal in container."""
|
||||
zone = Zone(name="test", width=10, height=10)
|
||||
player = Player(name="test", x=5, y=5, location=zone, writer=mock_writer)
|
||||
chest = Container(name="chest", x=5, y=5, location=zone)
|
||||
sword1 = Thing(name="sword", location=chest)
|
||||
sword2 = Thing(name="sword", location=chest)
|
||||
|
||||
await cmd_open(player, "chest")
|
||||
await cmd_get(player, "2.sword from chest")
|
||||
|
||||
assert sword2.location is player
|
||||
assert sword1.location is chest
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_all_from_container(mock_writer):
|
||||
"""Test 'get all from chest' — take everything."""
|
||||
zone = Zone(name="test", width=10, height=10)
|
||||
player = Player(name="test", x=5, y=5, location=zone, writer=mock_writer)
|
||||
chest = Container(name="chest", x=5, y=5, location=zone)
|
||||
sword = Thing(name="sword", location=chest)
|
||||
shield = Thing(name="shield", location=chest)
|
||||
helmet = Thing(name="helmet", location=chest)
|
||||
|
||||
await cmd_open(player, "chest")
|
||||
await cmd_get(player, "all from chest")
|
||||
|
||||
assert sword.location is player
|
||||
assert shield.location is player
|
||||
assert helmet.location is player
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_all_from_empty_container(mock_writer):
|
||||
"""Test 'get all from chest' when empty — no items moved."""
|
||||
zone = Zone(name="test", width=10, height=10)
|
||||
player = Player(name="test", x=5, y=5, location=zone, writer=mock_writer)
|
||||
chest = Container(name="chest", x=5, y=5, location=zone)
|
||||
|
||||
await cmd_open(player, "chest")
|
||||
await cmd_get(player, "all from chest")
|
||||
|
||||
# Verify no items were added to inventory
|
||||
assert len([obj for obj in player.contents if isinstance(obj, Thing)]) == 0
|
||||
assert chest.location is zone # chest should remain on ground
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_put_in_container_prefix_match(mock_writer):
|
||||
"""Test 'put sw in chest' — prefix match in inventory for put."""
|
||||
zone = Zone(name="test", width=10, height=10)
|
||||
player = Player(name="test", x=5, y=5, location=zone, writer=mock_writer)
|
||||
chest = Container(name="chest", x=5, y=5, location=zone)
|
||||
sword = Thing(name="sword", location=player)
|
||||
|
||||
await cmd_open(player, "chest")
|
||||
await cmd_put(player, "sw in chest")
|
||||
|
||||
assert sword.location is chest
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_open_container_prefix_match(mock_writer):
|
||||
"""Test 'open che' — prefix match container name."""
|
||||
zone = Zone(name="test", width=10, height=10)
|
||||
player = Player(name="test", x=5, y=5, location=zone, writer=mock_writer)
|
||||
chest = Container(name="chest", x=5, y=5, location=zone, closed=True)
|
||||
|
||||
await cmd_open(player, "che")
|
||||
|
||||
assert not chest.closed
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_from_container_prefix_match_container(mock_writer):
|
||||
"""Test 'get sword from che' — prefix match container name."""
|
||||
zone = Zone(name="test", width=10, height=10)
|
||||
player = Player(name="test", x=5, y=5, location=zone, writer=mock_writer)
|
||||
chest = Container(name="chest", x=5, y=5, location=zone)
|
||||
sword = Thing(name="sword", location=chest)
|
||||
|
||||
await cmd_open(player, "chest")
|
||||
await cmd_get(player, "sword from che")
|
||||
|
||||
assert sword.location is player
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_all_from_container_skips_non_portable(mock_writer):
|
||||
"""Test 'get all from chest' skips items player can't carry."""
|
||||
zone = Zone(name="test", width=10, height=10)
|
||||
player = Player(name="test", x=5, y=5, location=zone, writer=mock_writer)
|
||||
chest = Container(name="chest", x=5, y=5, location=zone)
|
||||
sword = Thing(name="sword", location=chest, portable=True)
|
||||
anvil = Thing(name="anvil", location=chest, portable=False)
|
||||
|
||||
await cmd_open(player, "chest")
|
||||
await cmd_get(player, "all from chest")
|
||||
|
||||
assert sword.location is player
|
||||
assert anvil.location is chest
|
||||
Loading…
Reference in a new issue