"""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"}