@@ -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