载入中...
搜索中...
未找到
HexSearch.cpp
浏览该文件的文档.
1
6#include "hexmap/HexSearch.h"
7
8#include "common/Diagnostic.h"
9
10#include <algorithm>
11#include <string>
12
13namespace eve::hexmap {
14namespace {
15
16[[nodiscard]] Result<HexPath> pathInvalidArgument(const std::string& message) {
18}
19
20[[nodiscard]]
21[[nodiscard]] Result<HexPath> unreachable(const std::string& message) {
23}
24
31[[nodiscard]] std::int32_t turnOf(std::int32_t distance, std::int32_t speed) noexcept {
32 if (distance <= 0) return 0;
33 return (distance - 1) / speed;
34}
35
36} // namespace
37
38// --- HexSearchContext -------------------------------------------------------
39
40void HexSearchContext::resize(std::int32_t cellCount) {
41 if (cellCount <= 0) {
42 data_.clear();
43 } else {
44 data_.assign(static_cast<std::size_t>(cellCount), HexSearchData{});
45 }
46 buckets_.clear();
47 minimum_ = 0;
48}
49
50HexSearchData& HexSearchContext::data(std::int32_t cellIndex) noexcept {
51 if (cellIndex < 0 || cellIndex >= cellCount()) return sink_;
52 return data_[static_cast<std::size_t>(cellIndex)];
53}
54
55const HexSearchData& HexSearchContext::data(std::int32_t cellIndex) const noexcept {
56 if (cellIndex < 0 || cellIndex >= cellCount()) return sink_;
57 return data_[static_cast<std::size_t>(cellIndex)];
58}
59
60std::int32_t HexSearchContext::beginPhase() noexcept {
61 phase_ += 2;
62 // Clearing the frontier is only this: the bucket array holds no owned state,
63 // and the per-cell records are invalidated lazily by the phase check, so a
64 // new search never pays for resetting the grid.
65 buckets_.clear();
66 minimum_ = 0;
67 return phase_;
68}
69
70void HexSearchContext::enqueue(std::int32_t cellIndex) noexcept {
71 if (data_.empty() || cellIndex < 0 || cellIndex >= cellCount()) return;
72 std::int32_t priority = data_[cellIndex].priority();
73 if (priority < 0) priority = 0;
74 if (priority < minimum_) minimum_ = priority;
75 if (static_cast<std::size_t>(priority) >= buckets_.size())
76 buckets_.resize(static_cast<std::size_t>(priority) + 1, -1);
77 // Link the cell at the head of its bucket: equal-priority cells form an
78 // intrusive singly linked list through `nextWithSamePriority`.
79 data_[cellIndex].nextWithSamePriority = buckets_[static_cast<std::size_t>(priority)];
80 buckets_[static_cast<std::size_t>(priority)] = cellIndex;
81}
82
83HexSearchPop HexSearchContext::dequeue(std::int32_t& outCellIndex) noexcept {
84 const std::int32_t bucketCount = static_cast<std::int32_t>(buckets_.size());
85 for (; minimum_ < bucketCount; ++minimum_) {
86 const std::int32_t head = buckets_[static_cast<std::size_t>(minimum_)];
87 if (head < 0) continue;
88 buckets_[static_cast<std::size_t>(minimum_)] = data(head).nextWithSamePriority;
89 outCellIndex = head;
90 return HexSearchPop::Cell;
91 }
93}
94
95void HexSearchContext::unlink(std::int32_t cellIndex, std::int32_t bucket) noexcept {
96 if (bucket < 0 || static_cast<std::size_t>(bucket) >= buckets_.size()) return;
97 std::int32_t current = buckets_[static_cast<std::size_t>(bucket)];
98 if (current < 0) return;
99 if (current == cellIndex) {
100 buckets_[static_cast<std::size_t>(bucket)] = data(current).nextWithSamePriority;
101 return;
102 }
103 while (true) {
104 const std::int32_t next = data(current).nextWithSamePriority;
105 if (next < 0) return;
106 if (next == cellIndex) {
107 data(current).nextWithSamePriority = data(next).nextWithSamePriority;
108 return;
109 }
110 current = next;
111 }
112}
113
114void HexSearchContext::change(std::int32_t cellIndex, std::int32_t oldPriority) noexcept {
115 unlink(cellIndex, oldPriority);
116 enqueue(cellIndex);
117}
118
119// --- movement rules ---------------------------------------------------------
120
122 if (!map.contains(coordinates)) return false;
123 // `canHoldUnit` is the shared "explored, explorable, above water" terrain rule;
124 // occupancy by another actor is the caller's extra clause.
125 const HexCellData* cell = map.cell(coordinates);
126 if (cell == nullptr || !canHoldUnit(*cell)) return false;
127 if (occupied && occupied(coordinates)) return false;
128 return true;
129}
130
133 if (!isValidDestination(map, to, occupied)) return -1;
134
135 const HexEdgeType type = edgeType(map.elevation(from), map.elevation(to));
136 if (type == HexEdgeType::Cliff) return -1;
137
138 if (map.flags(from).hasRoad(direction)) return 1;
139 if (map.isWalled(from) != map.isWalled(to)) return -1;
140
141 const std::int32_t base = type == HexEdgeType::Flat ? 5 : 10;
142 return base + map.urbanLevel(to) + map.farmLevel(to) + map.plantLevel(to);
143}
144
145// --- pathfinding ------------------------------------------------------------
146
148 const HexMoveRules& rules, const HexOccupancyQuery& occupied) {
149 if (map.empty()) return pathInvalidArgument("cannot search an empty hex map");
150 if (!map.contains(from)) return pathInvalidArgument("path origin is outside the hex map");
151 if (!map.contains(to)) return pathInvalidArgument("path goal is outside the hex map");
152 if (scratch.cellCount() != map.cellCount())
153 return pathInvalidArgument("search scratch must be resized to map.cellCount() before findPath");
154 if (!isValidDestination(map, to, occupied)) return unreachable("path goal is not a valid destination");
155
156 const std::int32_t speed = std::max<std::int32_t>(1, rules.speed);
157 const std::int32_t start = map.indexOf(from);
158 const std::int32_t goal = map.indexOf(to);
159 const HexCoordinates goalCoord = to;
160 const std::int32_t phase = scratch.beginPhase();
161
162 // The origin has to be written completely, not just tagged with the new phase.
163 // `beginPhase` only bumps the phase and clears the buckets, so a reused context
164 // (the module owns exactly one `scratch_`) still holds the previous search's
165 // `distance`, `heuristic` and `pathFrom` here. `enqueue` derives the bucket from
166 // `priority()`, and the first relaxation reads `data(start).distance`, so a stale
167 // origin shifted every turn number and could pick a different route. The start
168 // cell is never relaxed as a neighbour, so nothing else can fix it up.
169 HexSearchData& startData = scratch.data(start);
170 startData.searchPhase = phase;
171 startData.distance = 0;
172 startData.pathFrom = -1;
173 startData.heuristic = from.distanceTo(goalCoord);
174 scratch.enqueue(start);
175
176 bool found = false;
177 while (true) {
178 std::int32_t current = -1;
179 if (scratch.dequeue(current) != HexSearchPop::Cell) break;
180
181 const std::int32_t currentDistance = scratch.data(current).distance;
182 // Marking the record with `phase + 1` is how the reference says "expanded":
183 // an untouched record still carries the older phase, so the distinction
184 // needs no clearing pass.
185 scratch.data(current).searchPhase = phase + 1;
186
187 if (current == goal) {
188 found = true;
189 break;
190 }
191
192 const std::int32_t currentTurn = turnOf(currentDistance, speed);
193 const HexCoordinates currentCoord = map.coordinatesAt(current);
194
195 for (std::int32_t i = 0; i < kHexDirectionCount; ++i) {
196 const HexDirection direction = static_cast<HexDirection>(i);
197 HexCoordinates neighbour{};
198 if (!map.getNeighbor(currentCoord, direction, neighbour)) continue;
199
200 const std::int32_t neighbourIndex = map.indexOf(neighbour);
201 if (neighbourIndex < 0) continue;
202 // Already expanded: its bucket entry was consumed and it must not be
203 // relaxed again within this phase.
204 if (scratch.data(neighbourIndex).searchPhase == phase + 1) continue;
205
206 const std::int32_t step = moveCost(map, currentCoord, neighbour, direction, occupied);
207 if (step < 0) continue;
208
209 std::int32_t distance = currentDistance + step;
210 const std::int32_t turn = turnOf(distance, speed);
211 if (turn > currentTurn) distance = turn * speed + step;
212
213 HexSearchData& neighbourData = scratch.data(neighbourIndex);
214 if (neighbourData.searchPhase != phase) {
215 // A fresh record is fully overwritten; it carries no bucket link yet.
216 neighbourData.searchPhase = phase;
217 neighbourData.distance = distance;
218 neighbourData.pathFrom = current;
219 neighbourData.heuristic = neighbour.distanceTo(goalCoord);
220 scratch.enqueue(neighbourIndex);
221 } else if (distance < neighbourData.distance) {
222 // The priority must be read before the record is updated: `change`
223 // unlinks the cell from the bucket it is still sitting in.
224 const std::int32_t oldPriority = neighbourData.priority();
225 neighbourData.distance = distance;
226 neighbourData.pathFrom = current;
227 scratch.change(neighbourIndex, oldPriority);
228 }
229 }
230 }
231
232 if (!found) return unreachable("no path exists between the two cells");
233
235 for (std::int32_t index = goal; index != start; index = scratch.data(index).pathFrom) {
236 if (index < 0) return unreachable("path reconstruction lost the chain to the origin");
237 const std::int32_t distance = scratch.data(index).distance;
238 path.cells.push_back(index);
239 path.turns.push_back(turnOf(distance, speed));
240 }
241 path.cells.push_back(start);
242 path.turns.push_back(turnOf(scratch.data(start).distance, speed));
243 std::reverse(path.cells.begin(), path.cells.end());
244 std::reverse(path.turns.begin(), path.turns.end());
245 return Result<HexPath>::success(std::move(path));
246}
247
248// --- visibility -------------------------------------------------------------
249
251 std::int32_t range, std::vector<std::int32_t>& out) {
252 out.clear();
253 if (map.empty()) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "cannot collect visibility from an empty hex map", "hexmap"));
254 if (!map.contains(from)) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "view origin is outside the hex map", "hexmap"));
255 if (scratch.cellCount() != map.cellCount())
256 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "search scratch must be resized to map.cellCount() before collectVisibleCells", "hexmap"));
257
258 const std::int32_t searchRange = range + map.values(from).viewElevation();
259 const std::int32_t start = map.indexOf(from);
260 const std::int32_t phase = scratch.beginPhase();
261
262 scratch.data(start).searchPhase = phase;
263 scratch.data(start).distance = 0;
264 scratch.enqueue(start);
265
266 while (true) {
267 std::int32_t current = -1;
268 if (scratch.dequeue(current) != HexSearchPop::Cell) break;
269
270 // Nearest first: the frontier always pops the cheapest step count, so the
271 // produced order is stable for a given map.
272 scratch.data(current).searchPhase = phase + 1;
273 out.push_back(current);
274
275 const std::int32_t currentDistance = scratch.data(current).distance;
276 const HexCoordinates currentCoord = map.coordinatesAt(current);
277
278 for (std::int32_t i = 0; i < kHexDirectionCount; ++i) {
279 const HexDirection direction = static_cast<HexDirection>(i);
280 HexCoordinates neighbour{};
281 if (!map.getNeighbor(currentCoord, direction, neighbour)) continue;
282 if (!map.isExplorable(neighbour)) continue;
283
284 const std::int32_t neighbourIndex = map.indexOf(neighbour);
285 if (neighbourIndex < 0) continue;
286 if (scratch.data(neighbourIndex).searchPhase == phase + 1) continue;
287
288 const std::int32_t distance = currentDistance + 1;
289 if (distance + map.values(neighbour).viewElevation() > searchRange) continue;
290 if (distance > from.distanceTo(neighbour)) continue;
291
292 HexSearchData& neighbourData = scratch.data(neighbourIndex);
293 if (neighbourData.searchPhase != phase) {
294 neighbourData.searchPhase = phase;
295 neighbourData.distance = distance;
296 neighbourData.pathFrom = current;
297 neighbourData.heuristic = 0;
298 scratch.enqueue(neighbourIndex);
299 } else if (distance < neighbourData.distance) {
300 const std::int32_t oldPriority = neighbourData.priority();
301 neighbourData.distance = distance;
302 neighbourData.pathFrom = current;
303 scratch.change(neighbourIndex, oldPriority);
304 }
305 }
306 }
307 return Result<void>::success();
308}
309
310} // namespace eve::hexmap
Duration start
std::string from
int priority
float phase
Definition CaveMesh.cpp:58
Stable, structured diagnostics shared by engine modules.
std::string message
Cell-graph search used by pathfinding and visibility.
HexCoordinates to
Cell the unit walks towards on this segment.
Definition HexUnits.cpp:64
Range range
float distance
std::string path
Definition PlayHost.cpp:110
RoadLaneDirection direction
bool found
double current
bool occupied
Cell cell
TacticalUnit::TurnResources turn
float step
Definition TreeMesh.cpp:314
uint32_t index
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
constexpr bool hasRoad(HexDirection d) const noexcept
True when road.
Definition HexCell.h:152
An editable, chunked, pointy-top hex map.
Definition HexMap.h:72
bool empty() const noexcept
Whether the map holds any cell.
Definition HexMap.h:92
HexCoordinates coordinatesAt(std::int32_t index) const noexcept
Coordinates of a linear cell index; out-of-range indices return (0, 0).
Definition HexMap.cpp:69
std::int32_t indexOf(HexCoordinates coordinates) const noexcept
Linear cell index of coordinates (offset order), or -1 when outside the grid.
Definition HexMap.cpp:64
HexValues values(HexCoordinates c) const noexcept
Full packed value record of a cell (zero when outside the grid).
Definition HexMap.cpp:356
bool contains(HexCoordinates coordinates) const noexcept
Whether coordinates address a cell inside the grid.
Definition HexMap.h:122
std::int32_t cellCount() const noexcept
Total number of cells.
Definition HexMap.h:104
bool getNeighbor(HexCoordinates coordinates, HexDirection direction, HexCoordinates &out) const noexcept
Neighbour of coordinates in direction; false when it is off-grid.
Definition HexMap.cpp:87
bool isExplorable(HexCoordinates c) const noexcept
Whether a cell may ever be revealed by a viewer.
Definition HexMap.cpp:352
bool isWalled(HexCoordinates c) const noexcept
Definition HexMap.cpp:344
HexFlags flags(HexCoordinates c) const noexcept
Full packed flag record of a cell (zero when outside the grid).
Definition HexMap.cpp:360
std::int32_t elevation(HexCoordinates c) const noexcept
Definition HexMap.cpp:300
const HexCellData * cell(HexCoordinates coordinates) const noexcept
Cell record, or null when outside the grid. @ownership Borrowed; the map owns the record....
Definition HexMap.cpp:79
std::int32_t urbanLevel(HexCoordinates c) const noexcept
Definition HexMap.cpp:312
std::int32_t plantLevel(HexCoordinates c) const noexcept
Definition HexMap.cpp:320
std::int32_t farmLevel(HexCoordinates c) const noexcept
Definition HexMap.cpp:316
Reusable search scratch: one record per cell plus a priority bucket queue.
Definition HexSearch.h:68
void resize(std::int32_t cellCount)
Sizes the scratch for cellCount cells, preserving nothing.
Definition HexSearch.cpp:40
HexSearchPop dequeue(std::int32_t &outCellIndex) noexcept
Removes the lowest-priority cell.
Definition HexSearch.cpp:83
void change(std::int32_t cellIndex, std::int32_t oldPriority) noexcept
Re-queues a cell whose priority changed after it was enqueued.
std::int32_t beginPhase() noexcept
Starts a new search: clears the frontier and advances the phase.
Definition HexSearch.cpp:60
HexSearchData & data(std::int32_t cellIndex) noexcept
Mutable record of one cell.
Definition HexSearch.cpp:50
void enqueue(std::int32_t cellIndex) noexcept
Adds a cell at its current priority.
Definition HexSearch.cpp:70
std::int32_t cellCount() const noexcept
Number of cells this context can hold.
Definition HexSearch.h:80
constexpr std::int32_t viewElevation() const noexcept
Elevation used for line of sight: the higher of land and water.
Definition HexCell.h:70
constexpr bool canHoldUnit(const HexCellData &cell) noexcept
Whether a cell record is in a state that can hold a unit.
Definition HexCell.h:261
HexSearchPop
Outcome of one frontier pop.
Definition HexSearch.h:45
@ Cell
A cell was removed; the out-parameter holds it.
@ Empty
The frontier is empty; the out-parameter is untouched.
std::int32_t moveCost(const HexMap &map, HexCoordinates from, HexCoordinates to, HexDirection direction, const HexOccupancyQuery &occupied)
Cost of moving between two adjacent cells.
constexpr std::int32_t kHexDirectionCount
Number of hex edges / facing directions.
Definition HexMetrics.h:62
constexpr HexEdgeType edgeType(int elevation1, int elevation2) noexcept
The relationship between two elevations (single-step changes are slopes).
Definition HexMetrics.h:96
HexEdgeType
Relationship between two neighbouring cells of different elevation.
Definition HexMetrics.h:93
std::function< bool(HexCoordinates)> HexOccupancyQuery
Predicate answering whether a cell is already occupied by another actor.
Definition HexSearch.h:143
bool isValidDestination(const HexMap &map, HexCoordinates coordinates, const HexOccupancyQuery &occupied)
Whether an actor may occupy a cell.
constexpr HexDirection next(HexDirection d) noexcept
The next direction clockwise (NW wraps to NE).
Definition HexMetrics.h:76
Result< HexPath > findPath(const HexMap &map, HexSearchContext &scratch, HexCoordinates from, HexCoordinates to, const HexMoveRules &rules, const HexOccupancyQuery &occupied)
Finds the cheapest path between two cells.
Result< void > collectVisibleCells(const HexMap &map, HexSearchContext &scratch, HexCoordinates from, std::int32_t range, std::vector< std::int32_t > &out)
Collects every cell visible from from within range.
HexDirection
Hex facing directions, counter-clockwise from north-east.
Definition HexMetrics.h:59
One cell of the hex map: packed values plus packed flags.
Definition HexCell.h:244
Axial coordinates of one hex cell.
Movement tuning of one actor; cells per turn and vision radius in cells.
Definition HexSearch.h:130
std::int32_t speed
Movement points available per turn.
Definition HexSearch.h:132
One found path: the cells from origin to goal, with the turn each one is reached on.
Definition HexSearch.h:178
Per-cell scratch record of one search.
Definition HexSearch.h:23
std::int32_t priority() const noexcept
Ordering key of the priority queue (distance + heuristic).
Definition HexSearch.h:36
std::int32_t distance
Shortest distance from the search origin found so far.
Definition HexSearch.h:25
std::int32_t pathFrom
Cell the path entered this cell from, or -1.
Definition HexSearch.h:27
std::int32_t searchPhase
Phase this record was written in; a record from an older phase is unvisited.
Definition HexSearch.h:31
std::int32_t heuristic
Remaining-distance estimate towards the search goal (0 for blind searches).
Definition HexSearch.h:29
uint32_t bucket