tess 1.0.0
Performance-first tile and path simulation substrate
Loading...
Searching...
No Matches
precheck.h
1#pragma once
2
3#include <tess/path/request.h>
4#include <tess/topology/topology.h>
5
6#include <cstdint>
7
8namespace tess {
9
10// Outcome of a topology precheck: a cheap region-graph reachability query run
11// before A* so a definitively unreachable goal is rejected without expanding
12// the grid. Only `Unreachable` licenses skipping A* -- every other value is
13// "inconclusive, run A*" -- so the precheck can never turn a solvable query
14// into a wrong failure (see precheck_rules_out_path).
16enum class PrecheckStatus : std::uint8_t {
17 // The graph admits a region path from start to goal; run A* to realize it.
18 Reachable,
19 // The graph definitively rules out any route within known topology. This is
20 // the ONLY status that lets the caller skip A*.
21 Unreachable,
22 // The search reached the edge of the resident set (a boundary exit into a
23 // non-resident chunk): a route through the non-resident region cannot be
24 // ruled
25 // out, so run A*. Sparse worlds only.
26 MissingChunk,
27 // Start not resolvable in the graph; run A* (it is authoritative on the
28 // start tile's validity/passability).
29 InvalidStart,
30 // Goal not resolvable in the graph; run A*.
31 InvalidGoal,
32 // The graph no longer matches the world (a topology edit or residency change
33 // since it was built); run A* rather than trust a stale snapshot.
34 GraphStale,
35 // No built graph was supplied; run A*.
36 NoGraph,
37};
38
39// True iff the precheck definitively established that no path exists, so the
40// caller may skip A* entirely. Every other status means "run A*".
42[[nodiscard]] constexpr bool precheck_rules_out_path(
43 PrecheckStatus status) noexcept {
44 return status == PrecheckStatus::Unreachable;
45}
46
47// Cheap pre-A* topology gate. Consults `graph` (built over `world`) for whether
48// `start` can reach `goal` through region connectivity, WITHOUT expanding the
49// grid. Detectable staleness is resolved first and conservatively: an empty
50// graph is NoGraph, and a graph whose recorded stamps no longer match the
51// world is GraphStale -- both BEFORE calling reachable(), because a stale
52// graph can otherwise return a definitive but wrong Unreachable from an
53// outdated snapshot. `scratch` is caller-owned and reused across queries
54// (allocation-free once warm); it must not be shared across concurrent
55// queries.
56//
57// STALENESS IS DETECTED, NOT INFERRED. The freshness check compares recorded
58// chunk topology versions, residency generations, the shape, and the class
59// and provider stamps. A raw field write bumps none of those: only
60// `mark_topology_dirty` and `mark_topology_rebuilt` advance
61// `topology_version`. So editing a field that a movement class or its
62// provider reads -- opening a wall, placing a stair -- leaves this reporting
63// a fresh graph, and a caller acting on `precheck_rules_out_path` skips a
64// search that would have succeeded. Mark every chunk whose transitions the
65// edit can change topology-dirty afterwards -- for an arbitrary provider
66// that is not necessarily just the edited tile's chunk; see
67// `docs/architecture/topology.md`. The built-in
68// `StairTransitions` cannot compensate through provider stamps either: it is
69// an empty type, so its instance identity is always null and its revision
70// always zero.
71//
72// `ClassOrTag` (explicit first template argument; `World` stays deduced) is
73// the movement class the SEARCH uses -- a raw passable tag normalizes to its
74// UnitCostFieldMovement identity, exactly as in astar_path. The historical
75// precondition that the graph be built over the same passability is now
76// ENFORCED through the graph's class stamp: a graph built for a different
77// movement class (or predating any stamp) reports GraphStale via
78// is_region_graph_fresh_for, so it degrades to running A* rather than letting
79// `Unreachable` prune a route the search's own class could walk. Cost
80// weighting does not change that conclusion, though note a zero weight
81// removes a tile rather than ordering it: the graph's labelled set is a
82// superset of what the search traverses, and a superset can only cost a
83// wasted search, never prune a reachable route.
93template <typename ClassOrTag, typename World, typename Provider>
94[[nodiscard]] auto precheck_path(
96 const World& world, PathRequest request, RegionGraphScratch& scratch,
97 MissingChunkPolicy missing_chunk_policy, const Provider& provider)
98 -> PrecheckStatus {
99 if (graph.local_topologies().empty()) {
100 return PrecheckStatus::NoGraph;
101 }
102 if (!is_region_graph_fresh_for<ClassOrTag>(world, graph) ||
103 !graph.matches_provider(provider)) {
104 return PrecheckStatus::GraphStale;
105 }
106 const auto result =
107 reachable<typename World::shape_type>(graph, request, scratch);
108 switch (result.status) {
109 case ReachabilityStatus::Reachable:
110 return PrecheckStatus::Reachable;
111 case ReachabilityStatus::Unreachable:
112 return PrecheckStatus::Unreachable;
113 case ReachabilityStatus::Indeterminate:
114 if (missing_chunk_policy == MissingChunkPolicy::ReportIndeterminate) {
115 return PrecheckStatus::MissingChunk;
116 }
117 if constexpr (!std::is_same_v<typename World::residency_type,
119 using Shape = typename World::shape_type;
120 if (contains<Shape>(request.start) &&
121 world.try_chunk(chunk_key<Shape>(
122 chunk_coord<Shape>(request.start))) == nullptr) {
123 return PrecheckStatus::InvalidStart;
124 }
125 if (contains<Shape>(request.goal) &&
126 world.try_chunk(chunk_key<Shape>(
127 chunk_coord<Shape>(request.goal))) == nullptr) {
128 return PrecheckStatus::InvalidGoal;
129 }
130 }
131 return PrecheckStatus::Unreachable;
132 case ReachabilityStatus::InvalidStart:
133 return PrecheckStatus::InvalidStart;
134 case ReachabilityStatus::InvalidGoal:
135 return PrecheckStatus::InvalidGoal;
136 }
137 return PrecheckStatus::NoGraph; // unreachable: all statuses handled above
138}
139
145template <typename ClassOrTag, typename World>
146[[nodiscard]] auto precheck_path(
148 const World& world, PathRequest request, RegionGraphScratch& scratch,
149 MissingChunkPolicy missing_chunk_policy =
150 MissingChunkPolicy::ReportIndeterminate) -> PrecheckStatus {
151 return precheck_path<ClassOrTag>(graph, world, request, scratch,
152 missing_chunk_policy, AdjacentTransitions{});
153}
154
155} // namespace tess
Reusable frontier and visitation storage for graph reachability queries.
Definition topology.h:198
Region graph storage specialized by dense or sparse residency policy.
Definition topology.h:382
Definition world.h:22
Supplies no special transitions beyond ordinary face adjacency.
Definition transition_provider.h:132
Definition world.h:18
Specifies inclusive start and goal coordinates for a path query.
Definition request.h:10
Definition shape.h:296