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.
89 lines
2.9 KiB
Python
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
|