Add zone TOML export
This commit is contained in:
parent
3a756cc589
commit
058ba1b7de
4 changed files with 423 additions and 0 deletions
96
src/mudlib/export.py
Normal file
96
src/mudlib/export.py
Normal 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)
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
324
tests/test_zone_export.py
Normal 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
|
||||||
Loading…
Reference in a new issue