From 21c6553784ebcc844f8c5b97f3ae0296e54bbe7b Mon Sep 17 00:00:00 2001 From: Dominik Roth Date: Sun, 9 Aug 2026 18:28:48 +0200 Subject: [PATCH] Fix explain_unresolved() not excluding toleranced bearings like solve_location() does Latent inconsistency flagged during the ocr.py refactor: solve_location() already excludes a compass-word bearing (bearing_tolerance_deg set) from its own bearings list before triangulating, since a toleranced bearing names a sector, not a precise ray, and shouldn't be treated as if it were one. explain_unresolved()'s bearings list didn't have the same exclusion, so a toleranced-only bearing plus a distance clue could get described as inconsistent geometry ('the bearing from X never crosses the Ykm circle...') when the real reason nothing resolved is just that the bearing was never usable for that math in the first place. Locked in with a new test. --- src/fenigma/solver.py | 10 +++++++++- tests/test_solver.py | 17 +++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/fenigma/solver.py b/src/fenigma/solver.py index 4e75577..54b2319 100644 --- a/src/fenigma/solver.py +++ b/src/fenigma/solver.py @@ -294,7 +294,15 @@ def explain_unresolved(location: Location, board: Board) -> str | None: return "waiting on " + ", ".join(unresolved_refs) + " to have a known position first" resolved = [(clue, pt) for clue in location.clues if (pt := _entity_point(board, clue.reference)) is not None] - bearings = [(c, p) for c, p in resolved if c.bearing_deg is not None and c.distance_km is None] + # A toleranced bearing (compass word, not a precise degree reading) + # is never used to triangulate in solve_location() either (see its + # own comment above), excluded here too so this explanation doesn't + # claim a toleranced-only bearing could resolve something when it + # never actually could. + bearings = [ + (c, p) for c, p in resolved + if c.bearing_deg is not None and c.distance_km is None and c.bearing_tolerance_deg is None + ] distances = [(c, p) for c, p in resolved if c.distance_km is not None and c.bearing_deg is None] if bearings and distances: diff --git a/tests/test_solver.py b/tests/test_solver.py index fe60519..773d4c4 100644 --- a/tests/test_solver.py +++ b/tests/test_solver.py @@ -76,6 +76,23 @@ def test_toleranced_bearing_is_never_used_to_triangulate(): assert not target.location.potential_coords +def test_explain_unresolved_ignores_a_toleranced_bearing_too(): + """A real inconsistency: solve_location() already excluded a + toleranced bearing from triangulation, but explain_unresolved()'s + own bearings list didn't, so it could describe a toleranced-only + bearing as if it were usable geometry. Only a distance clue is left + once the toleranced bearing is (correctly) excluded, one clue alone + is never inconsistent with itself, so there's nothing to explain.""" + board = _board_with_spotters(Coord("J", 5, 0, 0), Coord("L", 4, 3, 0)) + target = board.add_target(TargetType.HOSTILE_TANK, id_="1") + target.location = Location.from_desc("x", [ + Clue(reference="Spotter#1", distance_km=3.0), + Clue(reference="Spotter#2", bearing_deg=270.0, bearing_tolerance_deg=11.25), + ]) + solver.resolve_board(board) + assert solver.explain_unresolved(target.location, board) is None + + def test_manual_coord_override_clears_a_stale_note(): board = _board_with_spotters(Coord("J", 5, 0, 0), Coord("J", 5, 1, 0)) target = board.add_target(TargetType.HOSTILE_TANK, id_="1")