155 lines
5.4 KiB
Python
155 lines
5.4 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_multi_character_legend_key(tmp_path: Path) -> None:
|
|
"""One char = one square: a legend key like "ZZ" can never match a cell
|
|
and is dead data -- reject it at load."""
|
|
body = _minimal_map().replace(
|
|
' ".": { type: floor, move_cost: 1 }\n',
|
|
' ".": { type: floor, move_cost: 1 }\n "ZZ": { type: floor, move_cost: 1 }\n',
|
|
)
|
|
with pytest.raises(MapValidationError, match="legend keys must be exactly one character"):
|
|
load_map(_write(tmp_path, body))
|
|
|
|
|
|
def test_rejects_empty_legend_key(tmp_path: Path) -> None:
|
|
body = _minimal_map().replace(
|
|
' ".": { type: floor, move_cost: 1 }\n',
|
|
' "": { type: floor, move_cost: 1 }\n',
|
|
)
|
|
with pytest.raises(MapValidationError, match="legend keys must be exactly one character"):
|
|
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"}
|