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