tess 1.0.0
Performance-first tile and path simulation substrate
Loading...
Searching...
No Matches
movement_class.h
1#pragma once
2
3#include <tess/storage/chunk_page.h>
4#include <tess/topology/step_policy.h>
5
6#include <concepts>
7#include <cstdint>
8#include <limits>
9#include <type_traits>
10
11// Movement vocabulary DSL. A MovementClass is a compile-time type that
12// fuses a passability predicate and an entry-cost expression, both composed
13// from typed-field leaves. Every leaf reads the constexpr
14// ChunkPage::field<Tag>(LocalTileId) at the (page, tile) seam -- world-scope
15// accessors are not constexpr, so the vocabulary deliberately operates on a
16// resolved page. Because the whole predicate inlines to the same &&/||/! a
17// hand-written cast would emit, threading a class through labeling / A* /
18// commit keeps single-field codegen (no std::function, no virtual).
19namespace tess::movement {
20
21// Marker base so movement_class_of can distinguish a class from a raw field tag
22// without needing a Page type to probe the MovementClassFor concept.
25
26// --- Boolean terms over typed fields -----------------------------------------
27
28// Truthy iff the named field is truthy at the tile.
30template <typename Tag>
31struct Field {
32 template <typename Page>
33 [[nodiscard]] static constexpr bool eval(const Page& page,
34 LocalTileId id) noexcept {
35 static_assert(Page::schema_type::template contains<Tag>,
36 "MovementClass references a field absent from the schema.");
37 return static_cast<bool>(page.template field<Tag>(id));
38 }
39};
40
41// Truthy iff the named (integral) field is non-zero -- e.g. a positive weight.
43template <typename Tag>
44struct NotZero {
45 template <typename Page>
46 [[nodiscard]] static constexpr bool eval(const Page& page,
47 LocalTileId id) noexcept {
48 static_assert(Page::schema_type::template contains<Tag>,
49 "MovementClass references a field absent from the schema.");
50 return page.template field<Tag>(id) != 0;
51 }
52};
53
55template <typename Term>
56struct Not {
57 template <typename Page>
58 [[nodiscard]] static constexpr bool eval(const Page& page,
59 LocalTileId id) noexcept {
60 return !Term::eval(page, id);
61 }
62};
63
65template <typename... Terms>
66struct AllOf {
67 template <typename Page>
68 [[nodiscard]] static constexpr bool eval(const Page& page,
69 LocalTileId id) noexcept {
70 return (true && ... && Terms::eval(page, id));
71 }
72};
73
75template <typename... Terms>
76struct AnyOf {
77 template <typename Page>
78 [[nodiscard]] static constexpr bool eval(const Page& page,
79 LocalTileId id) noexcept {
80 return (false || ... || Terms::eval(page, id));
81 }
82};
83
84// --- cost normalization ------------------------------------------------------
85
86// Byte-exact match to path::detail::tile_entry_cost_index (path.h): 0 (or any
87// non-positive signed value) means impassable, and the result saturates to
88// u32. The overflow compare casts through u64 first, exactly as the A* leaf
89// does, so a class-driven cost read is bit-identical to the legacy read.
94template <typename Value>
95[[nodiscard]] constexpr std::uint32_t normalize_cost(Value value) noexcept {
96 static_assert(std::is_integral_v<std::remove_cvref_t<Value>>,
97 "MovementClass cost field must be integral.");
98 if constexpr (std::is_signed_v<std::remove_cvref_t<Value>>) {
99 if (value <= 0) {
100 return 0;
101 }
102 } else if (value == 0) {
103 return 0;
104 }
105 if (static_cast<std::uint64_t>(value) >
106 std::numeric_limits<std::uint32_t>::max()) {
107 return std::numeric_limits<std::uint32_t>::max();
108 }
109 return static_cast<std::uint32_t>(value);
110}
111
112// --- cost EXPRESSIONS (0 == impassable, u32-saturated) -----------------------
113
115struct UnitCost {
116 template <typename Page>
117 [[nodiscard]] static constexpr std::uint32_t eval(const Page&,
118 LocalTileId) noexcept {
119 return 1;
120 }
121};
122
124template <std::uint32_t N>
126 template <typename Page>
127 [[nodiscard]] static constexpr std::uint32_t eval(const Page&,
128 LocalTileId) noexcept {
129 return N;
130 }
131};
132
134template <typename CostTag>
135struct FieldCost {
136 template <typename Page>
137 [[nodiscard]] static constexpr std::uint32_t eval(const Page& page,
138 LocalTileId id) noexcept {
139 static_assert(Page::schema_type::template contains<CostTag>,
140 "MovementClass references a field absent from the schema.");
141 return normalize_cost(page.template field<CostTag>(id));
142 }
143};
144
145// cost = Base == 0 ? 0 : saturating(Base + Overlay).
146// Base/Overlay are cost EXPRESSION types, not values.
167template <typename Base, typename Overlay>
169 template <typename Page>
170 [[nodiscard]] static constexpr std::uint32_t eval(const Page& page,
171 LocalTileId id) noexcept {
172 const auto base = Base::eval(page, id);
173 if (base == 0) {
174 return 0;
175 }
176 const auto sum = static_cast<std::uint64_t>(base) +
177 static_cast<std::uint64_t>(Overlay::eval(page, id));
178 constexpr auto ceiling =
179 static_cast<std::uint64_t>(std::numeric_limits<std::uint32_t>::max());
180 return static_cast<std::uint32_t>(sum > ceiling ? ceiling : sum);
181 }
182};
183
184// cost = SelTag(truthy) ? WhenSet::eval(page, id) : WhenClear::eval(page, id).
185// WhenSet/WhenClear are cost EXPRESSION types, not values.
187template <typename SelTag, typename WhenSet, typename WhenClear>
189 template <typename Page>
190 [[nodiscard]] static constexpr std::uint32_t eval(const Page& page,
191 LocalTileId id) noexcept {
192 static_assert(Page::schema_type::template contains<SelTag>,
193 "MovementClass references a field absent from the schema.");
194 return static_cast<bool>(page.template field<SelTag>(id))
195 ? WhenSet::eval(page, id)
196 : WhenClear::eval(page, id);
197 }
198};
199
200// --- the class ---------------------------------------------------------------
201
203template <typename PassExpr, typename CostExpr,
204 typename StepPolicyT = DefaultSteps>
206 using pass_expr = PassExpr;
207 using cost_expr = CostExpr;
208 using step_policy = StepPolicyT;
209
210 template <typename Page>
211 [[nodiscard]] static constexpr bool passable(const Page& page,
212 LocalTileId id) noexcept {
213 return PassExpr::eval(page, id);
214 }
215
216 template <typename Page>
217 [[nodiscard]] static constexpr std::uint32_t entry_cost(
218 const Page& page, LocalTileId id) noexcept {
219 return CostExpr::eval(page, id);
220 }
221};
222
223// A class C is usable against pages of type Page.
225template <typename C, typename Page>
226concept MovementClassFor = std::derived_from<C, movement_class_tag> &&
227 requires(const Page& page, LocalTileId id) {
228 {
229 C::passable(page, id)
230 } -> std::convertible_to<bool>;
231 {
232 C::entry_cost(page, id)
233 } -> std::convertible_to<std::uint32_t>;
234 };
235
236// --- field adapters ----------------------------------------------------------
237
238// The identity class for a single-field, unweighted world. It is a
239// distinct struct (NOT an alias) so it can carry the raw passability tag and
240// expose passable_span: per-class region labeling uses that fast path to keep
241// the identity flood a byte-identical field_span<Tag> scan.
243template <typename PassableTag>
244struct UnitCostFieldMovement : MovementClass<Field<PassableTag>, UnitCost> {
245 using passable_tag = PassableTag;
246
247 template <typename Page>
248 [[nodiscard]] static constexpr auto passable_span(Page& page) noexcept {
249 return page.template field_span<PassableTag>();
250 }
251};
252
253// Weighted identity that folds cost>0 into passability, so the region graph and
254// the weighted search agree exactly (recommended for new weighted classes).
255// Deliberately does NOT advertise the span fast path: its passability reads
256// two fields, so a raw single-field span scan would label cost-zero tiles.
258template <typename PassableTag, typename CostTag>
260 : MovementClass<AllOf<Field<PassableTag>, NotZero<CostTag>>,
261 FieldCost<CostTag>> {};
262
263// True for classes that expose the field_span fast path. `passable_tag` is
264// the advertisement: a class declaring it promises its passability predicate
265// is exactly the raw truthiness of that one field and MUST provide the
266// matching passable_span (the topology flood scans it verbatim). Composed
267// classes -- including PositiveCostFieldMovement, whose predicate reads two
268// fields -- must not declare it.
270template <typename C>
271concept HasPassableSpan = requires { typename C::passable_tag; };
272
273// Normalize a template argument that is EITHER a movement class OR a raw field
274// tag, so every unit-cost <World, PassableTag> call site compiles unchanged and
275// resolves to the byte-identical UnitCostFieldMovement.
277template <typename T>
278using movement_class_of =
279 std::conditional_t<std::derived_from<T, movement_class_tag>, T,
281
282namespace detail {
283
284// Adapts any normalized class to unit entry costs while retaining its
285// passability predicate and regular-step policy. Used by minimum-step APIs.
286template <typename ClassOrTag>
287struct UnitMovementClass : movement_class_tag {
288 using source_class = movement_class_of<ClassOrTag>;
289 using cost_expr = UnitCost;
290 using step_policy = step_policy_of<source_class>;
291
292 template <typename Page>
293 [[nodiscard]] static constexpr bool passable(const Page& page,
294 LocalTileId id) noexcept {
295 return source_class::passable(page, id);
296 }
297
298 template <typename Page>
299 [[nodiscard]] static constexpr std::uint32_t entry_cost(
300 const Page&, LocalTileId) noexcept {
301 return 1;
302 }
303};
304
305} // namespace detail
306
307} // namespace tess::movement
Checks whether a movement class advertises the exact field-span fast path.
Definition movement_class.h:271
Checks that C provides movement operations compatible with Page.
Definition movement_class.h:226
Definition shape.h:78
Requires every supplied passability term to evaluate true.
Definition movement_class.h:66
Requires at least one supplied passability term to evaluate true.
Definition movement_class.h:76
Produces compile-time constant entry cost N for every tile.
Definition movement_class.h:125
Definition step_policy.h:26
Reads and normalizes integral entry cost field CostTag.
Definition movement_class.h:135
Evaluates the truthiness of field Tag at a resolved tile.
Definition movement_class.h:31
Combines compile-time passability and entry-cost expressions.
Definition movement_class.h:205
Evaluates whether integral field Tag is nonzero at a resolved tile.
Definition movement_class.h:44
Negates one compile-time passability term.
Definition movement_class.h:56
Definition movement_class.h:168
Combines a truthy field and positive cost field into weighted movement.
Definition movement_class.h:261
Selects between two cost expressions using field SelTag.
Definition movement_class.h:188
Adapts a truthy passability field to unit-cost movement.
Definition movement_class.h:244
Produces unit entry cost for every tile.
Definition movement_class.h:115
Marks types that implement the compile-time movement-class contract.
Definition movement_class.h:24