Skip to content

Commit

Permalink
diagonal support cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
val antonini committed Apr 28, 2024
1 parent 9a3d675 commit 9f7cf47
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 81 deletions.
40 changes: 21 additions & 19 deletions astar.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@ var diagonalSuccessors = []Vec2{
{-1, -1}, // Up-Left
}

// distanceHeuristic is a function that calculates the distance between two vectors.
type distanceHeuristic func(Vec2, Vec2) int
// heuristicFunc is a function that calculates the distance between two vectors.
type heuristicFunc func(Vec2, Vec2) int

// getSuccessorsFunc is a function that returns the successors of a vector for
// a given search space.
type getSuccessorsFunc func(v Vec2) []Vec2

// node is a node in the search space.
type node struct {
Expand All @@ -43,8 +47,9 @@ type node struct {

// Pathfinder is a simple A* pathfinding algorithm implementation.
type Pathfinder struct {
weights Grid[int]
diagonals bool
weights Grid[int]
heuristic heuristicFunc
getSuccessors getSuccessorsFunc
}

// NewPathfinder creates a new Pathfinder with the given weights. The weights
Expand All @@ -53,7 +58,11 @@ type Pathfinder struct {
// traversable.
func NewPathfinder(weights Grid[int]) Pathfinder {
return Pathfinder{
weights: weights,
weights: weights,
heuristic: manhattan,
getSuccessors: func(v Vec2) []Vec2 {
return getSuccessors(v, weights.Width, weights.Height, cardinalSuccessors)
},
}
}

Expand All @@ -63,23 +72,16 @@ func NewPathfinder(weights Grid[int]) Pathfinder {
func NewDiagonalPathfinder(weights Grid[int]) Pathfinder {
return Pathfinder{
weights: weights,
diagonals: true,
heuristic: diagonalDistance,
getSuccessors: func(v Vec2) []Vec2 {
return getSuccessors(v, weights.Width, weights.Height, diagonalSuccessors)
},
}
}

// Find returns a path from start to end. If no path is found, an empty slice
// is returned.
func (p Pathfinder) Find(startPos, endPos Vec2) []Vec2 {
offsets := cardinalSuccessors
if p.diagonals {
offsets = diagonalSuccessors
}

heuristic := manhattan
if p.diagonals {
heuristic = diagonalDistance
}

searchSpace := newSearchSpace(p.weights) // tracks the open, closed and f values of each node
open := newMinHeap(searchSpace.Width, searchSpace.Height) // prioritised queue of f

Expand All @@ -93,7 +95,7 @@ func (p Pathfinder) Find(startPos, endPos Vec2) []Vec2 {
for open.len() > 0 {
qPos := open.pop().pos
q := searchSpace.Get(qPos)
for _, succPos := range getSuccessors(qPos, searchSpace.Width, searchSpace.Height, offsets) {
for _, succPos := range p.getSuccessors(qPos) {
successor := searchSpace.Get(succPos)

// not traversable
Expand All @@ -102,8 +104,8 @@ func (p Pathfinder) Find(startPos, endPos Vec2) []Vec2 {
}

successor.parent = &q
successor.g = q.g + heuristic(qPos, succPos)
successor.h = heuristic(succPos, endPos)
successor.g = q.g + p.heuristic(qPos, succPos)
successor.h = p.heuristic(succPos, endPos)
successor.f = successor.g + successor.h
successor.open = true

Expand Down
107 changes: 45 additions & 62 deletions astar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,7 @@ func TestGetSuccessors_Diagonal(t *testing.T) {
{0, 1},
{0, 0},
}
if len(got) != len(want) {
t.Fatalf("len want %d got %d", len(want), len(got))
}
for i := range want {
if !reflect.DeepEqual(got[i], want[i]) {
t.Errorf("pos %d want %v got %v", i, want[i], got[i])
}
}
equal(t, got, want, &grid)
}

func TestPath_NoDiagonal1(t *testing.T) {
Expand All @@ -125,6 +118,7 @@ func TestPath_NoDiagonal1(t *testing.T) {

pathfinder := NewPathfinder(grid)
got := pathfinder.Find(Vec2{1, 1}, Vec2{3, 1})

want := []Vec2{
{1, 1},
{1, 2},
Expand All @@ -134,20 +128,7 @@ func TestPath_NoDiagonal1(t *testing.T) {
{3, 2},
{3, 1},
}
if len(got) != len(want) {
t.Logf("got: %v", got)
t.Fatalf("len want %d got %d", len(want), len(got))
}
for i := range want {
if got[i] != want[i] {
t.Errorf("pos %d want %v got %v", i, want[i], got[i])
}
}
if t.Failed() {
t.Logf(renderAsString(&grid))
t.Logf("want: %v", want)
t.Logf("got: %v", got)
}
equal(t, got, want, &grid)
}

func TestPath_NoPath(t *testing.T) {
Expand All @@ -164,7 +145,7 @@ func TestPath_NoPath(t *testing.T) {
got := pathfinder.Find(Vec2{1, 1}, Vec2{3, 1})

if len(got) != 0 {
t.Logf(renderAsString(&grid))
t.Logf(renderWithPathAsString(&grid, got))
t.Logf("got: %v", got)
t.Fatalf("len want %d got %d", 0, len(got))
}
Expand Down Expand Up @@ -193,22 +174,58 @@ func TestPath_NoDiagonal2(t *testing.T) {
{5, 2},
{6, 2},
}
equal(t, got, want, &grid)
}

func TestPath_Diagonal1(t *testing.T) {
w := []int{
0, 0, 0, 0, 0,
0, 1, 0, 1, 0,
0, 1, 0, 1, 0,
0, 1, 1, 1, 0,
0, 0, 0, 0, 0,
}
grid := NewGridFromSlice(5, 5, w)

pathfinder := NewDiagonalPathfinder(grid)
got := pathfinder.Find(Vec2{1, 1}, Vec2{3, 1})

want := []Vec2{
{1, 1},
{1, 2},
{2, 3},
{3, 2},
{3, 1},
}
equal(t, got, want, &grid)
}

func equal(t *testing.T, got, want []Vec2, grid *Grid[int]) {
t.Helper()

if len(got) != len(want) {
t.Logf("got: %v", got)
t.Fatalf("len want %d got %d", len(want), len(got))
t.Errorf("len want %d got %d", len(want), len(got))
}

for i := range want {
if i >= len(got) {
break
}
if got[i] != want[i] {
t.Errorf("pos %d want %v got %v", i, want[i], got[i])
}
}
if t.Failed() {
t.Log(renderAsString(&grid))
t.Logf("want: %v", want)
t.Logf(renderWithPathAsString(grid, want))
t.Logf("got: %v", got)
t.Logf(renderWithPathAsString(grid, got))
}
}

var _ = renderAsString // suppress unused

// renderAsString returns a string representation of the grid.
func renderAsString(grid *Grid[int]) string {
sb := &strings.Builder{}
sb.WriteString("\n")
Expand All @@ -227,6 +244,8 @@ func renderAsString(grid *Grid[int]) string {
return sb.String()
}

// renderWithPathAsString returns a string representation of the grid with the
// path drawn.
func renderWithPathAsString(grid *Grid[int], path []Vec2) string {
sb := &strings.Builder{}
sb.WriteString("\n")
Expand All @@ -250,39 +269,3 @@ func renderWithPathAsString(grid *Grid[int], path []Vec2) string {
}
return sb.String()
}

func TestPath_Diagonal1(t *testing.T) {
w := []int{
0, 0, 0, 0, 0,
0, 1, 0, 1, 0,
0, 1, 0, 1, 0,
0, 1, 1, 1, 0,
0, 0, 0, 0, 0,
}
grid := NewGridFromSlice(5, 5, w)

pathfinder := NewDiagonalPathfinder(grid)
got := pathfinder.Find(Vec2{1, 1}, Vec2{3, 1})

want := []Vec2{
{1, 1},
{1, 2},
{2, 3},
{3, 2},
{3, 1},
}
if len(got) != len(want) {
t.Logf("got: %v", got)
t.Fatalf("len want %d got %d", len(want), len(got))
}
for i := range want {
if got[i] != want[i] {
t.Errorf("pos %d want %v got %v", i, want[i], got[i])
}
}
if t.Failed() {
t.Logf(renderAsString(&grid))
t.Logf("want: %v", want)
t.Logf("got: %v", got)
}
}

0 comments on commit 9f7cf47

Please sign in to comment.