Add Thing class and Entity.can_accept() for inventory
Thing is an Object subclass with description, portable flag, and aliases. Entity.can_accept() returns True for portable Things, enabling the containment model where entities carry items in their contents.
This commit is contained in:
parent
957a411601
commit
9437728435
3 changed files with 150 additions and 0 deletions
|
|
@ -1,9 +1,15 @@
|
|||
"""Base entity class for characters in the world."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from mudlib.object import Object
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mudlib.thing import Thing
|
||||
|
||||
|
||||
@dataclass
|
||||
class Entity(Object):
|
||||
|
|
@ -23,6 +29,12 @@ class Entity(Object):
|
|||
defense_locked_until: float = 0.0 # monotonic time when defense recovery ends
|
||||
resting: bool = False # whether this entity is currently resting
|
||||
|
||||
def can_accept(self, obj: Object) -> bool:
|
||||
"""Entities accept portable Things (inventory)."""
|
||||
from mudlib.thing import Thing
|
||||
|
||||
return isinstance(obj, Thing) and obj.portable
|
||||
|
||||
async def send(self, message: str) -> None:
|
||||
"""Send a message to this entity. Base implementation is a no-op."""
|
||||
pass
|
||||
|
|
|
|||
21
src/mudlib/thing.py
Normal file
21
src/mudlib/thing.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
"""Thing — an item that can exist in zones or inventories."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from mudlib.object import Object
|
||||
|
||||
|
||||
@dataclass
|
||||
class Thing(Object):
|
||||
"""An item in the world.
|
||||
|
||||
Things can be on the ground (location=zone, with x/y) or carried
|
||||
by an entity (location=entity, no x/y). The portable flag controls
|
||||
whether entities can pick them up.
|
||||
"""
|
||||
|
||||
description: str = ""
|
||||
portable: bool = True
|
||||
aliases: list[str] = field(default_factory=list)
|
||||
117
tests/test_thing.py
Normal file
117
tests/test_thing.py
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
"""Tests for the Thing class."""
|
||||
|
||||
from mudlib.entity import Entity
|
||||
from mudlib.object import Object
|
||||
from mudlib.thing import Thing
|
||||
from mudlib.zone import Zone
|
||||
|
||||
|
||||
# --- construction ---
|
||||
|
||||
|
||||
def test_thing_creation_minimal():
|
||||
"""Thing can be created with just a name."""
|
||||
t = Thing(name="rock")
|
||||
assert t.name == "rock"
|
||||
assert t.location is None
|
||||
assert t.description == ""
|
||||
assert t.portable is True # default: things are portable
|
||||
|
||||
|
||||
def test_thing_creation_with_description():
|
||||
"""Thing can have a description."""
|
||||
t = Thing(name="sword", description="a rusty iron sword")
|
||||
assert t.description == "a rusty iron sword"
|
||||
|
||||
|
||||
def test_thing_creation_non_portable():
|
||||
"""Thing can be marked as non-portable (fixture, scenery)."""
|
||||
t = Thing(name="fountain", portable=False)
|
||||
assert t.portable is False
|
||||
|
||||
|
||||
def test_thing_in_zone():
|
||||
"""Thing 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)
|
||||
rock = Thing(name="rock", location=zone, x=3, y=7)
|
||||
assert rock.location is zone
|
||||
assert rock.x == 3
|
||||
assert rock.y == 7
|
||||
assert rock in zone.contents
|
||||
|
||||
|
||||
def test_thing_in_entity_inventory():
|
||||
"""Thing can be placed in an entity (inventory)."""
|
||||
terrain = [["." for _ in range(10)] for _ in range(10)]
|
||||
zone = Zone(name="test", width=10, height=10, terrain=terrain)
|
||||
entity = Entity(name="player", location=zone, x=5, y=5)
|
||||
sword = Thing(name="sword", location=entity)
|
||||
assert sword.location is entity
|
||||
assert sword in entity.contents
|
||||
# Inventory items don't need coordinates
|
||||
assert sword.x is None
|
||||
assert sword.y is None
|
||||
|
||||
|
||||
def test_thing_is_subclass_of_object():
|
||||
"""Thing inherits from Object."""
|
||||
t = Thing(name="gem")
|
||||
assert isinstance(t, Object)
|
||||
|
||||
|
||||
def test_thing_aliases_default_empty():
|
||||
"""Thing aliases default to empty list."""
|
||||
t = Thing(name="rock")
|
||||
assert t.aliases == []
|
||||
|
||||
|
||||
def test_thing_aliases():
|
||||
"""Thing can have aliases for matching."""
|
||||
t = Thing(name="pepsi can", aliases=["can", "pepsi"])
|
||||
assert t.aliases == ["can", "pepsi"]
|
||||
|
||||
|
||||
# --- entity.can_accept ---
|
||||
|
||||
|
||||
def test_entity_can_accept_portable_thing():
|
||||
"""Entity accepts portable things (inventory)."""
|
||||
entity = Entity(name="player")
|
||||
sword = Thing(name="sword", portable=True)
|
||||
assert entity.can_accept(sword) is True
|
||||
|
||||
|
||||
def test_entity_rejects_non_portable_thing():
|
||||
"""Entity rejects non-portable things."""
|
||||
entity = Entity(name="player")
|
||||
fountain = Thing(name="fountain", portable=False)
|
||||
assert entity.can_accept(fountain) is False
|
||||
|
||||
|
||||
def test_entity_rejects_non_thing():
|
||||
"""Entity rejects objects that aren't Things."""
|
||||
entity = Entity(name="player")
|
||||
other = Object(name="abstract")
|
||||
assert entity.can_accept(other) is False
|
||||
|
||||
|
||||
# --- zone interaction ---
|
||||
|
||||
|
||||
def test_zone_contents_at_finds_things():
|
||||
"""Zone.contents_at finds things at a position."""
|
||||
terrain = [["." for _ in range(10)] for _ in range(10)]
|
||||
zone = Zone(name="test", width=10, height=10, terrain=terrain)
|
||||
rock = Thing(name="rock", location=zone, x=5, y=5)
|
||||
result = zone.contents_at(5, 5)
|
||||
assert rock in result
|
||||
|
||||
|
||||
def test_zone_contents_near_finds_things():
|
||||
"""Zone.contents_near finds things within range."""
|
||||
terrain = [["." for _ in range(10)] for _ in range(10)]
|
||||
zone = Zone(name="test", width=10, height=10, terrain=terrain)
|
||||
rock = Thing(name="rock", location=zone, x=5, y=5)
|
||||
result = zone.contents_near(5, 5, 3)
|
||||
assert rock in result
|
||||
Loading…
Reference in a new issue