-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathpathfinding.go
283 lines (267 loc) · 9.95 KB
/
pathfinding.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
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
/*
Copyright (c) 2018, Tomasz "VedVid" Nowakowski
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package main
import (
"errors"
"fmt"
"math"
"strconv"
blt "bearlibterminal"
)
const (
// Values that are important for creating and backtracking graph.
nodeInitialWeight = -1 // Nodes not traversed.
)
type Node struct {
/* Node is struct that mimics some properties
of Tile struct (implemented in map.go).
X, Y are coords of Node, and Weight is value important
for graph creating, and later - finding shortest path
from source (creature) to goal (coords).
Weight is supposed to be set to -1 initially - it
marks Node as not traversed during
graph creation process. */
X, Y int
Weight int
}
func TilesToNodes() [][]*Node {
/* TilesToNodes is function that takes Board
(ie map, or fragment, of level) as argument. It converts
Tiles to Nodes, and returns 2d array of *Node to mimic
Board behaviour.
In future, it may be worth to create new type, ie
type Nodes [][]*Node.
During initialization, every newly created Node has
its Weight set to -1 to mark that it's not traversed. */
nodes := make([][]*Node, MapSizeX)
for i := range nodes {
nodes[i] = make([]*Node, MapSizeY)
}
for x := 0; x < MapSizeX; x++ {
for y := 0; y < MapSizeY; y++ {
nodes[x][y] = &Node{x, y, nodeInitialWeight}
}
}
return nodes
}
func FindAdjacent(b Board, c Creatures, nodes [][]*Node, frontiers []*Node, start *Node, w int) ([]*Node, bool) {
/* Function FindAdjacent takes Board, Board-like [][]*Node array,
coords of starting point, and current value to attribute Weight field
of Node; FindAdjacent returns slice of adjacent tiles and startFound
bool flag.
At start, empty slice of *Node is created, and boolean flag startFound
is set to false; this flag will be set to true, if function will find
node that is source of path, and it'll break the loops.
Primary for loop uses one of frontiers, and x, y nested loops
checks for its adjacent tiles (more details in in-line comments);
if tile met conditions, its Weight is set to current w value, then node
is added to list of adjacent tiles. */
var adjacent = []*Node{}
startFound := false
for i := 0; i < len(frontiers); i++ {
for x := frontiers[i].X - 1; x <= frontiers[i].X+1; x++ {
for y := frontiers[i].Y - 1; y <= frontiers[i].Y+1; y++ {
if x == start.X && y == start.Y {
startFound = true
nodes[x][y].Weight = w
goto End
}
if x < 0 || x >= MapSizeX || y < 0 || y >= MapSizeY {
continue //node is out of map bounds
}
if nodes[x][y].Weight != nodeInitialWeight {
continue //node is marked as traversed already
}
if x == frontiers[i].X && y == frontiers[i].Y {
continue //it's the current frontier node
}
if b[x][y].Blocked == true {
continue //tile is blocked, or it blocks line of sight
}
if GetAliveCreatureFromTile(x, y, c) != nil {
continue //tile is occupied by other monster
}
nodes[x][y].Weight = w
adjacent = append(adjacent, nodes[x][y])
}
}
}
End:
return adjacent, startFound
}
func (c *Creature) MoveTowardsPath(b Board, cs Creatures, tx, ty int) {
/* MoveTowardsPath is one of main pathfinding methods. It takes
Board and ints tx, ty (ie target coords) as arguments.
MoveTowardsPath uses weighted graph to find shortest path
from goal (tx, ty - it's more universal than passing Node or
Creature) to source (creature, ie receiver).
At first, it creates simple graph with all nodes' Weight set to
-1 as not-yet-traversed. Later, it starts potentially infinite loop
that breaks if starting position is found by FindAdjacent function,
or when FindAdjacent won't find any proper tiles that are
adjacent to previously found ones (ie frontiers).
After every iteration, local variable "w" used to attribute
node Weight increases by one, to mark that it's another step away
from goal position; it makes backtracking easy - Creature position
is end of path / graph, so Creature has only find node with
Weight set to lesser value that node occupied by Creature.
Effect may be a bit strange as it takes first node that met
conditions, but works rather well with basic MoveTowards method. */
nodes := TilesToNodes()
start := nodes[c.X][c.Y]
startFound := false
goal := nodes[tx][ty]
goal.Weight = 0
var frontiers = []*Node{goal}
w := 0
for {
w++
if len(frontiers) == 0 || startFound == true {
break
}
frontiers, startFound = FindAdjacent(b, cs, nodes, frontiers, start, w)
}
// Uncomment line below, if you want to see nodes' weights.
//RenderWeights(nodes)
dx, dy, err := BacktrackPath(nodes, start)
if err != nil {
fmt.Println(err)
}
c.Move(dx, dy, b, cs)
}
func BacktrackPath(nodes [][]*Node, start *Node) (int, int, error) {
/* Function BacktrackPath takes 2d array of *Node, and
starting *Node as arguments; it returns two ints, that serves
as coords.
BacktrackPath is used in pathfinding. It uses weighted graph
that has some sort of path already created (more in comments for
MoveTowardsPath and FindAdjacent). Instead of creating
proper path, or using search algorithm, structure of graph
allows to use just node with smaller Weight than start node.
It returns error if can't find proper tile.
Note: returning three values at once is ugly. */
direction := *start
for x := start.X - 1; x <= start.X+1; x++ {
for y := start.Y - 1; y <= start.Y+1; y++ {
if x < 0 || x >= MapSizeX || y < 0 || y >= MapSizeY {
continue // Node is out of map bounds.
}
if x == start.X && y == start.Y {
continue // This node is the current node.
}
if nodes[x][y].Weight == nodeInitialWeight {
continue // Node is not part of path.
}
if nodes[x][y].Weight < direction.Weight {
direction = *nodes[x][y] // Node is closer to goal than current node.
break
}
}
}
var err error
if direction == *start {
// This error doesn't need helper function from err_.go.
err = errors.New("Warning: function BacktrackPath could not find direction that met all requirements." +
"\n Returned coords are coords of starting position.")
}
dx := direction.X - start.X
dy := direction.Y - start.Y
return dx, dy, err
}
func RenderWeights(nodes [][]*Node) {
/* RenderWeights is created for debugging purposes.
Clears whole map, and prints Weights of all nodes
of graph, then waits for user input to continue
game loop.
It's supposed to be called near the end of
MoveTowardsPath method. */
blt.Clear()
for x := 0; x < MapSizeX; x++ {
for y := 0; y < MapSizeY; y++ {
glyph := strconv.Itoa(nodes[x][y].Weight)
if nodes[x][y].Weight == nodeInitialWeight {
glyph = "-"
} else if nodes[x][y].Weight > 9 {
glyph = "+"
}
blt.Print(x, y, glyph)
}
}
blt.Refresh()
blt.Read()
}
func (c *Creature) MoveTowards(b Board, cs Creatures, tx, ty int, ai int) {
/* MoveTowards is *the* main method for pathfinding.
Has *Creature as receiver, and takes Board (ie map of level),
ints tx and ty (ie coords of Node - in that case, it's more
universal than passing whole Node or Creature), and ai - it's
style of ai (these style markers are enums declared in ai.go)
as arguments.
Standard behaviour is always the same - check next tile on the single
path between source and target; if it's available to pass, make a move;
if not, behavior is different for every style.
Creatures with DumbAI style checks for adjacent tiles - if are available,
takes a step, otherwise stands still.
Creatures with other styles (currently only PatherAI is implemented)
calls MoveTowardsPath function, that creates weighted graph and finds
shortest path from source to goal. */
if ai == MeleePatherAI || ai == RangedPatherAI {
c.MoveTowardsPath(b, cs, tx, ty)
} else {
dx := tx - c.X
dy := ty - c.Y
ddx, ddy := 0, 0
if dx > 0 {
ddx = 1
} else if dx < 0 {
ddx = -1
}
if dy > 0 {
ddy = 1
} else if dy < 0 {
ddy = -1
}
newX, newY := c.X+ddx, c.Y+ddy
if b[newX][newY].Blocked == false && GetAliveCreatureFromTile(newX, newY, cs) == nil {
c.Move(ddx, ddy, b, cs)
} else {
if ai == MeleeDumbAI || ai == RangedDumbAI {
if ddx != 0 {
if b[newX][c.Y].Blocked == false && GetAliveCreatureFromTile(newX, c.Y, cs) == nil {
c.Move(ddx, 0, b, cs)
}
} else if ddy != 0 {
if b[c.X][newY].Blocked == false && GetAliveCreatureFromTile(c.X, newY, cs) == nil {
c.Move(0, ddy, b, cs)
}
}
}
}
}
}
func (c *Creature) DistanceTo(tx, ty int) int {
/* DistanceTo is Creature method. It takes target x and target y as args.
Computes, then returns, distance from receiver to target. */
dx := float64(tx - c.X)
dy := float64(ty - c.Y)
return RoundFloatToInt(math.Sqrt(math.Pow(dx, 2) + math.Pow(dy, 2)))
}