## PathFinder A* (T-1088 Live-session feedback item 2): 8-directional search over ## the greybox KNOWN-tile store, exercised on synthetic floor sets. Covers the ## uniform cardinal/diagonal cost (no sqrt(2), D-248), no-corner-cutting, the ## information boundary (unknown tiles impassable), the terrain-cost seam, ## unreachable-returns-empty, and deterministic tie-breaking. ## ## Pure static: no scene, autoload, or GameState — floor knowledge is injected as ## a Callable, so the whole search is headless (design §10.4). class_name TestPathFinder extends GdUnitTestSuite ## A floor lookup over an explicit set of known-floor tiles — everything else is ## impassable (unknown/void/wall), mirroring GreyboxWorld.Store.kind_of. func _lookup(floors: Array) -> Callable: var known := {} for t: Vector3i in floors: known[t] = true return func(tile: Vector3i) -> bool: return known.has(tile) ## Every floor tile in an inclusive rectangle (z=0). func _rect(x0: int, x1: int, y0: int, y1: int) -> Array: var out: Array = [] for x in range(x0, x1 + 1): for y in range(y0, y1 + 1): out.append(Vector3i(x, y, 0)) return out ## True if every consecutive pair in the path differs by a king-move (adjacent ## incl. diagonal) — a well-formed contiguous path. func _is_contiguous(path: Array[Vector3i]) -> bool: for i in range(1, path.size()): var d: Vector3i = path[i] - path[i - 1] if absi(d.x) > 1 or absi(d.y) > 1 or d.z != 0 or d == Vector3i.ZERO: return false return true # -- shortest path: straight (cardinal) --------------------------------------------- func test_straight_cardinal_shortest() -> void: var lookup := _lookup(_rect(0, 5, 0, 0)) # one row, y=0 var path := PathFinder.find_path(Vector3i(0, 0, 0), Vector3i(5, 0, 0), lookup) # 5 steps east -> 6 tiles, endpoints inclusive, contiguous. assert_int(path.size()).is_equal(6) assert_object(path[0]).is_equal(Vector3i(0, 0, 0)) assert_object(path[path.size() - 1]).is_equal(Vector3i(5, 0, 0)) assert_bool(_is_contiguous(path)).is_true() # -- shortest path: diagonal (uniform cost — no sqrt(2), D-248) ----------------------- func test_diagonal_shortest_beats_cardinal() -> void: var lookup := _lookup(_rect(0, 3, 0, 3)) var path := PathFinder.find_path(Vector3i(0, 0, 0), Vector3i(3, 3, 0), lookup) # Chebyshev distance 3 -> 4 tiles all-diagonal; a diagonal costs the same as a # cardinal, so 3 diagonal steps (cost 3) beat 6 cardinal steps (cost 6). assert_int(path.size()).is_equal(4) assert_bool(_is_contiguous(path)).is_true() # Every leg is a true diagonal. for i in range(1, path.size()): var d: Vector3i = path[i] - path[i - 1] assert_int(absi(d.x)).is_equal(1) assert_int(absi(d.y)).is_equal(1) # -- no corner-cutting --------------------------------------------------------------- func test_no_corner_cut_blocks_bare_diagonal() -> void: # Only the two diagonal-opposite tiles are floor; both shared cardinals are # missing, so the diagonal is illegal and there is no other route. var lookup := _lookup([Vector3i(0, 0, 0), Vector3i(1, 1, 0)]) var path := PathFinder.find_path(Vector3i(0, 0, 0), Vector3i(1, 1, 0), lookup) assert_array(path).is_empty() func test_no_corner_cut_takes_legal_l_route() -> void: # One shared cardinal present -> the bare diagonal is still illegal (needs # BOTH), but an L via the open cardinal is legal: 2 cardinal steps. var lookup := _lookup([Vector3i(0, 0, 0), Vector3i(1, 0, 0), Vector3i(1, 1, 0)]) var path := PathFinder.find_path(Vector3i(0, 0, 0), Vector3i(1, 1, 0), lookup) assert_array(path).is_equal( [Vector3i(0, 0, 0), Vector3i(1, 0, 0), Vector3i(1, 1, 0)] as Array[Vector3i] ) func test_diagonal_allowed_when_both_cardinals_open() -> void: # Full 2x2 -> the diagonal is legal and cheaper than the L, so it wins. var lookup := _lookup(_rect(0, 1, 0, 1)) var path := PathFinder.find_path(Vector3i(0, 0, 0), Vector3i(1, 1, 0), lookup) assert_array(path).is_equal([Vector3i(0, 0, 0), Vector3i(1, 1, 0)] as Array[Vector3i]) # -- information boundary: unknown tiles impassable (fog is unpathable) ---------------- func test_unknown_tile_blocks_route() -> void: # A gap in the row (tile (2,0) never observed) severs the only path. var lookup := _lookup([Vector3i(0, 0, 0), Vector3i(1, 0, 0), Vector3i(3, 0, 0)]) var path := PathFinder.find_path(Vector3i(0, 0, 0), Vector3i(3, 0, 0), lookup) assert_array(path).is_empty() func test_goal_not_floor_returns_empty() -> void: # Hovering an unknown/void/wall tile: no path, ever. var lookup := _lookup(_rect(0, 3, 0, 0)) var path := PathFinder.find_path(Vector3i(0, 0, 0), Vector3i(9, 9, 0), lookup) assert_array(path).is_empty() func test_start_equals_goal_is_single_tile() -> void: var lookup := _lookup([Vector3i(5, 5, 0)]) var path := PathFinder.find_path(Vector3i(5, 5, 0), Vector3i(5, 5, 0), lookup) assert_array(path).is_equal([Vector3i(5, 5, 0)] as Array[Vector3i]) # -- terrain-cost seam ---------------------------------------------------------------- func test_terrain_cost_forces_detour() -> void: # 3x3 open. Straight (0,1)->(1,1)->(2,1) is 2 steps, but (1,1) costs 100, so # A* detours through the cheap corner (1,0) instead — proving the seam steers # the search without touching passability. var lookup := _lookup(_rect(0, 2, 0, 2)) var costly := Vector3i(1, 1, 0) var terrain := func(tile: Vector3i) -> float: return 100.0 if tile == costly else 1.0 var path := PathFinder.find_path(Vector3i(0, 1, 0), Vector3i(2, 1, 0), lookup, terrain) assert_bool(path.has(costly)).is_false() assert_int(path.size()).is_equal(3) assert_bool(_is_contiguous(path)).is_true() func test_uniform_default_takes_straight_line() -> void: # No terrain provider -> uniform cost 1: the same query goes straight through # the middle (the contrast case for the detour above). var lookup := _lookup(_rect(0, 2, 0, 2)) var path := PathFinder.find_path(Vector3i(0, 1, 0), Vector3i(2, 1, 0), lookup) assert_array(path).is_equal( [Vector3i(0, 1, 0), Vector3i(1, 1, 0), Vector3i(2, 1, 0)] as Array[Vector3i] ) # -- unreachable ---------------------------------------------------------------------- func test_unreachable_returns_empty() -> void: # Goal is known-floor but on a disconnected island — the open set drains. var lookup := _lookup([Vector3i(0, 0, 0), Vector3i(1, 0, 0), Vector3i(9, 9, 0)]) var path := PathFinder.find_path(Vector3i(0, 0, 0), Vector3i(9, 9, 0), lookup) assert_array(path).is_empty() # -- determinism ---------------------------------------------------------------------- func test_tie_break_is_deterministic() -> void: # 3x3 open, (0,0)->(2,0): the straight cardinal route and the via-(1,1) route # both cost 2 (uniform diagonal). The fixed neighbour order + (f, h, seq) # tiebreak must return the SAME path every call, never Dictionary hash order. var lookup := _lookup(_rect(0, 2, 0, 2)) var a := PathFinder.find_path(Vector3i(0, 0, 0), Vector3i(2, 0, 0), lookup) var b := PathFinder.find_path(Vector3i(0, 0, 0), Vector3i(2, 0, 0), lookup) assert_array(a).is_equal(b) # And it is a valid shortest path (3 tiles, contiguous, correct endpoints). assert_int(a.size()).is_equal(3) assert_object(a[0]).is_equal(Vector3i(0, 0, 0)) assert_object(a[a.size() - 1]).is_equal(Vector3i(2, 0, 0)) assert_bool(_is_contiguous(a)).is_true()