FEnigma/tests/test_solver.py
Dominik Roth 512a0a41b4 Fix click/right-click placement landing one sub-cell off from the actual click
A real, reproducible bug reported as 'misplaced sometimes by a few
small squares': Coord.as_fraction() centers a sub-cell at x + 0.5 (so
a marker drawn at its own coord's exact pixel position round-trips
back to the same coord on click), which meant point_to_coord()'s own
rounding was landing exactly on a .5 boundary, the single worst case
for floating point, tiny representation error from the col/row math
upstream could tip round() to either side and silently return a coord
one sub-cell off from the one actually clicked.

Reproduced with zero pixel math involved at all, just feeding
Coord(...).as_fraction() straight back into point_to_coord(), ruling
out the zoom/pan refactor or the legend margin as the cause (both were
suspected first). Fixed by subtracting the 0.5 offset before rounding,
which recovers a value that's supposed to be an exact integer instead
of an exact half-integer, round() is robust to tiny float noise around
a true integer, just not around X.5.

Verified exhaustively (all 20,000 possible coordinates round-trip
correctly now, not just a handful of samples, since the original bug
was itself float-pattern-dependent) and locked in with a permanent
regression test.
2026-08-09 21:18:47 +02:00

128 lines
5.6 KiB
Python

"""Regression coverage for solver.py's geometric resolution."""
from fenigma import solver
from fenigma.models import LARGE_X, Board, Clue, Coord, Location, TargetType
def _board_with_spotters(*coords):
board = Board()
for i, coord in enumerate(coords, start=1):
board.add_spotter(coord, i)
return board
def test_bearing_and_distance_from_one_reference_resolves_directly():
board = _board_with_spotters(Coord("J", 5, 0, 0))
target = board.add_target(TargetType.TANK, id_="1")
target.location = Location.from_desc("x", [Clue(reference="Spotter#1", bearing_deg=90.0, distance_km=3.0)])
solver.resolve_board(board)
assert target.coord is not None
def test_two_bearings_resolve_via_ray_ray_intersection():
board = _board_with_spotters(Coord("J", 5, 0, 0), Coord("L", 4, 3, 0))
target = board.add_target(TargetType.TANK, id_="1")
target.location = Location.from_desc("x", [
Clue(reference="Spotter#1", bearing_deg=90.0),
Clue(reference="Spotter#2", bearing_deg=180.0),
])
solver.resolve_board(board)
assert target.coord is not None
def test_two_distances_that_actually_cross_are_ambiguous_not_resolved():
board = _board_with_spotters(Coord("J", 5, 0, 0), Coord("P", 5, 0, 0))
target = board.add_target(TargetType.TANK, id_="1")
target.location = Location.from_desc("x", [
Clue(reference="Spotter#1", distance_km=7.33),
Clue(reference="Spotter#2", distance_km=3.43),
])
solver.resolve_board(board)
assert target.coord is None
assert len(target.location.potential_coords) == 2
def test_nested_distance_circles_fall_back_to_compromise_point():
"""Two distances that don't actually cross (one circle nested inside
the other, given how close the two spotters are) used to just give
up entirely. closest_compromise_point() finds a bounded, sane
stand-in instead, flagged via Location.note rather than treated as
a clean resolution."""
board = _board_with_spotters(Coord("J", 5, 0, 0), Coord("J", 5, 1, 0))
target = board.add_target(TargetType.TANK, id_="1")
target.location = Location.from_desc("x", [
Clue(reference="Spotter#1", distance_km=7.33),
Clue(reference="Spotter#2", distance_km=3.43),
])
solver.resolve_board(board)
assert target.coord is not None
assert target.location.note is not None
assert "approximate" in target.location.note
def test_toleranced_bearing_is_never_used_to_triangulate():
"""A compass-word bearing (bearing_tolerance_deg set) names a whole
sector, solve_location() must never use it as if it were a precise
ray, even when it's the only bearing-shaped clue available."""
board = _board_with_spotters(Coord("J", 5, 0, 0), Coord("L", 4, 3, 0))
target = board.add_target(TargetType.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)
# only one usable (distance-only) clue remains once the toleranced
# bearing is excluded, not enough to resolve anything on its own.
assert target.coord is None
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.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.TANK, id_="1")
target.location = Location.from_desc("x", [
Clue(reference="Spotter#1", distance_km=7.33),
Clue(reference="Spotter#2", distance_km=3.43),
])
solver.resolve_board(board)
assert target.location.note is not None
target.coord = Coord("A", 1, 0, 0)
assert target.location.note is None
def test_point_to_coord_round_trips_every_sub_cell():
"""A real, reproducible bug: Coord.as_fraction() centers a sub-cell
at x + 0.5 (so a marker drawn at its own coord's exact pixel
position round-trips back to the same coord on click), which put
point_to_coord()'s own rounding exactly on a .5 boundary, the worst
case for floating point. A tiny representation error from the
subtraction it used to do could tip round() to either side,
silently returning a coord one sub-cell off from the one actually
clicked, this reproduced with zero pixel math involved at all, just
feeding as_fraction() straight back into point_to_coord(). Checked
exhaustively, not just a couple of samples, since the failure was
itself pattern-dependent (only some coords tripped the float
rounding the wrong way)."""
for X in LARGE_X:
for Y in range(1, 11):
for x in range(10):
for y in range(10):
c = Coord(X, Y, x, y)
assert solver.point_to_coord(c.as_fraction()) == c