diff --git a/.gitignore b/.gitignore index fd4f2b0..22d59e5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules .DS_Store +.idea diff --git a/astar.js b/astar.js index 8a25115..8820620 100644 --- a/astar.js +++ b/astar.js @@ -4,148 +4,37 @@ // Implements the astar search algorithm in javascript using a Binary Heap. // Includes Binary Heap (with modifications) from Marijn Haverbeke. // http://eloquentjavascript.net/appendix2.html -(function(definition) { - /* global module, define */ - if (typeof module === 'object' && typeof module.exports === 'object') { - module.exports = definition(); - } else if (typeof define === 'function' && define.amd) { - define([], definition); - } else { - var exports = definition(); - window.astar = exports.astar; - window.Graph = exports.Graph; - } -})(function() { - -function pathTo(node) { - var curr = node; - var path = []; - while (curr.parent) { - path.unshift(curr); - curr = curr.parent; - } - return path; -} -function getHeap() { - return new BinaryHeap(function(node) { - return node.f; - }); +function GridNode(x, y, weight) { + this.x = x; + this.y = y; + this.weight = weight; + this.clean(); } -var astar = { - /** - * Perform an A* Search on a graph given a start and end node. - * @param {Graph} graph - * @param {GridNode} start - * @param {GridNode} end - * @param {Object} [options] - * @param {bool} [options.closest] Specifies whether to return the - path to the closest node if the target is unreachable. - * @param {Function} [options.heuristic] Heuristic function (see - * astar.heuristics). - */ - search: function(graph, start, end, options) { - graph.cleanDirty(); - options = options || {}; - var heuristic = options.heuristic || astar.heuristics.manhattan; - var closest = options.closest || false; - - var openHeap = getHeap(); - var closestNode = start; // set the start node to be the closest if required - - start.h = heuristic(start, end); - graph.markDirty(start); - - openHeap.push(start); - - while (openHeap.size() > 0) { - - // Grab the lowest f(x) to process next. Heap keeps this sorted for us. - var currentNode = openHeap.pop(); - - // End case -- result has been found, return the traced path. - if (currentNode === end) { - return pathTo(currentNode); - } - - // Normal case -- move currentNode from open to closed, process each of its neighbors. - currentNode.closed = true; - - // Find all neighbors for the current node. - var neighbors = graph.neighbors(currentNode); - - for (var i = 0, il = neighbors.length; i < il; ++i) { - var neighbor = neighbors[i]; - - if (neighbor.closed || neighbor.isWall()) { - // Not a valid node to process, skip to next neighbor. - continue; - } - - // The g score is the shortest distance from start to current node. - // We need to check if the path we have arrived at this neighbor is the shortest one we have seen yet. - var gScore = currentNode.g + neighbor.getCost(currentNode); - var beenVisited = neighbor.visited; - - if (!beenVisited || gScore < neighbor.g) { - - // Found an optimal (so far) path to this node. Take score for node to see how good it is. - neighbor.visited = true; - neighbor.parent = currentNode; - neighbor.h = neighbor.h || heuristic(neighbor, end); - neighbor.g = gScore; - neighbor.f = neighbor.g + neighbor.h; - graph.markDirty(neighbor); - if (closest) { - // If the neighbour is closer than the current closestNode or if it's equally close but has - // a cheaper path than the current closest node then it becomes the closest node - if (neighbor.h < closestNode.h || (neighbor.h === closestNode.h && neighbor.g < closestNode.g)) { - closestNode = neighbor; - } - } +GridNode.prototype.toString = function() { + return `[${this.x} ${this.y}]`; +}; - if (!beenVisited) { - // Pushing to heap will put it in proper place based on the 'f' value. - openHeap.push(neighbor); - } else { - // Already seen the node, but since it has been rescored we need to reorder it in the heap - openHeap.rescoreElement(neighbor); - } - } - } - } +GridNode.prototype.getCost = function(fromNeighbor) { + // Take diagonal weight into consideration. + if (fromNeighbor && fromNeighbor.x !== this.x && fromNeighbor.y !== this.y) { + return this.weight + 0.5; + } + return this.weight; +}; - if (closest) { - return pathTo(closestNode); - } +GridNode.prototype.isWall = function() { + return this.weight === 0; +}; - // No result was found - empty array signifies failure to find path. - return []; - }, - // See list of heuristics: http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html - heuristics: { - manhattan: function(pos0, pos1) { - var d1 = Math.abs(pos1.x - pos0.x); - var d2 = Math.abs(pos1.y - pos0.y); - return d1 + d2; - }, - diagonal: function(pos0, pos1) { - var D = 1; - var D2 = Math.sqrt(2); - var d1 = Math.abs(pos1.x - pos0.x); - var d2 = Math.abs(pos1.y - pos0.y); - return (D * (d1 + d2)) + ((D2 - (2 * D)) * Math.min(d1, d2)); - } - }, - cleanNode: function(node) { - node.f = 0; - node.g = 0; - node.h = 0; - node.visited = false; - node.closed = false; - node.parent = null; - } +GridNode.prototype.clean = function() { + this.f = 0; + this.g = 0; + this.h = 0; + this.visited = false; + this.closed = false; + this.parent = null; }; /** @@ -154,16 +43,16 @@ var astar = { * @param {Object} [options] * @param {bool} [options.diagonal] Specifies whether diagonal moves are allowed */ -function Graph(gridIn, options) { +export function Graph(gridIn, options) { options = options || {}; this.nodes = []; this.diagonal = !!options.diagonal; this.grid = []; - for (var x = 0; x < gridIn.length; x++) { + for (let x = 0; x < gridIn.length; x++) { this.grid[x] = []; - for (var y = 0, row = gridIn[x]; y < row.length; y++) { - var node = new GridNode(x, y, row[y]); + for (let y = 0, row = gridIn[x]; y < row.length; y++) { + const node = new GridNode(x, y, row[y]); this.grid[x][y] = node; this.nodes.push(node); } @@ -173,14 +62,14 @@ function Graph(gridIn, options) { Graph.prototype.init = function() { this.dirtyNodes = []; - for (var i = 0; i < this.nodes.length; i++) { - astar.cleanNode(this.nodes[i]); + for (let i = 0; i < this.nodes.length; i++) { + this.nodes[i].clean(); } }; Graph.prototype.cleanDirty = function() { - for (var i = 0; i < this.dirtyNodes.length; i++) { - astar.cleanNode(this.dirtyNodes[i]); + for (let i = 0; i < this.dirtyNodes.length; i++) { + this.dirtyNodes[i].clean(); } this.dirtyNodes = []; }; @@ -190,10 +79,10 @@ Graph.prototype.markDirty = function(node) { }; Graph.prototype.neighbors = function(node) { - var ret = []; - var x = node.x; - var y = node.y; - var grid = this.grid; + const ret = []; + const x = node.x; + const y = node.y; + const grid = this.grid; // West if (grid[x - 1] && grid[x - 1][y]) { @@ -241,39 +130,17 @@ Graph.prototype.neighbors = function(node) { }; Graph.prototype.toString = function() { - var graphString = []; - var nodes = this.grid; - for (var x = 0; x < nodes.length; x++) { - var rowDebug = []; - var row = nodes[x]; - for (var y = 0; y < row.length; y++) { + const graphString = []; + const nodes = this.grid; + for (let x = 0; x < nodes.length; x++) { + const rowDebug = []; + const row = nodes[x]; + for (let y = 0; y < row.length; y++) { rowDebug.push(row[y].weight); } - graphString.push(rowDebug.join(" ")); - } - return graphString.join("\n"); -}; - -function GridNode(x, y, weight) { - this.x = x; - this.y = y; - this.weight = weight; -} - -GridNode.prototype.toString = function() { - return "[" + this.x + " " + this.y + "]"; -}; - -GridNode.prototype.getCost = function(fromNeighbor) { - // Take diagonal weight into consideration. - if (fromNeighbor && fromNeighbor.x != this.x && fromNeighbor.y != this.y) { - return this.weight * 1.41421; + graphString.push(rowDebug.join(' ')); } - return this.weight; -}; - -GridNode.prototype.isWall = function() { - return this.weight === 0; + return graphString.join('\n'); }; function BinaryHeap(scoreFunction) { @@ -291,9 +158,9 @@ BinaryHeap.prototype = { }, pop: function() { // Store the first element so we can return it later. - var result = this.content[0]; + const result = this.content[0]; // Get the element at the end of the array. - var end = this.content.pop(); + const end = this.content.pop(); // If there are any elements left, put the end element at the // start, and let it bubble up. if (this.content.length > 0) { @@ -303,18 +170,19 @@ BinaryHeap.prototype = { return result; }, remove: function(node) { - var i = this.content.indexOf(node); + const i = this.content.indexOf(node); // When it is found, the process seen in 'pop' is repeated // to fill up the hole. - var end = this.content.pop(); + const end = this.content.pop(); if (i !== this.content.length - 1) { this.content[i] = end; if (this.scoreFunction(end) < this.scoreFunction(node)) { this.sinkDown(i); - } else { + } + else { this.bubbleUp(i); } } @@ -327,14 +195,13 @@ BinaryHeap.prototype = { }, sinkDown: function(n) { // Fetch the element that has to be sunk. - var element = this.content[n]; + const element = this.content[n]; // When at 0, an element can not sink any further. while (n > 0) { - // Compute the parent element's index, and fetch it. - var parentN = ((n + 1) >> 1) - 1; - var parent = this.content[parentN]; + const parentN = ((n + 1) >> 1) - 1; + const parent = this.content[parentN]; // Swap the elements if the parent is greater. if (this.scoreFunction(element) < this.scoreFunction(parent)) { this.content[parentN] = element; @@ -350,21 +217,22 @@ BinaryHeap.prototype = { }, bubbleUp: function(n) { // Look up the target element and its score. - var length = this.content.length; - var element = this.content[n]; - var elemScore = this.scoreFunction(element); + const length = this.content.length; + const element = this.content[n]; + const elemScore = this.scoreFunction(element); - while (true) { + let shouldLoop = true; + while (shouldLoop) { // Compute the indices of the child elements. - var child2N = (n + 1) << 1; - var child1N = child2N - 1; + const child2N = (n + 1) << 1; + const child1N = child2N - 1; // This is used to store the new position of the element, if any. - var swap = null; - var child1Score; + let swap = null; + let child1Score; // If the first child exists (is inside the array)... if (child1N < length) { // Look it up and compute its score. - var child1 = this.content[child1N]; + const child1 = this.content[child1N]; child1Score = this.scoreFunction(child1); // If the score is less than our element's, we need to swap. @@ -375,8 +243,8 @@ BinaryHeap.prototype = { // Do the same checks for the other child. if (child2N < length) { - var child2 = this.content[child2N]; - var child2Score = this.scoreFunction(child2); + const child2 = this.content[child2N]; + const child2Score = this.scoreFunction(child2); if (child2Score < (swap === null ? elemScore : child1Score)) { swap = child2N; } @@ -390,15 +258,129 @@ BinaryHeap.prototype = { } // Otherwise, we are done. else { - break; + shouldLoop = false; } } } }; -return { - astar: astar, - Graph: Graph -}; +export const astar = { + pathTo: function(node) { + let curr = node; + const path = []; + while (curr.parent) { + path.unshift(curr); + curr = curr.parent; + } + return path; + }, + getHeap: function() { + return new BinaryHeap(function(node) { + return node.f; + }); + }, + + /** + * Perform an A* Search on a graph given a start and end node. + * @param {Graph} graph + * @param {GridNode} start + * @param {GridNode} end + * @param {Object} [options] + * @param {bool} [options.closest] Specifies whether to return the + path to the closest node if the target is unreachable. + * @param {Function} [options.heuristic] Heuristic function (see + * astar.heuristics). + */ + search: function(graph, start, end, options) { + graph.cleanDirty(); + options = options || {}; + const heuristic = options.heuristic || astar.heuristics.manhattan; + const closest = options.closest || false; + + const openHeap = this.getHeap(); + let closestNode = start; // set the start node to be the closest if required + + start.h = heuristic(start, end); + graph.markDirty(start); + + openHeap.push(start); + + while (openHeap.size() > 0) { + // Grab the lowest f(x) to process next. Heap keeps this sorted for us. + const currentNode = openHeap.pop(); + + // End case -- result has been found, return the traced path. + if (currentNode === end) { + return this.pathTo(currentNode); + } + + // Normal case -- move currentNode from open to closed, process each of its neighbors. + currentNode.closed = true; + + // Find all neighbors for the current node. + const neighbors = graph.neighbors(currentNode); + + for (let i = 0, il = neighbors.length; i < il; ++i) { + const neighbor = neighbors[i]; + + if (neighbor.closed || neighbor.isWall()) { + // Not a valid node to process, skip to next neighbor. + continue; + } + + // The g score is the shortest distance from start to current node. + // We need to check if the path we have arrived at this neighbor is the shortest one we have seen yet. + const gScore = currentNode.g + neighbor.getCost(currentNode); + const beenVisited = neighbor.visited; + + if (!beenVisited || gScore < neighbor.g) { + // Found an optimal (so far) path to this node. Take score for node to see how good it is. + neighbor.visited = true; + neighbor.parent = currentNode; + neighbor.h = neighbor.h || heuristic(neighbor, end); + neighbor.g = gScore; + neighbor.f = neighbor.g + neighbor.h; + graph.markDirty(neighbor); + if (closest) { + // If the neighbour is closer than the current closestNode or if it's equally close but has + // a cheaper path than the current closest node then it becomes the closest node + if (neighbor.h < closestNode.h || (neighbor.h === closestNode.h && neighbor.g < closestNode.g)) { + closestNode = neighbor; + } + } + + if (!beenVisited) { + // Pushing to heap will put it in proper place based on the 'f' value. + openHeap.push(neighbor); + } + else { + // Already seen the node, but since it has been rescored we need to reorder it in the heap + openHeap.rescoreElement(neighbor); + } + } + } + } + + if (closest) { + return this.pathTo(closestNode); + } -}); \ No newline at end of file + // No result was found - empty array signifies failure to find path. + return []; + }, + // See list of heuristics: http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html + heuristics: { + manhattan: function(pos0, pos1) { + const d1 = Math.abs(pos1.x - pos0.x); + const d2 = Math.abs(pos1.y - pos0.y); + return d1 + d2; + }, + diagonal: function(pos0, pos1) { + const D = 1; + const D2 = Math.sqrt(2); + const d1 = Math.abs(pos1.x - pos0.x); + const d2 = Math.abs(pos1.y - pos0.y); + return (D * (d1 + d2)) + ((D2 - (2 * D)) * Math.min(d1, d2)); + } + } +};