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:
parent
9671f3c286
commit
d9e9d1b785
2 changed files with 110 additions and 0 deletions
37
src/mudlib/object.py
Normal file
37
src/mudlib/object.py
Normal 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
73
tests/test_object.py
Normal 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
|
||||||
Loading…
Reference in a new issue