tess 1.0.0
Performance-first tile and path simulation substrate
Loading...
Searching...
No Matches
portal_route.h
1#pragma once
2
3#include <tess/path/path.h>
4#include <tess/path/portal_segment_cache.h>
5
6#include <array>
7#include <cstddef>
8#include <cstdint>
9#include <limits>
10#include <optional>
11#include <vector>
12
13namespace tess {
14
15namespace detail {
16
17template <typename World, typename Class>
18[[nodiscard]] auto weighted_endpoint_failure(const World& world,
19 PathRequest request)
20 -> std::optional<PathStatus> {
21 using Shape = typename World::shape_type;
22 if (!contains<Shape>(request.start) ||
23 !is_passable<World, Class>(world, request.start)) {
24 return PathStatus::InvalidStart;
25 }
26 if (!contains<Shape>(request.goal) ||
27 !is_passable<World, Class>(world, request.goal)) {
28 return PathStatus::InvalidGoal;
29 }
30 const auto start = tile_index<Shape>(request.start);
31 const auto goal = tile_index<Shape>(request.goal);
32 if (tile_entry_cost_index<World, Class>(world, start) == 0) {
33 return PathStatus::InvalidStart;
34 }
35 if (tile_entry_cost_index<World, Class>(world, goal) == 0) {
36 return PathStatus::InvalidGoal;
37 }
38 return std::nullopt;
39}
40
41template <typename World, typename Class>
42[[nodiscard]] auto build_greedy_chunk_portal_candidate(
43 const World& world, PathRequest request, std::vector<Coord3>& waypoints)
44 -> PortalRouteCandidate {
45 using Shape = typename World::shape_type;
46
47 waypoints.clear();
48 auto current = request.start;
49 auto current_chunk = chunk_coord<Shape>(request.start);
50 const auto goal_chunk = chunk_coord<Shape>(request.goal);
51 auto result = PortalRouteCandidate{true, 0, 0};
52
53 while (current_chunk != goal_chunk) {
54 auto found_step = false;
55 auto best_score = std::numeric_limits<std::uint32_t>::max();
56 auto best_chunk = ChunkCoord3{};
57 auto best_portal = Coord3{};
58
59 const auto consider = [&](ChunkCoord3 next_chunk) {
60 auto portal = Coord3{};
61 auto scan_tiles = std::size_t{0};
62 if (!memoized_chunk_portal<World, Class>(world, current_chunk, next_chunk,
63 current, request.goal, portal,
64 &scan_tiles)) {
65 result.scan_tiles += scan_tiles;
66 return;
67 }
68 result.scan_tiles += scan_tiles;
69 const auto score = saturating_add(manhattan(current, portal),
70 manhattan(portal, request.goal));
71 if (!found_step || score < best_score) {
72 found_step = true;
73 best_score = score;
74 best_chunk = next_chunk;
75 best_portal = portal;
76 }
77 };
78
79 if (current_chunk.x != goal_chunk.x) {
80 auto next = current_chunk;
81 if (current_chunk.x < goal_chunk.x) {
82 ++next.x;
83 } else {
84 --next.x;
85 }
86 consider(next);
87 }
88 if (current_chunk.y != goal_chunk.y) {
89 auto next = current_chunk;
90 if (current_chunk.y < goal_chunk.y) {
91 ++next.y;
92 } else {
93 --next.y;
94 }
95 consider(next);
96 }
97 if (current_chunk.z != goal_chunk.z) {
98 auto next = current_chunk;
99 if (current_chunk.z < goal_chunk.z) {
100 ++next.z;
101 } else {
102 --next.z;
103 }
104 consider(next);
105 }
106
107 if (!found_step) {
108 result.found = false;
109 return result;
110 }
111 result.score =
112 saturating_add(result.score, manhattan(current, best_portal));
113 waypoints.push_back(best_portal);
114 current = best_portal;
115 current_chunk = best_chunk;
116 }
117
118 result.score = saturating_add(result.score, manhattan(current, request.goal));
119 return result;
120}
121
122// Runs the chunk-portal candidate scan (six axis orders plus the greedy
123// walk) and leaves the best-scoring waypoint sequence in
124// `product.best_waypoints_`, with candidate/scan statistics accumulated on
125// the product. Returns false when no goal-monotone candidate exists — the
126// heuristic-tier limitation documented on the public builder below.
127template <typename World, typename Class>
128[[nodiscard]] auto select_chunk_portal_waypoints(
129 const World& world, PathRequest request,
130 WeightedPortalRouteProduct& product) -> bool {
131 constexpr auto orders = std::array{
132 std::array{Axis::X, Axis::Y, Axis::Z},
133 std::array{Axis::X, Axis::Z, Axis::Y},
134 std::array{Axis::Y, Axis::X, Axis::Z},
135 std::array{Axis::Y, Axis::Z, Axis::X},
136 std::array{Axis::Z, Axis::X, Axis::Y},
137 std::array{Axis::Z, Axis::Y, Axis::X},
138 };
139
140 // Opens the memo for this selection and closes it however the
141 // selection exits. A nested selection — reachable through a
142 // user-supplied passability predicate — bypasses the memo rather than
143 // sharing its generation, because the key omits the goal.
144 const detail::PortalMemoScope memo_scope{detail::active_portal_memo()};
145
146 auto found_route = false;
147 auto best_score = std::numeric_limits<std::uint32_t>::max();
148 product.best_waypoints_.clear();
149 for (const auto& order : orders) {
150 const auto candidate = build_chunk_portal_candidate<World, Class>(
151 world, request, order, product.candidate_waypoints_);
152 ++product.route_candidates_;
153 product.portal_scan_tiles_ += candidate.scan_tiles;
154 if (!candidate.found) {
155 continue;
156 }
157 if (!found_route || candidate.score < best_score) {
158 found_route = true;
159 best_score = candidate.score;
160 product.best_waypoints_.assign(product.candidate_waypoints_.begin(),
161 product.candidate_waypoints_.end());
162 }
163 }
164 {
165 const auto candidate = build_greedy_chunk_portal_candidate<World, Class>(
166 world, request, product.candidate_waypoints_);
167 ++product.route_candidates_;
168 product.portal_scan_tiles_ += candidate.scan_tiles;
169 if (candidate.found && (!found_route || candidate.score < best_score)) {
170 found_route = true;
171 best_score = candidate.score;
172 product.best_waypoints_.assign(product.candidate_waypoints_.begin(),
173 product.candidate_waypoints_.end());
174 }
175 }
176 return found_route;
177}
178
179} // namespace detail
180
181// HEURISTIC TIER: candidates walk only goal-monotone chunk staircases (each
182// step moves one chunk closer on some axis) and only the single
183// best-scoring candidate is stitched. `NoCandidate` means this tier found no
184// route candidate; it is not a reachability conclusion. Topologies that
185// require a chunk detour away from the goal (or whose best seam tile is sealed
186// off) can return `NoCandidate` here while weighted_astar_path finds a route.
187// Callers needing an authoritative answer must fall back to exact search.
192template <typename World, typename Class>
194 const World& world, PathRequest request, PathScratch& scratch,
196 using Shape = typename World::shape_type;
197 // Builds a portal route over the dense chunk-portal topology graph and tracks
198 // content-version dependencies. The portal topology is dense-only; direct
199 // weighted A* runs natively on sparse worlds.
200 static_assert(
201 std::is_same_v<typename World::residency_type, AlwaysResident>,
202 "build_weighted_chunk_portal_route_product requires an "
203 "AlwaysResidentWorld; use weighted_astar_path for sparse worlds.");
204
205 product.clear();
206 product.request_ = request;
207
208 if (const auto failure =
209 detail::weighted_endpoint_failure<World, Class>(world, request)) {
210 product.status_ = *failure;
211 detail::capture_failure_dependencies<Shape>(world, request, product.status_,
212 product.dependencies_);
213 return PathResult{product.status_, 0, 0, 0, product.path_};
214 }
215
216 const auto found_route = detail::select_chunk_portal_waypoints<World, Class>(
217 world, request, product);
218 if (!found_route) {
219 product.status_ = PathStatus::NoCandidate;
220 // Failure results depend on world content the portal scans sampled;
221 // depend on every chunk so any edit invalidates a replayed failure.
222 product.dependencies_.capture_all(world);
223 return PathResult{product.status_, 0, 0, 0, product.path_};
224 }
225 product.waypoints_.assign(product.best_waypoints_.begin(),
226 product.best_waypoints_.end());
227
228 auto from = request.start;
229 auto total_cost = std::uint64_t{0};
230 auto total_expanded = std::size_t{0};
231 auto total_reached = std::size_t{0};
232 auto append_segment = [&](PathRequest segment_request) {
233 const auto result =
234 weighted_astar_path<World, Class>(world, segment_request, scratch);
235 total_expanded += result.expanded_nodes;
236 total_reached += result.reached_nodes;
237 if (result.status != PathStatus::Found) {
238 product.path_.clear();
239 product.status_ = PathStatus::NoCandidate;
240 product.expanded_nodes_ = total_expanded;
241 product.reached_nodes_ = total_reached;
242 // Same failure-dependency contract as build_weighted_route_product;
243 // the failing segment's endpoints are the offending tiles.
244 detail::capture_failure_dependencies<Shape>(
245 world, request, product.status_, product.dependencies_);
246 return false;
247 }
248 total_cost += result.cost;
249 if (total_cost >= std::numeric_limits<std::uint32_t>::max()) {
250 product.path_.clear();
251 product.status_ = PathStatus::CostOverflow;
252 product.expanded_nodes_ = total_expanded;
253 product.reached_nodes_ = total_reached;
254 detail::capture_failure_dependencies<Shape>(
255 world, request, product.status_, product.dependencies_);
256 return false;
257 }
258 product.segment_.assign(result.path.begin(), result.path.end());
259 for (std::size_t i = product.path_.empty() ? 0u : 1u;
260 i < product.segment_.size(); ++i) {
261 product.path_.push_back(product.segment_[i]);
262 }
263 return true;
264 };
265
266 for (const auto waypoint : product.waypoints_) {
267 if (!append_segment(PathRequest{from, waypoint})) {
268 return PathResult{product.status_, 0, total_expanded, total_reached,
269 product.path_};
270 }
271 from = waypoint;
272 }
273 if (!append_segment(PathRequest{from, request.goal})) {
274 return PathResult{product.status_, 0, total_expanded, total_reached,
275 product.path_};
276 }
277
278 product.status_ = PathStatus::Found;
279 product.cost_ = static_cast<std::uint32_t>(total_cost);
280 product.expanded_nodes_ = total_expanded;
281 product.reached_nodes_ = total_reached;
282 for (const auto coord : product.path_) {
283 const auto key = tile_key<Shape>(coord);
284 product.dependencies_.add_chunk(world, chunk_key<Shape>(key));
285 }
286 return PathResult{product.status_, product.cost_, product.expanded_nodes_,
287 product.reached_nodes_, product.path_};
288}
289
290// Same heuristic tier and NoCandidate semantics as the uncached builder above:
291// chunk-portal candidates select the waypoints, and stitching runs through
292// the class-bound segment cache so repeated corridors serve without fresh
293// searches. Unlike the uncached builder, the product records segment-level
294// dependencies only (inside the cache); it is a serving product, not a
295// replay product, and its returned path borrows the product exactly like
296// the other builders.
298template <typename World, typename Class>
300 const World& world, PathRequest request, PathScratch& scratch,
302 -> PathResult {
303 static_assert(
304 std::is_same_v<typename World::residency_type, AlwaysResident>,
305 "build_weighted_chunk_portal_route_product_cached requires an "
306 "AlwaysResidentWorld; use weighted_astar_path for sparse worlds.");
307
308 product.clear();
309 product.request_ = request;
310
311 if (const auto failure =
312 detail::weighted_endpoint_failure<World, Class>(world, request)) {
313 product.status_ = *failure;
314 return PathResult{product.status_, 0, 0, 0, product.path_};
315 }
316
317 if (!detail::select_chunk_portal_waypoints<World, Class>(world, request,
318 product)) {
319 product.status_ = PathStatus::NoCandidate;
320 return PathResult{product.status_, 0, 0, 0, product.path_};
321 }
322 product.waypoints_.assign(product.best_waypoints_.begin(),
323 product.best_waypoints_.end());
324
325 auto class_cache = cache.template for_class<Class>();
326 auto from = request.start;
327 // Segment costs accumulate in 64 bits: each segment cost is
328 // representable, but a stitched total at or above the uint32 infinity
329 // sentinel must report CostOverflow exactly as the exact search would —
330 // never a Found route carrying an unrepresentable cost.
331 auto total_cost = std::uint64_t{0};
332 auto total_expanded = std::size_t{0};
333 auto total_reached = std::size_t{0};
334 auto append_segment = [&](PathRequest segment_request) {
335 if (const auto hit =
336 class_cache.lookup_append(world, segment_request, product.path_);
337 hit.found) {
338 total_cost += hit.cost;
339 return true;
340 }
341 const auto result =
342 weighted_astar_path<World, Class>(world, segment_request, scratch);
343 class_cache.store(world, segment_request, result);
344 total_expanded += result.expanded_nodes;
345 total_reached += result.reached_nodes;
346 if (result.status != PathStatus::Found) {
347 product.path_.clear();
348 product.status_ = PathStatus::NoCandidate;
349 product.expanded_nodes_ = total_expanded;
350 product.reached_nodes_ = total_reached;
351 return false;
352 }
353 total_cost += result.cost;
354 for (std::size_t i = product.path_.empty() ? 0u : 1u;
355 i < result.path.size(); ++i) {
356 product.path_.push_back(result.path[i]);
357 }
358 return true;
359 };
360
361 for (const auto waypoint : product.waypoints_) {
362 if (!append_segment(PathRequest{from, waypoint})) {
363 return PathResult{product.status_, 0, total_expanded, total_reached,
364 product.path_};
365 }
366 from = waypoint;
367 }
368 if (!append_segment(PathRequest{from, request.goal})) {
369 return PathResult{product.status_, 0, total_expanded, total_reached,
370 product.path_};
371 }
372
373 if (total_cost >= std::numeric_limits<std::uint32_t>::max()) {
374 product.path_.clear();
375 product.status_ = PathStatus::CostOverflow;
376 product.expanded_nodes_ = total_expanded;
377 product.reached_nodes_ = total_reached;
378 return PathResult{product.status_, 0, total_expanded, total_reached,
379 product.path_};
380 }
381
382 product.status_ = PathStatus::Found;
383 product.cost_ = static_cast<std::uint32_t>(total_cost);
384 product.expanded_nodes_ = total_expanded;
385 product.reached_nodes_ = total_reached;
386 return PathResult{product.status_, product.cost_, product.expanded_nodes_,
387 product.reached_nodes_, product.path_};
388}
389
390} // namespace tess
Definition path.h:741
friend auto build_weighted_chunk_portal_route_product_cached(const World &world, PathRequest request, PathScratch &scratch, WeightedPortalSegmentCache &cache, WeightedPortalRouteProduct &product) -> PathResult
Builds a chunk-portal weighted route through the segment cache.
Definition portal_route.h:299
friend auto build_weighted_chunk_portal_route_product(const World &world, PathRequest request, PathScratch &scratch, WeightedPortalRouteProduct &product) -> PathResult
Definition portal_route.h:193
Definition portal_segment_cache.h:61
Definition world.h:22
Specifies inclusive start and goal coordinates for a path query.
Definition request.h:10
Definition path.h:60
Definition shape.h:296