From ff4091ca37c571d98c5223e3932f5203ce5d0804 Mon Sep 17 00:00:00 2001 From: Eva Date: Thu, 16 Jul 2026 16:51:20 +0700 Subject: [PATCH] generator: non-collinear fire-beacon fields (dress_focal v2 + beacon-geometry bar, #1618) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dwing recovery proved the focal braziers double as plate-registration beacons. Two braziers on the same row are a collinear pair, blind to vertical scale in the 2-point similarity fit (room_1: 0.07-cell beacon error beside ~1.5 cells of wall misfit — plate unfixable by warping). dress_focal v2: after the door-count plan lands its two lane/shrine braziers, author a THIRD fire beacon (corner watch-brazier) at the connectivity-safe cell that maximises the three-beacon triangle area — deterministic, reusing the existing candidate/landing-exclusion/flood-fill machinery. check_dressing_bars gains a third bar: >=3 fire-kind props with best triangle area >= 2.0 cell^2, or fail loud (beacon-geometry class). --allow-undressed still skips all bars. Tests: every committed wing room now carries 3 fire beacons (areas 12/5/11); the bar unit-tested on 2-fire, 3-collinear, 3-non-collinear geos; existing dressing-bar test updated for the third bar. Refs #1618 (lands after #1611). --- qa/test_generate_town.py | 66 ++++++++++++++++++++++--- tools/dungen_to_fixtures.py | 98 ++++++++++++++++++++++++++++++++++--- 2 files changed, 150 insertions(+), 14 deletions(-) diff --git a/qa/test_generate_town.py b/qa/test_generate_town.py index 74f8caa9..406acd50 100644 --- a/qa/test_generate_town.py +++ b/qa/test_generate_town.py @@ -217,18 +217,68 @@ def test_plates_fragment_pins_carry_the_full_camera_contract(tmp_path): def test_dressing_bars_fail_loud_when_a_pass_places_nothing(): - """codex P2 pair (#1611): a narrow/dense crop can defeat dress_focal (returns silently) or leave - dress_tall_anchors no connectivity-safe pair AFTER focal placement — either silent miss re-ships - the exact drift/generic class the dressing exists to fix. check_dressing_bars is the emit-time + """codex P2 pair (#1611) + beacon geometry (#1618): a narrow/dense crop can defeat dress_focal + (returns silently), leave dress_tall_anchors no connectivity-safe pair AFTER focal placement, or + leave the fire beacons short/collinear — any silent miss re-ships the exact drift/generic/ + degenerate-plate class the dressing exists to fix. check_dressing_bars is the emit-time enforcement generate_town folds into its self-gate (escape hatch: --allow-undressed).""" - base = {"cols": 5, "rows": 5, "door_cells": [[0, 2]], "walls": [], + base = {"cols": 6, "rows": 6, "door_cells": [[0, 2]], "walls": [], "props": [{"id": "w", "kind": "wall_run", "cells": [[0, 0]]}]} - both_missing = d2f.check_dressing_bars(dict(base, props=list(base["props"])), name="bare") - assert len(both_missing) == 2 and any("tall" in f for f in both_missing) and any("focal" in f for f in both_missing) + all_missing = d2f.check_dressing_bars(dict(base, props=list(base["props"])), name="bare") + # three bars: tall mass, focal presence, beacon geometry (>=3 non-collinear fire) + assert len(all_missing) == 3 + assert any("tall" in f for f in all_missing) + assert any("focal" in f for f in all_missing) + assert any("beacon" in f for f in all_missing) tall_only = dict(base, props=base["props"] + [ {"id": "a", "kind": "pillar", "cells": [[2, 2], [2, 3]]}]) fails = d2f.check_dressing_bars(tall_only, name="tallonly") - assert len(fails) == 1 and "focal" in fails[0] + # tall bar met; focal + beacon still fail (no fire at all) + assert len(fails) == 2 and any("focal" in f for f in fails) and any("beacon" in f for f in fails) + # fully dressed: pillar (tall) + THREE non-collinear braziers (focal + beacon geometry) dressed = dict(base, props=tall_only["props"] + [ - {"id": "b", "kind": "brazier", "cells": [[3, 2]]}]) + {"id": "b0", "kind": "brazier", "cells": [[1, 1]]}, + {"id": "b1", "kind": "brazier", "cells": [[4, 1]]}, + {"id": "b2", "kind": "brazier", "cells": [[2, 4]]}]) assert d2f.check_dressing_bars(dressed, name="ok") == [] + + +def test_beacon_geometry_bar_needs_three_non_collinear_fire(): + """#1618: the focal braziers double as plate-registration beacons. The beacon-geometry bar fails + a room with <3 fire beacons (a 2-point similarity fit is vertical-scale-blind) OR 3+ fire beacons + that are collinear (zero triangle area — same failure in disguise); it passes only a real triangle + with area >= _FIRE_TRI_MIN_AREA. A `pillar` clears the tall bar; braziers clear the focal bar, so + each case isolates the beacon bar.""" + base = {"cols": 8, "rows": 8, "door_cells": [], "walls": [], + "props": [{"id": "a", "kind": "pillar", "cells": [[3, 3], [3, 4]]}]} + two_fire = dict(base, props=base["props"] + [ + {"id": "b0", "kind": "brazier", "cells": [[1, 1]]}, + {"id": "b1", "kind": "brazier", "cells": [[5, 1]]}]) + f2 = d2f.check_dressing_bars(two_fire, name="two") + assert any("beacon" in f and "<3" in f for f in f2), f2 + collinear = dict(base, props=base["props"] + [ + {"id": "b0", "kind": "brazier", "cells": [[1, 1]]}, + {"id": "b1", "kind": "brazier", "cells": [[3, 1]]}, + {"id": "b2", "kind": "brazier", "cells": [[5, 1]]}]) # all on row 1 -> area 0 + fc = d2f.check_dressing_bars(collinear, name="coll") + assert any("beacon" in f and "COLLINEAR" in f for f in fc), fc + non_collinear = dict(base, props=base["props"] + [ + {"id": "b0", "kind": "brazier", "cells": [[1, 1]]}, + {"id": "b1", "kind": "brazier", "cells": [[5, 1]]}, + {"id": "b2", "kind": "brazier", "cells": [[3, 5]]}]) # triangle area 8.0 + assert d2f._best_tri_area([(1, 1), (5, 1), (3, 5)]) >= d2f._FIRE_TRI_MIN_AREA + assert d2f.check_dressing_bars(non_collinear, name="ok") == [] + + +def test_every_room_has_three_non_collinear_fire_beacons(town): + """#1618: the dwing recovery proved the focal braziers double as plate-registration beacons; two + on the same row make the 2-point similarity fit blind to vertical scale (room_1's plate was + unfixable by warping). Every generated room must carry >=3 fire-kind props (brazier/campfire) + whose best triangle area >= _FIRE_TRI_MIN_AREA, so the plate solve is observable in both axes with + residual redundancy.""" + for rid, geo in town.items(): + fire = d2f._fire_cells(geo) + assert len(fire) >= 3, f"{rid}: only {len(fire)} fire beacons {fire}" + area = d2f._best_tri_area(fire) + assert area >= d2f._FIRE_TRI_MIN_AREA, ( + f"{rid}: fire beacons near-collinear, best triangle area {area} < {d2f._FIRE_TRI_MIN_AREA}") diff --git a/tools/dungen_to_fixtures.py b/tools/dungen_to_fixtures.py index 9871b052..024c2047 100644 --- a/tools/dungen_to_fixtures.py +++ b/tools/dungen_to_fixtures.py @@ -333,6 +333,39 @@ def _door_landing(cell: tuple, cols: int, rows: int) -> tuple: _FOCAL_KINDS = {"altar", "stone_well", "brazier", "sarcophagus", "campfire", "hearth"} +# Fire-BEARING focal kinds — the only ones qa/overlay_boxes.py::blob_solve detects as registration +# beacons (bright fire bowls); an `altar` is a focal narrative mass but casts NO fire blob, so it does +# NOT count toward the beacon geometry (#1618). +_FIRE_KINDS = {"brazier", "campfire"} +# Min area (cell²) of the triangle formed by a room's three fire beacons. Two beacons on the same row +# are collinear (zero area): the 2-point plate-registration similarity fit is then blind to vertical +# scale (dwing room_1: a 0.07-cell beacon error coexisted with ~1.5 cells of bottom-wall misfit). A +# real triangle makes the solve both-axes-observable with residual redundancy (3 pts = 6 constraints +# vs 4 dof). 2.0 is comfortably above the sub-cell jitter of adjacent placements. +_FIRE_TRI_MIN_AREA = 2.0 + + +def _tri_area(a: tuple, b: tuple, c: tuple) -> float: + """Absolute area (cell²) of the triangle on three (col, row) cells — shoelace. 0.0 when collinear.""" + (ax, ay), (bx, by), (cx, cy) = a, b, c + return abs((bx - ax) * (cy - ay) - (cx - ax) * (by - ay)) / 2.0 + + +def _fire_cells(geo: dict) -> list: + """Representative (col, row) cell per fire-bearing prop (its first/anchor cell — every dressed fire + beacon is a single-cell footprint, so this is exact).""" + return [tuple(p["cells"][0]) for p in geo.get("props", []) + if p.get("kind") in _FIRE_KINDS and p.get("cells")] + + +def _best_tri_area(cells: list) -> float: + """Largest triangle area (cell²) over any three of `cells` (0.0 when fewer than three).""" + best = 0.0 + for i in range(len(cells)): + for j in range(i + 1, len(cells)): + for k in range(j + 1, len(cells)): + best = max(best, _tri_area(cells[i], cells[j], cells[k])) + return best def dress_focal(geo: dict, *, name: str = "room") -> dict: @@ -344,7 +377,15 @@ def dress_focal(geo: dict, *, name: str = "room") -> dict: two BRAZIERS by the crossing centre. Fire doubles as the paint stage's warm-core chiaroscuro anchor (the scorers' own lever) and the runtime's animated-VFX anchor. Skipped when the room already carries any focal kind. Same safety machinery as dress_tall_anchors: deterministic - grid-derived candidates, never on/adjacent to a door landing, flood-fill connectivity-verified.""" + grid-derived candidates, never on/adjacent to a door landing, flood-fill connectivity-verified. + + v2 (#1618): every plan's two lane/shrine braziers share a ROW — a collinear fire pair, which the + 2-point plate-registration similarity solve reads as vertical-scale-blind (dwing room_1's plate is + unfixable by warping for exactly this reason). So after the plan lands, author a THIRD fire beacon + (a corner watch-brazier) at the connectivity-safe cell that MAXIMISES the three-beacon triangle + area, guaranteeing a both-axes-solvable, residually-redundant beacon field. check_dressing_bars + then enforces >=3 fire kinds with best triangle area >= _FIRE_TRI_MIN_AREA, so a crop that defeats + the placement fails LOUD rather than shipping a degenerate plate.""" interior = [p for p in geo.get("props", []) if p.get("kind") != "wall_run"] if any(p.get("kind") in _FOCAL_KINDS for p in interior): return geo @@ -407,6 +448,7 @@ def shapes(ideal: tuple, footprint: int) -> list: ("focal_brazier_e", "brazier", 1, (cols // 2 + 2, rows // 2))] used: set = set() + fire_cells: list = [] for pid, kind, footprint, ideal in plan: for cells in shapes(ideal, footprint): if any(cell in used for cell in cells): @@ -414,7 +456,38 @@ def shapes(ideal: tuple, footprint: int) -> list: if connectivity_ok(used | set(cells)): geo["props"].append({"id": pid, "kind": kind, "cells": [list(c) for c in cells]}) used |= set(cells) + if kind in _FIRE_KINDS: + fire_cells.append(cells[0]) break + + # THIRD fire beacon (#1618): break the collinear lane pair. Among all connectivity-safe interior + # candidate cells, take the one that MAXIMISES the min triangle area against every placed fire + # pair (with two beacons down that is a single triangle); ties broken by (row, col) for + # determinism. Max-area naturally lands it toward a far corner (the narrative "corner + # watch-brazier"). We place the best candidate even if its area is under the bar — a genuinely + # degenerate crop then trips check_dressing_bars and fails loud, rather than silently shipping. + if len(fire_cells) >= 2: + best = None # (-min_area, r, c, cell) + for c in range(1, cols - 1): + for r in range(1, rows - 1): + cell = (c, r) + if (cell in used or cell in doors or cell in landing_block + or cell not in base_free): + continue + if not connectivity_ok(used | {cell}): + continue + min_area = min(_tri_area(fa, fb, cell) + for i, fa in enumerate(fire_cells) + for fb in fire_cells[i + 1:]) + key = (-min_area, r, c, cell) + if best is None or key < best: + best = key + if best is not None: + _neg, r, c, cell = best + geo["props"].append({"id": "focal_brazier_n", "kind": "brazier", "cells": [[c, r]]}) + used |= {cell} + fire_cells.append(cell) + if used: wall_cells = {tuple(c) for c in geo.get("walls", [])} prop_cells = {tuple(c) for p in geo.get("props", []) if p.get("kind") != "wall_run" @@ -425,13 +498,16 @@ def shapes(ideal: tuple, footprint: int) -> list: def check_dressing_bars(geo: dict, *, name: str = "room") -> list: - """Emit-time enforcement of the two DRESSING bars the generator promises (codex P2 pair, - #1611): (1) FLAT-INTERIOR bar — at least one interior prop with authored height >= + """Emit-time enforcement of the three DRESSING bars the generator promises (codex P2 pair, #1611; + beacon geometry #1618): (1) FLAT-INTERIOR bar — at least one interior prop with authored height >= _ANCHOR_MIN_TALL (a narrow crop can leave dress_tall_anchors no connectivity-safe pair AFTER focal placement); (2) BEAUTY-FLOOR bar — at least one _FOCAL_KINDS prop (dress_focal returns - silently when every candidate is rejected). Returns failure strings; empty == both bars met. - A silent miss here would re-ship the exact drift/generic classes the dressing exists to fix — - the generator must fail LOUD instead so the crop gets deliberate attention.""" + silently when every candidate is rejected); (3) BEACON-GEOMETRY bar — at least three _FIRE_KINDS + props whose best triangle area >= _FIRE_TRI_MIN_AREA, so the plate-registration solve is + both-axes-observable (two same-row braziers register in one axis only). Returns failure strings; + empty == all three bars met. A silent miss here would re-ship the exact drift/generic/degenerate- + plate classes the dressing exists to fix — the generator must fail LOUD instead so the crop gets + deliberate attention.""" fails = [] interior = [p for p in geo.get("props", []) if p.get("kind") != "wall_run"] tallest = max((_KIND_HEIGHT.get(p.get("kind"), 0.0) for p in interior), default=0.0) @@ -441,6 +517,16 @@ def check_dressing_bars(geo: dict, *, name: str = "room") -> list: if not any(p.get("kind") in _FOCAL_KINDS for p in interior): fails.append(f"{name}: NO narrative focal prop ({sorted(_FOCAL_KINDS)}) — beauty-floor bar; " "focal placement found no connectivity-safe cell") + fire = _fire_cells(geo) + if len(fire) < 3: + fails.append(f"{name}: only {len(fire)} fire beacon(s) (<3) — beacon-geometry class; the " + "2-point plate-registration fit is vertical-scale-blind without a third beacon") + else: + area = _best_tri_area(fire) + if area < _FIRE_TRI_MIN_AREA: + fails.append(f"{name}: fire beacons COLLINEAR (best triangle area {area:.2f} < " + f"{_FIRE_TRI_MIN_AREA}) — beacon-geometry class; a same-row/near-collinear " + "beacon field is blind to vertical scale in the similarity solve") return fails