tess 1.0.0
Performance-first tile and path simulation substrate
Loading...
Searching...
No Matches
transition_model.h
1#pragma once
2
3#include <tess/core/shape.h>
4#include <tess/topology/movement_class.h>
5#include <tess/topology/transition_provider.h>
6
7#include <algorithm>
8#include <concepts>
9#include <cstddef>
10#include <cstdint>
11#include <limits>
12#include <type_traits>
13#include <utility>
14
15namespace tess {
16
18enum class TransitionKind : std::uint8_t {
19 Regular,
20 Special,
21};
22
24enum class TransitionAvailability : std::uint8_t {
25 Legal,
26 Blocked,
27 MissingTopology,
28};
29
31template <typename Cost = std::uint32_t>
33 Coord3 to{};
34 std::uint64_t to_index = 0;
35 Cost cost = 0;
36 TransitionKind kind = TransitionKind::Regular;
37 TransitionAvailability availability = TransitionAvailability::Blocked;
38 bool cost_overflow = false;
39};
40
41namespace detail {
42
43struct RegularTransitionCandidate {
44 Coord3 to{};
45 Coord3 clearance_a{};
46 Coord3 clearance_b{};
47 std::uint32_t multiplier = 1;
48 std::uint8_t clearance_count = 0;
49};
50
51template <typename Shape, typename Sink>
52constexpr void emit_regular_candidate(Coord3 to, std::uint32_t multiplier,
53 Sink&& sink) {
54 if (contains<Shape>(to)) {
55 sink(RegularTransitionCandidate{.to = to, .multiplier = multiplier});
56 }
57}
58
59template <typename Shape, typename Policy, typename Sink>
60constexpr void emit_diagonal_candidate(Coord3 to, Coord3 clearance_a,
61 Coord3 clearance_b, Sink&& sink) {
62 if (contains<Shape>(to)) {
63 sink(RegularTransitionCandidate{
64 .to = to,
65 .clearance_a = clearance_a,
66 .clearance_b = clearance_b,
67 .multiplier = Policy::diagonal_step_multiplier,
68 .clearance_count = 2,
69 });
70 }
71}
72
73template <typename Shape, typename Sink>
74constexpr void for_each_orthogonal_face_candidate(Coord3 from,
75 std::uint32_t multiplier,
76 Sink&& sink) {
77 emit_regular_candidate<Shape>(Coord3{from.x + 1, from.y, from.z}, multiplier,
78 sink);
79 emit_regular_candidate<Shape>(Coord3{from.x - 1, from.y, from.z}, multiplier,
80 sink);
81 emit_regular_candidate<Shape>(Coord3{from.x, from.y + 1, from.z}, multiplier,
82 sink);
83 emit_regular_candidate<Shape>(Coord3{from.x, from.y - 1, from.z}, multiplier,
84 sink);
85 emit_regular_candidate<Shape>(Coord3{from.x, from.y, from.z + 1}, multiplier,
86 sink);
87 emit_regular_candidate<Shape>(Coord3{from.x, from.y, from.z - 1}, multiplier,
88 sink);
89}
90
91template <typename Shape, typename Sink>
92constexpr void for_each_hex_candidate(Coord3 from, Sink&& sink) {
93 // Axial coordinates occupy only z=0. Public geometric probes can receive an
94 // unchecked `from`; rejecting another plane prevents it from appearing to
95 // jump vertically into the hex plane merely because candidates use z=0.
96 if (from.z != 0) {
97 return;
98 }
99 emit_regular_candidate<Shape>(Coord3{from.x + 1, from.y, from.z}, 1, sink);
100 emit_regular_candidate<Shape>(Coord3{from.x - 1, from.y, from.z}, 1, sink);
101 emit_regular_candidate<Shape>(Coord3{from.x, from.y + 1, from.z}, 1, sink);
102 emit_regular_candidate<Shape>(Coord3{from.x, from.y - 1, from.z}, 1, sink);
103 emit_regular_candidate<Shape>(Coord3{from.x + 1, from.y - 1, from.z}, 1,
104 sink);
105 emit_regular_candidate<Shape>(Coord3{from.x - 1, from.y + 1, from.z}, 1,
106 sink);
107}
108
109template <typename Shape, typename Policy, typename Sink>
110constexpr void for_each_diagonal_candidate(Coord3 from, Sink&& sink) {
111 for_each_orthogonal_face_candidate<Shape>(from, Policy::cost_scale, sink);
112 if constexpr (!ShapeTraits<Shape>::degenerate_x &&
113 !ShapeTraits<Shape>::degenerate_y) {
114 emit_diagonal_candidate<Shape, Policy>(
115 Coord3{from.x + 1, from.y + 1, from.z},
116 Coord3{from.x + 1, from.y, from.z}, Coord3{from.x, from.y + 1, from.z},
117 sink);
118 emit_diagonal_candidate<Shape, Policy>(
119 Coord3{from.x + 1, from.y - 1, from.z},
120 Coord3{from.x + 1, from.y, from.z}, Coord3{from.x, from.y - 1, from.z},
121 sink);
122 emit_diagonal_candidate<Shape, Policy>(
123 Coord3{from.x - 1, from.y + 1, from.z},
124 Coord3{from.x - 1, from.y, from.z}, Coord3{from.x, from.y + 1, from.z},
125 sink);
126 emit_diagonal_candidate<Shape, Policy>(
127 Coord3{from.x - 1, from.y - 1, from.z},
128 Coord3{from.x - 1, from.y, from.z}, Coord3{from.x, from.y - 1, from.z},
129 sink);
130 } else if constexpr (!ShapeTraits<Shape>::degenerate_x &&
131 !ShapeTraits<Shape>::degenerate_z) {
132 emit_diagonal_candidate<Shape, Policy>(
133 Coord3{from.x + 1, from.y, from.z + 1},
134 Coord3{from.x + 1, from.y, from.z}, Coord3{from.x, from.y, from.z + 1},
135 sink);
136 emit_diagonal_candidate<Shape, Policy>(
137 Coord3{from.x + 1, from.y, from.z - 1},
138 Coord3{from.x + 1, from.y, from.z}, Coord3{from.x, from.y, from.z - 1},
139 sink);
140 emit_diagonal_candidate<Shape, Policy>(
141 Coord3{from.x - 1, from.y, from.z + 1},
142 Coord3{from.x - 1, from.y, from.z}, Coord3{from.x, from.y, from.z + 1},
143 sink);
144 emit_diagonal_candidate<Shape, Policy>(
145 Coord3{from.x - 1, from.y, from.z - 1},
146 Coord3{from.x - 1, from.y, from.z}, Coord3{from.x, from.y, from.z - 1},
147 sink);
148 } else {
149 emit_diagonal_candidate<Shape, Policy>(
150 Coord3{from.x, from.y + 1, from.z + 1},
151 Coord3{from.x, from.y + 1, from.z}, Coord3{from.x, from.y, from.z + 1},
152 sink);
153 emit_diagonal_candidate<Shape, Policy>(
154 Coord3{from.x, from.y + 1, from.z - 1},
155 Coord3{from.x, from.y + 1, from.z}, Coord3{from.x, from.y, from.z - 1},
156 sink);
157 emit_diagonal_candidate<Shape, Policy>(
158 Coord3{from.x, from.y - 1, from.z + 1},
159 Coord3{from.x, from.y - 1, from.z}, Coord3{from.x, from.y, from.z + 1},
160 sink);
161 emit_diagonal_candidate<Shape, Policy>(
162 Coord3{from.x, from.y - 1, from.z - 1},
163 Coord3{from.x, from.y - 1, from.z}, Coord3{from.x, from.y, from.z - 1},
164 sink);
165 }
166}
167
168template <typename Shape, typename Policy, typename Sink>
169constexpr void for_each_regular_candidate(Coord3 from, Sink&& sink) {
170 static_assert(movement::StepPolicyFor<Policy, Shape>);
171 // Public geometric probes accept unchecked coordinates. Reject them before
172 // constructing signed +/-1 offsets; every contained coordinate is at most
173 // Shape::size-1, and ShapeTraits requires each size axis to fit int64_t, so
174 // the candidate arithmetic below is overflow-safe after this guard.
175 if (!contains<Shape>(from)) {
176 return;
177 }
178 if constexpr (std::is_same_v<Policy, movement::DefaultSteps>) {
179 if constexpr (std::is_same_v<typename ShapeTraits<Shape>::lattice_type,
180 lattice::Orthogonal>) {
181 for_each_orthogonal_face_candidate<Shape>(from, 1, sink);
182 } else {
183 for_each_hex_candidate<Shape>(from, sink);
184 }
185 } else {
186 for_each_diagonal_candidate<Shape, Policy>(from, sink);
187 }
188}
189
190template <typename Shape>
191[[nodiscard]] constexpr auto transition_index(Coord3 coord) noexcept
192 -> std::uint64_t {
193 static_assert(ShapeTraits<Shape>::tile_key_bits <= 64,
194 "Resolved transitions require u64 tile keys.");
195 return static_cast<std::uint64_t>(tile_key<Shape>(coord).value);
196}
197
198[[nodiscard]] constexpr auto saturating_u32(UInt128 value) noexcept
199 -> std::uint32_t {
200 constexpr auto max = std::numeric_limits<std::uint32_t>::max();
201 return value.hi != 0 || value.lo > max ? max
202 : static_cast<std::uint32_t>(value.lo);
203}
204
205struct TransitionProbeSink {
206 constexpr void operator()(TransitionProbe<>) const noexcept {}
207};
208
209struct ChunkKeySink {
210 constexpr void operator()(ChunkKey) const noexcept {}
211};
212
213template <typename Expr, typename Schema, typename = void>
214struct CostExpressionMaximum {
215 static constexpr bool known = false;
216 static constexpr std::uint32_t value = 0;
217};
218
219template <typename Expr, typename Schema>
220struct CostExpressionMaximum<Expr, Schema,
221 std::void_t<decltype(Expr::maximum_entry_cost)>> {
222 static constexpr bool known = true;
223 static constexpr std::uint32_t value = Expr::maximum_entry_cost;
224};
225
226template <typename Schema>
227struct CostExpressionMaximum<movement::UnitCost, Schema> {
228 static constexpr bool known = true;
229 static constexpr std::uint32_t value = 1;
230};
231
232template <std::uint32_t N, typename Schema>
233struct CostExpressionMaximum<movement::ConstantCost<N>, Schema> {
234 static constexpr bool known = true;
235 static constexpr std::uint32_t value = N;
236};
237
238template <typename Tag, typename Schema>
239struct CostExpressionMaximum<movement::FieldCost<Tag>, Schema> {
240 using value_type = typename Schema::template value_type<Tag>;
241 static_assert(std::is_integral_v<value_type>);
242 static constexpr bool known = true;
243 static constexpr auto source_max = std::numeric_limits<value_type>::max();
244 static constexpr std::uint32_t value =
245 static_cast<std::uintmax_t>(source_max) >
246 std::numeric_limits<std::uint32_t>::max()
247 ? std::numeric_limits<std::uint32_t>::max()
248 : static_cast<std::uint32_t>(source_max);
249};
250
251template <typename Tag, typename Set, typename Clear, typename Schema>
252struct CostExpressionMaximum<movement::SelectCost<Tag, Set, Clear>, Schema> {
253 using set_max = CostExpressionMaximum<Set, Schema>;
254 using clear_max = CostExpressionMaximum<Clear, Schema>;
255 static constexpr bool known = set_max::known && clear_max::known;
256 static constexpr std::uint32_t value =
257 set_max::value > clear_max::value ? set_max::value : clear_max::value;
258};
259
260// Absorption only lowers the value, so the saturating sum of the two
261// operand maxima stays a sound upper bound.
262template <typename Base, typename Overlay, typename Schema>
263struct CostExpressionMaximum<movement::OverlayCost<Base, Overlay>, Schema> {
264 using base_max = CostExpressionMaximum<Base, Schema>;
265 using overlay_max = CostExpressionMaximum<Overlay, Schema>;
266 static constexpr bool known = base_max::known && overlay_max::known;
267 static constexpr std::uint32_t value = [] {
268 const auto sum = static_cast<std::uint64_t>(base_max::value) +
269 static_cast<std::uint64_t>(overlay_max::value);
270 constexpr auto ceiling =
271 static_cast<std::uint64_t>(std::numeric_limits<std::uint32_t>::max());
272 return static_cast<std::uint32_t>(sum > ceiling ? ceiling : sum);
273 }();
274};
275
276template <typename Class, typename Schema, typename = void>
277struct MovementClassMaximum {
278 static constexpr bool known = false;
279 static constexpr std::uint32_t value = 0;
280};
281
282template <typename Class, typename Schema>
283struct MovementClassMaximum<Class, Schema,
284 std::void_t<typename Class::cost_expr>>
285 : CostExpressionMaximum<typename Class::cost_expr, Schema> {};
286
287template <typename Provider>
288struct IsBuiltInStairProvider : std::false_type {};
289
290template <typename Tag>
291struct IsBuiltInStairProvider<StairTransitions<Tag>> : std::true_type {};
292
293template <typename Provider>
294inline constexpr bool provider_maximum_known =
295 std::is_same_v<Provider, AdjacentTransitions> ||
296 IsBuiltInStairProvider<Provider>::value ||
297 requires { Provider::maximum_transition_cost; };
298
299template <typename Provider>
300[[nodiscard]] consteval auto provider_maximum() -> std::uint32_t {
301 if constexpr (requires { Provider::maximum_transition_cost; }) {
302 return Provider::maximum_transition_cost;
303 } else {
304 return 0;
305 }
306}
307
308// Tests whether the conservative bound reaches the reserved u32 infinity
309// sentinel without ever materializing the potentially >128-bit product.
310// Both factors are u32, so their product and the rounded-up threshold fit u64.
311[[nodiscard]] constexpr auto compact_cost_bound_overflows(
312 UInt128 edges, std::uint32_t maximum_cost,
313 std::uint32_t maximum_step_multiplier) noexcept -> bool {
314 if (edges == UInt128{0} || maximum_cost == 0 ||
315 maximum_step_multiplier == 0) {
316 return false;
317 }
318 const auto factor =
319 static_cast<std::uint64_t>(maximum_cost) * maximum_step_multiplier;
320 constexpr auto infinity = std::numeric_limits<std::uint32_t>::max();
321 const auto first_overflowing_edge_count =
322 (static_cast<std::uint64_t>(infinity) + factor - 1u) / factor;
323 return edges >= UInt128{first_overflowing_edge_count};
324}
325
326} // namespace detail
327
329enum class CostRangeAssessment : std::uint8_t {
330 ProvenSafe,
331 PotentialOverflow,
332 Unknown,
333};
334
336template <typename World, typename MovementClass,
337 typename Provider = AdjacentTransitions>
338inline constexpr CostRangeAssessment path_cost_range_assessment = [] {
339 using Class = movement::movement_class_of<MovementClass>;
340 using Maximum =
341 detail::MovementClassMaximum<Class, typename World::schema_type>;
342 const auto tile_count =
343 UInt128{World::chunk_count} * UInt128{World::local_tile_count};
344 const auto edges = tile_count - UInt128{1};
345 if (edges == UInt128{0}) {
346 return CostRangeAssessment::ProvenSafe;
347 }
348 if constexpr (!Maximum::known || !detail::provider_maximum_known<Provider>) {
349 return CostRangeAssessment::Unknown;
350 } else {
351 constexpr auto provider_max = detail::provider_maximum<Provider>();
352 constexpr auto maximum_cost =
353 Maximum::value > provider_max ? Maximum::value : provider_max;
354 using Policy = movement::step_policy_of<Class>;
355 constexpr auto maximum_step_multiplier =
356 std::is_same_v<Policy, movement::DefaultSteps>
357 ? movement::DefaultSteps::maximum_step_multiplier
358 : Policy::maximum_step_multiplier;
359 return detail::compact_cost_bound_overflows(edges, maximum_cost,
360 maximum_step_multiplier)
361 ? CostRangeAssessment::PotentialOverflow
362 : CostRangeAssessment::ProvenSafe;
363 }
364}();
365
368template <typename World, typename MovementClass,
369 typename Provider = AdjacentTransitions>
370consteval void require_proven_path_cost_range() {
371 static_assert(
372 path_cost_range_assessment<World, MovementClass, Provider> ==
373 CostRangeAssessment::ProvenSafe,
374 "Path cost range is not proven safe for the compact uint32 domain.");
375}
376
378template <typename World, typename ClassOrTag,
379 typename Provider = AdjacentTransitions>
380class ResolvedTransitionModel {
381 public:
382 using world_type = World;
383 using shape_type = typename World::shape_type;
384 using class_type = movement::movement_class_of<ClassOrTag>;
385 using step_policy = movement::step_policy_of<class_type>;
386 using provider_type = Provider;
387 using cost_type = std::uint32_t;
388
390 "Movement step policy is invalid for the world shape.");
392 "Resolved exact search requires per-origin provider "
393 "enumeration.");
394
395 constexpr ResolvedTransitionModel() = default;
396 constexpr explicit ResolvedTransitionModel(Provider provider)
397 : provider_(std::move(provider)) {}
398
399 static constexpr auto lattice_identity =
400 ShapeTraits<shape_type>::lattice_identity;
401 static constexpr auto lattice_version =
402 ShapeTraits<shape_type>::lattice_version;
403 static constexpr auto step_policy_identity = step_policy::identity;
404 static constexpr std::uint32_t cost_scale = step_policy::cost_scale;
405 static constexpr bool preserves_default_connectivity =
406 std::is_same_v<typename ShapeTraits<shape_type>::lattice_type,
408 std::is_same_v<Provider, AdjacentTransitions>;
409 static constexpr bool has_special_transitions =
410 !std::is_same_v<Provider, AdjacentTransitions>;
411
412 template <typename Sink>
413 constexpr void for_each_forward(const World& world, Coord3 from,
414 std::uint64_t from_index, Sink&& sink) const {
415 (void)from_index;
416 detail::for_each_regular_candidate<shape_type, step_policy>(
417 from, [&](detail::RegularTransitionCandidate candidate) {
418 emit_candidate(world, candidate, candidate.to, sink);
419 });
420 provider_.for_each_forward(
421 world, from, [&](SpecialTransitionCandidate candidate) {
422 emit_special_candidate(world, candidate, candidate.to, sink);
423 });
424 }
425
426 template <typename Sink>
427 constexpr void for_each_reverse(const World& world, Coord3 to,
428 std::uint64_t to_index, Sink&& sink) const {
430 "Reverse fields require per-target provider enumeration.");
431 (void)to_index;
432 // Reverse enumeration describes forward edges whose destination is `to`.
433 // Rejecting a blocked/missing destination here makes the primitive
434 // symmetric with forward enumeration even for external callers that did
435 // not obtain `to` from a prevalidated field frontier.
436 if (!contains<shape_type>(to)) {
437 return;
438 }
439 const auto origin = world.resolve(to);
440 const auto* origin_page = world.try_chunk(origin.chunk_key);
441 if (origin_page == nullptr ||
442 !class_type::passable(*origin_page, origin.local_tile_id)) {
443 return;
444 }
445 detail::for_each_regular_candidate<shape_type, step_policy>(
446 to, [&](detail::RegularTransitionCandidate candidate) {
447 emit_candidate(world, candidate, to, sink);
448 });
449 provider_.for_each_reverse(
450 world, to, [&](SpecialTransitionCandidate candidate) {
451 emit_special_candidate(world, candidate, to, sink);
452 });
453 }
454
455 template <typename Sink>
456 constexpr void for_each_dependency_chunk(const World& world, Coord3 from,
457 Sink&& sink) const {
458 // Symmetric with the forward and reverse probes, which both reject an
459 // out-of-world coordinate. Without this, `chunk_coord` casts a negative
460 // component to unsigned and the sink receives an arbitrary out-of-range
461 // key -- which `capture_field_product_dependencies` uses to index an
462 // unchecked `seen` array.
463 if (!contains<shape_type>(from)) {
464 return;
465 }
466 sink(chunk_key<shape_type>(chunk_coord<shape_type>(from)));
467 detail::for_each_regular_candidate<shape_type, step_policy>(
468 from, [&](detail::RegularTransitionCandidate candidate) {
469 sink(chunk_key<shape_type>(chunk_coord<shape_type>(candidate.to)));
470 if (candidate.clearance_count != 0) {
471 sink(chunk_key<shape_type>(
472 chunk_coord<shape_type>(candidate.clearance_a)));
473 sink(chunk_key<shape_type>(
474 chunk_coord<shape_type>(candidate.clearance_b)));
475 }
476 });
477 provider_.for_each_forward(
478 world, from, [&](SpecialTransitionCandidate candidate) {
479 if (contains<shape_type>(candidate.to)) {
480 sink(chunk_key<shape_type>(chunk_coord<shape_type>(candidate.to)));
481 }
482 });
483 }
484
486 [[nodiscard]] static constexpr auto is_regular_candidate(Coord3 from,
487 Coord3 to) noexcept
488 -> bool {
489 auto found = false;
490 detail::for_each_regular_candidate<shape_type, step_policy>(
491 from, [&](detail::RegularTransitionCandidate candidate) {
492 found = found || candidate.to == to;
493 });
494 return found;
495 }
496
498 [[nodiscard]] static constexpr auto regular_availability(const World& world,
499 Coord3 from,
500 Coord3 to) noexcept
501 -> TransitionAvailability {
502 auto availability = TransitionAvailability::Blocked;
503 detail::for_each_regular_candidate<shape_type, step_policy>(
504 from, [&](detail::RegularTransitionCandidate candidate) {
505 if (candidate.to == to) {
506 availability = clearance_availability(world, candidate);
507 }
508 });
509 return availability;
510 }
511
512 [[nodiscard]] static constexpr auto heuristic(const World&, Coord3 from,
513 Coord3 goal) noexcept
514 -> cost_type {
515 if constexpr (has_special_transitions) {
516 return 0;
517 } else if constexpr (std::is_same_v<step_policy, movement::DefaultSteps>) {
518 if constexpr (std::is_same_v<
519 typename ShapeTraits<shape_type>::lattice_type,
520 lattice::HexAxial>) {
521 const auto distance =
522 hex_distance(to_hex_coord(from), to_hex_coord(goal));
523 return distance > std::numeric_limits<cost_type>::max()
524 ? std::numeric_limits<cost_type>::max()
525 : static_cast<cost_type>(distance);
526 } else {
527 const auto distance = manhattan_distance(from, goal);
528 return distance > std::numeric_limits<cost_type>::max()
529 ? std::numeric_limits<cost_type>::max()
530 : static_cast<cost_type>(distance);
531 }
532 } else {
533 return diagonal_heuristic(from, goal);
534 }
535 }
536
537 [[nodiscard]] constexpr auto revision() const noexcept -> std::uint64_t {
538 return detail::transition_provider_revision(provider_);
539 }
540
541 private:
542 enum class Clearance : std::uint8_t {
543 Clear,
544 Blocked,
545 Missing,
546 };
547
548 [[nodiscard]] static constexpr auto clearance_at(const World& world,
549 Coord3 coord) noexcept
550 -> Clearance {
551 const auto resolved = world.resolve(coord);
552 const auto* page = world.try_chunk(resolved.chunk_key);
553 if (page == nullptr) {
554 return Clearance::Missing;
555 }
556 return class_type::passable(*page, resolved.local_tile_id)
557 ? Clearance::Clear
558 : Clearance::Blocked;
559 }
560
561 [[nodiscard]] static constexpr auto clearance_availability(
562 const World& world_value,
563 detail::RegularTransitionCandidate candidate) noexcept
564 -> TransitionAvailability {
565 if constexpr (std::is_same_v<step_policy, movement::DefaultSteps>) {
566 return TransitionAvailability::Legal;
567 } else {
568 if (candidate.clearance_count == 0) {
569 return TransitionAvailability::Legal;
570 }
571 const auto a = clearance_at(world_value, candidate.clearance_a);
572 const auto b = clearance_at(world_value, candidate.clearance_b);
573 if constexpr (step_policy::corner_rule ==
574 movement::CornerRule::RequireBothClear) {
575 if (a == Clearance::Blocked || b == Clearance::Blocked) {
576 return TransitionAvailability::Blocked;
577 }
578 return a == Clearance::Missing || b == Clearance::Missing
579 ? TransitionAvailability::MissingTopology
580 : TransitionAvailability::Legal;
581 } else {
582 if (a == Clearance::Clear || b == Clearance::Clear) {
583 return TransitionAvailability::Legal;
584 }
585 return a == Clearance::Missing || b == Clearance::Missing
586 ? TransitionAvailability::MissingTopology
587 : TransitionAvailability::Blocked;
588 }
589 }
590 }
591
592 template <typename Sink>
593 static constexpr void emit_candidate(
594 const World& world, detail::RegularTransitionCandidate candidate,
595 Coord3 cost_coord, Sink&& sink) {
596 const auto resolved = world.resolve(candidate.to);
597 const auto* page = world.try_chunk(resolved.chunk_key);
598 if (page == nullptr) {
599 sink(TransitionProbe<>{
600 .to = candidate.to,
601 .to_index = detail::transition_index<shape_type>(candidate.to),
602 .availability = TransitionAvailability::MissingTopology,
603 });
604 return;
605 }
606 if (!class_type::passable(*page, resolved.local_tile_id)) {
607 return;
608 }
609 const auto clearance = clearance_availability(world, candidate);
610 if (clearance == TransitionAvailability::Blocked) {
611 return;
612 }
613 const auto cost_resolved = world.resolve(cost_coord);
614 const auto* cost_page = world.try_chunk(cost_resolved.chunk_key);
615 if (cost_page == nullptr) {
616 sink(TransitionProbe<>{
617 .to = candidate.to,
618 .to_index = detail::transition_index<shape_type>(candidate.to),
619 .availability = TransitionAvailability::MissingTopology,
620 });
621 return;
622 }
623 const auto entry_cost =
624 class_type::entry_cost(*cost_page, cost_resolved.local_tile_id);
625 if (entry_cost == 0) {
626 return;
627 }
628 const auto precise_cost =
629 UInt128{entry_cost} * UInt128{candidate.multiplier};
630 constexpr auto infinite = std::numeric_limits<std::uint32_t>::max();
631 sink(TransitionProbe<>{
632 .to = candidate.to,
633 .to_index = detail::transition_index<shape_type>(candidate.to),
634 .cost = detail::saturating_u32(precise_cost),
635 .availability = clearance,
636 .cost_overflow = precise_cost.hi != 0 || precise_cost.lo >= infinite,
637 });
638 }
639
640 template <typename Sink>
641 static constexpr void emit_special_candidate(
642 const World& world, SpecialTransitionCandidate candidate,
643 Coord3 forward_destination, Sink&& sink) {
644 if (!contains<shape_type>(candidate.to)) {
645 return;
646 }
647 const auto target_index =
648 detail::transition_index<shape_type>(candidate.to);
649 if (candidate.missing_topology) {
650 sink(TransitionProbe<>{
651 .to = candidate.to,
652 .to_index = target_index,
653 .kind = TransitionKind::Special,
654 .availability = TransitionAvailability::MissingTopology,
655 });
656 return;
657 }
658 const auto resolved = world.resolve(candidate.to);
659 const auto* page = world.try_chunk(resolved.chunk_key);
660 if (page == nullptr) {
661 sink(TransitionProbe<>{
662 .to = candidate.to,
663 .to_index = target_index,
664 .kind = TransitionKind::Special,
665 .availability = TransitionAvailability::MissingTopology,
666 });
667 return;
668 }
669 if (!class_type::passable(*page, resolved.local_tile_id)) {
670 return;
671 }
672 // Cost zero is the movement-class impassable sentinel. A provider's cost
673 // prices its edge; it does not override destination entry legality. This
674 // keeps planning aligned with commit for legacy classes whose passability
675 // predicate intentionally ignores cost.
676 const auto destination = world.resolve(forward_destination);
677 const auto* destination_page = world.try_chunk(destination.chunk_key);
678 if (destination_page == nullptr ||
679 class_type::entry_cost(*destination_page, destination.local_tile_id) ==
680 0) {
681 return;
682 }
683 if (candidate.cost == 0) {
684 return;
685 }
686 const auto precise_cost = UInt128{candidate.cost} * UInt128{cost_scale};
687 constexpr auto infinite = std::numeric_limits<std::uint32_t>::max();
688 sink(TransitionProbe<>{
689 .to = candidate.to,
690 .to_index = target_index,
691 .cost = detail::saturating_u32(precise_cost),
692 .kind = TransitionKind::Special,
693 .availability = TransitionAvailability::Legal,
694 .cost_overflow = precise_cost.hi != 0 || precise_cost.lo >= infinite,
695 });
696 }
697
698 [[nodiscard]] static constexpr auto diagonal_heuristic(Coord3 from,
699 Coord3 goal) noexcept
700 -> cost_type {
701 auto a = std::uint64_t{0};
702 auto b = std::uint64_t{0};
703 if constexpr (!ShapeTraits<shape_type>::degenerate_x &&
704 !ShapeTraits<shape_type>::degenerate_y) {
705 a = detail::abs_delta(from.x, goal.x);
706 b = detail::abs_delta(from.y, goal.y);
707 } else if constexpr (!ShapeTraits<shape_type>::degenerate_x &&
708 !ShapeTraits<shape_type>::degenerate_z) {
709 a = detail::abs_delta(from.x, goal.x);
710 b = detail::abs_delta(from.z, goal.z);
711 } else {
712 a = detail::abs_delta(from.y, goal.y);
713 b = detail::abs_delta(from.z, goal.z);
714 }
715 const auto minor = std::min(a, b);
716 const auto major = std::max(a, b);
717 const auto ticks = detail::add(
718 UInt128{minor} * UInt128{step_policy::diagonal_step_multiplier},
719 UInt128{major - minor} * UInt128{step_policy::cost_scale});
720 return detail::saturating_u32(ticks);
721 }
722
723 [[no_unique_address]] Provider provider_{};
724};
725
727template <typename Model, typename World>
728concept ForwardTransitionModelFor = requires(
729 const Model& model, const World& world, Coord3 from, std::uint64_t index) {
730 model.for_each_forward(world, from, index, detail::TransitionProbeSink{});
731 model.for_each_dependency_chunk(world, from, detail::ChunkKeySink{});
732 { model.heuristic(world, from, from) } -> std::convertible_to<std::uint32_t>;
733 { model.revision() } -> std::convertible_to<std::uint64_t>;
734};
735
737template <typename Model, typename World>
740 requires(const Model& model, const World& world, Coord3 to,
741 std::uint64_t index) {
742 model.for_each_reverse(world, to, index, detail::TransitionProbeSink{});
743 };
744
745} // namespace tess
static constexpr auto regular_availability(const World &world, Coord3 from, Coord3 to) noexcept -> TransitionAvailability
Definition transition_model.h:498
static constexpr auto is_regular_candidate(Coord3 from, Coord3 to) noexcept -> bool
Definition transition_model.h:486
Definition world.h:22
Definition transition_model.h:728
Definition transition_provider.h:73
Definition transition_model.h:738
Definition transition_provider.h:86
Definition step_policy.h:91
Supplies no special transitions beyond ordinary face adjacency.
Definition transition_provider.h:132
Definition shape.h:46
Definition transition_provider.h:14
Definition transition_model.h:32
Packed 128-bit key storage with a deliberately partial operator set.
Definition uint128.h:34
Definition lattice.h:15