tess 0.4.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
5#include <array>
6#include <cstddef>
7#include <cstdint>
8#include <limits>
9#include <vector>
10
11namespace tess {
12
13namespace detail {
14
15template <typename World, typename PassableTag>
16[[nodiscard]] auto build_greedy_chunk_portal_candidate(
17 const World& world, PathRequest request, std::vector<Coord3>& waypoints)
18 -> PortalRouteCandidate {
19 using Shape = typename World::shape_type;
20
21 waypoints.clear();
22 auto current = request.start;
23 auto current_chunk = chunk_coord<Shape>(request.start);
24 const auto goal_chunk = chunk_coord<Shape>(request.goal);
25 auto result = PortalRouteCandidate{true, 0, 0};
26
27 while (current_chunk != goal_chunk) {
28 auto found_step = false;
29 auto best_score = std::numeric_limits<std::uint32_t>::max();
30 auto best_chunk = ChunkCoord3{};
31 auto best_portal = Coord3{};
32
33 const auto consider = [&](ChunkCoord3 next_chunk) {
34 auto portal = Coord3{};
35 auto scan_tiles = std::size_t{0};
36 if (!best_chunk_portal<World, PassableTag>(
37 world, current_chunk, next_chunk, current, request.goal, portal,
38 &scan_tiles)) {
39 result.scan_tiles += scan_tiles;
40 return;
41 }
42 result.scan_tiles += scan_tiles;
43 const auto score = saturating_add(manhattan(current, portal),
44 manhattan(portal, request.goal));
45 if (!found_step || score < best_score) {
46 found_step = true;
47 best_score = score;
48 best_chunk = next_chunk;
49 best_portal = portal;
50 }
51 };
52
53 if (current_chunk.x != goal_chunk.x) {
54 auto next = current_chunk;
55 if (current_chunk.x < goal_chunk.x) {
56 ++next.x;
57 } else {
58 --next.x;
59 }
60 consider(next);
61 }
62 if (current_chunk.y != goal_chunk.y) {
63 auto next = current_chunk;
64 if (current_chunk.y < goal_chunk.y) {
65 ++next.y;
66 } else {
67 --next.y;
68 }
69 consider(next);
70 }
71 if (current_chunk.z != goal_chunk.z) {
72 auto next = current_chunk;
73 if (current_chunk.z < goal_chunk.z) {
74 ++next.z;
75 } else {
76 --next.z;
77 }
78 consider(next);
79 }
80
81 if (!found_step) {
82 result.found = false;
83 return result;
84 }
85 result.score =
86 saturating_add(result.score, manhattan(current, best_portal));
87 waypoints.push_back(best_portal);
88 current = best_portal;
89 current_chunk = best_chunk;
90 }
91
92 result.score = saturating_add(result.score, manhattan(current, request.goal));
93 return result;
94}
95
96} // namespace detail
97
98// HEURISTIC TIER: candidates walk only goal-monotone chunk staircases (each
99// step moves one chunk closer on some axis) and only the single
100// best-scoring candidate is stitched, so a NoPath from this builder means
101// "no route found by this tier", NOT "no route exists" -- topologies that
102// require a chunk detour away from the goal (or whose best seam tile is
103// sealed off) yield NoPath here while weighted_astar_path finds a route.
104// Callers needing an authoritative answer must fall back to
105// weighted_astar_path on NoPath. Promoting this to a distinct
106// non-authoritative status is deferred to the next API rev.
111template <typename World, typename PassableTag, typename CostTag>
113 const World& world, PathRequest request, PathScratch& scratch,
115 using Shape = typename World::shape_type;
116 // Builds a portal route over the dense chunk-portal topology graph and tracks
117 // chunk-version dependencies; dense-only until sparse topology (Slice 4) and
118 // the sparse route-cache slice land. Its per-segment weighted A* already runs
119 // natively on sparse worlds via weighted_astar_path.
120 static_assert(
121 std::is_same_v<typename World::residency_type, AlwaysResident>,
122 "build_weighted_chunk_portal_route_product is dense-only; it needs "
123 "sparse topology and route-cache support from a later slice.");
124
125 product.clear();
126 product.request_ = request;
127
128 if (!contains<Shape>(request.start)) {
129 product.status_ = PathStatus::InvalidStart;
130 return PathResult{product.status_, 0, 0, 0, product.path_};
131 }
132 if (!contains<Shape>(request.goal)) {
133 product.status_ = PathStatus::InvalidGoal;
134 return PathResult{product.status_, 0, 0, 0, product.path_};
135 }
136
137 constexpr auto orders = std::array{
138 std::array{detail::Axis::X, detail::Axis::Y, detail::Axis::Z},
139 std::array{detail::Axis::X, detail::Axis::Z, detail::Axis::Y},
140 std::array{detail::Axis::Y, detail::Axis::X, detail::Axis::Z},
141 std::array{detail::Axis::Y, detail::Axis::Z, detail::Axis::X},
142 std::array{detail::Axis::Z, detail::Axis::X, detail::Axis::Y},
143 std::array{detail::Axis::Z, detail::Axis::Y, detail::Axis::X},
144 };
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 =
151 detail::build_chunk_portal_candidate<World, PassableTag>(
152 world, request, order, product.candidate_waypoints_);
153 ++product.route_candidates_;
154 product.portal_scan_tiles_ += candidate.scan_tiles;
155 if (!candidate.found) {
156 continue;
157 }
158 if (!found_route || candidate.score < best_score) {
159 found_route = true;
160 best_score = candidate.score;
161 product.best_waypoints_.assign(product.candidate_waypoints_.begin(),
162 product.candidate_waypoints_.end());
163 }
164 }
165 {
166 const auto candidate =
167 detail::build_greedy_chunk_portal_candidate<World, PassableTag>(
168 world, request, product.candidate_waypoints_);
169 ++product.route_candidates_;
170 product.portal_scan_tiles_ += candidate.scan_tiles;
171 if (candidate.found && (!found_route || candidate.score < best_score)) {
172 found_route = true;
173 best_score = candidate.score;
174 product.best_waypoints_.assign(product.candidate_waypoints_.begin(),
175 product.candidate_waypoints_.end());
176 }
177 }
178 if (!found_route) {
179 product.status_ = PathStatus::NoPath;
180 // Failure results depend on world content the portal scans sampled;
181 // depend on every chunk so any edit invalidates a replayed failure.
182 product.dependencies_.capture_all(world);
183 return PathResult{product.status_, 0, 0, 0, product.path_};
184 }
185 product.waypoints_.assign(product.best_waypoints_.begin(),
186 product.best_waypoints_.end());
187
188 auto from = request.start;
189 auto total_cost = std::uint32_t{0};
190 auto total_expanded = std::size_t{0};
191 auto total_reached = std::size_t{0};
192 auto append_segment = [&](PathRequest segment_request) {
193 const auto result = weighted_astar_path<World, PassableTag, CostTag>(
194 world, segment_request, scratch);
195 total_expanded += result.expanded_nodes;
196 total_reached += result.reached_nodes;
197 if (result.status != PathStatus::Found) {
198 product.path_.clear();
199 product.status_ = result.status;
200 product.expanded_nodes_ = total_expanded;
201 product.reached_nodes_ = total_reached;
202 // Same failure-dependency contract as build_weighted_route_product;
203 // the failing segment's endpoints are the offending tiles.
204 detail::capture_failure_dependencies<Shape>(
205 world, segment_request, result.status, product.dependencies_);
206 return false;
207 }
208 total_cost = detail::saturating_add(total_cost, result.cost);
209 product.segment_.assign(result.path.begin(), result.path.end());
210 for (std::size_t i = product.path_.empty() ? 0u : 1u;
211 i < product.segment_.size(); ++i) {
212 product.path_.push_back(product.segment_[i]);
213 }
214 return true;
215 };
216
217 for (const auto waypoint : product.waypoints_) {
218 if (!append_segment(PathRequest{from, waypoint})) {
219 return PathResult{product.status_, 0, total_expanded, total_reached,
220 product.path_};
221 }
222 from = waypoint;
223 }
224 if (!append_segment(PathRequest{from, request.goal})) {
225 return PathResult{product.status_, 0, total_expanded, total_reached,
226 product.path_};
227 }
228
229 product.status_ = PathStatus::Found;
230 product.cost_ = total_cost;
231 product.expanded_nodes_ = total_expanded;
232 product.reached_nodes_ = total_reached;
233 for (const auto coord : product.path_) {
234 const auto key = tile_key<Shape>(coord);
235 product.dependencies_.add_chunk(world, chunk_key<Shape>(key));
236 }
237 return PathResult{product.status_, product.cost_, product.expanded_nodes_,
238 product.reached_nodes_, product.path_};
239}
240
241} // namespace tess
Definition path.h:535
friend auto build_weighted_chunk_portal_route_product(const World &world, PathRequest request, PathScratch &scratch, WeightedPortalRouteProduct &product) -> PathResult
Definition portal_route.h:112
Definition world.h:22
Specifies inclusive start and goal coordinates for a path query.
Definition path.h:54
Definition path.h:63
Definition shape.h:225