载入中...
搜索中...
未找到
TacticsPath.cpp
浏览该文件的文档.
2
3#include <algorithm>
4#include <cstdlib>
5#include <limits>
6#include <optional>
7#include <queue>
8#include <utility>
9
10namespace eve::tactics {
11namespace {
12
13struct FrontierNode {
14 int cost = 0;
15 Cell cell;
16};
17
18struct FrontierLater {
19 bool operator()(const FrontierNode& left, const FrontierNode& right) const noexcept {
20 if (left.cost != right.cost) return left.cost > right.cost;
21 return left.cell > right.cell;
22 }
23};
24
26struct SearchOutcome {
27 std::map<Cell, int> best;
28 std::map<Cell, Cell> predecessor;
29};
30
37Result<Cell> resolveQueryOrigin(const BoardState& board, SubjectRef subject, int budget) {
38 if (!subject.isValid())
40 "tactics reachability requires a valid subject", "subject"));
41 if (budget < 0)
43 "tactics reachability budget must be non-negative", "budget"));
44 const auto origin = board.position(subject);
45 if (!origin)
47 Diagnostic::error(DiagnosticCode::NotFound, "tactics reachability subject is not placed", "subject"));
49}
50
64Result<SearchOutcome> expand(const BoardState& board, SubjectRef subject, Cell origin, int budget,
65 const std::optional<Cell>& stopAt) {
66 SearchOutcome outcome;
67 std::priority_queue<FrontierNode, std::vector<FrontierNode>, FrontierLater> frontier;
68 outcome.best.emplace(origin, 0);
69 frontier.push({0, origin});
70
71 while (!frontier.empty()) {
72 const FrontierNode current = frontier.top();
73 frontier.pop();
74 const auto known = outcome.best.find(current.cell);
75 if (known == outcome.best.end() || known->second != current.cost) continue;
76 if (stopAt && current.cell == *stopAt) break;
77 for (const Cell next : board.neighbours(current.cell)) {
78 const auto occupied = board.occupant(next);
79 // The moving subject keeps its own origin traversable.
80 if (occupied && *occupied != subject) continue;
81 auto state = board.cell(next);
82 if (!state) return Result<SearchOutcome>::failure(state.status());
83 const CellState cellState = std::move(state).takeValue();
84 if (!cellState.passable) continue;
85 // A declared directed edge refines this direction only; an undeclared
86 // direction keeps the pure cell-cost behaviour.
87 const std::optional<EdgeState> directed = board.tryEdge(current.cell, next);
88 if (directed && !directed->passable) continue;
89 const int extra = directed ? directed->extraCost : 0;
90 const long long step = static_cast<long long>(cellState.moveCost) + static_cast<long long>(extra);
91 if (step > std::numeric_limits<int>::max()) continue;
92 if (current.cost > std::numeric_limits<int>::max() - static_cast<int>(step)) continue;
93 const int candidate = current.cost + static_cast<int>(step);
94 if (candidate > budget) continue;
95 const auto old = outcome.best.find(next);
96 if (old != outcome.best.end() && candidate >= old->second) continue;
97 outcome.best[next] = candidate;
98 outcome.predecessor[next] = current.cell;
99 frontier.push({candidate, next});
100 }
101 }
102 return Result<SearchOutcome>::success(std::move(outcome));
103}
104
105} // namespace
106
107bool Reachability::contains(Cell cellValue) const noexcept {
108 const auto found = std::lower_bound(cells_.begin(), cells_.end(), cellValue,
109 [](const ReachableCell& entry, Cell value) { return entry.cell < value; });
110 return found != cells_.end() && found->cell == cellValue;
111}
112
114 const auto found = std::lower_bound(cells_.begin(), cells_.end(), cellValue,
115 [](const ReachableCell& entry, Cell value) { return entry.cell < value; });
116 if (found == cells_.end() || found->cell != cellValue)
118 Diagnostic::error(DiagnosticCode::NotFound, "tactics cell is not reachable", "cell"));
119 return Result<int>::success(found->cost);
120}
121
123 if (!contains(target))
124 return Result<std::vector<Cell>>::failure(
125 Diagnostic::error(DiagnosticCode::NotFound, "tactics target is not reachable", "target"));
126 std::vector<Cell> result{target};
127 while (result.back() != origin_) {
128 const auto found = predecessor_.find(result.back());
129 if (found == predecessor_.end())
131 DiagnosticCode::InvariantViolation, "tactics reachability predecessor chain is incomplete", "path"));
132 result.push_back(found->second);
133 }
134 std::reverse(result.begin(), result.end());
135 return Result<std::vector<Cell>>::success(std::move(result));
136}
137
139 auto origin = resolveQueryOrigin(board, subject, budget);
140 if (!origin) return Result<Reachability>::failure(origin.status());
141
142 // A reachability query needs every cell, so it never stops early.
143 auto outcome = expand(board, subject, origin.value(), budget, std::nullopt);
144 if (!outcome) return Result<Reachability>::failure(outcome.status());
145
146 Reachability result;
147 result.origin_ = origin.value();
148 result.predecessor_ = std::move(outcome.value().predecessor);
149 result.cells_.reserve(outcome.value().best.size());
150 for (const auto& [cellValue, cost] : outcome.value().best) result.cells_.push_back({cellValue, cost});
151 return Result<Reachability>::success(std::move(result));
152}
153
154Result<std::vector<Cell>> PathQuery::path(const BoardState& board, SubjectRef subject, Cell target, int budget) {
155 auto origin = resolveQueryOrigin(board, subject, budget);
156 if (!origin) return Result<std::vector<Cell>>::failure(origin.status());
157
158 // Single-target query: stop expanding once the target is finalised instead of
159 // building the whole reachable set.
160 auto outcome = expand(board, subject, origin.value(), budget, target);
161 if (!outcome) return Result<std::vector<Cell>>::failure(outcome.status());
162 if (!outcome.value().best.contains(target))
163 return Result<std::vector<Cell>>::failure(
164 Diagnostic::error(DiagnosticCode::NotFound, "tactics target is not reachable", "target"));
165
166 std::vector<Cell> result{target};
167 while (result.back() != origin.value()) {
168 const auto found = outcome.value().predecessor.find(result.back());
169 if (found == outcome.value().predecessor.end())
171 DiagnosticCode::InvariantViolation, "tactics reachability predecessor chain is incomplete", "path"));
172 result.push_back(found->second);
173 }
174 std::reverse(result.begin(), result.end());
175 return Result<std::vector<Cell>>::success(std::move(result));
176}
177
178Result<std::vector<Cell>> PathQuery::cellsInRange(const BoardState& board, Cell origin, int minimum, int maximum,
179 CellRangeMetric metric) {
180 if (board.topology() == BoardTopology::ExplicitGraph)
183 "coordinate range metrics do not apply to graph cell identities; use reachability", "topology"));
184 if (minimum < 0 || maximum < minimum)
186 DiagnosticCode::InvalidArgument, "tactics range bounds must be ordered and non-negative", "range"));
187 std::vector<Cell> result;
188 for (const Cell cell : board.cells()) {
189 const std::int64_t deltaX = static_cast<std::int64_t>(cell.x) - origin.x;
190 const std::int64_t deltaY = static_cast<std::int64_t>(cell.y) - origin.y;
191 const std::int64_t dx = std::abs(deltaX);
192 const std::int64_t dy = std::abs(deltaY);
193 const std::int64_t dz =
194 std::abs(static_cast<std::int64_t>(cell.layer) - origin.layer);
195 std::int64_t distance = 0;
196 switch (metric) {
197 case CellRangeMetric::Manhattan: distance = dx + dy + dz; break;
198 case CellRangeMetric::Chebyshev: distance = std::max({dx, dy, dz}); break;
199 case CellRangeMetric::Hex:
200 if (cell.layer != origin.layer) continue;
201 distance = std::max({dx, dy, std::abs(deltaX + deltaY)});
202 break;
203 }
204 if (distance >= minimum && distance <= maximum) result.push_back(cell);
205 }
206 return Result<std::vector<Cell>>::success(std::move(result));
207}
208
209} // namespace eve::tactics
LogicalId target
double value
int subject
Definition AnimSmr.cpp:163
eve::resource::CostSpec cost
float maximum[3]
float minimum[3]
HexVec3 left
HexVec3 right
float distance
V3 origin
Definition RoadBake.cpp:138
bool found
double current
float dz
float dy
float dx
bool occupied
std::map< Cell, Cell > predecessor
Cell cell
std::map< Cell, int > best
Deterministic fixed-cost tactics path queries.
BoardState board
float step
Definition TreeMesh.cpp:314
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
Strong, domain-neutral reference to a runtime subject.
Definition SubjectRef.h:26
Authoritative board facts and occupancy indexes for one battle.
Result< CellState > cell(Cell cell) const
Return an owning copy of a cell fact, or NotFound.
std::optional< EdgeState > tryEdge(Cell from, Cell to) const
Return a declared directed edge, or empty when none is declared.
std::optional< SubjectRef > occupant(Cell cell) const
Return the occupying subject, or empty when the cell is unoccupied.
std::optional< Cell > position(SubjectRef subject) const
Return the subject cell, or empty when it is not placed.
static Result< Reachability > reachable(const BoardState &board, SubjectRef subject, int budget)
Compute all unoccupied reachable cells for a placed subject.
Owning result of one deterministic reachability query.
Definition TacticsPath.h:30
bool contains(Cell cell) const noexcept
Return whether a cell is reachable within the queried budget.
Result< int > cost(Cell cell) const
Return the minimum cost for a reachable cell, or NotFound.
Result< std::vector< Cell > > pathTo(Cell target) const
Reconstruct an origin-to-target path, or NotFound.
constexpr HexDirection next(HexDirection d) noexcept
The next direction clockwise (NW wraps to NE).
Definition HexMetrics.h:76
CellRangeMetric
Deterministic logical distance used by cellsInRange.
Definition TacticsPath.h:15
Logical board coordinate independent of rendering projection.
One reachable cell and its minimum fixed-point movement cost.
Definition TacticsPath.h:18