Add zone TOML export

This commit is contained in:
Jared Miller 2026-02-11 22:18:20 -05:00
parent 3a756cc589
commit 058ba1b7de
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
4 changed files with 423 additions and 0 deletions

96
src/mudlib/export.py Normal file
View file

@ -0,0 +1,96 @@
"""Export zone data to TOML files."""
from __future__ import annotations
from pathlib import Path
from mudlib.portal import Portal
from mudlib.zone import Zone
def export_zone(zone: Zone) -> str:
"""Export a Zone to TOML string.
Args:
zone: Zone instance to export
Returns:
TOML-formatted string representation of the zone
"""
lines = []
# Basic fields
lines.append(f'name = "{zone.name}"')
if zone.description:
lines.append(f'description = "{zone.description}"')
lines.append(f"width = {zone.width}")
lines.append(f"height = {zone.height}")
lines.append(f"toroidal = {str(zone.toroidal).lower()}")
lines.append(f"spawn_x = {zone.spawn_x}")
lines.append(f"spawn_y = {zone.spawn_y}")
lines.append("")
# Terrain section
lines.append("[terrain]")
lines.append("rows = [")
for row in zone.terrain:
row_str = "".join(row)
lines.append(f' "{row_str}",')
lines.append("]")
lines.append("")
# Impassable tiles
lines.append("[terrain.impassable]")
impassable_list = sorted(zone.impassable)
tiles_str = ", ".join(f'"{tile}"' for tile in impassable_list)
lines.append(f"tiles = [{tiles_str}]")
# Ambient messages (if present)
if zone.ambient_messages:
lines.append("")
lines.append("[ambient]")
lines.append(f"interval = {zone.ambient_interval}")
lines.append("messages = [")
for msg in zone.ambient_messages:
# Escape quotes in messages
escaped_msg = msg.replace('"', '\\"')
lines.append(f' "{escaped_msg}",')
lines.append("]")
# Portals (if any)
portals = [obj for obj in zone._contents if isinstance(obj, Portal)]
if portals:
lines.append("")
for portal in portals:
lines.append("[[portals]]")
lines.append(f"x = {portal.x}")
lines.append(f"y = {portal.y}")
target = f"{portal.target_zone}:{portal.target_x},{portal.target_y}"
lines.append(f'target = "{target}"')
lines.append(f'label = "{portal.name}"')
if portal.aliases:
aliases_str = ", ".join(f'"{alias}"' for alias in portal.aliases)
lines.append(f"aliases = [{aliases_str}]")
lines.append("")
# Spawn rules (if any)
if zone.spawn_rules:
for spawn_rule in zone.spawn_rules:
lines.append("[[spawns]]")
lines.append(f'mob = "{spawn_rule.mob}"')
lines.append(f"max_count = {spawn_rule.max_count}")
lines.append(f"respawn_seconds = {spawn_rule.respawn_seconds}")
lines.append("")
return "\n".join(lines)
def export_zone_to_file(zone: Zone, path: Path) -> None:
"""Export a Zone to a TOML file.
Args:
zone: Zone instance to export
path: Path where the TOML file should be written
"""
toml_str = export_zone(zone)
path.write_text(toml_str)

View file

@ -30,6 +30,7 @@ class Zone(Object):
a zone. A tavern interior is a zone. A pocket dimension is a zone. a zone. A tavern interior is a zone. A pocket dimension is a zone.
""" """
description: str = ""
width: int = 0 width: int = 0
height: int = 0 height: int = 0
toroidal: bool = True toroidal: bool = True

View file

@ -51,6 +51,7 @@ def load_zone(path: Path) -> Zone:
# Extract basic properties # Extract basic properties
name = data["name"] name = data["name"]
description = data.get("description", "")
width = data["width"] width = data["width"]
height = data["height"] height = data["height"]
toroidal = data.get("toroidal", True) toroidal = data.get("toroidal", True)
@ -85,6 +86,7 @@ def load_zone(path: Path) -> Zone:
zone = Zone( zone = Zone(
name=name, name=name,
description=description,
width=width, width=width,
height=height, height=height,
toroidal=toroidal, toroidal=toroidal,

324
tests/test_zone_export.py Normal file
View file

@ -0,0 +1,324 @@
"""Tests for zone export to TOML files."""
import pathlib
import tempfile
from mudlib.export import export_zone, export_zone_to_file
from mudlib.portal import Portal
from mudlib.zone import SpawnRule, Zone
from mudlib.zones import load_zone
def test_export_basic_zone():
"""Export a simple zone and verify TOML output has correct fields."""
zone = Zone(
name="test_zone",
width=4,
height=3,
toroidal=False,
terrain=[
["#", "#", "#", "#"],
["#", ".", ".", "#"],
["#", "#", "#", "#"],
],
impassable={"#"},
spawn_x=0,
spawn_y=0,
)
# Set description as an attribute (zones loaded from TOML have this)
zone.description = "a test zone"
toml_str = export_zone(zone)
# Verify basic fields are present
assert 'name = "test_zone"' in toml_str
assert 'description = "a test zone"' in toml_str
assert "width = 4" in toml_str
assert "height = 3" in toml_str
assert "toroidal = false" in toml_str
assert "spawn_x = 0" in toml_str
assert "spawn_y = 0" in toml_str
# Verify terrain section
assert "[terrain]" in toml_str
assert '"####"' in toml_str
assert '"#..#"' in toml_str
# Verify impassable tiles
assert "[terrain.impassable]" in toml_str
assert '"#"' in toml_str
def test_export_zone_round_trip():
"""Export a zone, load it back, verify it matches the original."""
original = Zone(
name="round_trip",
width=5,
height=4,
toroidal=True,
terrain=[
[".", ".", ".", ".", "."],
[".", "#", "#", "#", "."],
[".", "#", ".", "#", "."],
[".", ".", ".", ".", "."],
],
impassable={"#"},
spawn_x=2,
spawn_y=1,
)
original.description = "round trip test"
toml_str = export_zone(original)
# Write to temp file and load back
with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
f.write(toml_str)
temp_path = pathlib.Path(f.name)
try:
loaded = load_zone(temp_path)
# Verify all fields match
assert loaded.name == original.name
assert loaded.description == original.description
assert loaded.width == original.width
assert loaded.height == original.height
assert loaded.toroidal == original.toroidal
assert loaded.spawn_x == original.spawn_x
assert loaded.spawn_y == original.spawn_y
assert loaded.terrain == original.terrain
assert loaded.impassable == original.impassable
finally:
temp_path.unlink()
def test_export_zone_with_portals():
"""Zone with Portal objects exports [[portals]] sections."""
zone = Zone(
name="portal_zone",
width=10,
height=10,
terrain=[["." for _ in range(10)] for _ in range(10)],
impassable=set(),
)
zone.description = "a zone with portals"
# Add portals
Portal(
name="tavern door",
location=zone,
x=5,
y=3,
target_zone="tavern",
target_x=1,
target_y=1,
)
Portal(
name="forest path",
aliases=["path", "entrance"],
location=zone,
x=2,
y=7,
target_zone="forest",
target_x=10,
target_y=5,
)
toml_str = export_zone(zone)
# Verify portals section exists
assert "[[portals]]" in toml_str
# Verify first portal
assert "x = 5" in toml_str
assert "y = 3" in toml_str
assert 'target = "tavern:1,1"' in toml_str
assert 'label = "tavern door"' in toml_str
# Verify second portal
assert "x = 2" in toml_str
assert "y = 7" in toml_str
assert 'target = "forest:10,5"' in toml_str
assert 'label = "forest path"' in toml_str
assert 'aliases = ["path", "entrance"]' in toml_str
def test_export_zone_with_portals_round_trip():
"""Export a zone with portals, load it back, verify portals match."""
zone = Zone(
name="portal_round_trip",
width=8,
height=6,
toroidal=False,
terrain=[["." for _ in range(8)] for _ in range(6)],
impassable=set(),
spawn_x=0,
spawn_y=0,
)
zone.description = "portal round trip test"
# Add portals
Portal(
name="tavern door",
location=zone,
x=5,
y=3,
target_zone="tavern",
target_x=1,
target_y=2,
)
Portal(
name="forest path",
aliases=["path"],
location=zone,
x=2,
y=4,
target_zone="forest",
target_x=10,
target_y=5,
)
toml_str = export_zone(zone)
# Write to temp file and load back
with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
f.write(toml_str)
temp_path = pathlib.Path(f.name)
try:
loaded = load_zone(temp_path)
# Verify basic zone fields
assert loaded.name == zone.name
assert loaded.description == zone.description
assert loaded.width == zone.width
assert loaded.height == zone.height
# Verify portals were loaded correctly
portals = [
obj for obj in loaded._contents if obj.__class__.__name__ == "Portal"
]
assert len(portals) == 2
# Sort by y coordinate for consistent ordering
portals.sort(key=lambda p: (p.y, p.x))
# Verify first portal (tavern door at 5,3)
assert portals[0].name == "tavern door"
assert portals[0].x == 5
assert portals[0].y == 3
assert portals[0].target_zone == "tavern"
assert portals[0].target_x == 1
assert portals[0].target_y == 2
# Verify second portal (forest path at 2,4)
assert portals[1].name == "forest path"
assert portals[1].x == 2
assert portals[1].y == 4
assert portals[1].target_zone == "forest"
assert portals[1].target_x == 10
assert portals[1].target_y == 5
assert "path" in portals[1].aliases
finally:
temp_path.unlink()
def test_export_zone_with_spawn_point():
"""spawn_x/spawn_y are in the output."""
zone = Zone(
name="spawn_zone",
width=10,
height=10,
terrain=[["." for _ in range(10)] for _ in range(10)],
spawn_x=5,
spawn_y=7,
)
zone.description = "a zone with spawn point"
toml_str = export_zone(zone)
assert "spawn_x = 5" in toml_str
assert "spawn_y = 7" in toml_str
def test_export_zone_to_file():
"""Write TOML to a file, load it back."""
zone = Zone(
name="file_zone",
width=3,
height=3,
terrain=[
["#", "#", "#"],
["#", ".", "#"],
["#", "#", "#"],
],
impassable={"#"},
)
zone.description = "exported to file"
with tempfile.TemporaryDirectory() as tmpdir:
output_path = pathlib.Path(tmpdir) / "test_zone.toml"
export_zone_to_file(zone, output_path)
# Verify file exists
assert output_path.exists()
# Load it back
loaded = load_zone(output_path)
assert loaded.name == zone.name
assert loaded.width == zone.width
assert loaded.height == zone.height
assert loaded.terrain == zone.terrain
def test_export_zone_with_ambient_messages():
"""Zone with ambient messages exports [ambient] section."""
zone = Zone(
name="ambient_zone",
width=5,
height=5,
terrain=[["." for _ in range(5)] for _ in range(5)],
ambient_messages=[
"Birds chirp overhead.",
"A cool breeze passes by.",
"Leaves rustle in the distance.",
],
ambient_interval=90,
)
zone.description = "a zone with ambient messages"
toml_str = export_zone(zone)
# Verify ambient section
assert "[ambient]" in toml_str
assert "interval = 90" in toml_str
assert "Birds chirp overhead." in toml_str
assert "A cool breeze passes by." in toml_str
assert "Leaves rustle in the distance." in toml_str
def test_export_zone_with_spawn_rules():
"""Zone with spawn rules exports [[spawns]] sections."""
zone = Zone(
name="spawn_zone",
width=10,
height=10,
terrain=[["." for _ in range(10)] for _ in range(10)],
spawn_rules=[
SpawnRule(mob="squirrel", max_count=2, respawn_seconds=180),
SpawnRule(mob="crow", max_count=1, respawn_seconds=300),
],
)
toml_str = export_zone(zone)
# Verify spawns sections
assert "[[spawns]]" in toml_str
assert 'mob = "squirrel"' in toml_str
assert "max_count = 2" in toml_str
assert "respawn_seconds = 180" in toml_str
assert 'mob = "crow"' in toml_str
assert "max_count = 1" in toml_str
assert "respawn_seconds = 300" in toml_str