-
Notifications
You must be signed in to change notification settings - Fork 0
/
graph.go
196 lines (167 loc) · 4.56 KB
/
graph.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
package astar
import (
"fmt"
"math"
)
// TableEntry is a row in the A* table.
type TableEntry struct {
Node *Tile
// Distance from start.
G *float64
// Heuristic distance from end.
H float64
// For tracking the path to how we got here.
PreviousVertex *Tile
}
// F is the A* distance function - g() + h().
func (te *TableEntry) F() float64 {
if te.G == nil {
return te.H
}
return *te.G + te.H
}
func FloatPtr(f float64) *float64 {
return &f
}
type AStarGraph struct {
Board *Board
Entries map[*Tile]*TableEntry
OpenNodes map[*Tile]struct{}
ClosedNodes map[*Tile]struct{}
CurrentNode *Tile
}
func NewAStarGraph(b *Board) *AStarGraph {
g := &AStarGraph{
Board: b,
OpenNodes: map[*Tile]struct{}{},
ClosedNodes: map[*Tile]struct{}{},
}
entries := map[*Tile]*TableEntry{}
for _, tile := range b.tiles {
entries[tile] = &TableEntry{
Node: tile,
G: nil,
H: tile.HeuristicDistanceFrom(b.End()),
PreviousVertex: nil,
}
}
g.Entries = entries
// Initialize the graph - open the neighbors of start.
for _, neighbor := range g.Neighbors(b.Start()) {
g.OpenNodes[neighbor.Tile] = struct{}{}
neighbor.Tile.SetKind(TypeOpen)
entry := g.Entries[neighbor.Tile]
entry.G = &neighbor.Cost
entry.PreviousVertex = b.Start()
entry.Node.value = fmt.Sprintf("%.1f", entry.F())
}
return g
}
// MarkPath resets the isPath value for every tile in the grid.
// It then paints the path to the CurrentNode in our graph traversal.
// This is very inefficient, and not necessary ffor solving A*.
// Instead we do this to give the user a visual representation of what the
// algorithm is "doing".
func (a *AStarGraph) MarkPath() {
for _, tile := range a.Board.tiles {
tile.isPath = false
}
entry := a.Entries[a.CurrentNode]
tile := entry.PreviousVertex
for tile != nil && tile != a.Board.Start() {
tile.isPath = true
entry := a.Entries[tile]
tile = entry.PreviousVertex
}
}
// Step explores the next "Open" node with the lowest F-value.
// If that node is End, then we have reached the end and found the shortest path.
func (a *AStarGraph) Step() {
if a.CurrentNode == a.Board.End() {
a.MarkPath()
return
}
// Get the open node with the lowest f-value.
smallestF := math.MaxFloat64
var tile *Tile
for t := range a.OpenNodes {
entry := a.Entries[t]
if entry.F() < smallestF {
smallestF = entry.F()
tile = t
}
}
a.CurrentNode = tile
currentEntry := a.Entries[tile]
if a.CurrentNode == a.Board.End() || a.CurrentNode == nil {
// We have found the shortest path.
return
}
for _, neighbor := range a.Neighbors(tile) {
// Open the node.
a.OpenNodes[neighbor.Tile] = struct{}{}
neighbor.Tile.SetKind(TypeOpen)
// Update the entries table if the new f-value is smaller.
entry := a.Entries[neighbor.Tile]
if entry.G == nil || entry.F() > *currentEntry.G+neighbor.Cost {
entry.G = FloatPtr(*currentEntry.G + neighbor.Cost)
entry.Node.value = fmt.Sprintf("%.1f", entry.F())
entry.PreviousVertex = tile
}
}
// Close the tile.
delete(a.OpenNodes, tile)
a.ClosedNodes[tile] = struct{}{}
tile.SetKind(TypeClosed)
// Draw the path on the map.
a.MarkPath()
}
// EdgeTo represents the edge from a tile to its valid neighbors.
type EdgeTo struct {
Tile *Tile
Cost float64
}
// Neighbors returns all neighbors to the tile that are not a wall or closed.
// Neighbors in cardinal directions have a cost of 1 (Lef, Right, Up, Down).
// Neighbors in corners have a cost of sqrt2 (TopRight, TopLeft, BotRight, BotLeft).
// Walls and closed (fully visited) nodes are not returned.
func (a *AStarGraph) Neighbors(tile *Tile) []*EdgeTo {
result := []*EdgeTo{}
for i := -1; i <= 1; i++ {
for j := -1; j <= 1; j++ {
neighbor, ok := a.isNeighbor(tile.x+i, tile.y+j, tile)
if ok {
cost := math.Sqrt2
if i == 0 || j == 0 {
cost = 1.0
}
result = append(result, &EdgeTo{Tile: neighbor, Cost: cost})
}
}
}
return result
}
// isNeighbor takes a tile, and a relative position to the tile.
// If the neighbor is on the board, not closed, and not a wall we return it.
func (a *AStarGraph) isNeighbor(i, j int, tile *Tile) (*Tile, bool) {
// Neighbor is off the grid.
if i < 0 || i >= a.Board.width {
return nil, false
}
if j < 0 || j >= a.Board.height {
return nil, false
}
// Neighbor is the tile itself.
if tile.x == i && tile.y == j {
return nil, false
}
index := a.Board.Index(i, j)
neighbor := a.Board.tiles[index]
if neighbor.kind == TypeWall {
return nil, false
}
if _, closed := a.ClosedNodes[neighbor]; closed {
return nil, false
}
return neighbor, true
}