mud/src/mudlib/object.py
Jared Miller 5a0c1b2151
Fix dataclass equality causing duplicate items in move_to
Objects were comparing by value instead of identity, causing
list.remove() to remove the wrong object when moving items with
identical attributes. Set eq=False on all dataclasses to use
identity-based comparison.
2026-02-14 01:39:45 -05:00

89 lines
2.9 KiB
Python

"""Base class for everything in the world."""
from __future__ import annotations
from collections.abc import Awaitable, Callable
from dataclasses import dataclass, field
@dataclass(eq=False)
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
)
_verbs: dict[str, Callable[..., Awaitable[None]]] = field(
default_factory=dict, init=False, repr=False, compare=False
)
def __post_init__(self) -> None:
if self.location is not None:
self.location._contents.append(self)
# Auto-register verbs from @verb decorated methods
for attr_name in dir(self):
# Skip private/magic attributes
if attr_name.startswith("_"):
continue
attr = getattr(self, attr_name)
# Check if this is a verb-decorated method
if hasattr(attr, "_verb_name"):
self.register_verb(attr._verb_name, attr)
@property
def contents(self) -> list[Object]:
"""Everything whose location is this object."""
return list(self._contents)
def move_to(
self,
destination: Object | None,
*,
x: int | None = None,
y: int | None = None,
) -> None:
"""Move this object to a new location.
Removes from old location's contents, updates the location pointer,
and adds to new location's contents. Coordinates are set from the
keyword arguments (cleared to None if not provided).
"""
# Remove from old location
if self.location is not None and self in self.location._contents:
self.location._contents.remove(self)
# Update location and coordinates
self.location = destination
self.x = x
self.y = y
# Add to new location
if destination is not None:
destination._contents.append(self)
def can_accept(self, obj: Object) -> bool:
"""Whether this object accepts obj as contents. Default: no."""
return False
def register_verb(self, name: str, handler: Callable[..., Awaitable[None]]) -> None:
"""Register a verb handler on this object."""
self._verbs[name] = handler
def get_verb(self, name: str) -> Callable[..., Awaitable[None]] | None:
"""Get a verb handler by name, or None if not found."""
return self._verbs.get(name)
def has_verb(self, name: str) -> bool:
"""Check if this object has a verb registered."""
return name in self._verbs