From ba5eb4c95f4fce0f1506b77fc2dcd7d7f4b09b41 Mon Sep 17 00:00:00 2001 From: ctan Date: Sat, 19 Sep 2026 17:12:42 +0200 Subject: [PATCH] fix(map): load_map rejects two deployment sides bound to the same zone letter (build_states resets its placed set per side, so shared letters deployed camps on top of each other) - fail-fast MapValidationError + README rule, reject/accept tests --- README.md | 3 +++ src/pf1e_simulator/map.py | 8 ++++++++ tests/test_map.py | 17 +++++++++++++++++ 3 files changed, 28 insertions(+) diff --git a/README.md b/README.md index 762f4a7..53ac0de 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,9 @@ Règles de construction d'une rencontre : - Les combattants d'un camp remplissent les cases de leur zone dans l'ordre (ligne, colonne). Une zone trop petite pour le nombre de combattants est une erreur. +- Chaque camp du déploiement doit être lié à une lettre de zone **distincte** : + deux camps partageant la même lettre se déploieraient l'un sur l'autre, ce + qui est une erreur de carte (`share zone letters`). ## Interpréter le rapport diff --git a/src/pf1e_simulator/map.py b/src/pf1e_simulator/map.py index 1adc4f4..95f6fe0 100644 --- a/src/pf1e_simulator/map.py +++ b/src/pf1e_simulator/map.py @@ -140,6 +140,14 @@ def _check_deployment( if missing: msg = f"deployment zones absent from zones layer: {missing}" raise MapValidationError(path, msg) + letters = list(deployment.values()) + shared = sorted({letter for letter in letters if letters.count(letter) > 1}) + if shared: + msg = ( + "deployment sides share zone letters (each side needs its own " + f"distinct zone, or they deploy on top of each other): {shared}" + ) + raise MapValidationError(path, msg) def load_map(path: Path) -> MapSpec: diff --git a/tests/test_map.py b/tests/test_map.py index d1f50f2..d89793d 100644 --- a/tests/test_map.py +++ b/tests/test_map.py @@ -109,3 +109,20 @@ 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_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"}