Add zone TOML loader and tavern interior zone
Implements load_zone() and load_zones() functions to parse zone definitions from TOML files. Wires zone loading into server startup to register all zones from content/zones/ directory. Updates player zone lookup to use the registry instead of hardcoded overworld check. Includes tavern.toml as first hand-built interior zone (8x6 bounded).
This commit is contained in:
parent
5b9a43617f
commit
d18f21a031
4 changed files with 252 additions and 5 deletions
19
content/zones/tavern.toml
Normal file
19
content/zones/tavern.toml
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
name = "tavern"
|
||||||
|
description = "a cozy tavern with a crackling fireplace"
|
||||||
|
width = 8
|
||||||
|
height = 6
|
||||||
|
toroidal = false
|
||||||
|
|
||||||
|
[terrain]
|
||||||
|
# rows as strings, one per line
|
||||||
|
rows = [
|
||||||
|
"########",
|
||||||
|
"#......#",
|
||||||
|
"#......#",
|
||||||
|
"#......#",
|
||||||
|
"#......#",
|
||||||
|
"####.###",
|
||||||
|
]
|
||||||
|
|
||||||
|
[terrain.impassable]
|
||||||
|
tiles = ["#"]
|
||||||
|
|
@ -49,6 +49,7 @@ from mudlib.thing import Thing
|
||||||
from mudlib.things import load_thing_templates, spawn_thing, thing_templates
|
from mudlib.things import load_thing_templates, spawn_thing, thing_templates
|
||||||
from mudlib.world.terrain import World
|
from mudlib.world.terrain import World
|
||||||
from mudlib.zone import Zone
|
from mudlib.zone import Zone
|
||||||
|
from mudlib.zones import get_zone, load_zones, register_zone
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -270,12 +271,10 @@ async def shell(
|
||||||
"inventory": [],
|
"inventory": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Resolve zone from zone_name (currently only overworld exists)
|
# Resolve zone from zone_name using zone registry
|
||||||
zone_name = player_data.get("zone_name", "overworld")
|
zone_name = player_data.get("zone_name", "overworld")
|
||||||
if zone_name == "overworld":
|
player_zone = get_zone(zone_name)
|
||||||
player_zone = _overworld
|
if player_zone is None:
|
||||||
else:
|
|
||||||
# Future: lookup zone by name from a zone registry
|
|
||||||
log.warning(
|
log.warning(
|
||||||
"unknown zone '%s' for player '%s', defaulting to overworld",
|
"unknown zone '%s' for player '%s', defaulting to overworld",
|
||||||
zone_name,
|
zone_name,
|
||||||
|
|
@ -460,6 +459,17 @@ async def run_server() -> None:
|
||||||
)
|
)
|
||||||
log.info("created overworld zone (%dx%d, toroidal)", world.width, world.height)
|
log.info("created overworld zone (%dx%d, toroidal)", world.width, world.height)
|
||||||
|
|
||||||
|
# Register overworld zone
|
||||||
|
register_zone("overworld", _overworld)
|
||||||
|
|
||||||
|
# Load and register zones from content/zones/
|
||||||
|
zones_dir = pathlib.Path(__file__).resolve().parents[2] / "content" / "zones"
|
||||||
|
if zones_dir.exists():
|
||||||
|
loaded_zones = load_zones(zones_dir)
|
||||||
|
for zone_name, zone in loaded_zones.items():
|
||||||
|
register_zone(zone_name, zone)
|
||||||
|
log.info("loaded %d zones from %s", len(loaded_zones), zones_dir)
|
||||||
|
|
||||||
# Load content-defined commands from TOML files
|
# Load content-defined commands from TOML files
|
||||||
content_dir = pathlib.Path(__file__).resolve().parents[2] / "content" / "commands"
|
content_dir = pathlib.Path(__file__).resolve().parents[2] / "content" / "commands"
|
||||||
if content_dir.exists():
|
if content_dir.exists():
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,14 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import tomllib
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from mudlib.zone import Zone
|
from mudlib.zone import Zone
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Module-level zone registry
|
# Module-level zone registry
|
||||||
zone_registry: dict[str, Zone] = {}
|
zone_registry: dict[str, Zone] = {}
|
||||||
|
|
||||||
|
|
@ -28,3 +34,69 @@ def get_zone(name: str) -> Zone | None:
|
||||||
Zone instance if found, None otherwise
|
Zone instance if found, None otherwise
|
||||||
"""
|
"""
|
||||||
return zone_registry.get(name)
|
return zone_registry.get(name)
|
||||||
|
|
||||||
|
|
||||||
|
def load_zone(path: Path) -> Zone:
|
||||||
|
"""Load a zone from a TOML file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: Path to TOML file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Zone instance loaded from file
|
||||||
|
"""
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
data = tomllib.load(f)
|
||||||
|
|
||||||
|
# Extract basic properties
|
||||||
|
name = data["name"]
|
||||||
|
width = data["width"]
|
||||||
|
height = data["height"]
|
||||||
|
toroidal = data.get("toroidal", True)
|
||||||
|
|
||||||
|
# Parse terrain rows into 2D list
|
||||||
|
terrain_rows = data.get("terrain", {}).get("rows", [])
|
||||||
|
terrain = []
|
||||||
|
for row in terrain_rows:
|
||||||
|
terrain.append(list(row))
|
||||||
|
|
||||||
|
# Parse impassable tiles
|
||||||
|
impassable_list = data.get("terrain", {}).get("impassable", {}).get("tiles", [])
|
||||||
|
impassable = set(impassable_list) if impassable_list else {"^", "~"}
|
||||||
|
|
||||||
|
zone = Zone(
|
||||||
|
name=name,
|
||||||
|
width=width,
|
||||||
|
height=height,
|
||||||
|
toroidal=toroidal,
|
||||||
|
terrain=terrain,
|
||||||
|
impassable=impassable,
|
||||||
|
)
|
||||||
|
|
||||||
|
return zone
|
||||||
|
|
||||||
|
|
||||||
|
def load_zones(directory: Path) -> dict[str, Zone]:
|
||||||
|
"""Load all zones from a directory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
directory: Path to directory containing zone TOML files
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict mapping zone names to Zone instances
|
||||||
|
"""
|
||||||
|
zones = {}
|
||||||
|
|
||||||
|
if not directory.exists():
|
||||||
|
log.warning("zones directory does not exist: %s", directory)
|
||||||
|
return zones
|
||||||
|
|
||||||
|
for toml_file in directory.glob("*.toml"):
|
||||||
|
try:
|
||||||
|
zone = load_zone(toml_file)
|
||||||
|
zones[zone.name] = zone
|
||||||
|
log.debug("loaded zone '%s' from %s", zone.name, toml_file)
|
||||||
|
except Exception as e:
|
||||||
|
log.error("failed to load zone from %s: %s", toml_file, e, exc_info=True)
|
||||||
|
|
||||||
|
return zones
|
||||||
|
|
|
||||||
146
tests/test_zone_loading.py
Normal file
146
tests/test_zone_loading.py
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
"""Tests for zone loading from TOML files."""
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from mudlib.zones import load_zone, load_zones
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_zone():
|
||||||
|
"""Load a zone from TOML file."""
|
||||||
|
# Create a temporary TOML file
|
||||||
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
|
||||||
|
f.write("""
|
||||||
|
name = "test_zone"
|
||||||
|
description = "a test zone"
|
||||||
|
width = 4
|
||||||
|
height = 3
|
||||||
|
toroidal = false
|
||||||
|
|
||||||
|
[terrain]
|
||||||
|
rows = [
|
||||||
|
"####",
|
||||||
|
"#..#",
|
||||||
|
"####",
|
||||||
|
]
|
||||||
|
|
||||||
|
[terrain.impassable]
|
||||||
|
tiles = ["#"]
|
||||||
|
""")
|
||||||
|
temp_path = pathlib.Path(f.name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
zone = load_zone(temp_path)
|
||||||
|
|
||||||
|
assert zone.name == "test_zone"
|
||||||
|
assert zone.width == 4
|
||||||
|
assert zone.height == 3
|
||||||
|
assert zone.toroidal is False
|
||||||
|
assert len(zone.terrain) == 3
|
||||||
|
assert zone.terrain[0] == ["#", "#", "#", "#"]
|
||||||
|
assert zone.terrain[1] == ["#", ".", ".", "#"]
|
||||||
|
assert zone.terrain[2] == ["#", "#", "#", "#"]
|
||||||
|
assert zone.impassable == {"#"}
|
||||||
|
finally:
|
||||||
|
temp_path.unlink()
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_zone_toroidal():
|
||||||
|
"""Load a toroidal zone."""
|
||||||
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
|
||||||
|
f.write("""
|
||||||
|
name = "toroidal_zone"
|
||||||
|
description = "a toroidal test zone"
|
||||||
|
width = 3
|
||||||
|
height = 2
|
||||||
|
toroidal = true
|
||||||
|
|
||||||
|
[terrain]
|
||||||
|
rows = [
|
||||||
|
"...",
|
||||||
|
"...",
|
||||||
|
]
|
||||||
|
""")
|
||||||
|
temp_path = pathlib.Path(f.name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
zone = load_zone(temp_path)
|
||||||
|
|
||||||
|
assert zone.toroidal is True
|
||||||
|
# Default impassable set from Zone class
|
||||||
|
assert zone.impassable == {"^", "~"}
|
||||||
|
finally:
|
||||||
|
temp_path.unlink()
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_zones_from_directory():
|
||||||
|
"""Load all zones from a directory."""
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
tmpdir_path = pathlib.Path(tmpdir)
|
||||||
|
|
||||||
|
# Create two zone files
|
||||||
|
zone1_path = tmpdir_path / "zone1.toml"
|
||||||
|
zone1_path.write_text("""
|
||||||
|
name = "zone1"
|
||||||
|
description = "first zone"
|
||||||
|
width = 2
|
||||||
|
height = 2
|
||||||
|
toroidal = false
|
||||||
|
|
||||||
|
[terrain]
|
||||||
|
rows = [
|
||||||
|
"..",
|
||||||
|
"..",
|
||||||
|
]
|
||||||
|
""")
|
||||||
|
|
||||||
|
zone2_path = tmpdir_path / "zone2.toml"
|
||||||
|
zone2_path.write_text("""
|
||||||
|
name = "zone2"
|
||||||
|
description = "second zone"
|
||||||
|
width = 3
|
||||||
|
height = 3
|
||||||
|
toroidal = true
|
||||||
|
|
||||||
|
[terrain]
|
||||||
|
rows = [
|
||||||
|
"###",
|
||||||
|
"#.#",
|
||||||
|
"###",
|
||||||
|
]
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Create a non-TOML file that should be ignored
|
||||||
|
(tmpdir_path / "readme.txt").write_text("not a zone file")
|
||||||
|
|
||||||
|
zones = load_zones(tmpdir_path)
|
||||||
|
|
||||||
|
assert len(zones) == 2
|
||||||
|
assert "zone1" in zones
|
||||||
|
assert "zone2" in zones
|
||||||
|
assert zones["zone1"].name == "zone1"
|
||||||
|
assert zones["zone2"].name == "zone2"
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_tavern_zone():
|
||||||
|
"""Load the actual tavern zone file."""
|
||||||
|
# This tests the real tavern.toml file in content/zones/
|
||||||
|
project_root = pathlib.Path(__file__).resolve().parents[1]
|
||||||
|
tavern_path = project_root / "content" / "zones" / "tavern.toml"
|
||||||
|
|
||||||
|
zone = load_zone(tavern_path)
|
||||||
|
|
||||||
|
assert zone.name == "tavern"
|
||||||
|
assert zone.width == 8
|
||||||
|
assert zone.height == 6
|
||||||
|
assert zone.toroidal is False
|
||||||
|
assert len(zone.terrain) == 6
|
||||||
|
assert zone.terrain[0] == ["#", "#", "#", "#", "#", "#", "#", "#"]
|
||||||
|
assert zone.terrain[5] == ["#", "#", "#", "#", ".", "#", "#", "#"]
|
||||||
|
assert zone.impassable == {"#"}
|
||||||
|
# Check that interior is passable
|
||||||
|
assert zone.is_passable(1, 1)
|
||||||
|
assert zone.is_passable(4, 3)
|
||||||
|
# Check that walls are impassable
|
||||||
|
assert not zone.is_passable(0, 0)
|
||||||
|
assert not zone.is_passable(7, 0)
|
||||||
Loading…
Reference in a new issue