Documentation
¶
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Cost ¶
type Cost[T comparable] func(T) float64
Cost defines the exact penalty (weight) for moving into a given state. In a standard A* search, this value should be strictly accurate to guarantee optimal paths.
type Heuristic ¶
type Heuristic[T comparable] func(current, goal T) float64
Heuristic defines the estimated cost to reach the goal state from the current state.
Note on Performance vs. Optimality: To drastically narrow the search space and speed up the solver, the heuristic's contribution should dominate the actual step cost. You can achieve this by multiplying the heuristic result by a weight (e.g., > 1.0) or by reducing the output of the Cost function. This effectively turns the algorithm into Weighted A*, which visits far fewer nodes but sacrifices the guarantee of finding the absolute shortest path.
type Indexer ¶
type Indexer[T comparable] func(T) int
Indexer maps a complex state of type T to a unique, contiguous integer identifier. It is required when using highly optimized internal structures like IndexedSliceDict.
type Solver ¶
type Solver[T comparable] struct { // contains filtered or unexported fields }
Solver is a generic, high-performance pathfinding and state-space search engine based on the A* algorithm. It is entirely agnostic to the underlying domain problem.
func New ¶
func New[T comparable]( heuristic Heuristic[T], cost Cost[T], successors SuccessorsFunc[T], opts ...SolverOption[T], ) *Solver[T]
New initializes and returns a new Solver configured with the provided rules (heuristic, cost, and successors) and optional performance tuning parameters (SolverOptions).
func (*Solver[T]) Iter ¶
Iter returns a Go 1.23 iterator sequence that allows stepping through the algorithm's execution. It yields 'false' while actively searching, and 'true' the moment the goal is reached. This is exceptionally useful for visualizing the pathfinding process, debugging, or aborting the search early based on custom conditions.
Example (DefaultDict) ¶
package main
import (
"fmt"
"math"
"github.com/kjkrol/astar"
)
type Point struct {
X, Y int
}
func setupPathFinder(opts ...astar.SolverOption[Point]) (*astar.Solver[Point], Point, Point) {
const size = 64
const insurmountableObstacle = 100.0
grid := make([][]float64, size)
for y := range size {
grid[y] = make([]float64, size)
for x := range size {
val := float64(((x + y) % 5) + 1)
if x%4 == 2 && y%4 != 2 {
val = insurmountableObstacle
}
if (x == 0 && y == 0) || (x == size-1 && y == size-1) {
val = 1
}
grid[y][x] = val
}
}
start := Point{X: 0, Y: 0}
goal := Point{X: 63, Y: 63}
heuristic := func(p, g Point) float64 {
return (math.Abs(float64(g.X-p.X)) + math.Abs(float64(g.Y-p.Y))) * 10.0
}
cost := func(p Point) float64 {
return grid[p.Y][p.X]
}
dirs := []Point{{-1, 0}, {1, 0}, {0, -1}, {0, 1}}
var successors astar.SuccessorsFunc[Point] = func(p Point, buffer []Point) []Point {
for _, d := range dirs {
nx, ny := p.X+d.X, p.Y+d.Y
if nx >= 0 && nx < size && ny >= 0 && ny < size {
if grid[ny][nx] == insurmountableObstacle {
continue
}
buffer = append(buffer, Point{X: nx, Y: ny})
}
}
return buffer
}
pathFinder := astar.New(heuristic, cost, successors, opts...)
return pathFinder, start, goal
}
func main() {
pathFinder, start, goal := setupPathFinder()
var path []Point
for goalAchieved := range pathFinder.Iter(start, goal) {
if goalAchieved {
path = pathFinder.Result()
break
}
}
if len(path) > 0 {
fmt.Println("Path found:", true)
fmt.Println("Start is correct:", path[0] == start)
fmt.Println("Goal is correct:", path[len(path)-1] == goal)
fmt.Println("Path length >= 127:", len(path) >= 127)
} else {
fmt.Println("Path not found")
}
}
Output: Path found: true Start is correct: true Goal is correct: true Path length >= 127: true
Example (IndexedMapDict) ¶
package main
import (
"fmt"
"math"
"github.com/kjkrol/astar"
)
type Point struct {
X, Y int
}
func setupPathFinder(opts ...astar.SolverOption[Point]) (*astar.Solver[Point], Point, Point) {
const size = 64
const insurmountableObstacle = 100.0
grid := make([][]float64, size)
for y := range size {
grid[y] = make([]float64, size)
for x := range size {
val := float64(((x + y) % 5) + 1)
if x%4 == 2 && y%4 != 2 {
val = insurmountableObstacle
}
if (x == 0 && y == 0) || (x == size-1 && y == size-1) {
val = 1
}
grid[y][x] = val
}
}
start := Point{X: 0, Y: 0}
goal := Point{X: 63, Y: 63}
heuristic := func(p, g Point) float64 {
return (math.Abs(float64(g.X-p.X)) + math.Abs(float64(g.Y-p.Y))) * 10.0
}
cost := func(p Point) float64 {
return grid[p.Y][p.X]
}
dirs := []Point{{-1, 0}, {1, 0}, {0, -1}, {0, 1}}
var successors astar.SuccessorsFunc[Point] = func(p Point, buffer []Point) []Point {
for _, d := range dirs {
nx, ny := p.X+d.X, p.Y+d.Y
if nx >= 0 && nx < size && ny >= 0 && ny < size {
if grid[ny][nx] == insurmountableObstacle {
continue
}
buffer = append(buffer, Point{X: nx, Y: ny})
}
}
return buffer
}
pathFinder := astar.New(heuristic, cost, successors, opts...)
return pathFinder, start, goal
}
func main() {
indexer := func(p Point) int { return p.Y*64 + p.X }
pathFinder, start, goal := setupPathFinder(
astar.WithInitCapacity[Point](64*64),
astar.WithIndexedMapDict(indexer),
)
var path []Point
for goalAchieved := range pathFinder.Iter(start, goal) {
if goalAchieved {
path = pathFinder.Result()
break
}
}
if len(path) > 0 {
fmt.Println("Path found:", true)
fmt.Println("Start is correct:", path[0] == start)
fmt.Println("Goal is correct:", path[len(path)-1] == goal)
fmt.Println("Path length >= 127:", len(path) >= 127)
} else {
fmt.Println("Path not found")
}
}
Output: Path found: true Start is correct: true Goal is correct: true Path length >= 127: true
Example (IndexedSliceDict) ¶
package main
import (
"fmt"
"math"
"github.com/kjkrol/astar"
)
type Point struct {
X, Y int
}
func setupPathFinder(opts ...astar.SolverOption[Point]) (*astar.Solver[Point], Point, Point) {
const size = 64
const insurmountableObstacle = 100.0
grid := make([][]float64, size)
for y := range size {
grid[y] = make([]float64, size)
for x := range size {
val := float64(((x + y) % 5) + 1)
if x%4 == 2 && y%4 != 2 {
val = insurmountableObstacle
}
if (x == 0 && y == 0) || (x == size-1 && y == size-1) {
val = 1
}
grid[y][x] = val
}
}
start := Point{X: 0, Y: 0}
goal := Point{X: 63, Y: 63}
heuristic := func(p, g Point) float64 {
return (math.Abs(float64(g.X-p.X)) + math.Abs(float64(g.Y-p.Y))) * 10.0
}
cost := func(p Point) float64 {
return grid[p.Y][p.X]
}
dirs := []Point{{-1, 0}, {1, 0}, {0, -1}, {0, 1}}
var successors astar.SuccessorsFunc[Point] = func(p Point, buffer []Point) []Point {
for _, d := range dirs {
nx, ny := p.X+d.X, p.Y+d.Y
if nx >= 0 && nx < size && ny >= 0 && ny < size {
if grid[ny][nx] == insurmountableObstacle {
continue
}
buffer = append(buffer, Point{X: nx, Y: ny})
}
}
return buffer
}
pathFinder := astar.New(heuristic, cost, successors, opts...)
return pathFinder, start, goal
}
func main() {
indexer := func(p Point) int { return p.Y*64 + p.X }
pathFinder, start, goal := setupPathFinder(
astar.WithIndexedSliceDict(64*64, indexer),
)
var path []Point
for goalAchieved := range pathFinder.Iter(start, goal) {
if goalAchieved {
path = pathFinder.Result()
break
}
}
if len(path) > 0 {
fmt.Println("Path found:", true)
fmt.Println("Start is correct:", path[0] == start)
fmt.Println("Goal is correct:", path[len(path)-1] == goal)
fmt.Println("Path length >= 127:", len(path) >= 127)
} else {
fmt.Println("Path not found")
}
}
Output: Path found: true Start is correct: true Goal is correct: true Path length >= 127: true
func (*Solver[T]) Result ¶
func (a *Solver[T]) Result() []T
Result reconstructs and returns the path from the starting state to the current state. It is typically called immediately after the Iter sequence yields 'true'.
func (*Solver[T]) Solve ¶
func (a *Solver[T]) Solve(start, goal T) []T
Solve executes the search from the start state to the goal state. It runs the iterator to completion and returns the final path. If no path is found, it returns nil.
Example (DefaultDict) ¶
package main
import (
"fmt"
"math"
"github.com/kjkrol/astar"
)
type Point struct {
X, Y int
}
func setupPathFinder(opts ...astar.SolverOption[Point]) (*astar.Solver[Point], Point, Point) {
const size = 64
const insurmountableObstacle = 100.0
grid := make([][]float64, size)
for y := range size {
grid[y] = make([]float64, size)
for x := range size {
val := float64(((x + y) % 5) + 1)
if x%4 == 2 && y%4 != 2 {
val = insurmountableObstacle
}
if (x == 0 && y == 0) || (x == size-1 && y == size-1) {
val = 1
}
grid[y][x] = val
}
}
start := Point{X: 0, Y: 0}
goal := Point{X: 63, Y: 63}
heuristic := func(p, g Point) float64 {
return (math.Abs(float64(g.X-p.X)) + math.Abs(float64(g.Y-p.Y))) * 10.0
}
cost := func(p Point) float64 {
return grid[p.Y][p.X]
}
dirs := []Point{{-1, 0}, {1, 0}, {0, -1}, {0, 1}}
var successors astar.SuccessorsFunc[Point] = func(p Point, buffer []Point) []Point {
for _, d := range dirs {
nx, ny := p.X+d.X, p.Y+d.Y
if nx >= 0 && nx < size && ny >= 0 && ny < size {
if grid[ny][nx] == insurmountableObstacle {
continue
}
buffer = append(buffer, Point{X: nx, Y: ny})
}
}
return buffer
}
pathFinder := astar.New(heuristic, cost, successors, opts...)
return pathFinder, start, goal
}
func main() {
pathFinder, start, goal := setupPathFinder()
path := pathFinder.Solve(start, goal)
if len(path) > 0 {
fmt.Println("Path found:", true)
fmt.Println("Start is correct:", path[0] == start)
fmt.Println("Goal is correct:", path[len(path)-1] == goal)
fmt.Println("Path length >= 127:", len(path) >= 127)
} else {
fmt.Println("Path not found")
}
}
Output: Path found: true Start is correct: true Goal is correct: true Path length >= 127: true
Example (IndexedMapDict) ¶
package main
import (
"fmt"
"math"
"github.com/kjkrol/astar"
)
type Point struct {
X, Y int
}
func setupPathFinder(opts ...astar.SolverOption[Point]) (*astar.Solver[Point], Point, Point) {
const size = 64
const insurmountableObstacle = 100.0
grid := make([][]float64, size)
for y := range size {
grid[y] = make([]float64, size)
for x := range size {
val := float64(((x + y) % 5) + 1)
if x%4 == 2 && y%4 != 2 {
val = insurmountableObstacle
}
if (x == 0 && y == 0) || (x == size-1 && y == size-1) {
val = 1
}
grid[y][x] = val
}
}
start := Point{X: 0, Y: 0}
goal := Point{X: 63, Y: 63}
heuristic := func(p, g Point) float64 {
return (math.Abs(float64(g.X-p.X)) + math.Abs(float64(g.Y-p.Y))) * 10.0
}
cost := func(p Point) float64 {
return grid[p.Y][p.X]
}
dirs := []Point{{-1, 0}, {1, 0}, {0, -1}, {0, 1}}
var successors astar.SuccessorsFunc[Point] = func(p Point, buffer []Point) []Point {
for _, d := range dirs {
nx, ny := p.X+d.X, p.Y+d.Y
if nx >= 0 && nx < size && ny >= 0 && ny < size {
if grid[ny][nx] == insurmountableObstacle {
continue
}
buffer = append(buffer, Point{X: nx, Y: ny})
}
}
return buffer
}
pathFinder := astar.New(heuristic, cost, successors, opts...)
return pathFinder, start, goal
}
func main() {
indexer := func(p Point) int { return p.Y*64 + p.X }
pathFinder, start, goal := setupPathFinder(
astar.WithInitCapacity[Point](64*64),
astar.WithIndexedMapDict(indexer),
)
path := pathFinder.Solve(start, goal)
if len(path) > 0 {
fmt.Println("Path found:", true)
fmt.Println("Start is correct:", path[0] == start)
fmt.Println("Goal is correct:", path[len(path)-1] == goal)
fmt.Println("Path length >= 127:", len(path) >= 127)
} else {
fmt.Println("Path not found")
}
}
Output: Path found: true Start is correct: true Goal is correct: true Path length >= 127: true
Example (IndexedSliceDict) ¶
package main
import (
"fmt"
"math"
"github.com/kjkrol/astar"
)
type Point struct {
X, Y int
}
func setupPathFinder(opts ...astar.SolverOption[Point]) (*astar.Solver[Point], Point, Point) {
const size = 64
const insurmountableObstacle = 100.0
grid := make([][]float64, size)
for y := range size {
grid[y] = make([]float64, size)
for x := range size {
val := float64(((x + y) % 5) + 1)
if x%4 == 2 && y%4 != 2 {
val = insurmountableObstacle
}
if (x == 0 && y == 0) || (x == size-1 && y == size-1) {
val = 1
}
grid[y][x] = val
}
}
start := Point{X: 0, Y: 0}
goal := Point{X: 63, Y: 63}
heuristic := func(p, g Point) float64 {
return (math.Abs(float64(g.X-p.X)) + math.Abs(float64(g.Y-p.Y))) * 10.0
}
cost := func(p Point) float64 {
return grid[p.Y][p.X]
}
dirs := []Point{{-1, 0}, {1, 0}, {0, -1}, {0, 1}}
var successors astar.SuccessorsFunc[Point] = func(p Point, buffer []Point) []Point {
for _, d := range dirs {
nx, ny := p.X+d.X, p.Y+d.Y
if nx >= 0 && nx < size && ny >= 0 && ny < size {
if grid[ny][nx] == insurmountableObstacle {
continue
}
buffer = append(buffer, Point{X: nx, Y: ny})
}
}
return buffer
}
pathFinder := astar.New(heuristic, cost, successors, opts...)
return pathFinder, start, goal
}
func main() {
indexer := func(p Point) int { return p.Y*64 + p.X }
pathFinder, start, goal := setupPathFinder(
astar.WithIndexedSliceDict(64*64, indexer),
)
path := pathFinder.Solve(start, goal)
if len(path) > 0 {
fmt.Println("Path found:", true)
fmt.Println("Start is correct:", path[0] == start)
fmt.Println("Goal is correct:", path[len(path)-1] == goal)
fmt.Println("Path length >= 127:", len(path) >= 127)
} else {
fmt.Println("Path not found")
}
}
Output: Path found: true Start is correct: true Goal is correct: true Path length >= 127: true
type SolverOption ¶
type SolverOption[T comparable] func(*config[T])
SolverOption configures the internal behavior and data structures of the Solver.
func WithIndexedMapDict ¶
func WithIndexedMapDict[T comparable](indexer Indexer[T]) SolverOption[T]
WithIndexedMapDict configures the solver to use standard Go maps backed by an Indexer function. By mapping complex state types to simple integer keys, it bypasses Go's interface boxing and complex hashing overhead.
This provides a noticeable performance boost (often in the double-digit percentages) compared to the default map implementation, while remaining more memory-efficient than slice-based dictionaries for problems with sparse state spaces.
func WithIndexedSliceDict ¶
func WithIndexedSliceDict[T comparable](maxSize int, indexer Indexer[T]) SolverOption[T]
WithIndexedSliceDict configures the solver to use slice-based dictionaries for its internal "open" and "closed" sets. Because the underlying slice does not grow dynamically, the provided maxSize must represent the absolute maximum number of possible states (nodes). It also requires an Indexer function to map node properties to contiguous integer indices.
This is the fastest dictionary implementation available, typically doubling the solver's overall performance by completely eliminating map hashing overhead and reducing memory allocations.
func WithInitCapacity ¶
func WithInitCapacity[T comparable](capacity int) SolverOption[T]
WithInitCapacity sets the initial capacity for the solver's internal data structures, such as dictionaries and memory arenas. Providing an accurate estimate prevents costly dynamic allocations during the search execution. If the capacity is exceeded, the underlying structures will automatically grow (typically doubling their size).
func WithSuccessorCapacity ¶
func WithSuccessorCapacity[T comparable](capacity int) SolverOption[T]
WithSuccessorCapacity pre-allocates the internal buffer used for fetching neighbor nodes. Setting this to the maximum expected number of successors per node (e.g., 4 or 8 for 2D grids) guarantees zero allocations for successor retrieval from the very first search iteration.
type SuccessorsFunc ¶
type SuccessorsFunc[T comparable] func(current T, buffer []T) []T
SuccessorsFunc defines the rules of movement or state transitions. It populates the provided buffer with all valid, reachable neighbors from the current state and returns it. This is the ideal place to filter out unreachable nodes (like walls or obstacles).