From c43b3346ae729f83fd2eb68ba9fed769dad21e7a Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Wed, 11 Feb 2026 20:01:15 -0500 Subject: [PATCH] Add Thing templates, TOML loading, and spawning ThingTemplate dataclass mirrors MobTemplate pattern. load_thing_template and load_thing_templates parse TOML files from content/things/. spawn_thing creates Thing instances from templates. Includes rock and fountain examples. --- content/things/fountain.toml | 3 + content/things/rock.toml | 4 + src/mudlib/entity.py | 4 - src/mudlib/things.py | 62 ++++++++++++++ tests/test_thing_templates.py | 152 ++++++++++++++++++++++++++++++++++ 5 files changed, 221 insertions(+), 4 deletions(-) create mode 100644 content/things/fountain.toml create mode 100644 content/things/rock.toml create mode 100644 src/mudlib/things.py create mode 100644 tests/test_thing_templates.py diff --git a/content/things/fountain.toml b/content/things/fountain.toml new file mode 100644 index 0000000..97e3555 --- /dev/null +++ b/content/things/fountain.toml @@ -0,0 +1,3 @@ +name = "fountain" +description = "a weathered stone fountain, water trickling into a mossy basin" +portable = false diff --git a/content/things/rock.toml b/content/things/rock.toml new file mode 100644 index 0000000..d3976b1 --- /dev/null +++ b/content/things/rock.toml @@ -0,0 +1,4 @@ +name = "rock" +description = "a smooth grey rock, worn by wind and rain" +portable = true +aliases = ["stone"] diff --git a/src/mudlib/entity.py b/src/mudlib/entity.py index 2238159..2b166fd 100644 --- a/src/mudlib/entity.py +++ b/src/mudlib/entity.py @@ -3,13 +3,9 @@ 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): diff --git a/src/mudlib/things.py b/src/mudlib/things.py new file mode 100644 index 0000000..915181a --- /dev/null +++ b/src/mudlib/things.py @@ -0,0 +1,62 @@ +"""Thing template loading, registry, and spawning.""" + +import tomllib +from dataclasses import dataclass, field +from pathlib import Path + +from mudlib.object import Object +from mudlib.thing import Thing + + +@dataclass +class ThingTemplate: + """Definition loaded from TOML — used to spawn Thing instances.""" + + name: str + description: str + portable: bool = True + aliases: list[str] = field(default_factory=list) + + +# Module-level registry +thing_templates: dict[str, ThingTemplate] = {} + + +def load_thing_template(path: Path) -> ThingTemplate: + """Parse a thing TOML file into a ThingTemplate.""" + with open(path, "rb") as f: + data = tomllib.load(f) + return ThingTemplate( + name=data["name"], + description=data["description"], + portable=data.get("portable", True), + aliases=data.get("aliases", []), + ) + + +def load_thing_templates(directory: Path) -> dict[str, ThingTemplate]: + """Load all .toml files in a directory into a dict keyed by name.""" + templates: dict[str, ThingTemplate] = {} + for path in sorted(directory.glob("*.toml")): + template = load_thing_template(path) + templates[template.name] = template + return templates + + +def spawn_thing( + template: ThingTemplate, + location: Object | None, + *, + x: int | None = None, + y: int | None = None, +) -> Thing: + """Create a Thing instance from a template at the given location.""" + return Thing( + name=template.name, + description=template.description, + portable=template.portable, + aliases=list(template.aliases), + location=location, + x=x, + y=y, + ) diff --git a/tests/test_thing_templates.py b/tests/test_thing_templates.py new file mode 100644 index 0000000..bac5fc9 --- /dev/null +++ b/tests/test_thing_templates.py @@ -0,0 +1,152 @@ +"""Tests for thing template loading and spawning.""" + +import textwrap + +import pytest + +from mudlib.thing import Thing +from mudlib.things import ( + ThingTemplate, + load_thing_template, + load_thing_templates, + spawn_thing, +) +from mudlib.zone import Zone + + +@pytest.fixture +def test_zone(): + terrain = [["." for _ in range(10)] for _ in range(10)] + return Zone( + name="testzone", + width=10, + height=10, + terrain=terrain, + ) + + +# --- ThingTemplate --- + + +def test_thing_template_creation(): + """ThingTemplate holds definition data.""" + t = ThingTemplate( + name="rock", + description="a smooth grey rock", + portable=True, + aliases=["stone"], + ) + assert t.name == "rock" + assert t.description == "a smooth grey rock" + assert t.portable is True + assert t.aliases == ["stone"] + + +def test_thing_template_defaults(): + """ThingTemplate has sensible defaults.""" + t = ThingTemplate(name="rock", description="a rock") + assert t.portable is True + assert t.aliases == [] + + +# --- load_thing_template --- + + +def test_load_thing_template(tmp_path): + """Load a thing template from a TOML file.""" + toml_content = textwrap.dedent("""\ + name = "rusty sword" + description = "a sword covered in rust" + portable = true + aliases = ["sword", "rusty"] + """) + p = tmp_path / "sword.toml" + p.write_text(toml_content) + + template = load_thing_template(p) + assert template.name == "rusty sword" + assert template.description == "a sword covered in rust" + assert template.portable is True + assert template.aliases == ["sword", "rusty"] + + +def test_load_thing_template_minimal(tmp_path): + """Load thing template with only required fields.""" + toml_content = textwrap.dedent("""\ + name = "rock" + description = "a rock" + """) + p = tmp_path / "rock.toml" + p.write_text(toml_content) + + template = load_thing_template(p) + assert template.name == "rock" + assert template.portable is True # default + assert template.aliases == [] # default + + +def test_load_thing_template_non_portable(tmp_path): + """Load a non-portable thing template.""" + toml_content = textwrap.dedent("""\ + name = "fountain" + description = "a stone fountain" + portable = false + """) + p = tmp_path / "fountain.toml" + p.write_text(toml_content) + + template = load_thing_template(p) + assert template.portable is False + + +# --- load_thing_templates --- + + +def test_load_thing_templates(tmp_path): + """Load all thing templates from a directory.""" + for name in ["rock", "sword"]: + p = tmp_path / f"{name}.toml" + p.write_text(f'name = "{name}"\ndescription = "a {name}"\n') + + templates = load_thing_templates(tmp_path) + assert "rock" in templates + assert "sword" in templates + assert len(templates) == 2 + + +def test_load_thing_templates_empty_dir(tmp_path): + """Empty directory returns empty dict.""" + templates = load_thing_templates(tmp_path) + assert templates == {} + + +# --- spawn_thing --- + + +def test_spawn_thing_in_zone(test_zone): + """spawn_thing creates a Thing instance in a zone.""" + template = ThingTemplate( + name="rock", + description="a smooth grey rock", + portable=True, + aliases=["stone"], + ) + thing = spawn_thing(template, test_zone, x=3, y=7) + assert isinstance(thing, Thing) + assert thing.name == "rock" + assert thing.description == "a smooth grey rock" + assert thing.portable is True + assert thing.aliases == ["stone"] + assert thing.location is test_zone + assert thing.x == 3 + assert thing.y == 7 + assert thing in test_zone.contents + + +def test_spawn_thing_without_coordinates(test_zone): + """spawn_thing can create a thing without position (template/inventory).""" + template = ThingTemplate(name="gem", description="a sparkling gem") + thing = spawn_thing(template, None) + assert thing.location is None + assert thing.x is None + assert thing.y is None