Skip to content

Commit 7595d8b

Browse files
committed
add AStar.WalkCosts
A method to find all reachable tiles along with their costs from a given coordinate.
1 parent 63811fd commit 7595d8b

1 file changed

Lines changed: 82 additions & 0 deletions

File tree

astar.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,88 @@ func NewAStar(config AStarConfig) *AStar {
5757
return astar
5858
}
5959

60+
// WalkCosts iterates over all reachable tiles, going from the given pos.
61+
// It's like pathfinding, but in all directions, but without the need
62+
// to actually build any paths - therefore it is much faster than building
63+
// paths from pos to every coord in the area to calculate tile costs around the pos.
64+
//
65+
// The provided f callback is called for every such reachable tile, given
66+
// the current coord and the cost associated with a path from pos to that coord.
67+
// The function is not called for the pos itself.
68+
// The return value of "true" means the value was found and the iteration
69+
// will be stopped.
70+
//
71+
// There are many use cases for this, one being showing the reachable area
72+
// for the given movement budget.
73+
func (astar *AStar) WalkCosts(g *Grid, pos GridCoord, l GridLayer, maxCost int, f func(c GridCoord, cost int) bool) {
74+
// Almost identical to the BuildPath, with a few key differences.
75+
// It doesn't need a pathmap as paths are never reconstructed.
76+
// It can't stop as soon as it finds the solution, as it needs as many of them
77+
// as possible - but the user can stop the process by returning true,
78+
// so even with a large maxCost it should terminate easily.
79+
80+
origin := findPathOrigin(pos)
81+
82+
localStart := pos.Sub(origin)
83+
84+
frontier := astar.frontier
85+
frontier.Reset()
86+
87+
costmap := astar.costmap
88+
costmap.Reset()
89+
90+
frontier.Push(0, astarCoord{Coord: localStart, Cost: 0})
91+
92+
for !frontier.IsEmpty() {
93+
current := frontier.Pop()
94+
95+
// Accept the tile path: the first pop wins, subsequent pops for the
96+
// same tile are stale duplicates produced by the lazy-deletion
97+
// approach and must be ignored.
98+
k := costmap.packCoord(current.Coord)
99+
if costmap.Contains(k) {
100+
continue
101+
}
102+
costmap.Set(k, uint32(current.Cost))
103+
104+
// For now we have an explicit start coord check here,
105+
// but ideally this coord should be ignored as a side effect
106+
// of the algorithm (just need to figure out how).
107+
if current.Coord != localStart {
108+
if f(current.Coord.Add(origin), int(current.Cost)) {
109+
break
110+
}
111+
}
112+
113+
for _, offset := range &neighborOffsets {
114+
next := current.Coord.Add(offset)
115+
116+
cx := uint(next.X) + uint(origin.X)
117+
cy := uint(next.Y) + uint(origin.Y)
118+
if cx >= g.numCols || cy >= g.numRows {
119+
continue
120+
}
121+
122+
nextCellCost := g.getCellCost(cx, cy, l)
123+
if nextCellCost == 0 {
124+
continue
125+
}
126+
127+
newCost := int(current.Cost) + int(nextCellCost)
128+
if newCost > maxCost {
129+
continue
130+
}
131+
132+
k := costmap.packCoord(next)
133+
if costmap.Contains(k) {
134+
continue
135+
}
136+
137+
frontier.Push(newCost, astarCoord{Coord: next, Cost: int32(newCost)})
138+
}
139+
}
140+
}
141+
60142
// BuildPath attempts to find a path between the two coordinates.
61143
// It will use a provided Grid in combination with a GridLayer.
62144
// The Grid is expected to store the tile tags and the GridLayer is

0 commit comments

Comments
 (0)