UNPKG

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