You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

410 lines
11 KiB

  1. // javascript-astar 0.4.1
  2. // http://github.com/bgrins/javascript-astar
  3. // Freely distributable under the MIT License.
  4. // Implements the astar search algorithm in javascript using a Binary Heap.
  5. // Includes Binary Heap (with modifications) from Marijn Haverbeke.
  6. // http://eloquentjavascript.net/appendix2.html
  7. (function (definition) {
  8. /* global module, define */
  9. if (typeof module === 'object' && typeof module.exports === 'object')
  10. module.exports = definition();
  11. else if (typeof define === 'function' && define.amd)
  12. define([], definition);
  13. else {
  14. let exports = definition();
  15. window.astar = exports.astar;
  16. window.Graph = exports.Graph;
  17. }
  18. })(function () {
  19. function pathTo (node) {
  20. let curr = node;
  21. let path = [];
  22. while (curr.parent) {
  23. path.unshift(curr);
  24. curr = curr.parent;
  25. }
  26. return path;
  27. }
  28. function getHeap () {
  29. return new BinaryHeap(function (node) {
  30. return node.f;
  31. });
  32. }
  33. let astar = {
  34. /**
  35. * Perform an A* Search on a graph given a start and end node.
  36. * @param {Graph} graph
  37. * @param {GridNode} start
  38. * @param {GridNode} end
  39. * @param {Object} [options]
  40. * @param {bool} [options.closest] Specifies whether to return the
  41. path to the closest node if the target is unreachable.
  42. * @param {Function} [options.heuristic] Heuristic function (see
  43. * astar.heuristics).
  44. */
  45. search: function (graph, start, end, options) {
  46. start = graph.grid[start.x][start.y] || start;
  47. end = graph.grid[end.x][end.y] || end;
  48. graph.cleanDirty();
  49. options = options || {};
  50. let heuristic = options.heuristic || astar.heuristics.manhattan;
  51. let closest = options.closest || false;
  52. let distance = options.distance;
  53. if (distance)
  54. heuristic = astar.heuristics.manhattanDistance;
  55. let openHeap = getHeap();
  56. let closestNode = start; // set the start node to be the closest if required
  57. start.h = heuristic(start, end, distance);
  58. graph.markDirty(start);
  59. openHeap.push(start);
  60. while (openHeap.size() > 0) {
  61. // Grab the lowest f(x) to process next. Heap keeps this sorted for us.
  62. let currentNode = openHeap.pop();
  63. let onWall = !currentNode.isWall || currentNode.isWall();
  64. if (!onWall) {
  65. if (distance) {
  66. if (currentNode.h === distance)
  67. return pathTo(currentNode);
  68. } else {
  69. // End case -- result has been found, return the traced path.
  70. if (currentNode === end)
  71. return pathTo(currentNode);
  72. }
  73. }
  74. // Normal case -- move currentNode from open to closed, process each of its neighbors.
  75. currentNode.closed = true;
  76. // Find all neighbors for the current node.
  77. let neighbors = graph.neighbors(currentNode);
  78. for (let i = 0, il = neighbors.length; i < il; ++i) {
  79. let neighbor = neighbors[i];
  80. if (neighbor.closed || neighbor.isWall()) {
  81. // Not a valid node to process, skip to next neighbor.
  82. continue;
  83. }
  84. // The g score is the shortest distance from start to current node.
  85. // We need to check if the path we have arrived at this neighbor is the shortest one we have seen yet.
  86. let gScore = currentNode.g + neighbor.getCost(currentNode);
  87. let beenVisited = neighbor.visited;
  88. if (!beenVisited || gScore < neighbor.g) {
  89. // Found an optimal (so far) path to this node. Take score for node to see how good it is.
  90. neighbor.visited = true;
  91. neighbor.parent = currentNode;
  92. neighbor.h = neighbor.h || heuristic(neighbor, end, distance);
  93. neighbor.g = gScore;
  94. neighbor.f = neighbor.g + neighbor.h;
  95. graph.markDirty(neighbor);
  96. if (closest) {
  97. // If the neighbour is closer than the current closestNode or if it's equally close but has
  98. // a cheaper path than the current closest node then it becomes the closest node
  99. if (neighbor.h < closestNode.h || (neighbor.h === closestNode.h && neighbor.g < closestNode.g))
  100. closestNode = neighbor;
  101. }
  102. if (!beenVisited) {
  103. // Pushing to heap will put it in proper place based on the 'f' value.
  104. openHeap.push(neighbor);
  105. } else {
  106. // Already seen the node, but since it has been rescored we need to reorder it in the heap
  107. openHeap.rescoreElement(neighbor);
  108. }
  109. }
  110. }
  111. }
  112. if (closest)
  113. return pathTo(closestNode);
  114. // No result was found - empty array signifies failure to find path.
  115. return [];
  116. },
  117. // See list of heuristics: http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html
  118. heuristics: {
  119. manhattan: function (pos0, pos1) {
  120. let d1 = Math.abs(pos1.x - pos0.x);
  121. let d2 = Math.abs(pos1.y - pos0.y);
  122. return Math.max(d1, d2);
  123. },
  124. manhattanDistance: function (pos0, pos1, distance) {
  125. let d1 = Math.abs(pos1.x - pos0.x);
  126. let d2 = Math.abs(pos1.y - pos0.y);
  127. return Math.abs(distance - Math.max(d1, d2)) + 1;
  128. },
  129. diagonal: function (pos0, pos1) {
  130. let D = 1;
  131. let D2 = Math.sqrt(2);
  132. let d1 = Math.abs(pos1.x - pos0.x);
  133. let d2 = Math.abs(pos1.y - pos0.y);
  134. return (D * (d1 + d2)) + ((D2 - (2 * D)) * Math.min(d1, d2));
  135. }
  136. },
  137. cleanNode: function (node) {
  138. if (!node)
  139. return;
  140. node.f = 0;
  141. node.g = 0;
  142. node.h = 0;
  143. node.visited = false;
  144. node.closed = false;
  145. node.parent = null;
  146. }
  147. };
  148. /**
  149. * A graph memory structure
  150. * @param {Array} gridIn 2D array of input weights
  151. * @param {Object} [options]
  152. * @param {bool} [options.diagonal] Specifies whether diagonal moves are allowed
  153. */
  154. function Graph (gridIn, options) {
  155. options = options || {};
  156. this.nodes = [];
  157. this.diagonal = !!options.diagonal;
  158. this.grid = [];
  159. for (let x = 0; x < gridIn.length; x++) {
  160. this.grid[x] = [];
  161. for (let y = 0, row = gridIn[x]; y < row.length; y++) {
  162. if (!row[y]) {
  163. node = new GridNode(x, y, row[y] ? 0 : 1);
  164. this.grid[x][y] = node;
  165. this.nodes.push(node);
  166. }
  167. }
  168. }
  169. this.init();
  170. }
  171. Graph.prototype.init = function () {
  172. this.dirtyNodes = [];
  173. for (let i = 0; i < this.nodes.length; i++)
  174. astar.cleanNode(this.nodes[i]);
  175. };
  176. Graph.prototype.cleanDirty = function () {
  177. for (let i = 0; i < this.dirtyNodes.length; i++)
  178. astar.cleanNode(this.dirtyNodes[i]);
  179. this.dirtyNodes = [];
  180. };
  181. Graph.prototype.markDirty = function (node) {
  182. this.dirtyNodes.push(node);
  183. };
  184. Graph.prototype.neighbors = function (node) {
  185. let ret = [];
  186. let x = node.x;
  187. let y = node.y;
  188. let grid = this.grid;
  189. // West
  190. if (grid[x - 1] && grid[x - 1][y])
  191. ret.push(grid[x - 1][y]);
  192. // East
  193. if (grid[x + 1] && grid[x + 1][y])
  194. ret.push(grid[x + 1][y]);
  195. // South
  196. if (grid[x] && grid[x][y - 1])
  197. ret.push(grid[x][y - 1]);
  198. // North
  199. if (grid[x] && grid[x][y + 1])
  200. ret.push(grid[x][y + 1]);
  201. if (this.diagonal) {
  202. // Southwest
  203. if (grid[x - 1] && grid[x - 1][y - 1])
  204. ret.push(grid[x - 1][y - 1]);
  205. // Southeast
  206. if (grid[x + 1] && grid[x + 1][y - 1])
  207. ret.push(grid[x + 1][y - 1]);
  208. // Northwest
  209. if (grid[x - 1] && grid[x - 1][y + 1])
  210. ret.push(grid[x - 1][y + 1]);
  211. // Northeast
  212. if (grid[x + 1] && grid[x + 1][y + 1])
  213. ret.push(grid[x + 1][y + 1]);
  214. }
  215. return ret;
  216. };
  217. Graph.prototype.toString = function () {
  218. let graphString = [];
  219. let nodes = this.grid;
  220. for (let x = 0; x < nodes.length; x++) {
  221. let rowDebug = [];
  222. let row = nodes[x];
  223. for (let y = 0; y < row.length; y++)
  224. rowDebug.push(row[y].weight);
  225. graphString.push(rowDebug.join(' '));
  226. }
  227. return graphString.join('\n');
  228. };
  229. function GridNode (x, y, weight) {
  230. this.x = x;
  231. this.y = y;
  232. this.weight = weight;
  233. }
  234. GridNode.prototype.toString = function () {
  235. return '[' + this.x + ' ' + this.y + ']';
  236. };
  237. GridNode.prototype.getCost = function (fromNeighbor) {
  238. // Take diagonal weight into consideration.
  239. if (fromNeighbor && fromNeighbor.x !== this.x && fromNeighbor.y !== this.y)
  240. return this.weight * 1.41421;
  241. return this.weight;
  242. };
  243. GridNode.prototype.isWall = function () {
  244. return this.weight === 0;
  245. };
  246. function BinaryHeap (scoreFunction) {
  247. this.content = [];
  248. this.scoreFunction = scoreFunction;
  249. }
  250. BinaryHeap.prototype = {
  251. push: function (element) {
  252. // Add the new element to the end of the array.
  253. this.content.push(element);
  254. // Allow it to sink down.
  255. this.sinkDown(this.content.length - 1);
  256. },
  257. pop: function () {
  258. // Store the first element so we can return it later.
  259. let result = this.content[0];
  260. // Get the element at the end of the array.
  261. let end = this.content.pop();
  262. // If there are any elements left, put the end element at the
  263. // start, and let it bubble up.
  264. if (this.content.length > 0) {
  265. this.content[0] = end;
  266. this.bubbleUp(0);
  267. }
  268. return result;
  269. },
  270. remove: function (node) {
  271. let i = this.content.indexOf(node);
  272. // When it is found, the process seen in 'pop' is repeated
  273. // to fill up the hole.
  274. let end = this.content.pop();
  275. if (i !== this.content.length - 1) {
  276. this.content[i] = end;
  277. if (this.scoreFunction(end) < this.scoreFunction(node))
  278. this.sinkDown(i);
  279. else
  280. this.bubbleUp(i);
  281. }
  282. },
  283. size: function () {
  284. return this.content.length;
  285. },
  286. rescoreElement: function (node) {
  287. this.sinkDown(this.content.indexOf(node));
  288. },
  289. sinkDown: function (n) {
  290. // Fetch the element that has to be sunk.
  291. let element = this.content[n];
  292. // When at 0, an element can not sink any further.
  293. while (n > 0) {
  294. // Compute the parent element's index, and fetch it.
  295. let parentN = ((n + 1) >> 1) - 1;
  296. let parent = this.content[parentN];
  297. // Swap the elements if the parent is greater.
  298. if (this.scoreFunction(element) < this.scoreFunction(parent)) {
  299. this.content[parentN] = element;
  300. this.content[n] = parent;
  301. // Update 'n' to continue at the new position.
  302. n = parentN;
  303. }
  304. // Found a parent that is less, no need to sink any further.
  305. else
  306. break;
  307. }
  308. },
  309. bubbleUp: function (n) {
  310. // Look up the target element and its score.
  311. let length = this.content.length;
  312. let element = this.content[n];
  313. let elemScore = this.scoreFunction(element);
  314. while (true) {
  315. // Compute the indices of the child elements.
  316. let child2N = (n + 1) << 1;
  317. let child1N = child2N - 1;
  318. // This is used to store the new position of the element, if any.
  319. let swap = null;
  320. let child1Score;
  321. // If the first child exists (is inside the array)...
  322. if (child1N < length) {
  323. // Look it up and compute its score.
  324. let child1 = this.content[child1N];
  325. child1Score = this.scoreFunction(child1);
  326. // If the score is less than our element's, we need to swap.
  327. if (child1Score < elemScore)
  328. swap = child1N;
  329. }
  330. // Do the same checks for the other child.
  331. if (child2N < length) {
  332. let child2 = this.content[child2N];
  333. let child2Score = this.scoreFunction(child2);
  334. if (child2Score < (swap === null ? elemScore : child1Score))
  335. swap = child2N;
  336. }
  337. // If the element needs to be moved, swap it, and continue.
  338. if (swap !== null) {
  339. this.content[n] = this.content[swap];
  340. this.content[swap] = element;
  341. n = swap;
  342. }
  343. // Otherwise, we are done.
  344. else
  345. break;
  346. }
  347. }
  348. };
  349. return {
  350. astar: astar,
  351. Graph: Graph,
  352. gridNode: GridNode
  353. };
  354. });