Skip to content

Add GetReachable movement-range query to Pathfinder#22

Merged
migus88 merged 2 commits into
masterfrom
feature/get-reachable-range-query
Jun 24, 2026
Merged

Add GetReachable movement-range query to Pathfinder#22
migus88 merged 2 commits into
masterfrom
feature/get-reachable-range-query

Conversation

@migus88

@migus88 migus88 commented Jun 24, 2026

Copy link
Copy Markdown
Owner

What

Adds Pathfinder.GetReachable(IAgent agent, Coordinate from, float budget) — a movement-range query that returns every cell whose cheapest path cost from the origin is within a budget. This is the core mechanic behind tactics/strategy games that highlight where a unit can move.

It is a bounded uniform-cost (Dijkstra) flood fill that reuses the existing infrastructure:

  • Same movement rules as GetPath — diagonal movement, corner-cutting, occupied cells, and agent clearance (Size > 1).
  • Same pooled, reusable Cell[] buffer and unsafe hot path.

Per-step cost is StraightMovementMultiplier (cardinal) / DiagonalMovementMultiplier (diagonal), multiplied by the destination cell's Weight when IsCellWeightEnabled is set. The origin is always included with cost 0.

Results come back as a poolable, IDisposable RangeResult of ReachableCell (Coordinate + Cost), mirroring PathResult. Contains / TryGetCost convenience lookups are provided.

using var range = pathfinder.GetReachable(agent, new Coordinate(4, 4), 5f);
foreach (var cell in range.Cells)
    Highlight(cell.Coordinate, cell.Cost);

Why

GetPath answers "how do I get from A to B?". Range/reachability ("where can I go?") is the other half of grid movement and is commonly requested for turn-based and tactics games. It can't be expressed with GetPath (no single destination), so it needs its own search.

Supporting changes

  • UnsafePriorityQueue.UpdatePriority — a real decrease-key (re-heapify via cascade up/down) used by the bounded Dijkstra search. The A* hot path is unchanged.
  • Fix Utils.CellsComparison to sort X-major then Y-minor so the Cell[] (CellsArray) constructor's sorted layout matches GetCell's x * Height + y indexing — consistent with the matrix/holder init modes which write cells at Coordinate.X * Height + Coordinate.Y. The previous (Y, X) ordering transposed the grid and scrambled neighbor adjacency. This was a latent bug masked in existing GetPath tests because they use symmetric (x == y) start/destination coordinates; it surfaced immediately for the range query. A dedicated asymmetric-origin test now guards against regressing it.

Verification

  • dotnet build src/mpath-source/Migs.MPath.sln — succeeds, 0 errors.
  • dotnet test src/mpath-source/Migs.MPath.Tests/Migs.MPath.Tests.csproj78/78 passing, including 13 new reachability tests (zero budget, negative budget, Manhattan diamond, diagonals, walls, cell weights, larger-agent clearance, asymmetric-origin orientation, invalid origin, null agent, disposal, instance reuse) and all pre-existing tests (confirming the CellsComparison fix introduced no regressions).

Docs

  • New docs/api/RangeResult.md and docs/api/ReachableCell.md; updated docs/api/Pathfinder.md and the API index.
  • New docs/guides/movement-range.md (linked from the docs index).
  • Updated README.md (feature list + quick-start snippet) and CLAUDE.md (architecture notes for the new partial, data types, queue decrease-key, and the CellsComparison ordering invariant).

🤖 Generated with Claude Code

migus88 and others added 2 commits June 24, 2026 18:09
Add Pathfinder.GetReachable(IAgent, Coordinate from, float budget), a
uniform-cost (Dijkstra) flood fill that returns every cell whose cheapest
path cost from the origin is within a movement budget. Useful for tactics/
strategy "where can this unit move?" mechanics. It reuses the same movement
rules as GetPath (diagonal movement, corner-cutting, occupied cells, agent
clearance) and the same pooled, reusable cell buffer.

Per-step cost is StraightMovementMultiplier / DiagonalMovementMultiplier,
multiplied by the destination cell's Weight when cell weighting is enabled.
Results are returned as a poolable, IDisposable RangeResult of ReachableCell
(Coordinate + Cost), mirroring PathResult.

Supporting changes:
- UnsafePriorityQueue.UpdatePriority: a real decrease-key (cascade up/down)
  used by the bounded Dijkstra search.
- Fix Utils.CellsComparison to sort X-major then Y-minor so the Cell[]
  (CellsArray) constructor's sorted layout matches GetCell's x*Height+y
  indexing — consistent with the matrix/holder init modes. The previous
  (Y, X) ordering transposed the grid and scrambled neighbor adjacency; it
  was masked in existing GetPath tests because they use symmetric (x==y)
  coordinates.

Tests: 13 new reachability tests (budget bounds, diagonals, walls, weights,
agent clearance, asymmetric-origin orientation, disposal, reuse). Full suite
78/78 green. Docs: new api/RangeResult, api/ReachableCell, guides/movement-range;
updated Pathfinder API doc, README, and CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The reachability flood fill computed step cost as `travel * Weight`, while
the main A* pathfinder folds weight in additively. Cell.Weight defaults to 0
and IsCellWeightEnabled defaults to true, so for any grid that doesn't set
weights the multiplicative model made every step cost `travel * 0 = 0` — the
budget never bit and GetReachable returned the entire walkable board. GetPath
was unaffected because its weight term is additive (`+ 0`).

Change GetReachableStepCost to `travel + GetCellWeightMultiplier(neighbor)`,
reusing the same helper as the main pathfinder so the two cost models treat
weight consistently. With a default Weight of 0 the step cost is now just the
travel multiplier, and budgets bound the search as intended.

Tests: add a regression test that reproduces the client's scenario (default
Weight 0 + weighting enabled) and asserts the result stays within budget
(13-cell diamond, not the full 100-cell board) — verified to fail on the old
multiplicative formula. Update the existing weight test (weight is now
additive: a normal neighbour costs travel+weight = 2) and bump a reuse test's
budget above the new minimum step cost. Docs/CLAUDE.md updated to describe
weight as additive. Full suite 79/79 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@migus88 migus88 merged commit 7ab2170 into master Jun 24, 2026
1 check passed
@migus88 migus88 deleted the feature/get-reachable-range-query branch June 24, 2026 19:49
migus88 added a commit that referenced this pull request Jun 24, 2026
Resolve the MazeBenchmarkRunner.cs conflict by keeping the restructured Suites/
version with the competitor benchmarks enabled (master had commented them out in
the now-relocated file). Brings in GetReachable, so ReachabilityBenchmarkRunner
compiles and the whole solution builds again.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant