130087a556
The README documents deployment as required when zones exist, but load_map only enforced the reverse direction (deployment without zones). A map with a zones layer and no deployment now fails with MapValidationError instead of loading with unreachable deployment zones.
135 lines
4.6 KiB
Python
135 lines
4.6 KiB
Python
"""Tests for the map YAML schema (map.py)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from pf1e_simulator.map import MapValidationError, load_map, zone_cells
|
|
|
|
MAPS_DIR = Path(__file__).resolve().parents[1] / "data" / "maps"
|
|
|
|
|
|
def _write(tmp_path: Path, body: str) -> Path:
|
|
target = tmp_path / "map.yaml"
|
|
target.write_text(body, encoding="utf-8")
|
|
return target
|
|
|
|
|
|
def _minimal_map() -> str:
|
|
return """
|
|
name: "Test"
|
|
terrain: |
|
|
###
|
|
#.#
|
|
###
|
|
legend:
|
|
"#": { type: wall, blocks_los: true }
|
|
".": { type: floor, move_cost: 1 }
|
|
"""
|
|
|
|
|
|
def test_sample_arena_loads() -> None:
|
|
spec = load_map(MAPS_DIR / "sample_arena.yaml")
|
|
assert spec.square_size_ft == 5
|
|
assert len(spec.terrain) == 8
|
|
assert all(len(row) == 20 for row in spec.terrain)
|
|
assert spec.legend["#"].move_cost is None
|
|
assert spec.legend["#"].blocks_los is True
|
|
assert spec.legend["C"].cover is True
|
|
assert spec.legend["T"].move_cost == 2
|
|
assert spec.deployment == {"players": "A", "monsters": "B"}
|
|
assert spec.markers["autel"] == (1, 16)
|
|
|
|
|
|
def test_sample_arena_zones_and_elevation() -> None:
|
|
spec = load_map(MAPS_DIR / "sample_arena.yaml")
|
|
assert spec.elevation is not None
|
|
# Flattened to '.' — platform removed so elevation has no mechanical effect
|
|
assert spec.elevation[1][2] == "."
|
|
assert all(
|
|
ch == "." for row in spec.elevation for ch in row
|
|
)
|
|
cells_a = zone_cells(spec, "A")
|
|
assert cells_a == [
|
|
(1, 1), (1, 2), (1, 3), (1, 4),
|
|
(2, 1), (2, 2), (2, 3), (2, 4),
|
|
]
|
|
assert len(zone_cells(spec, "B")) == 8
|
|
|
|
|
|
def test_minimal_map_without_optional_layers(tmp_path: Path) -> None:
|
|
spec = load_map(_write(tmp_path, _minimal_map()))
|
|
assert spec.elevation is None
|
|
assert spec.zones is None
|
|
assert spec.markers == {}
|
|
assert zone_cells(spec, "A") == []
|
|
|
|
|
|
def test_rejects_ragged_rows(tmp_path: Path) -> None:
|
|
body = _minimal_map().replace(" #.#\n", " #.##\n")
|
|
with pytest.raises(MapValidationError):
|
|
load_map(_write(tmp_path, body))
|
|
|
|
|
|
def test_rejects_non_five_foot_square_size(tmp_path: Path) -> None:
|
|
"""SRD Measuring Distance: one grid square is 5 ft; the engine geometry
|
|
is hard-coded to that scale, so any other declared size is a load error
|
|
(fail fast) rather than a silent mis-scaling."""
|
|
body = _minimal_map().replace('name: "Test"\n', 'name: "Test"\nsquare_size_ft: 10\n')
|
|
with pytest.raises(MapValidationError, match="square_size_ft must be 5"):
|
|
load_map(_write(tmp_path, body))
|
|
|
|
|
|
def test_accepts_explicit_five_foot_square_size(tmp_path: Path) -> None:
|
|
body = _minimal_map().replace('name: "Test"\n', 'name: "Test"\nsquare_size_ft: 5\n')
|
|
assert load_map(_write(tmp_path, body)).square_size_ft == 5
|
|
|
|
|
|
def test_rejects_char_missing_from_legend(tmp_path: Path) -> None:
|
|
body = _minimal_map().replace(" #.#\n", " #x#\n")
|
|
with pytest.raises(MapValidationError):
|
|
load_map(_write(tmp_path, body))
|
|
|
|
|
|
def test_rejects_elevation_dimension_mismatch(tmp_path: Path) -> None:
|
|
body = _minimal_map() + "elevation: |\n 000\n 00\n"
|
|
with pytest.raises(MapValidationError):
|
|
load_map(_write(tmp_path, body))
|
|
|
|
|
|
def test_rejects_marker_out_of_bounds(tmp_path: Path) -> None:
|
|
body = _minimal_map() + "markers:\n autel: [9, 1]\n"
|
|
with pytest.raises(MapValidationError):
|
|
load_map(_write(tmp_path, body))
|
|
|
|
|
|
def test_rejects_deployment_without_zone(tmp_path: Path) -> None:
|
|
body = _minimal_map() + "zones: |\n ...\n .A.\n ...\ndeployment:\n players: B\n"
|
|
with pytest.raises(MapValidationError):
|
|
load_map(_write(tmp_path, body))
|
|
|
|
|
|
def test_rejects_zones_without_deployment(tmp_path: Path) -> None:
|
|
body = _minimal_map() + "zones: |\n ...\n .A.\n ...\n"
|
|
with pytest.raises(MapValidationError, match="no deployment section"):
|
|
load_map(_write(tmp_path, body))
|
|
|
|
|
|
def test_rejects_shared_deployment_zone_letter(tmp_path: Path) -> None:
|
|
"""Two sides bound to the same zone letter would deploy on top of each
|
|
other (build_states resets its placed set per side): a data error."""
|
|
body = _minimal_map() + (
|
|
"zones: |\n ...\n .A.\n ...\ndeployment:\n players: A\n monsters: A\n"
|
|
)
|
|
with pytest.raises(MapValidationError, match="share zone letters"):
|
|
load_map(_write(tmp_path, body))
|
|
|
|
|
|
def test_accepts_distinct_deployment_zone_letters(tmp_path: Path) -> None:
|
|
body = _minimal_map() + (
|
|
"zones: |\n ...\n AB.\n ...\ndeployment:\n players: A\n monsters: B\n"
|
|
)
|
|
assert load_map(_write(tmp_path, body)).deployment == {"players": "A", "monsters": "B"}
|