Compare commits
No commits in common. "7d4a75f9732165dfadfb8dac5abaffa8ba544589" and "05a739da74c31a66da8cb2ddbb2abfcd9906a1d6" have entirely different histories.
7d4a75f973
...
05a739da74
23 changed files with 11 additions and 2410 deletions
|
|
@ -1,7 +0,0 @@
|
||||||
name = "chest"
|
|
||||||
description = "a sturdy wooden chest with iron bindings"
|
|
||||||
portable = false
|
|
||||||
capacity = 5
|
|
||||||
closed = true
|
|
||||||
locked = false
|
|
||||||
aliases = ["box"]
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
name = "sack"
|
|
||||||
description = "a rough cloth sack with drawstring closure"
|
|
||||||
portable = true
|
|
||||||
capacity = 3
|
|
||||||
closed = false
|
|
||||||
locked = false
|
|
||||||
aliases = ["bag"]
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
name = "tavern"
|
|
||||||
description = "a cozy tavern with a crackling fireplace"
|
|
||||||
width = 8
|
|
||||||
height = 6
|
|
||||||
toroidal = false
|
|
||||||
|
|
||||||
[terrain]
|
|
||||||
# rows as strings, one per line
|
|
||||||
rows = [
|
|
||||||
"########",
|
|
||||||
"#......#",
|
|
||||||
"#......#",
|
|
||||||
"#......#",
|
|
||||||
"#......#",
|
|
||||||
"####.###",
|
|
||||||
]
|
|
||||||
|
|
||||||
[terrain.impassable]
|
|
||||||
tiles = ["#"]
|
|
||||||
|
|
@ -1,148 +0,0 @@
|
||||||
"""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")
|
|
||||||
|
|
||||||
|
|
||||||
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))
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
"""Look command for viewing the world."""
|
"""Look command for viewing the world."""
|
||||||
|
|
||||||
from mudlib.commands import CommandDefinition, register
|
from mudlib.commands import CommandDefinition, register
|
||||||
from mudlib.commands.things import _format_thing_name
|
|
||||||
from mudlib.effects import get_effects_at
|
from mudlib.effects import get_effects_at
|
||||||
from mudlib.entity import Entity
|
from mudlib.entity import Entity
|
||||||
from mudlib.player import Player
|
from mudlib.player import Player
|
||||||
|
|
@ -103,24 +102,13 @@ async def cmd_look(player: Player, args: str) -> None:
|
||||||
player.writer.write("\r\n".join(output_lines) + "\r\n")
|
player.writer.write("\r\n".join(output_lines) + "\r\n")
|
||||||
|
|
||||||
# Show items on the ground at player's position
|
# Show items on the ground at player's position
|
||||||
from mudlib.portal import Portal
|
|
||||||
|
|
||||||
contents_here = zone.contents_at(player.x, player.y)
|
|
||||||
ground_items = [
|
ground_items = [
|
||||||
obj
|
obj for obj in zone.contents_at(player.x, player.y) if isinstance(obj, Thing)
|
||||||
for obj in contents_here
|
|
||||||
if isinstance(obj, Thing) and not isinstance(obj, Portal)
|
|
||||||
]
|
]
|
||||||
portals = [obj for obj in contents_here if isinstance(obj, Portal)]
|
|
||||||
|
|
||||||
if ground_items:
|
if ground_items:
|
||||||
names = ", ".join(_format_thing_name(item) for item in ground_items)
|
names = ", ".join(item.name for item in ground_items)
|
||||||
player.writer.write(f"On the ground: {names}\r\n")
|
player.writer.write(f"On the ground: {names}\r\n")
|
||||||
|
|
||||||
if portals:
|
|
||||||
names = ", ".join(p.name for p in portals)
|
|
||||||
player.writer.write(f"Portals: {names}\r\n")
|
|
||||||
|
|
||||||
await player.writer.drain()
|
await player.writer.drain()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
"""Portal commands for zone transitions."""
|
|
||||||
|
|
||||||
from mudlib.commands import CommandDefinition, register
|
|
||||||
from mudlib.commands.movement import send_nearby_message
|
|
||||||
from mudlib.player import Player
|
|
||||||
from mudlib.portal import Portal
|
|
||||||
from mudlib.zone import Zone
|
|
||||||
from mudlib.zones import get_zone
|
|
||||||
|
|
||||||
|
|
||||||
async def cmd_enter(player: Player, args: str) -> None:
|
|
||||||
"""Enter a portal to transition to another zone.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
player: The player executing the command
|
|
||||||
args: Portal name or alias to enter
|
|
||||||
"""
|
|
||||||
if not args.strip():
|
|
||||||
await player.send("Enter what?\r\n")
|
|
||||||
return
|
|
||||||
|
|
||||||
zone = player.location
|
|
||||||
if not isinstance(zone, Zone):
|
|
||||||
await player.send("You are nowhere.\r\n")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Find portal at player's position
|
|
||||||
portals_here = [
|
|
||||||
obj for obj in zone.contents_at(player.x, player.y) if isinstance(obj, Portal)
|
|
||||||
]
|
|
||||||
|
|
||||||
# Match by name or alias (exact match only)
|
|
||||||
target_name = args.strip().lower()
|
|
||||||
portal = None
|
|
||||||
for p in portals_here:
|
|
||||||
if p.name.lower() == target_name:
|
|
||||||
portal = p
|
|
||||||
break
|
|
||||||
if target_name in (a.lower() for a in p.aliases):
|
|
||||||
portal = p
|
|
||||||
break
|
|
||||||
|
|
||||||
if not portal:
|
|
||||||
await player.send("You don't see that here.\r\n")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Look up target zone
|
|
||||||
target_zone = get_zone(portal.target_zone)
|
|
||||||
if not target_zone:
|
|
||||||
await player.send("The portal doesn't lead anywhere.\r\n")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Send departure message to nearby players in old zone
|
|
||||||
await send_nearby_message(
|
|
||||||
player, player.x, player.y, f"{player.name} enters {portal.name}.\r\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Move player to target zone
|
|
||||||
player.move_to(target_zone, x=portal.target_x, y=portal.target_y)
|
|
||||||
|
|
||||||
# Send arrival message to nearby players in new zone
|
|
||||||
await send_nearby_message(
|
|
||||||
player,
|
|
||||||
player.x,
|
|
||||||
player.y,
|
|
||||||
f"{player.name} arrives from {portal.name}.\r\n",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Show new zone to player
|
|
||||||
from mudlib.commands.look import cmd_look
|
|
||||||
|
|
||||||
await cmd_look(player, "")
|
|
||||||
|
|
||||||
|
|
||||||
# Register the enter command
|
|
||||||
register(CommandDefinition("enter", cmd_enter))
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
"""Get, drop, and inventory commands for items."""
|
"""Get, drop, and inventory commands for items."""
|
||||||
|
|
||||||
from mudlib.commands import CommandDefinition, register
|
from mudlib.commands import CommandDefinition, register
|
||||||
from mudlib.container import Container
|
|
||||||
from mudlib.player import Player
|
from mudlib.player import Player
|
||||||
from mudlib.thing import Thing
|
from mudlib.thing import Thing
|
||||||
from mudlib.zone import Zone
|
from mudlib.zone import Zone
|
||||||
|
|
@ -33,41 +32,12 @@ def _find_thing_in_inventory(name: str, player: Player) -> Thing | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _format_thing_name(thing: Thing) -> str:
|
|
||||||
"""Format a thing's name with container state if applicable."""
|
|
||||||
if not isinstance(thing, Container):
|
|
||||||
return thing.name
|
|
||||||
|
|
||||||
if thing.closed:
|
|
||||||
return f"{thing.name} (closed)"
|
|
||||||
|
|
||||||
# Container is open
|
|
||||||
contents = [obj for obj in thing.contents if isinstance(obj, Thing)]
|
|
||||||
if not contents:
|
|
||||||
return f"{thing.name} (open, empty)"
|
|
||||||
|
|
||||||
names = ", ".join(item.name for item in contents)
|
|
||||||
return f"{thing.name} (open, containing: {names})"
|
|
||||||
|
|
||||||
|
|
||||||
async def cmd_get(player: Player, args: str) -> None:
|
async def cmd_get(player: Player, args: str) -> None:
|
||||||
"""Pick up an item from the ground or take from a container."""
|
"""Pick up an item from the ground."""
|
||||||
if not args.strip():
|
if not args.strip():
|
||||||
await player.send("Get what?\r\n")
|
await player.send("Get what?\r\n")
|
||||||
return
|
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
|
zone = player.location
|
||||||
if zone is None or not isinstance(zone, Zone):
|
if zone is None or not isinstance(zone, Zone):
|
||||||
await player.send("You are nowhere.\r\n")
|
await player.send("You are nowhere.\r\n")
|
||||||
|
|
@ -86,54 +56,6 @@ async def cmd_get(player: Player, args: str) -> None:
|
||||||
await player.send(f"You pick up {thing.name}.\r\n")
|
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:
|
async def cmd_drop(player: Player, args: str) -> None:
|
||||||
"""Drop an item from inventory onto the ground."""
|
"""Drop an item from inventory onto the ground."""
|
||||||
if not args.strip():
|
if not args.strip():
|
||||||
|
|
@ -164,7 +86,7 @@ async def cmd_inventory(player: Player, args: str) -> None:
|
||||||
|
|
||||||
lines = ["You are carrying:\r\n"]
|
lines = ["You are carrying:\r\n"]
|
||||||
for thing in things:
|
for thing in things:
|
||||||
lines.append(f" {_format_thing_name(thing)}\r\n")
|
lines.append(f" {thing.name}\r\n")
|
||||||
|
|
||||||
await player.send("".join(lines))
|
await player.send("".join(lines))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
"""Container — a Thing that can hold other Things."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
from mudlib.object import Object
|
|
||||||
from mudlib.thing import Thing
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Container(Thing):
|
|
||||||
"""A container that can hold other items.
|
|
||||||
|
|
||||||
Containers are Things with capacity limits and open/closed state.
|
|
||||||
The locked flag is for command-layer logic (unlock/lock commands).
|
|
||||||
"""
|
|
||||||
|
|
||||||
capacity: int = 10
|
|
||||||
closed: bool = False
|
|
||||||
locked: bool = False
|
|
||||||
|
|
||||||
def can_accept(self, obj: Object) -> bool:
|
|
||||||
"""Accept Things when open and below capacity."""
|
|
||||||
if not isinstance(obj, Thing):
|
|
||||||
return False
|
|
||||||
if self.closed:
|
|
||||||
return False
|
|
||||||
return len(self._contents) < self.capacity
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
"""Portal — a transition point between zones."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
from mudlib.thing import Thing
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Portal(Thing):
|
|
||||||
"""A portal connecting zones.
|
|
||||||
|
|
||||||
Portals are non-portable Things that exist in zones and define
|
|
||||||
transitions to other zones via target coordinates.
|
|
||||||
"""
|
|
||||||
|
|
||||||
target_zone: str = ""
|
|
||||||
target_x: int = 0
|
|
||||||
target_y: int = 0
|
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
|
||||||
"""Force portals to be non-portable."""
|
|
||||||
self.portable = False
|
|
||||||
super().__post_init__()
|
|
||||||
|
|
@ -13,14 +13,12 @@ from telnetlib3.server_shell import readline2
|
||||||
|
|
||||||
import mudlib.combat.commands
|
import mudlib.combat.commands
|
||||||
import mudlib.commands
|
import mudlib.commands
|
||||||
import mudlib.commands.containers
|
|
||||||
import mudlib.commands.edit
|
import mudlib.commands.edit
|
||||||
import mudlib.commands.fly
|
import mudlib.commands.fly
|
||||||
import mudlib.commands.help
|
import mudlib.commands.help
|
||||||
import mudlib.commands.look
|
import mudlib.commands.look
|
||||||
import mudlib.commands.movement
|
import mudlib.commands.movement
|
||||||
import mudlib.commands.play
|
import mudlib.commands.play
|
||||||
import mudlib.commands.portals
|
|
||||||
import mudlib.commands.quit
|
import mudlib.commands.quit
|
||||||
import mudlib.commands.reload
|
import mudlib.commands.reload
|
||||||
import mudlib.commands.spawn
|
import mudlib.commands.spawn
|
||||||
|
|
@ -33,6 +31,8 @@ from mudlib.effects import clear_expired
|
||||||
from mudlib.if_session import broadcast_to_spectators
|
from mudlib.if_session import broadcast_to_spectators
|
||||||
from mudlib.mob_ai import process_mobs
|
from mudlib.mob_ai import process_mobs
|
||||||
from mudlib.mobs import load_mob_templates, mob_templates
|
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.player import Player, players
|
||||||
from mudlib.resting import process_resting
|
from mudlib.resting import process_resting
|
||||||
from mudlib.store import (
|
from mudlib.store import (
|
||||||
|
|
@ -49,7 +49,6 @@ from mudlib.thing import Thing
|
||||||
from mudlib.things import load_thing_templates, spawn_thing, thing_templates
|
from mudlib.things import load_thing_templates, spawn_thing, thing_templates
|
||||||
from mudlib.world.terrain import World
|
from mudlib.world.terrain import World
|
||||||
from mudlib.zone import Zone
|
from mudlib.zone import Zone
|
||||||
from mudlib.zones import get_zone, load_zones, register_zone
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -271,10 +270,12 @@ async def shell(
|
||||||
"inventory": [],
|
"inventory": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Resolve zone from zone_name using zone registry
|
# Resolve zone from zone_name (currently only overworld exists)
|
||||||
zone_name = player_data.get("zone_name", "overworld")
|
zone_name = player_data.get("zone_name", "overworld")
|
||||||
player_zone = get_zone(zone_name)
|
if zone_name == "overworld":
|
||||||
if player_zone is None:
|
player_zone = _overworld
|
||||||
|
else:
|
||||||
|
# Future: lookup zone by name from a zone registry
|
||||||
log.warning(
|
log.warning(
|
||||||
"unknown zone '%s' for player '%s', defaulting to overworld",
|
"unknown zone '%s' for player '%s', defaulting to overworld",
|
||||||
zone_name,
|
zone_name,
|
||||||
|
|
@ -459,17 +460,6 @@ async def run_server() -> None:
|
||||||
)
|
)
|
||||||
log.info("created overworld zone (%dx%d, toroidal)", world.width, world.height)
|
log.info("created overworld zone (%dx%d, toroidal)", world.width, world.height)
|
||||||
|
|
||||||
# Register overworld zone
|
|
||||||
register_zone("overworld", _overworld)
|
|
||||||
|
|
||||||
# Load and register zones from content/zones/
|
|
||||||
zones_dir = pathlib.Path(__file__).resolve().parents[2] / "content" / "zones"
|
|
||||||
if zones_dir.exists():
|
|
||||||
loaded_zones = load_zones(zones_dir)
|
|
||||||
for zone_name, zone in loaded_zones.items():
|
|
||||||
register_zone(zone_name, zone)
|
|
||||||
log.info("loaded %d zones from %s", len(loaded_zones), zones_dir)
|
|
||||||
|
|
||||||
# Load content-defined commands from TOML files
|
# Load content-defined commands from TOML files
|
||||||
content_dir = pathlib.Path(__file__).resolve().parents[2] / "content" / "commands"
|
content_dir = pathlib.Path(__file__).resolve().parents[2] / "content" / "commands"
|
||||||
if content_dir.exists():
|
if content_dir.exists():
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import tomllib
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from mudlib.container import Container
|
|
||||||
from mudlib.object import Object
|
from mudlib.object import Object
|
||||||
from mudlib.thing import Thing
|
from mudlib.thing import Thing
|
||||||
|
|
||||||
|
|
@ -17,10 +16,6 @@ class ThingTemplate:
|
||||||
description: str
|
description: str
|
||||||
portable: bool = True
|
portable: bool = True
|
||||||
aliases: list[str] = field(default_factory=list)
|
aliases: list[str] = field(default_factory=list)
|
||||||
# Container fields (presence of capacity indicates a container template)
|
|
||||||
capacity: int | None = None
|
|
||||||
closed: bool = False
|
|
||||||
locked: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
# Module-level registry
|
# Module-level registry
|
||||||
|
|
@ -36,9 +31,6 @@ def load_thing_template(path: Path) -> ThingTemplate:
|
||||||
description=data["description"],
|
description=data["description"],
|
||||||
portable=data.get("portable", True),
|
portable=data.get("portable", True),
|
||||||
aliases=data.get("aliases", []),
|
aliases=data.get("aliases", []),
|
||||||
capacity=data.get("capacity"),
|
|
||||||
closed=data.get("closed", False),
|
|
||||||
locked=data.get("locked", False),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -59,21 +51,6 @@ def spawn_thing(
|
||||||
y: int | None = None,
|
y: int | None = None,
|
||||||
) -> Thing:
|
) -> Thing:
|
||||||
"""Create a Thing instance from a template at the given location."""
|
"""Create a Thing instance from a template at the given location."""
|
||||||
# If template has capacity, spawn a Container instead of a Thing
|
|
||||||
if template.capacity is not None:
|
|
||||||
return Container(
|
|
||||||
name=template.name,
|
|
||||||
description=template.description,
|
|
||||||
portable=template.portable,
|
|
||||||
aliases=list(template.aliases),
|
|
||||||
capacity=template.capacity,
|
|
||||||
closed=template.closed,
|
|
||||||
locked=template.locked,
|
|
||||||
location=location,
|
|
||||||
x=x,
|
|
||||||
y=y,
|
|
||||||
)
|
|
||||||
|
|
||||||
return Thing(
|
return Thing(
|
||||||
name=template.name,
|
name=template.name,
|
||||||
description=template.description,
|
description=template.description,
|
||||||
|
|
|
||||||
|
|
@ -1,102 +0,0 @@
|
||||||
"""Zone registry and loading."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import tomllib
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from mudlib.zone import Zone
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
|
|
||||||
def load_zone(path: Path) -> Zone:
|
|
||||||
"""Load a zone from a TOML file.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
path: Path to TOML file
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Zone instance loaded from file
|
|
||||||
"""
|
|
||||||
with open(path, "rb") as f:
|
|
||||||
data = tomllib.load(f)
|
|
||||||
|
|
||||||
# Extract basic properties
|
|
||||||
name = data["name"]
|
|
||||||
width = data["width"]
|
|
||||||
height = data["height"]
|
|
||||||
toroidal = data.get("toroidal", True)
|
|
||||||
|
|
||||||
# Parse terrain rows into 2D list
|
|
||||||
terrain_rows = data.get("terrain", {}).get("rows", [])
|
|
||||||
terrain = []
|
|
||||||
for row in terrain_rows:
|
|
||||||
terrain.append(list(row))
|
|
||||||
|
|
||||||
# Parse impassable tiles
|
|
||||||
impassable_list = data.get("terrain", {}).get("impassable", {}).get("tiles", [])
|
|
||||||
impassable = set(impassable_list) if impassable_list else {"^", "~"}
|
|
||||||
|
|
||||||
zone = Zone(
|
|
||||||
name=name,
|
|
||||||
width=width,
|
|
||||||
height=height,
|
|
||||||
toroidal=toroidal,
|
|
||||||
terrain=terrain,
|
|
||||||
impassable=impassable,
|
|
||||||
)
|
|
||||||
|
|
||||||
return zone
|
|
||||||
|
|
||||||
|
|
||||||
def load_zones(directory: Path) -> dict[str, Zone]:
|
|
||||||
"""Load all zones from a directory.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
directory: Path to directory containing zone TOML files
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict mapping zone names to Zone instances
|
|
||||||
"""
|
|
||||||
zones = {}
|
|
||||||
|
|
||||||
if not directory.exists():
|
|
||||||
log.warning("zones directory does not exist: %s", directory)
|
|
||||||
return zones
|
|
||||||
|
|
||||||
for toml_file in directory.glob("*.toml"):
|
|
||||||
try:
|
|
||||||
zone = load_zone(toml_file)
|
|
||||||
zones[zone.name] = zone
|
|
||||||
log.debug("loaded zone '%s' from %s", zone.name, toml_file)
|
|
||||||
except Exception as e:
|
|
||||||
log.error("failed to load zone from %s: %s", toml_file, e, exc_info=True)
|
|
||||||
|
|
||||||
return zones
|
|
||||||
|
|
@ -1,134 +0,0 @@
|
||||||
"""Tests for the Container class."""
|
|
||||||
|
|
||||||
from mudlib.container import Container
|
|
||||||
from mudlib.object import Object
|
|
||||||
from mudlib.thing import Thing
|
|
||||||
from mudlib.zone import Zone
|
|
||||||
|
|
||||||
# --- construction ---
|
|
||||||
|
|
||||||
|
|
||||||
def test_container_creation_minimal():
|
|
||||||
"""Container can be created with just a name."""
|
|
||||||
c = Container(name="chest")
|
|
||||||
assert c.name == "chest"
|
|
||||||
assert c.capacity == 10
|
|
||||||
assert c.closed is False
|
|
||||||
assert c.locked is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_container_creation_with_custom_capacity():
|
|
||||||
"""Container can have a custom capacity."""
|
|
||||||
c = Container(name="pouch", capacity=5)
|
|
||||||
assert c.capacity == 5
|
|
||||||
|
|
||||||
|
|
||||||
def test_container_creation_closed():
|
|
||||||
"""Container can be created in closed state."""
|
|
||||||
c = Container(name="chest", closed=True)
|
|
||||||
assert c.closed is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_container_creation_locked():
|
|
||||||
"""Container can be created in locked state."""
|
|
||||||
c = Container(name="chest", locked=True)
|
|
||||||
assert c.locked is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_container_is_thing_subclass():
|
|
||||||
"""Container is a Thing subclass."""
|
|
||||||
c = Container(name="chest")
|
|
||||||
assert isinstance(c, Thing)
|
|
||||||
assert isinstance(c, Object)
|
|
||||||
|
|
||||||
|
|
||||||
def test_container_inherits_thing_properties():
|
|
||||||
"""Container has all Thing properties."""
|
|
||||||
c = Container(
|
|
||||||
name="ornate chest",
|
|
||||||
description="a beautifully carved wooden chest",
|
|
||||||
portable=False,
|
|
||||||
aliases=["chest", "box"],
|
|
||||||
)
|
|
||||||
assert c.description == "a beautifully carved wooden chest"
|
|
||||||
assert c.portable is False
|
|
||||||
assert c.aliases == ["chest", "box"]
|
|
||||||
|
|
||||||
|
|
||||||
# --- can_accept ---
|
|
||||||
|
|
||||||
|
|
||||||
def test_container_accepts_thing_when_open():
|
|
||||||
"""Container accepts Things when open and has capacity."""
|
|
||||||
c = Container(name="chest")
|
|
||||||
sword = Thing(name="sword")
|
|
||||||
assert c.can_accept(sword) is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_container_rejects_when_closed():
|
|
||||||
"""Container rejects Things when closed."""
|
|
||||||
c = Container(name="chest", closed=True)
|
|
||||||
sword = Thing(name="sword")
|
|
||||||
assert c.can_accept(sword) is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_container_rejects_when_at_capacity():
|
|
||||||
"""Container rejects Things when at capacity."""
|
|
||||||
c = Container(name="pouch", capacity=2)
|
|
||||||
# Add two items
|
|
||||||
Thing(name="rock1", location=c)
|
|
||||||
Thing(name="rock2", location=c)
|
|
||||||
# Third should be rejected
|
|
||||||
rock3 = Thing(name="rock3")
|
|
||||||
assert c.can_accept(rock3) is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_container_accepts_when_below_capacity():
|
|
||||||
"""Container accepts Things when below capacity."""
|
|
||||||
c = Container(name="pouch", capacity=2)
|
|
||||||
Thing(name="rock1", location=c)
|
|
||||||
# Second item should be accepted
|
|
||||||
rock2 = Thing(name="rock2")
|
|
||||||
assert c.can_accept(rock2) is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_container_rejects_non_thing():
|
|
||||||
"""Container rejects objects that aren't Things."""
|
|
||||||
c = Container(name="chest")
|
|
||||||
other = Object(name="abstract")
|
|
||||||
assert c.can_accept(other) is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_container_locked_is_just_flag():
|
|
||||||
"""Locked flag is stored but doesn't affect can_accept (used by commands)."""
|
|
||||||
c = Container(name="chest", locked=True, closed=False)
|
|
||||||
sword = Thing(name="sword")
|
|
||||||
# can_accept doesn't check locked (commands will check it separately)
|
|
||||||
# This test documents current behavior — locked is for command layer
|
|
||||||
assert c.locked is True
|
|
||||||
# can_accept only checks closed and capacity
|
|
||||||
assert c.can_accept(sword) is True
|
|
||||||
|
|
||||||
|
|
||||||
# --- integration with zones ---
|
|
||||||
|
|
||||||
|
|
||||||
def test_container_in_zone():
|
|
||||||
"""Container can be placed in a zone with coordinates."""
|
|
||||||
terrain = [["." for _ in range(10)] for _ in range(10)]
|
|
||||||
zone = Zone(name="test", width=10, height=10, terrain=terrain)
|
|
||||||
chest = Container(name="chest", location=zone, x=5, y=5)
|
|
||||||
assert chest.location is zone
|
|
||||||
assert chest.x == 5
|
|
||||||
assert chest.y == 5
|
|
||||||
assert chest in zone.contents
|
|
||||||
|
|
||||||
|
|
||||||
def test_container_with_contents():
|
|
||||||
"""Container can hold Things."""
|
|
||||||
chest = Container(name="chest")
|
|
||||||
sword = Thing(name="sword", location=chest)
|
|
||||||
gem = Thing(name="gem", location=chest)
|
|
||||||
assert sword in chest.contents
|
|
||||||
assert gem in chest.contents
|
|
||||||
assert len(chest.contents) == 2
|
|
||||||
|
|
@ -1,150 +0,0 @@
|
||||||
"""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
|
|
||||||
|
|
@ -1,149 +0,0 @@
|
||||||
"""Tests for container template loading and spawning."""
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
from tempfile import TemporaryDirectory
|
|
||||||
|
|
||||||
from mudlib.container import Container
|
|
||||||
from mudlib.thing import Thing
|
|
||||||
from mudlib.things import load_thing_template, spawn_thing
|
|
||||||
from mudlib.zone import Zone
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_container_template_with_capacity():
|
|
||||||
"""load_thing_template recognizes container templates (has capacity field)."""
|
|
||||||
with TemporaryDirectory() as tmpdir:
|
|
||||||
toml_path = Path(tmpdir) / "chest.toml"
|
|
||||||
toml_path.write_text(
|
|
||||||
"""
|
|
||||||
name = "chest"
|
|
||||||
description = "a wooden chest"
|
|
||||||
capacity = 5
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
template = load_thing_template(toml_path)
|
|
||||||
assert template.name == "chest"
|
|
||||||
assert template.description == "a wooden chest"
|
|
||||||
assert hasattr(template, "capacity")
|
|
||||||
assert template.capacity == 5
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_container_template_with_closed():
|
|
||||||
"""load_thing_template handles closed field."""
|
|
||||||
with TemporaryDirectory() as tmpdir:
|
|
||||||
toml_path = Path(tmpdir) / "chest.toml"
|
|
||||||
toml_path.write_text(
|
|
||||||
"""
|
|
||||||
name = "chest"
|
|
||||||
description = "a wooden chest"
|
|
||||||
capacity = 5
|
|
||||||
closed = true
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
template = load_thing_template(toml_path)
|
|
||||||
assert template.closed is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_container_template_with_locked():
|
|
||||||
"""load_thing_template handles locked field."""
|
|
||||||
with TemporaryDirectory() as tmpdir:
|
|
||||||
toml_path = Path(tmpdir) / "chest.toml"
|
|
||||||
toml_path.write_text(
|
|
||||||
"""
|
|
||||||
name = "chest"
|
|
||||||
description = "a wooden chest"
|
|
||||||
capacity = 5
|
|
||||||
closed = true
|
|
||||||
locked = true
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
template = load_thing_template(toml_path)
|
|
||||||
assert template.locked is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_container_template_defaults():
|
|
||||||
"""Container template fields have sensible defaults."""
|
|
||||||
with TemporaryDirectory() as tmpdir:
|
|
||||||
toml_path = Path(tmpdir) / "sack.toml"
|
|
||||||
toml_path.write_text(
|
|
||||||
"""
|
|
||||||
name = "sack"
|
|
||||||
description = "a cloth sack"
|
|
||||||
capacity = 3
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
template = load_thing_template(toml_path)
|
|
||||||
assert template.capacity == 3
|
|
||||||
assert template.closed is False # default: open
|
|
||||||
assert template.locked is False # default: unlocked
|
|
||||||
|
|
||||||
|
|
||||||
def test_spawn_container_from_template():
|
|
||||||
"""spawn_thing creates a Container when template has capacity."""
|
|
||||||
with TemporaryDirectory() as tmpdir:
|
|
||||||
toml_path = Path(tmpdir) / "chest.toml"
|
|
||||||
toml_path.write_text(
|
|
||||||
"""
|
|
||||||
name = "chest"
|
|
||||||
description = "a wooden chest"
|
|
||||||
capacity = 5
|
|
||||||
closed = true
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
template = load_thing_template(toml_path)
|
|
||||||
terrain = [["." for _ in range(10)] for _ in range(10)]
|
|
||||||
zone = Zone(name="test", width=10, height=10, terrain=terrain)
|
|
||||||
|
|
||||||
thing = spawn_thing(template, zone, x=5, y=5)
|
|
||||||
|
|
||||||
assert isinstance(thing, Container)
|
|
||||||
assert thing.name == "chest"
|
|
||||||
assert thing.description == "a wooden chest"
|
|
||||||
assert thing.capacity == 5
|
|
||||||
assert thing.closed is True
|
|
||||||
assert thing.location is zone
|
|
||||||
|
|
||||||
|
|
||||||
def test_spawn_regular_thing_from_template_without_capacity():
|
|
||||||
"""spawn_thing creates a regular Thing when template lacks capacity."""
|
|
||||||
with TemporaryDirectory() as tmpdir:
|
|
||||||
toml_path = Path(tmpdir) / "rock.toml"
|
|
||||||
toml_path.write_text(
|
|
||||||
"""
|
|
||||||
name = "rock"
|
|
||||||
description = "a smooth grey rock"
|
|
||||||
portable = true
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
template = load_thing_template(toml_path)
|
|
||||||
terrain = [["." for _ in range(10)] for _ in range(10)]
|
|
||||||
zone = Zone(name="test", width=10, height=10, terrain=terrain)
|
|
||||||
|
|
||||||
thing = spawn_thing(template, zone, x=5, y=5)
|
|
||||||
|
|
||||||
assert isinstance(thing, Thing)
|
|
||||||
assert not isinstance(thing, Container)
|
|
||||||
assert thing.name == "rock"
|
|
||||||
assert thing.portable is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_spawn_portable_container():
|
|
||||||
"""spawn_thing creates a portable Container (like a sack)."""
|
|
||||||
with TemporaryDirectory() as tmpdir:
|
|
||||||
toml_path = Path(tmpdir) / "sack.toml"
|
|
||||||
toml_path.write_text(
|
|
||||||
"""
|
|
||||||
name = "sack"
|
|
||||||
description = "a cloth sack"
|
|
||||||
capacity = 3
|
|
||||||
portable = true
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
template = load_thing_template(toml_path)
|
|
||||||
terrain = [["." for _ in range(10)] for _ in range(10)]
|
|
||||||
zone = Zone(name="test", width=10, height=10, terrain=terrain)
|
|
||||||
|
|
||||||
thing = spawn_thing(template, zone, x=5, y=5)
|
|
||||||
|
|
||||||
assert isinstance(thing, Container)
|
|
||||||
assert thing.portable is True
|
|
||||||
assert thing.capacity == 3
|
|
||||||
|
|
@ -1,284 +0,0 @@
|
||||||
"""Tests for portal enter command."""
|
|
||||||
|
|
||||||
from unittest.mock import AsyncMock, MagicMock
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from mudlib.commands import _registry
|
|
||||||
from mudlib.player import Player
|
|
||||||
from mudlib.portal import Portal
|
|
||||||
from mudlib.zone import Zone
|
|
||||||
from mudlib.zones import register_zone, zone_registry
|
|
||||||
|
|
||||||
|
|
||||||
@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 target_zone():
|
|
||||||
terrain = [["." for _ in range(10)] for _ in range(10)]
|
|
||||||
return Zone(
|
|
||||||
name="targetzone",
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
|
||||||
def clear_zones():
|
|
||||||
"""Clear zone registry before and after each test."""
|
|
||||||
zone_registry.clear()
|
|
||||||
yield
|
|
||||||
zone_registry.clear()
|
|
||||||
|
|
||||||
|
|
||||||
# --- cmd_enter ---
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_enter_moves_to_target_zone(player, test_zone, target_zone):
|
|
||||||
"""enter portal moves player to target zone."""
|
|
||||||
from mudlib.commands.portals import cmd_enter
|
|
||||||
|
|
||||||
register_zone("targetzone", target_zone)
|
|
||||||
Portal(
|
|
||||||
name="shimmering doorway",
|
|
||||||
aliases=["doorway"],
|
|
||||||
location=test_zone,
|
|
||||||
x=5,
|
|
||||||
y=5,
|
|
||||||
target_zone="targetzone",
|
|
||||||
target_x=3,
|
|
||||||
target_y=7,
|
|
||||||
)
|
|
||||||
|
|
||||||
await cmd_enter(player, "doorway")
|
|
||||||
assert player.location is target_zone
|
|
||||||
assert player.x == 3
|
|
||||||
assert player.y == 7
|
|
||||||
assert player in target_zone.contents
|
|
||||||
assert player not in test_zone.contents
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_enter_sends_departure_message(
|
|
||||||
player, test_zone, target_zone, mock_writer
|
|
||||||
):
|
|
||||||
"""enter portal sends departure message to old zone."""
|
|
||||||
from mudlib.commands.portals import cmd_enter
|
|
||||||
|
|
||||||
register_zone("targetzone", target_zone)
|
|
||||||
Portal(
|
|
||||||
name="portal",
|
|
||||||
location=test_zone,
|
|
||||||
x=5,
|
|
||||||
y=5,
|
|
||||||
target_zone="targetzone",
|
|
||||||
target_x=3,
|
|
||||||
target_y=7,
|
|
||||||
)
|
|
||||||
other_player = Player(
|
|
||||||
name="OtherPlayer",
|
|
||||||
x=5,
|
|
||||||
y=5,
|
|
||||||
reader=MagicMock(),
|
|
||||||
writer=MagicMock(write=MagicMock(), drain=AsyncMock()),
|
|
||||||
location=test_zone,
|
|
||||||
)
|
|
||||||
|
|
||||||
await cmd_enter(player, "portal")
|
|
||||||
|
|
||||||
# Check that other player received departure message
|
|
||||||
other_writer = other_player.writer
|
|
||||||
calls = [call[0][0] for call in other_writer.write.call_args_list]
|
|
||||||
assert any("TestPlayer" in call and "enter" in call.lower() for call in calls)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_enter_sends_arrival_message(player, test_zone, target_zone):
|
|
||||||
"""enter portal sends arrival message to new zone."""
|
|
||||||
from mudlib.commands.portals import cmd_enter
|
|
||||||
|
|
||||||
register_zone("targetzone", target_zone)
|
|
||||||
Portal(
|
|
||||||
name="portal",
|
|
||||||
location=test_zone,
|
|
||||||
x=5,
|
|
||||||
y=5,
|
|
||||||
target_zone="targetzone",
|
|
||||||
target_x=3,
|
|
||||||
target_y=7,
|
|
||||||
)
|
|
||||||
other_player = Player(
|
|
||||||
name="OtherPlayer",
|
|
||||||
x=3,
|
|
||||||
y=7,
|
|
||||||
reader=MagicMock(),
|
|
||||||
writer=MagicMock(write=MagicMock(), drain=AsyncMock()),
|
|
||||||
location=target_zone,
|
|
||||||
)
|
|
||||||
|
|
||||||
await cmd_enter(player, "portal")
|
|
||||||
|
|
||||||
# Check that other player in target zone received arrival message
|
|
||||||
other_writer = other_player.writer
|
|
||||||
calls = [call[0][0] for call in other_writer.write.call_args_list]
|
|
||||||
assert any("TestPlayer" in call and "arrive" in call.lower() for call in calls)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_enter_shows_look_in_new_zone(
|
|
||||||
player, test_zone, target_zone, mock_writer
|
|
||||||
):
|
|
||||||
"""enter portal triggers look command in new zone."""
|
|
||||||
from mudlib.commands.portals import cmd_enter
|
|
||||||
|
|
||||||
register_zone("targetzone", target_zone)
|
|
||||||
Portal(
|
|
||||||
name="portal",
|
|
||||||
location=test_zone,
|
|
||||||
x=5,
|
|
||||||
y=5,
|
|
||||||
target_zone="targetzone",
|
|
||||||
target_x=3,
|
|
||||||
target_y=7,
|
|
||||||
)
|
|
||||||
|
|
||||||
await cmd_enter(player, "portal")
|
|
||||||
|
|
||||||
# Check that look output was sent (viewport grid should be in output)
|
|
||||||
output = "".join([call[0][0] for call in mock_writer.write.call_args_list])
|
|
||||||
# Should have the @ symbol for player position
|
|
||||||
assert "@" in output
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_enter_no_args(player, mock_writer):
|
|
||||||
"""enter with no arguments gives usage hint."""
|
|
||||||
from mudlib.commands.portals import cmd_enter
|
|
||||||
|
|
||||||
await cmd_enter(player, "")
|
|
||||||
output = mock_writer.write.call_args_list[-1][0][0]
|
|
||||||
assert "enter what" in output.lower() or "usage" in output.lower()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_enter_portal_not_found(player, test_zone, mock_writer):
|
|
||||||
"""enter with portal not at position gives feedback."""
|
|
||||||
from mudlib.commands.portals import cmd_enter
|
|
||||||
|
|
||||||
await cmd_enter(player, "doorway")
|
|
||||||
output = mock_writer.write.call_args_list[-1][0][0]
|
|
||||||
assert "don't see" in output.lower()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_enter_target_zone_not_found(player, test_zone, mock_writer):
|
|
||||||
"""enter with invalid target zone gives graceful error."""
|
|
||||||
from mudlib.commands.portals import cmd_enter
|
|
||||||
|
|
||||||
Portal(
|
|
||||||
name="portal",
|
|
||||||
location=test_zone,
|
|
||||||
x=5,
|
|
||||||
y=5,
|
|
||||||
target_zone="nonexistent",
|
|
||||||
target_x=3,
|
|
||||||
target_y=7,
|
|
||||||
)
|
|
||||||
|
|
||||||
await cmd_enter(player, "portal")
|
|
||||||
output = mock_writer.write.call_args_list[-1][0][0]
|
|
||||||
assert "doesn't lead anywhere" in output.lower() or "nowhere" in output.lower()
|
|
||||||
# Player should still be in original zone
|
|
||||||
assert player.location is test_zone
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_enter_matches_aliases(player, test_zone, target_zone):
|
|
||||||
"""enter matches portal aliases."""
|
|
||||||
from mudlib.commands.portals import cmd_enter
|
|
||||||
|
|
||||||
register_zone("targetzone", target_zone)
|
|
||||||
Portal(
|
|
||||||
name="shimmering doorway",
|
|
||||||
aliases=["door", "doorway"],
|
|
||||||
location=test_zone,
|
|
||||||
x=5,
|
|
||||||
y=5,
|
|
||||||
target_zone="targetzone",
|
|
||||||
target_x=3,
|
|
||||||
target_y=7,
|
|
||||||
)
|
|
||||||
|
|
||||||
await cmd_enter(player, "door")
|
|
||||||
assert player.location is target_zone
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_enter_portal_elsewhere_in_zone(player, test_zone, mock_writer):
|
|
||||||
"""enter doesn't find portals not at player position."""
|
|
||||||
from mudlib.commands.portals import cmd_enter
|
|
||||||
|
|
||||||
Portal(
|
|
||||||
name="distant portal",
|
|
||||||
location=test_zone,
|
|
||||||
x=8,
|
|
||||||
y=8,
|
|
||||||
target_zone="targetzone",
|
|
||||||
target_x=3,
|
|
||||||
target_y=7,
|
|
||||||
)
|
|
||||||
|
|
||||||
await cmd_enter(player, "portal")
|
|
||||||
output = mock_writer.write.call_args_list[-1][0][0]
|
|
||||||
assert "don't see" in output.lower()
|
|
||||||
# Player should still be in original zone
|
|
||||||
assert player.location is test_zone
|
|
||||||
|
|
||||||
|
|
||||||
# --- command registration ---
|
|
||||||
|
|
||||||
|
|
||||||
def test_enter_command_registered():
|
|
||||||
"""enter command is registered."""
|
|
||||||
import mudlib.commands.portals # noqa: F401
|
|
||||||
|
|
||||||
assert "enter" in _registry
|
|
||||||
|
|
@ -1,226 +0,0 @@
|
||||||
"""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
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
"""Tests for the Portal class."""
|
|
||||||
|
|
||||||
from mudlib.object import Object
|
|
||||||
from mudlib.portal import Portal
|
|
||||||
from mudlib.thing import Thing
|
|
||||||
from mudlib.zone import Zone
|
|
||||||
|
|
||||||
# --- construction ---
|
|
||||||
|
|
||||||
|
|
||||||
def test_portal_creation_minimal():
|
|
||||||
"""Portal can be created with just a name."""
|
|
||||||
p = Portal(name="portal")
|
|
||||||
assert p.name == "portal"
|
|
||||||
assert p.location is None
|
|
||||||
assert p.target_zone == ""
|
|
||||||
assert p.target_x == 0
|
|
||||||
assert p.target_y == 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_portal_creation_with_target():
|
|
||||||
"""Portal can be created with target zone and coordinates."""
|
|
||||||
p = Portal(name="gateway", target_zone="dungeon", target_x=5, target_y=10)
|
|
||||||
assert p.target_zone == "dungeon"
|
|
||||||
assert p.target_x == 5
|
|
||||||
assert p.target_y == 10
|
|
||||||
|
|
||||||
|
|
||||||
def test_portal_is_thing_subclass():
|
|
||||||
"""Portal inherits from Thing."""
|
|
||||||
p = Portal(name="portal")
|
|
||||||
assert isinstance(p, Thing)
|
|
||||||
|
|
||||||
|
|
||||||
def test_portal_is_object_subclass():
|
|
||||||
"""Portal inherits from Object (via Thing)."""
|
|
||||||
p = Portal(name="portal")
|
|
||||||
assert isinstance(p, Object)
|
|
||||||
|
|
||||||
|
|
||||||
def test_portal_always_non_portable():
|
|
||||||
"""Portal is always non-portable (cannot be picked up)."""
|
|
||||||
p = Portal(name="portal")
|
|
||||||
assert p.portable is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_portal_forced_non_portable():
|
|
||||||
"""Portal forces portable=False even if explicitly set True."""
|
|
||||||
# Even if we try to make it portable, it should be forced to False
|
|
||||||
p = Portal(name="portal", portable=True)
|
|
||||||
assert p.portable is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_portal_inherits_description():
|
|
||||||
"""Portal can have a description (from Thing)."""
|
|
||||||
p = Portal(name="gateway", description="a shimmering portal")
|
|
||||||
assert p.description == "a shimmering portal"
|
|
||||||
|
|
||||||
|
|
||||||
def test_portal_inherits_aliases():
|
|
||||||
"""Portal can have aliases (from Thing)."""
|
|
||||||
p = Portal(name="gateway", aliases=["portal", "gate"])
|
|
||||||
assert p.aliases == ["portal", "gate"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_portal_in_zone():
|
|
||||||
"""Portal can exist in a zone with coordinates."""
|
|
||||||
terrain = [["." for _ in range(10)] for _ in range(10)]
|
|
||||||
zone = Zone(name="test", width=10, height=10, terrain=terrain)
|
|
||||||
p = Portal(
|
|
||||||
name="gateway",
|
|
||||||
location=zone,
|
|
||||||
x=3,
|
|
||||||
y=7,
|
|
||||||
target_zone="dungeon",
|
|
||||||
target_x=5,
|
|
||||||
target_y=5,
|
|
||||||
)
|
|
||||||
assert p.location is zone
|
|
||||||
assert p.x == 3
|
|
||||||
assert p.y == 7
|
|
||||||
assert p in zone.contents
|
|
||||||
|
|
||||||
|
|
||||||
def test_portal_rejects_contents():
|
|
||||||
"""Portal cannot accept other objects (uses Thing's default behavior)."""
|
|
||||||
p = Portal(name="portal")
|
|
||||||
obj = Object(name="rock")
|
|
||||||
assert p.can_accept(obj) is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_portal_rejects_things():
|
|
||||||
"""Portal cannot accept things."""
|
|
||||||
p = Portal(name="portal")
|
|
||||||
thing = Thing(name="sword")
|
|
||||||
assert p.can_accept(thing) is False
|
|
||||||
|
|
@ -1,118 +0,0 @@
|
||||||
"""Tests for portal display in look command."""
|
|
||||||
|
|
||||||
from unittest.mock import AsyncMock, MagicMock
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from mudlib.player import Player
|
|
||||||
from mudlib.portal import Portal
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_look_shows_portal_at_position(player, test_zone, mock_writer):
|
|
||||||
"""look command shows portals at player position."""
|
|
||||||
from mudlib.commands.look import cmd_look
|
|
||||||
|
|
||||||
Portal(
|
|
||||||
name="shimmering doorway",
|
|
||||||
location=test_zone,
|
|
||||||
x=5,
|
|
||||||
y=5,
|
|
||||||
target_zone="elsewhere",
|
|
||||||
target_x=0,
|
|
||||||
target_y=0,
|
|
||||||
)
|
|
||||||
|
|
||||||
await cmd_look(player, "")
|
|
||||||
output = "".join([call[0][0] for call in mock_writer.write.call_args_list])
|
|
||||||
assert "portal" in output.lower() and "shimmering doorway" in output.lower()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_look_shows_multiple_portals(player, test_zone, mock_writer):
|
|
||||||
"""look command shows multiple portals at player position."""
|
|
||||||
from mudlib.commands.look import cmd_look
|
|
||||||
|
|
||||||
Portal(
|
|
||||||
name="red portal",
|
|
||||||
location=test_zone,
|
|
||||||
x=5,
|
|
||||||
y=5,
|
|
||||||
target_zone="redzone",
|
|
||||||
target_x=0,
|
|
||||||
target_y=0,
|
|
||||||
)
|
|
||||||
Portal(
|
|
||||||
name="blue portal",
|
|
||||||
location=test_zone,
|
|
||||||
x=5,
|
|
||||||
y=5,
|
|
||||||
target_zone="bluezone",
|
|
||||||
target_x=0,
|
|
||||||
target_y=0,
|
|
||||||
)
|
|
||||||
|
|
||||||
await cmd_look(player, "")
|
|
||||||
output = "".join([call[0][0] for call in mock_writer.write.call_args_list])
|
|
||||||
assert "red portal" in output.lower()
|
|
||||||
assert "blue portal" in output.lower()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_look_no_portals_at_position(player, test_zone, mock_writer):
|
|
||||||
"""look command doesn't show portals when none at position."""
|
|
||||||
from mudlib.commands.look import cmd_look
|
|
||||||
|
|
||||||
Portal(
|
|
||||||
name="distant portal",
|
|
||||||
location=test_zone,
|
|
||||||
x=8,
|
|
||||||
y=8,
|
|
||||||
target_zone="elsewhere",
|
|
||||||
target_x=0,
|
|
||||||
target_y=0,
|
|
||||||
)
|
|
||||||
|
|
||||||
await cmd_look(player, "")
|
|
||||||
output = "".join([call[0][0] for call in mock_writer.write.call_args_list])
|
|
||||||
# Should not mention portals when none are at player position
|
|
||||||
assert "portal" not in output.lower() or "distant portal" not in output.lower()
|
|
||||||
|
|
@ -1,381 +0,0 @@
|
||||||
"""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
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
"""Tests for two-way portal transitions."""
|
|
||||||
|
|
||||||
from unittest.mock import AsyncMock, MagicMock
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from mudlib.player import Player
|
|
||||||
from mudlib.portal import Portal
|
|
||||||
from mudlib.zone import Zone
|
|
||||||
from mudlib.zones import register_zone, zone_registry
|
|
||||||
|
|
||||||
|
|
||||||
@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 zone_a():
|
|
||||||
terrain = [["." for _ in range(10)] for _ in range(10)]
|
|
||||||
return Zone(
|
|
||||||
name="zone_a",
|
|
||||||
width=10,
|
|
||||||
height=10,
|
|
||||||
toroidal=True,
|
|
||||||
terrain=terrain,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def zone_b():
|
|
||||||
terrain = [["." for _ in range(10)] for _ in range(10)]
|
|
||||||
return Zone(
|
|
||||||
name="zone_b",
|
|
||||||
width=10,
|
|
||||||
height=10,
|
|
||||||
toroidal=True,
|
|
||||||
terrain=terrain,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def player(mock_reader, mock_writer, zone_a):
|
|
||||||
p = Player(
|
|
||||||
name="TestPlayer",
|
|
||||||
x=2,
|
|
||||||
y=2,
|
|
||||||
reader=mock_reader,
|
|
||||||
writer=mock_writer,
|
|
||||||
location=zone_a,
|
|
||||||
)
|
|
||||||
return p
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
|
||||||
def clear_zones():
|
|
||||||
"""Clear zone registry before and after each test."""
|
|
||||||
zone_registry.clear()
|
|
||||||
yield
|
|
||||||
zone_registry.clear()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_two_way_portal_transitions(player, zone_a, zone_b):
|
|
||||||
"""Portals work bidirectionally between zones."""
|
|
||||||
from mudlib.commands.portals import cmd_enter
|
|
||||||
|
|
||||||
# Register zones
|
|
||||||
register_zone("zone_a", zone_a)
|
|
||||||
register_zone("zone_b", zone_b)
|
|
||||||
|
|
||||||
# Create portal in zone A pointing to zone B
|
|
||||||
Portal(
|
|
||||||
name="doorway to B",
|
|
||||||
location=zone_a,
|
|
||||||
x=2,
|
|
||||||
y=2,
|
|
||||||
target_zone="zone_b",
|
|
||||||
target_x=7,
|
|
||||||
target_y=7,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create portal in zone B pointing to zone A
|
|
||||||
Portal(
|
|
||||||
name="doorway to A",
|
|
||||||
location=zone_b,
|
|
||||||
x=7,
|
|
||||||
y=7,
|
|
||||||
target_zone="zone_a",
|
|
||||||
target_x=2,
|
|
||||||
target_y=2,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Player starts in zone A at (2, 2)
|
|
||||||
assert player.location is zone_a
|
|
||||||
assert player.x == 2
|
|
||||||
assert player.y == 2
|
|
||||||
|
|
||||||
# Enter portal to zone B
|
|
||||||
await cmd_enter(player, "doorway to B")
|
|
||||||
assert player.location is zone_b
|
|
||||||
assert player.x == 7
|
|
||||||
assert player.y == 7
|
|
||||||
|
|
||||||
# Enter portal back to zone A
|
|
||||||
await cmd_enter(player, "doorway to A")
|
|
||||||
assert player.location is zone_a
|
|
||||||
assert player.x == 2
|
|
||||||
assert player.y == 2
|
|
||||||
|
|
@ -1,146 +0,0 @@
|
||||||
"""Tests for zone loading from TOML files."""
|
|
||||||
|
|
||||||
import pathlib
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
from mudlib.zones import load_zone, load_zones
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_zone():
|
|
||||||
"""Load a zone from TOML file."""
|
|
||||||
# Create a temporary TOML file
|
|
||||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
|
|
||||||
f.write("""
|
|
||||||
name = "test_zone"
|
|
||||||
description = "a test zone"
|
|
||||||
width = 4
|
|
||||||
height = 3
|
|
||||||
toroidal = false
|
|
||||||
|
|
||||||
[terrain]
|
|
||||||
rows = [
|
|
||||||
"####",
|
|
||||||
"#..#",
|
|
||||||
"####",
|
|
||||||
]
|
|
||||||
|
|
||||||
[terrain.impassable]
|
|
||||||
tiles = ["#"]
|
|
||||||
""")
|
|
||||||
temp_path = pathlib.Path(f.name)
|
|
||||||
|
|
||||||
try:
|
|
||||||
zone = load_zone(temp_path)
|
|
||||||
|
|
||||||
assert zone.name == "test_zone"
|
|
||||||
assert zone.width == 4
|
|
||||||
assert zone.height == 3
|
|
||||||
assert zone.toroidal is False
|
|
||||||
assert len(zone.terrain) == 3
|
|
||||||
assert zone.terrain[0] == ["#", "#", "#", "#"]
|
|
||||||
assert zone.terrain[1] == ["#", ".", ".", "#"]
|
|
||||||
assert zone.terrain[2] == ["#", "#", "#", "#"]
|
|
||||||
assert zone.impassable == {"#"}
|
|
||||||
finally:
|
|
||||||
temp_path.unlink()
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_zone_toroidal():
|
|
||||||
"""Load a toroidal zone."""
|
|
||||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
|
|
||||||
f.write("""
|
|
||||||
name = "toroidal_zone"
|
|
||||||
description = "a toroidal test zone"
|
|
||||||
width = 3
|
|
||||||
height = 2
|
|
||||||
toroidal = true
|
|
||||||
|
|
||||||
[terrain]
|
|
||||||
rows = [
|
|
||||||
"...",
|
|
||||||
"...",
|
|
||||||
]
|
|
||||||
""")
|
|
||||||
temp_path = pathlib.Path(f.name)
|
|
||||||
|
|
||||||
try:
|
|
||||||
zone = load_zone(temp_path)
|
|
||||||
|
|
||||||
assert zone.toroidal is True
|
|
||||||
# Default impassable set from Zone class
|
|
||||||
assert zone.impassable == {"^", "~"}
|
|
||||||
finally:
|
|
||||||
temp_path.unlink()
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_zones_from_directory():
|
|
||||||
"""Load all zones from a directory."""
|
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
|
||||||
tmpdir_path = pathlib.Path(tmpdir)
|
|
||||||
|
|
||||||
# Create two zone files
|
|
||||||
zone1_path = tmpdir_path / "zone1.toml"
|
|
||||||
zone1_path.write_text("""
|
|
||||||
name = "zone1"
|
|
||||||
description = "first zone"
|
|
||||||
width = 2
|
|
||||||
height = 2
|
|
||||||
toroidal = false
|
|
||||||
|
|
||||||
[terrain]
|
|
||||||
rows = [
|
|
||||||
"..",
|
|
||||||
"..",
|
|
||||||
]
|
|
||||||
""")
|
|
||||||
|
|
||||||
zone2_path = tmpdir_path / "zone2.toml"
|
|
||||||
zone2_path.write_text("""
|
|
||||||
name = "zone2"
|
|
||||||
description = "second zone"
|
|
||||||
width = 3
|
|
||||||
height = 3
|
|
||||||
toroidal = true
|
|
||||||
|
|
||||||
[terrain]
|
|
||||||
rows = [
|
|
||||||
"###",
|
|
||||||
"#.#",
|
|
||||||
"###",
|
|
||||||
]
|
|
||||||
""")
|
|
||||||
|
|
||||||
# Create a non-TOML file that should be ignored
|
|
||||||
(tmpdir_path / "readme.txt").write_text("not a zone file")
|
|
||||||
|
|
||||||
zones = load_zones(tmpdir_path)
|
|
||||||
|
|
||||||
assert len(zones) == 2
|
|
||||||
assert "zone1" in zones
|
|
||||||
assert "zone2" in zones
|
|
||||||
assert zones["zone1"].name == "zone1"
|
|
||||||
assert zones["zone2"].name == "zone2"
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_tavern_zone():
|
|
||||||
"""Load the actual tavern zone file."""
|
|
||||||
# This tests the real tavern.toml file in content/zones/
|
|
||||||
project_root = pathlib.Path(__file__).resolve().parents[1]
|
|
||||||
tavern_path = project_root / "content" / "zones" / "tavern.toml"
|
|
||||||
|
|
||||||
zone = load_zone(tavern_path)
|
|
||||||
|
|
||||||
assert zone.name == "tavern"
|
|
||||||
assert zone.width == 8
|
|
||||||
assert zone.height == 6
|
|
||||||
assert zone.toroidal is False
|
|
||||||
assert len(zone.terrain) == 6
|
|
||||||
assert zone.terrain[0] == ["#", "#", "#", "#", "#", "#", "#", "#"]
|
|
||||||
assert zone.terrain[5] == ["#", "#", "#", "#", ".", "#", "#", "#"]
|
|
||||||
assert zone.impassable == {"#"}
|
|
||||||
# Check that interior is passable
|
|
||||||
assert zone.is_passable(1, 1)
|
|
||||||
assert zone.is_passable(4, 3)
|
|
||||||
# Check that walls are impassable
|
|
||||||
assert not zone.is_passable(0, 0)
|
|
||||||
assert not zone.is_passable(7, 0)
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
"""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
|
|
||||||
Loading…
Reference in a new issue