UNPKG

6.59 kBJavaScriptView Raw
1var BinaryHeap = require("./binary-heap");
2
3/**
4 * Implements the [A* pathfinding algorithm]{@link http://en.wikipedia.org/wiki/A*_search_algorithm} on a 2-dimensional grid. You can use this to find a path between a source and destination coordinate while avoiding obstacles.
5 * @constructor
6 * @alias Splat.AStar
7 * @param {isWalkable} isWalkable A function to test if a coordinate is walkable by the entity you're performing the pathfinding for.
8 */
9function AStar(isWalkable) {
10 this.destX = 0;
11 this.destY = 0;
12 this.scaleX = 1;
13 this.scaleY = 1;
14 this.openNodes = {};
15 this.closedNodes = {};
16 this.openHeap = new BinaryHeap(function(a, b) {
17 return a.f - b.f;
18 });
19 this.isWalkable = isWalkable;
20}
21/**
22 * The [A* heuristic]{@link http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html}, commonly referred to as h(x), that estimates how far a location is from the destination. This implementation is the [Manhattan method]{@link http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html#manhattan-distance}, which is good for situations when the entity can travel in four directions. Feel free to replace this with a different heuristic implementation.
23 * @param {number} x The x coordinate to estimate the distance to the destination.
24 * @param {number} y The y coordinate to estimate the distance to the destination.
25 */
26AStar.prototype.heuristic = function(x, y) {
27 // manhattan method
28 var dx = Math.abs(x - this.destX) / this.scaleX;
29 var dy = Math.abs(y - this.destY) / this.scaleY;
30 return dx + dy;
31};
32/**
33 * Make a node to track a given coordinate
34 * @param {number} x The x coordinate of the node
35 * @param {number} y The y coordinate of the node
36 * @param {object} parent The parent node for the current node. This chain of parents eventually points back at the starting node.
37 * @param {number} g The g(x) travel cost from the parent node to this node.
38 * @private
39 */
40AStar.prototype.makeNode = function(x, y, parent, g) {
41 g += parent.g;
42 var h = this.heuristic(x, y);
43
44 return {
45 x: x,
46 y: y,
47 parent: parent,
48 f: g + h,
49 g: parent.g + g,
50 h: h
51 };
52};
53/**
54 * Update the g(x) travel cost to a node if a new lower-cost path is found.
55 * @param {string} key The key of the node on the open list.
56 * @param {object} parent A parent node that may have a shorter path for the node specified in key.
57 * @param {number} g The g(x) travel cost from parent to the node specified in key.
58 * @private
59 */
60AStar.prototype.updateOpenNode = function(key, parent, g) {
61 var node = this.openNodes[key];
62 if (!node) {
63 return false;
64 }
65
66 var newG = parent.g + g;
67
68 if (newG >= node.g) {
69 return true;
70 }
71
72 node.parent = parent;
73 node.g = newG;
74 node.f = node.g + node.h;
75
76 var pos = this.openHeap.indexOf(node);
77 this.openHeap.bubbleUp(pos);
78
79 return true;
80};
81/**
82 * Create a neighbor node to a parent node, and add it to the open list for consideration.
83 * @param {string} key The key of the new neighbor node.
84 * @param {number} x The x coordinate of the new neighbor node.
85 * @param {number} y The y coordinate of the new neighbor node.
86 * @param {object} parent The parent node of the new neighbor node.
87 * @param {number} g The travel cost from the parent to the new parent node.
88 * @private
89 */
90AStar.prototype.insertNeighbor = function(key, x, y, parent, g) {
91 var node = this.makeNode(x, y, parent, g);
92 this.openNodes[key] = node;
93 this.openHeap.insert(node);
94};
95AStar.prototype.tryNeighbor = function(x, y, parent, g) {
96 var key = makeKey(x, y);
97 if (this.closedNodes[key]) {
98 return;
99 }
100 if (!this.isWalkable(x, y)) {
101 return;
102 }
103 if (!this.updateOpenNode(key, parent, g)) {
104 this.insertNeighbor(key, x, y, parent, g);
105 }
106};
107AStar.prototype.getNeighbors = function getNeighbors(parent) {
108 var diagonalCost = 1.4;
109 var straightCost = 1;
110 this.tryNeighbor(parent.x - this.scaleX, parent.y - this.scaleY, parent, diagonalCost);
111 this.tryNeighbor(parent.x, parent.y - this.scaleY, parent, straightCost);
112 this.tryNeighbor(parent.x + this.scaleX, parent.y - this.scaleY, parent, diagonalCost);
113
114 this.tryNeighbor(parent.x - this.scaleX, parent.y, parent, straightCost);
115 this.tryNeighbor(parent.x + this.scaleX, parent.y, parent, straightCost);
116
117 this.tryNeighbor(parent.x - this.scaleX, parent.y + this.scaleY, parent, diagonalCost);
118 this.tryNeighbor(parent.x, parent.y + this.scaleY, parent, straightCost);
119 this.tryNeighbor(parent.x + this.scaleX, parent.y + this.scaleY, parent, diagonalCost);
120};
121
122function generatePath(node) {
123 var path = [];
124 while (node.parent) {
125 var ix = node.x;
126 var iy = node.y;
127 while (ix !== node.parent.x || iy !== node.parent.y) {
128 path.unshift({ x: ix, y: iy });
129
130 var dx = node.parent.x - ix;
131 if (dx > 0) {
132 ix++;
133 } else if (dx < 0) {
134 ix--;
135 }
136 var dy = node.parent.y - iy;
137 if (dy > 0) {
138 iy++;
139 } else if (dy < 0) {
140 iy--;
141 }
142 }
143 node = node.parent;
144 }
145 return path;
146}
147
148function makeKey(x, y) {
149 return x + "," + y;
150}
151
152/**
153 * Search for an optimal path between srcX, srcY and destX, destY, while avoiding obstacles.
154 * @param {number} srcX The starting x coordinate
155 * @param {number} srcY The starting y coordinate
156 * @param {number} destX The destination x coordinate
157 * @param {number} destY The destination y coordinate
158 * @returns {Array} The optimal path, in the form of an array of objects that each have an x and y property.
159 */
160AStar.prototype.search = function aStar(srcX, srcY, destX, destY) {
161 function scale(c, s) {
162 var downscaled = Math.floor(c / s);
163 return downscaled * s;
164 }
165 srcX = scale(srcX, this.scaleX);
166 srcY = scale(srcY, this.scaleY);
167 this.destX = scale(destX, this.scaleX);
168 this.destY = scale(destY, this.scaleY);
169
170 if (!this.isWalkable(this.destX, this.destY)) {
171 return [];
172 }
173
174 var srcKey = makeKey(srcX, srcY);
175 var srcNode = {
176 x: srcX,
177 y: srcY,
178 g: 0,
179 h: this.heuristic(srcX, srcY)
180 };
181 srcNode.f = srcNode.h;
182 this.openNodes = {};
183 this.openNodes[srcKey] = srcNode;
184 this.openHeap = new BinaryHeap(function(a, b) {
185 return a.f - b.f;
186 });
187 this.openHeap.insert(srcNode);
188 this.closedNodes = {};
189
190 var node = this.openHeap.deleteRoot();
191 while (node) {
192 var key = makeKey(node.x, node.y);
193 delete this.openNodes[key];
194 this.closedNodes[key] = node;
195 if (node.x === this.destX && node.y === this.destY) {
196 return generatePath(node);
197 }
198 this.getNeighbors(node);
199 node = this.openHeap.deleteRoot();
200 }
201 return [];
202};
203
204module.exports = AStar;