Add Object base class with containment primitives

Object provides name, location, x/y, contents reverse-lookup, and
can_accept() — the foundation for the containment tree that zones,
things, and inventory will build on.
This commit is contained in:
Jared Miller 2026-02-11 18:40:31 -05:00
parent 9671f3c286
commit d9e9d1b785
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
2 changed files with 110 additions and 0 deletions

37
src/mudlib/object.py Normal file
View file

@ -0,0 +1,37 @@
"""Base class for everything in the world."""
from __future__ import annotations
from dataclasses import dataclass, field
@dataclass
class Object:
"""Base class for everything in the world.
Every object has a location (another Object, or None for top-level things
like zones). The location pointer is the entire containment system
contents is just the reverse lookup.
"""
name: str
location: Object | None = None
x: int | None = None
y: int | None = None
_contents: list[Object] = field(
default_factory=list, init=False, repr=False, compare=False
)
def __post_init__(self) -> None:
if self.location is not None:
self.location._contents.append(self)
@property
def contents(self) -> list[Object]:
"""Everything whose location is this object."""
return list(self._contents)
def can_accept(self, obj: Object) -> bool:
"""Whether this object accepts obj as contents. Default: no."""
return False

73
tests/test_object.py Normal file
View file

@ -0,0 +1,73 @@
"""Tests for the Object base class."""
from mudlib.object import Object
def test_object_creation_minimal():
"""Object can be created with just a name."""
obj = Object(name="rock")
assert obj.name == "rock"
assert obj.location is None
assert obj.x is None
assert obj.y is None
def test_object_creation_with_position():
"""Object can have spatial coordinates."""
obj = Object(name="tree", x=5, y=10)
assert obj.x == 5
assert obj.y == 10
def test_object_creation_with_location():
"""Object can be placed inside another object."""
zone = Object(name="overworld")
tree = Object(name="oak", location=zone, x=3, y=7)
assert tree.location is zone
def test_contents_empty_by_default():
"""An object with nothing inside has empty contents."""
obj = Object(name="empty")
assert obj.contents == []
def test_contents_tracks_children():
"""Contents lists objects whose location is self."""
zone = Object(name="overworld")
tree = Object(name="oak", location=zone, x=3, y=7)
rock = Object(name="rock", location=zone, x=1, y=1)
assert tree in zone.contents
assert rock in zone.contents
assert len(zone.contents) == 2
def test_contents_nested():
"""Containment can nest: zone > player > backpack > item."""
zone = Object(name="overworld")
player = Object(name="jared", location=zone, x=5, y=5)
backpack = Object(name="backpack", location=player)
gem = Object(name="gem", location=backpack)
assert player in zone.contents
assert backpack in player.contents
assert gem in backpack.contents
# gem is NOT directly in zone or player
assert gem not in zone.contents
assert gem not in player.contents
def test_can_accept_default_false():
"""Object.can_accept() returns False by default."""
obj = Object(name="rock")
other = Object(name="sword")
assert obj.can_accept(other) is False
def test_contents_returns_copy():
"""Contents returns a copy, not the internal list."""
zone = Object(name="zone")
Object(name="thing", location=zone)
contents = zone.contents
contents.append(Object(name="intruder"))
assert len(zone.contents) == 1 # internal list unchanged