Add zone registry with register and lookup

Implements a module-level zone registry for looking up zones by name.
Includes register_zone() and get_zone() functions with comprehensive
tests covering single/multiple zones, unknown lookups, and overwrites.
This commit is contained in:
Jared Miller 2026-02-11 20:40:31 -05:00
parent 303ce2c89e
commit b3471a8b94
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
6 changed files with 564 additions and 2 deletions

View file

@ -0,0 +1,94 @@
"""Open and close commands for containers."""
from mudlib.commands import CommandDefinition, register
from mudlib.container import Container
from mudlib.player import Player
from mudlib.thing import Thing
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.
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
# 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
async def cmd_open(player: Player, args: str) -> None:
"""Open a container."""
if not args.strip():
await player.send("Open what?\r\n")
return
thing = _find_container(args.strip(), player)
if thing is None:
await player.send("You don't see that here.\r\n")
return
if not isinstance(thing, Container):
await player.send("You can't open that.\r\n")
return
if not thing.closed:
await player.send("It's already open.\r\n")
return
if thing.locked:
await player.send("It's locked.\r\n")
return
thing.closed = False
await player.send(f"You open the {thing.name}.\r\n")
async def cmd_close(player: Player, args: str) -> None:
"""Close a container."""
if not args.strip():
await player.send("Close what?\r\n")
return
thing = _find_container(args.strip(), player)
if thing is None:
await player.send("You don't see that here.\r\n")
return
if not isinstance(thing, Container):
await player.send("You can't close that.\r\n")
return
if thing.closed:
await player.send("It's already closed.\r\n")
return
thing.closed = True
await player.send(f"You close the {thing.name}.\r\n")
register(CommandDefinition("open", cmd_open))
register(CommandDefinition("close", cmd_close))

View file

@ -31,8 +31,6 @@ from mudlib.effects import clear_expired
from mudlib.if_session import broadcast_to_spectators
from mudlib.mob_ai import process_mobs
from mudlib.mobs import load_mob_templates, mob_templates
from mudlib.thing import Thing
from mudlib.things import load_thing_templates, spawn_thing, thing_templates
from mudlib.player import Player, players
from mudlib.resting import process_resting
from mudlib.store import (

30
src/mudlib/zones.py Normal file
View file

@ -0,0 +1,30 @@
"""Zone registry and loading."""
from __future__ import annotations
from mudlib.zone import Zone
# Module-level zone registry
zone_registry: dict[str, Zone] = {}
def register_zone(name: str, zone: Zone) -> None:
"""Register a zone by name.
Args:
name: Unique name for the zone
zone: Zone instance to register
"""
zone_registry[name] = zone
def get_zone(name: str) -> Zone | None:
"""Look up a zone by name.
Args:
name: Zone name to look up
Returns:
Zone instance if found, None otherwise
"""
return zone_registry.get(name)

View file

@ -0,0 +1,152 @@
"""Tests for container state display in look and inventory commands."""
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,
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
# --- look command container display ---
@pytest.mark.asyncio
async def test_look_shows_closed_container(player, test_zone, mock_writer):
"""look shows closed containers with (closed) suffix."""
from mudlib.commands.look import cmd_look
Container(name="chest", location=test_zone, x=5, y=5, closed=True)
await cmd_look(player, "")
output = "".join(call[0][0] for call in mock_writer.write.call_args_list)
assert "chest (closed)" in output
@pytest.mark.asyncio
async def test_look_shows_open_empty_container(player, test_zone, mock_writer):
"""look shows open empty containers with (open, empty) suffix."""
from mudlib.commands.look import cmd_look
Container(name="chest", location=test_zone, x=5, y=5, closed=False)
await cmd_look(player, "")
output = "".join(call[0][0] for call in mock_writer.write.call_args_list)
assert "chest (open, empty)" in output
@pytest.mark.asyncio
async def test_look_shows_open_container_with_contents(
player, test_zone, mock_writer
):
"""look shows open containers with their contents."""
from mudlib.commands.look import cmd_look
chest = Container(name="chest", location=test_zone, x=5, y=5, closed=False)
Thing(name="rock", location=chest)
Thing(name="coin", location=chest)
await cmd_look(player, "")
output = "".join(call[0][0] for call in mock_writer.write.call_args_list)
assert "chest (open, containing: rock, coin)" in output
@pytest.mark.asyncio
async def test_look_shows_regular_things_unchanged(player, test_zone, mock_writer):
"""look shows regular Things without container suffixes."""
from mudlib.commands.look import cmd_look
Thing(name="rock", location=test_zone, x=5, y=5)
await cmd_look(player, "")
output = "".join(call[0][0] for call in mock_writer.write.call_args_list)
assert "On the ground: rock" in output
assert "(closed)" not in output
assert "(open" not in output
# --- inventory command container display ---
@pytest.mark.asyncio
async def test_inventory_shows_closed_container(player, mock_writer):
"""inventory shows closed containers with (closed) suffix."""
from mudlib.commands.things import cmd_inventory
Container(name="sack", location=player, closed=True)
await cmd_inventory(player, "")
output = "".join(call[0][0] for call in mock_writer.write.call_args_list)
assert "sack (closed)" in output
@pytest.mark.asyncio
async def test_inventory_shows_open_empty_container(player, mock_writer):
"""inventory shows open empty containers with (open, empty) suffix."""
from mudlib.commands.things import cmd_inventory
Container(name="sack", location=player, closed=False)
await cmd_inventory(player, "")
output = "".join(call[0][0] for call in mock_writer.write.call_args_list)
assert "sack (open, empty)" in output
@pytest.mark.asyncio
async def test_inventory_shows_container_with_contents(player, mock_writer):
"""inventory shows open containers with their contents."""
from mudlib.commands.things import cmd_inventory
sack = Container(name="sack", location=player, closed=False)
Thing(name="rock", location=sack)
Thing(name="gem", location=sack)
await cmd_inventory(player, "")
output = "".join(call[0][0] for call in mock_writer.write.call_args_list)
assert "sack (open, containing: rock, gem)" in output
@pytest.mark.asyncio
async def test_inventory_shows_regular_things_unchanged(player, mock_writer):
"""inventory shows regular Things without container suffixes."""
from mudlib.commands.things import cmd_inventory
Thing(name="rock", location=player)
await cmd_inventory(player, "")
output = "".join(call[0][0] for call in mock_writer.write.call_args_list)
assert " rock\r\n" in output
assert "(closed)" not in output
assert "(open" not in output

226
tests/test_open_close.py Normal file
View file

@ -0,0 +1,226 @@
"""Tests for open and close 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_open ---
@pytest.mark.asyncio
async def test_open_container_on_ground(player, test_zone, mock_writer):
"""open finds container on ground and sets closed=False."""
from mudlib.commands.containers import cmd_open
chest = Container(name="chest", location=test_zone, x=5, y=5, closed=True)
await cmd_open(player, "chest")
assert chest.closed is False
output = mock_writer.write.call_args_list[-1][0][0]
assert "open" in output.lower() and "chest" in output.lower()
@pytest.mark.asyncio
async def test_open_container_in_inventory(player, test_zone, mock_writer):
"""open finds container in player inventory and sets closed=False."""
from mudlib.commands.containers import cmd_open
box = Container(name="box", location=player, closed=True)
await cmd_open(player, "box")
assert box.closed is False
output = mock_writer.write.call_args_list[-1][0][0]
assert "open" in output.lower() and "box" in output.lower()
@pytest.mark.asyncio
async def test_open_no_args(player, mock_writer):
"""open with no arguments gives usage hint."""
from mudlib.commands.containers import cmd_open
await cmd_open(player, "")
output = mock_writer.write.call_args_list[-1][0][0]
assert "open what" in output.lower() or "what" in output.lower()
@pytest.mark.asyncio
async def test_open_already_open(player, test_zone, mock_writer):
"""open on already-open container gives feedback."""
from mudlib.commands.containers import cmd_open
chest = Container(name="chest", location=test_zone, x=5, y=5, closed=False)
await cmd_open(player, "chest")
output = mock_writer.write.call_args_list[-1][0][0]
assert "already open" in output.lower()
@pytest.mark.asyncio
async def test_open_locked_container(player, test_zone, mock_writer):
"""open on locked container gives feedback."""
from mudlib.commands.containers import cmd_open
chest = Container(
name="chest", location=test_zone, x=5, y=5, closed=True, locked=True
)
await cmd_open(player, "chest")
output = mock_writer.write.call_args_list[-1][0][0]
assert "locked" in output.lower()
# Container should still be closed
assert chest.closed is True
@pytest.mark.asyncio
async def test_open_not_found(player, test_zone, mock_writer):
"""open on non-existent thing gives feedback."""
from mudlib.commands.containers import cmd_open
await cmd_open(player, "chest")
output = mock_writer.write.call_args_list[-1][0][0]
assert "don't see" in output.lower()
@pytest.mark.asyncio
async def test_open_matches_aliases(player, test_zone):
"""open matches container aliases."""
from mudlib.commands.containers import cmd_open
chest = Container(
name="wooden chest",
aliases=["chest", "box"],
location=test_zone,
x=5,
y=5,
closed=True,
)
await cmd_open(player, "box")
assert chest.closed is False
@pytest.mark.asyncio
async def test_open_non_container(player, test_zone, mock_writer):
"""open on non-container thing gives feedback."""
from mudlib.commands.containers import cmd_open
rock = Thing(name="rock", location=test_zone, x=5, y=5)
await cmd_open(player, "rock")
output = mock_writer.write.call_args_list[-1][0][0]
assert "can't open" in output.lower()
# --- cmd_close ---
@pytest.mark.asyncio
async def test_close_container_on_ground(player, test_zone, mock_writer):
"""close finds container on ground and sets closed=True."""
from mudlib.commands.containers import cmd_close
chest = Container(name="chest", location=test_zone, x=5, y=5, closed=False)
await cmd_close(player, "chest")
assert chest.closed is True
output = mock_writer.write.call_args_list[-1][0][0]
assert "close" in output.lower() and "chest" in output.lower()
@pytest.mark.asyncio
async def test_close_container_in_inventory(player, test_zone, mock_writer):
"""close finds container in inventory and sets closed=True."""
from mudlib.commands.containers import cmd_close
box = Container(name="box", location=player, closed=False)
await cmd_close(player, "box")
assert box.closed is True
output = mock_writer.write.call_args_list[-1][0][0]
assert "close" in output.lower() and "box" in output.lower()
@pytest.mark.asyncio
async def test_close_already_closed(player, test_zone, mock_writer):
"""close on already-closed container gives feedback."""
from mudlib.commands.containers import cmd_close
chest = Container(name="chest", location=test_zone, x=5, y=5, closed=True)
await cmd_close(player, "chest")
output = mock_writer.write.call_args_list[-1][0][0]
assert "already closed" in output.lower()
@pytest.mark.asyncio
async def test_close_no_args(player, mock_writer):
"""close with no arguments gives usage hint."""
from mudlib.commands.containers import cmd_close
await cmd_close(player, "")
output = mock_writer.write.call_args_list[-1][0][0]
assert "close what" in output.lower() or "what" in output.lower()
@pytest.mark.asyncio
async def test_close_non_container(player, test_zone, mock_writer):
"""close on non-container thing gives feedback."""
from mudlib.commands.containers import cmd_close
rock = Thing(name="rock", location=test_zone, x=5, y=5)
await cmd_close(player, "rock")
output = mock_writer.write.call_args_list[-1][0][0]
assert "can't close" in output.lower()
# --- command registration ---
def test_open_command_registered():
"""open command is registered."""
import mudlib.commands.containers # noqa: F401
assert "open" in _registry
def test_close_command_registered():
"""close command is registered."""
import mudlib.commands.containers # noqa: F401
assert "close" in _registry

View file

@ -0,0 +1,62 @@
"""Tests for zone registry."""
import pytest
from mudlib.zone import Zone
from mudlib.zones import get_zone, register_zone, zone_registry
@pytest.fixture(autouse=True)
def clear_registry():
"""Clear zone registry before each test."""
zone_registry.clear()
yield
zone_registry.clear()
def test_register_zone():
"""Register a zone by name."""
zone = Zone(name="test_zone", width=10, height=10, terrain=[], toroidal=False)
register_zone("test_zone", zone)
assert "test_zone" in zone_registry
assert zone_registry["test_zone"] is zone
def test_get_zone():
"""Look up a zone by name."""
zone = Zone(name="test_zone", width=10, height=10, terrain=[], toroidal=False)
register_zone("test_zone", zone)
retrieved = get_zone("test_zone")
assert retrieved is zone
def test_get_zone_unknown():
"""Get None for unknown zone name."""
result = get_zone("nonexistent")
assert result is None
def test_register_multiple_zones():
"""Register multiple zones."""
zone1 = Zone(name="zone1", width=10, height=10, terrain=[], toroidal=False)
zone2 = Zone(name="zone2", width=20, height=15, terrain=[], toroidal=True)
register_zone("zone1", zone1)
register_zone("zone2", zone2)
assert len(zone_registry) == 2
assert get_zone("zone1") is zone1
assert get_zone("zone2") is zone2
def test_overwrite_zone():
"""Registering same name twice overwrites."""
zone1 = Zone(name="zone", width=10, height=10, terrain=[], toroidal=False)
zone2 = Zone(name="zone", width=20, height=20, terrain=[], toroidal=False)
register_zone("zone", zone1)
register_zone("zone", zone2)
assert get_zone("zone") is zone2