Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved pathfinding #14

Merged
merged 1 commit into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/kelindar/tile
go 1.23

require (
github.com/kelindar/intmap v1.4.1
github.com/kelindar/iostream v1.4.0
github.com/stretchr/testify v1.9.0
)
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/kelindar/intmap v1.4.1 h1:3jTPTrfNx4pxBPURR1+6f4YhbZS57CzsU0S9NEV51ZI=
github.com/kelindar/intmap v1.4.1/go.mod h1:NkypxhfaklmDTJqwano3Q1BWk6je77qgQwszDwu8Kc8=
github.com/kelindar/iostream v1.4.0 h1:ELKlinnM/K3GbRp9pYhWuZOyBxMMlYAfsOP+gauvZaY=
github.com/kelindar/iostream v1.4.0/go.mod h1:MkjMuVb6zGdPQVdwLnFRO0xOTOdDvBWTztFmjRDQkXk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand Down
271 changes: 144 additions & 127 deletions path.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ package tile

import (
"math"
"math/bits"
"sync"

"github.com/kelindar/intmap"
)

type costFn = func(Value) uint16
Expand All @@ -25,19 +28,20 @@ func (m *Grid[T]) Around(from Point, distance uint32, costOf costFn, fn func(Poi

fn(from, start)

// Acquire a frontier heap for search
frontier := acquireHeap()
frontier.Push(from.Integer(), 0)
defer releaseHeap(frontier)

// For pre-allocating, we use πr2 since BFS will result in a approximation
// of a circle, in the worst case.
maxArea := int(math.Ceil(math.Pi * float64(distance*distance)))
reached := make(map[uint32]struct{}, maxArea)
reached[from.Integer()] = struct{}{}

// Acquire a frontier heap for search
state := acquire(maxArea)
frontier := state.frontier
reached := state.edges
defer release(state)

frontier.Push(from.Integer(), 0)
reached.Store(from.Integer(), 0)
for !frontier.IsEmpty() {
pCurr, _ := frontier.Pop()
pCurr := frontier.Pop()
current := unpackPoint(pCurr)

// Get all of the neighbors
Expand All @@ -52,9 +56,9 @@ func (m *Grid[T]) Around(from Point, distance uint32, costOf costFn, fn func(Poi

// Add to the search queue
pNext := next.Integer()
if _, ok := reached[pNext]; !ok {
if _, ok := reached.Load(pNext); !ok {
frontier.Push(pNext, 1)
reached[pNext] = struct{}{}
reached.Store(pNext, 1)
fn(next, nextTile)
}
})
Expand All @@ -63,177 +67,190 @@ func (m *Grid[T]) Around(from Point, distance uint32, costOf costFn, fn func(Poi

// Path calculates a short path and the distance between the two locations
func (m *Grid[T]) Path(from, to Point, costOf costFn) ([]Point, int, bool) {

// Acquire a frontier heap for search
frontier := acquireHeap()
frontier.Push(from.Integer(), 0)
defer releaseHeap(frontier)
distance := float64(from.DistanceTo(to))
maxArea := int(math.Ceil(math.Pi * float64(distance*distance)))

// For pre-allocating, we use πr2 since BFS will result in a approximation
// of a circle, in the worst case.
distance := float64(from.DistanceTo(to))
maxArea := int(math.Ceil(math.Pi * float64(distance*distance)))
edges := make(map[uint32]edge, maxArea)
edges[from.Integer()] = edge{
Point: from,
Cost: 0,
}
state := acquire(maxArea)
edges := state.edges
frontier := state.frontier
defer release(state)

frontier.Push(from.Integer(), 0)
edges.Store(from.Integer(), encode(0, Direction(0))) // Starting point has no direction

for !frontier.IsEmpty() {
pCurr, _ := frontier.Pop()
pCurr := frontier.Pop()
current := unpackPoint(pCurr)

// We have a path to the goal
// Decode the cost to reach the current point
currentEncoded, _ := edges.Load(pCurr)
currentCost, _ := decode(currentEncoded)

// Check if we've reached the destination
if current.Equal(to) {
dist := int(edges[current.Integer()].Cost)
path := make([]Point, 0, dist)
curr, _ := edges[current.Integer()]
for !curr.Point.Equal(from) {
path = append(path, curr.Point)
curr = edges[curr.Point.Integer()]

// Reconstruct the path
path := make([]Point, 0, 64)
path = append(path, current)
for !current.Equal(from) {
currentEncoded, _ := edges.Load(current.Integer())
_, dir := decode(currentEncoded)
current = current.Move(oppositeDirection(dir))
path = append(path, current)
}

// Reverse the path to get from source to destination
for i, j := 0, len(path)-1; i < j; i, j = i+1, j-1 {
path[i], path[j] = path[j], path[i]
}

return path, dist, true
return path, int(currentCost), true
}

// Get all of the neighbors
// Explore neighbors
m.Neighbors(current.X, current.Y, func(next Point, nextTile Tile[T]) {
cNext := costOf(nextTile.Value())
if cNext == 0 {
return // Blocked tile, ignore completely
return // Blocked tile
}

nextCost := currentCost + uint32(cNext)
pNext := next.Integer()
newCost := edges[pCurr].Cost + uint32(cNext) // cost(current, next)

if e, ok := edges[pNext]; !ok || newCost < e.Cost {
priority := newCost + next.DistanceTo(to) // heuristic
frontier.Push(next.Integer(), priority)
existingEncoded, visited := edges.Load(pNext)
existingCost, _ := decode(existingEncoded)

edges[pNext] = edge{
Point: current,
Cost: newCost,
}
}
// If we haven't visited this node or we found a better path
if !visited || nextCost < existingCost {
angle := angleOf(current, next)
priority := nextCost + next.DistanceTo(to)

// Store the edge and push to the frontier
edges.Store(pNext, encode(nextCost, angle))
frontier.Push(pNext, priority)
}
})
}

return nil, 0, false
}

// -----------------------------------------------------------------------------

var heapPool = sync.Pool{
New: func() interface{} { return new(heap32) },
}

// Acquires a new instance of a heap
func acquireHeap() *heap32 {
h := heapPool.Get().(*heap32)
h.Reset()
return h
// encode packs the cost and direction into a uint32
func encode(cost uint32, dir Direction) uint32 {
return (cost << 4) | uint32(dir&0xF)
}

// Releases a heap instance back to the pool
func releaseHeap(h *heap32) {
heapPool.Put(h)
// decode unpacks the cost and direction from a uint32
func decode(value uint32) (cost uint32, dir Direction) {
cost = value >> 4
dir = Direction(value & 0xF)
return
}

// -----------------------------------------------------------------------------

// heapNode represents a ranked node for the heap.
type heapNode struct {
Value uint32 // The value of the ranked node.
Rank uint32 // The rank associated with the ranked node.
type pathfinder struct {
edges *intmap.Map
frontier *frontier
}

type heap32 []heapNode

func newHeap32(capacity int) heap32 {
return make(heap32, 0, capacity)
var pathfinders = sync.Pool{
New: func() any {
return &pathfinder{
edges: intmap.New(32, .95),
frontier: newFrontier(),
}
},
}

// Reset clears the heap for reuse
func (h *heap32) Reset() {
*h = (*h)[:0]
}
// Acquires a new instance of a pathfinding state
func acquire(capacity int) *pathfinder {
v := pathfinders.Get().(*pathfinder)
if v.edges.Capacity() < capacity {
v.edges = intmap.New(capacity, .95)
}

// Push pushes the element x onto the heap.
// The complexity is O(log n) where n = h.Len().
func (h *heap32) Push(v, rank uint32) {
*h = append(*h, heapNode{
Value: v,
Rank: rank,
})
h.up(h.Len() - 1)
return v
}

// Pop removes and returns the minimum element (according to Less) from the heap.
// The complexity is O(log n) where n = h.Len().
// Pop is equivalent to Remove(h, 0).
func (h *heap32) Pop() (uint32, bool) {
n := h.Len() - 1
if n < 0 {
return 0, false
}

h.Swap(0, n)
h.down(0, n)
return h.pop(), true
// release releases a pathfinding state back to the pool
func release(v *pathfinder) {
v.edges.Clear()
v.frontier.Reset()
pathfinders.Put(v)
}

func (h *heap32) pop() uint32 {
old := *h
n := len(old)
no := old[n-1]
*h = old[0 : n-1]
return no.Value
// -----------------------------------------------------------------------------

// frontier is a priority queue implementation that uses buckets to store
// elements. Original implementation by Iskander Sharipov (https://github.com/quasilyte/pathing)
type frontier struct {
buckets [64][]uint32
mask uint64
}

func (h *heap32) up(j int) {
for {
i := (j - 1) / 2 // parent
if i == j || !h.Less(j, i) {
break
}
h.Swap(i, j)
j = i
// newFrontier creates a new frontier priority queue
func newFrontier() *frontier {
h := &frontier{}
for i := range &h.buckets {
h.buckets[i] = make([]uint32, 0, 16)
}
return h
}

func (h *heap32) down(i0, n int) bool {
i := i0
for {
j1 := 2*i + 1
if j1 >= n || j1 < 0 { // j1 < 0 after int overflow
break
}
j := j1 // left child
if j2 := j1 + 1; j2 < n && h.Less(j2, j1) {
j = j2 // = 2*i + 2 // right child
func (q *frontier) Reset() {
buckets := &q.buckets

// Reslice storage slices back.
// To avoid traversing all len(q.buckets),
// we have some offset to skip uninteresting (already empty) buckets.
// We also stop when mask is 0 meaning all remaining buckets are empty too.
// In other words, it would only touch slices between min and max non-empty priorities.
mask := q.mask
offset := uint(bits.TrailingZeros64(mask))
mask >>= offset
i := offset
for mask != 0 {
if i < uint(len(buckets)) {
buckets[i] = buckets[i][:0]
}
if !h.Less(j, i) {
break
}
h.Swap(i, j)
i = j
mask >>= 1
i++
}
return i > i0
}

func (h heap32) Len() int {
return len(h)
q.mask = 0
}

func (h heap32) IsEmpty() bool {
return len(h) == 0
func (q *frontier) IsEmpty() bool {
return q.mask == 0
}

func (h heap32) Less(i, j int) bool {
return h[i].Rank < h[j].Rank
func (q *frontier) Push(value, priority uint32) {
// No bound checks since compiler knows that i will never exceed 64.
// We also get a cool truncation of values above 64 to store them
// in our biggest bucket.
i := priority & 0b111111
q.buckets[i] = append(q.buckets[i], value)
q.mask |= 1 << i
}

func (h *heap32) Swap(i, j int) {
(*h)[i], (*h)[j] = (*h)[j], (*h)[i]
func (q *frontier) Pop() uint32 {
buckets := &q.buckets

// Using uints here and explicit len check to avoid the
// implicitly inserted bound check.
i := uint(bits.TrailingZeros64(q.mask))
if i < uint(len(buckets)) {
e := buckets[i][len(buckets[i])-1]
buckets[i] = buckets[i][:len(buckets[i])-1]
if len(buckets[i]) == 0 {
q.mask &^= 1 << i
}
return e
}

// A queue is empty
return 0
}
Loading
Loading