tess 0.4.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
5#include <concepts>
6#include <cstdint>
7#include <limits>
8#include <type_traits>
9
10// Movement vocabulary DSL (M6). A MovementClass is a compile-time TYPE that
11// fuses a passability predicate and an entry-cost expression, both composed
12// from typed-field leaves. Every leaf reads the constexpr
13// ChunkPage::field<Tag>(LocalTileId) at the (page, tile) seam -- world-scope
14// accessors are not constexpr, so the vocabulary deliberately operates on a
15// resolved page. Because the whole predicate inlines to the same &&/||/! a
16// hand-written cast would emit, threading a class through labeling / A* /
17// commit keeps single-field codegen (no std::function, no virtual).
18namespace tess::movement {
19
20// Marker base so movement_class_of can distinguish a class from a raw field tag
21// without needing a Page type to probe the MovementClassFor concept.
24
25// --- boolean TERMS over typed fields -----------------------------------------
26
27// Truthy iff the named field is truthy at the tile.
29template <typename Tag>
30struct Field {
31 template <typename Page>
32 [[nodiscard]] static constexpr bool eval(const Page& page,
33 LocalTileId id) noexcept {
34 static_assert(Page::schema_type::template contains<Tag>,
35 "MovementClass references a field absent from the schema.");
36 return static_cast<bool>(page.template field<Tag>(id));
37 }
38};
39
40// Truthy iff the named (integral) field is non-zero -- e.g. a positive weight.
42template <typename Tag>
43struct NotZero {
44 template <typename Page>
45 [[nodiscard]] static constexpr bool eval(const Page& page,
46 LocalTileId id) noexcept {
47 static_assert(Page::schema_type::template contains<Tag>,
48 "MovementClass references a field absent from the schema.");
49 return page.template field<Tag>(id) != 0;
50 }
51};
52
54template <typename Term>
55struct Not {
56 template <typename Page>
57 [[nodiscard]] static constexpr bool eval(const Page& page,
58 LocalTileId id) noexcept {
59 return !Term::eval(page, id);
60 }
61};
62
64template <typename... Terms>
65struct AllOf {
66 template <typename Page>
67 [[nodiscard]] static constexpr bool eval(const Page& page,
68 LocalTileId id) noexcept {
69 return (true && ... && Terms::eval(page, id));
70 }
71};
72
74template <typename... Terms>
75struct AnyOf {
76 template <typename Page>
77 [[nodiscard]] static constexpr bool eval(const Page& page,
78 LocalTileId id) noexcept {
79 return (false || ... || Terms::eval(page, id));
80 }
81};
82
83// --- cost normalization ------------------------------------------------------
84
85// Byte-exact match to path::detail::tile_entry_cost_index (path.h): 0 (or any
86// non-positive signed value) means impassable, and the result saturates to
87// u32. The overflow compare casts through u64 first, exactly as the A* leaf
88// does, so a class-driven cost read is bit-identical to the legacy read.
93template <typename Value>
94[[nodiscard]] constexpr std::uint32_t normalize_cost(Value value) noexcept {
95 static_assert(std::is_integral_v<std::remove_cvref_t<Value>>,
96 "MovementClass cost field must be integral.");
97 if constexpr (std::is_signed_v<std::remove_cvref_t<Value>>) {
98 if (value <= 0) {
99 return 0;
100 }
101 } else if (value == 0) {
102 return 0;
103 }
104 if (static_cast<std::uint64_t>(value) >
105 std::numeric_limits<std::uint32_t>::max()) {
106 return std::numeric_limits<std::uint32_t>::max();
107 }
108 return static_cast<std::uint32_t>(value);
109}
110
111// --- cost EXPRESSIONS (0 == impassable, u32-saturated) -----------------------
112
114struct UnitCost {
115 template <typename Page>
116 [[nodiscard]] static constexpr std::uint32_t eval(const Page&,
117 LocalTileId) noexcept {
118 return 1;
119 }
120};
121
123template <std::uint32_t N>
125 template <typename Page>
126 [[nodiscard]] static constexpr std::uint32_t eval(const Page&,
127 LocalTileId) noexcept {
128 return N;
129 }
130};
131
133template <typename CostTag>
134struct FieldCost {
135 template <typename Page>
136 [[nodiscard]] static constexpr std::uint32_t eval(const Page& page,
137 LocalTileId id) noexcept {
138 static_assert(Page::schema_type::template contains<CostTag>,
139 "MovementClass references a field absent from the schema.");
140 return normalize_cost(page.template field<CostTag>(id));
141 }
142};
143
144// cost = SelTag(truthy) ? WhenSet::eval(page, id) : WhenClear::eval(page, id).
145// WhenSet/WhenClear are cost EXPRESSION types, not values.
147template <typename SelTag, typename WhenSet, typename WhenClear>
149 template <typename Page>
150 [[nodiscard]] static constexpr std::uint32_t eval(const Page& page,
151 LocalTileId id) noexcept {
152 static_assert(Page::schema_type::template contains<SelTag>,
153 "MovementClass references a field absent from the schema.");
154 return static_cast<bool>(page.template field<SelTag>(id))
155 ? WhenSet::eval(page, id)
156 : WhenClear::eval(page, id);
157 }
158};
159
160// --- the class ---------------------------------------------------------------
161
163template <typename PassExpr, typename CostExpr>
165 using pass_expr = PassExpr;
166 using cost_expr = CostExpr;
167
168 template <typename Page>
169 [[nodiscard]] static constexpr bool passable(const Page& page,
170 LocalTileId id) noexcept {
171 return PassExpr::eval(page, id);
172 }
173
174 template <typename Page>
175 [[nodiscard]] static constexpr std::uint32_t entry_cost(
176 const Page& page, LocalTileId id) noexcept {
177 return CostExpr::eval(page, id);
178 }
179};
180
181// A class C is usable against pages of type Page.
183template <typename C, typename Page>
184concept MovementClassFor = std::derived_from<C, movement_class_tag> &&
185 requires(const Page& page, LocalTileId id) {
186 {
187 C::passable(page, id)
188 } -> std::convertible_to<bool>;
189 {
190 C::entry_cost(page, id)
191 } -> std::convertible_to<std::uint32_t>;
192 };
193
194// --- identity / default classes (backward compatibility) ---------------------
195
196// The identity class for the legacy single-field, unweighted world. It is a
197// distinct struct (NOT an alias) so it can carry the raw passability tag and
198// expose passable_span: per-class region labeling uses that fast path to keep
199// the identity flood a byte-identical field_span<Tag> scan.
201template <typename PassableTag>
202struct WalkableField : MovementClass<Field<PassableTag>, UnitCost> {
203 using passable_tag = PassableTag;
204
205 template <typename Page>
206 [[nodiscard]] static constexpr auto passable_span(Page& page) noexcept {
207 return page.template field_span<PassableTag>();
208 }
209};
210
211// Weighted identity that folds cost>0 into passability, so the region graph and
212// the weighted search agree exactly (recommended for new weighted classes).
213// Deliberately does NOT advertise the span fast path: its passability reads
214// two fields, so a raw single-field span scan would label cost-zero tiles.
216template <typename PassableTag, typename CostTag>
218 : MovementClass<AllOf<Field<PassableTag>, NotZero<CostTag>>,
219 FieldCost<CostTag>> {};
220
221// Weighted class preserving the legacy asymmetry: passability ignores cost
222// (graph is more permissive than the weighted search, never the reverse), used
223// by the legacy <World, PassableTag, CostTag> forwarders.
225template <typename PassableTag, typename CostTag>
227
228// True for classes that expose the field_span fast path. `passable_tag` is
229// the advertisement: a class declaring it promises its passability predicate
230// is exactly the raw truthiness of that one field and MUST provide the
231// matching passable_span (the topology flood scans it verbatim). Composed
232// classes -- including WalkableCostField, whose predicate reads two fields --
233// must not declare it.
235template <typename C>
236concept HasPassableSpan = requires { typename C::passable_tag; };
237
238// Normalize a template argument that is EITHER a movement class OR a raw field
239// tag, so every legacy <World, PassableTag> call site compiles unchanged and
240// resolves to the byte-identical WalkableField.
242template <typename T>
243using movement_class_of =
244 std::conditional_t<std::derived_from<T, movement_class_tag>, T,
246
247} // namespace tess::movement
Checks whether a movement class advertises the exact field-span fast path.
Definition movement_class.h:236
Checks that C provides movement operations compatible with Page.
Definition movement_class.h:184
Definition shape.h:59
Requires every supplied passability term to evaluate true.
Definition movement_class.h:65
Requires at least one supplied passability term to evaluate true.
Definition movement_class.h:75
Produces compile-time constant entry cost N for every tile.
Definition movement_class.h:124
Reads and normalizes integral entry cost field CostTag.
Definition movement_class.h:134
Evaluates the truthiness of field Tag at a resolved tile.
Definition movement_class.h:30
Combines compile-time passability and entry-cost expressions.
Definition movement_class.h:164
Evaluates whether integral field Tag is nonzero at a resolved tile.
Definition movement_class.h:43
Negates one compile-time passability term.
Definition movement_class.h:55
Selects between two cost expressions using field SelTag.
Definition movement_class.h:148
Produces unit entry cost for every tile.
Definition movement_class.h:114
Combines a truthy field and positive cost field into weighted movement.
Definition movement_class.h:219
Adapts a truthy passability field to unit-cost movement.
Definition movement_class.h:202
Marks types that implement the compile-time movement-class contract.
Definition movement_class.h:23