diff --git a/docs/contributor-guide/authors.md b/docs/contributor-guide/authors.md index 55edc771..8a7db47c 100644 --- a/docs/contributor-guide/authors.md +++ b/docs/contributor-guide/authors.md @@ -9,6 +9,7 @@ Juan Pablo Canepa
Mat Gadd
Murilo Pereira
Nathan Witmer
+Paul Robello
rafaelcastrocouto
Raminder Singh
Ricardo Tomasi
diff --git a/src/core/Grid.js b/src/core/Grid.js index 31ae6ee3..33a06f8e 100644 --- a/src/core/Grid.js +++ b/src/core/Grid.js @@ -8,8 +8,11 @@ var DiagonalMovement = require('./DiagonalMovement'); * @param {number} height Number of rows of the grid. * @param {Array.>} [matrix] - A 0-1 matrix * representing the walkable status of the nodes(0 or false for walkable). - * If the matrix is not supplied, all the nodes will be walkable. */ -function Grid(width_or_matrix, height, matrix) { + * If the matrix is not supplied, all the nodes will be walkable. + * @param {Array.>} [costs] - A matrix + * representing the cost of walking the node. + * If the costs is not supplied, all the nodes will cost 0. */ +function Grid(width_or_matrix, height, matrix, costs) { var width; if (typeof width_or_matrix !== 'object') { @@ -34,7 +37,7 @@ function Grid(width_or_matrix, height, matrix) { /** * A 2D array of nodes. */ - this.nodes = this._buildNodes(width, height, matrix); + this.nodes = this._buildNodes(width, height, matrix, costs); } /** @@ -44,9 +47,11 @@ function Grid(width_or_matrix, height, matrix) { * @param {number} height * @param {Array.>} [matrix] - A 0-1 matrix representing * the walkable status of the nodes. + * @param {Array.>} [costs] - A matrix representing + * the costs to walk the nodes. * @see Grid */ -Grid.prototype._buildNodes = function(width, height, matrix) { +Grid.prototype._buildNodes = function(width, height, matrix, costs) { var i, j, nodes = new Array(height), row; @@ -58,7 +63,6 @@ Grid.prototype._buildNodes = function(width, height, matrix) { } } - if (matrix === undefined) { return nodes; } @@ -67,6 +71,10 @@ Grid.prototype._buildNodes = function(width, height, matrix) { throw new Error('Matrix size does not fit'); } + if (costs !== undefined && (costs.length !== height || costs[0].length !== width)) { + throw new Error('Costs size does not fit'); + } + for (i = 0; i < height; ++i) { for (j = 0; j < width; ++j) { if (matrix[i][j]) { @@ -74,6 +82,9 @@ Grid.prototype._buildNodes = function(width, height, matrix) { // while others will be un-walkable nodes[i][j].walkable = false; } + if (costs !== undefined) { + nodes[i][j].cost=costs[i][j]; + } } } @@ -98,6 +109,19 @@ Grid.prototype.isWalkableAt = function(x, y) { }; +/** + * Get cost to walk the node at the given position. + * (Also returns false if the position is outside the grid.) + * @param {number} x - The x coordinate of the node. + * @param {number} y - The y coordinate of the node. + * @return {number} - Cost to walk node. + */ +Grid.prototype.getCostAt = function(x, y) { + if (!this.isInside(x, y)) return false; + return this.nodes[y][x].cost; +}; + + /** * Determine whether the position is inside the grid. * XXX: `grid.isInside(x, y)` is wierd to read. @@ -124,6 +148,18 @@ Grid.prototype.setWalkableAt = function(x, y, walkable) { }; +/** + * Set cost of the node on the given position + * NOTE: throws exception if the coordinate is not inside the grid. + * @param {number} x - The x coordinate of the node. + * @param {number} y - The y coordinate of the node. + * @param {number} cost - Cost to walk the node. + */ +Grid.prototype.setCostAt = function(x, y, cost) { + this.nodes[y][x].cost = cost; +}; + + /** * Get the neighbors of the given node. * @@ -235,7 +271,7 @@ Grid.prototype.clone = function() { for (i = 0; i < height; ++i) { newNodes[i] = new Array(width); for (j = 0; j < width; ++j) { - newNodes[i][j] = new Node(j, i, thisNodes[i][j].walkable); + newNodes[i][j] = new Node(j, i, thisNodes[i][j].walkable, thisNodes[i][j].cost); } } diff --git a/src/core/Node.js b/src/core/Node.js index 8df8ba50..1235ca7a 100644 --- a/src/core/Node.js +++ b/src/core/Node.js @@ -6,8 +6,9 @@ * @param {number} x - The x coordinate of the node on the grid. * @param {number} y - The y coordinate of the node on the grid. * @param {boolean} [walkable] - Whether this node is walkable. + * @param {number} [cost] - node cost used by finders that allow non-uniform node costs */ -function Node(x, y, walkable) { +function Node(x, y, walkable, cost) { /** * The x coordinate of the node on the grid. * @type number @@ -23,6 +24,11 @@ function Node(x, y, walkable) { * @type boolean */ this.walkable = (walkable === undefined ? true : walkable); + /** + * Cost to walk this node if its walkable + * @type number + */ + this.cost = (cost === undefined) ? 0 : cost; } module.exports = Node; diff --git a/src/finders/AStarFinder.js b/src/finders/AStarFinder.js index c0a1b81e..95e865ee 100644 --- a/src/finders/AStarFinder.js +++ b/src/finders/AStarFinder.js @@ -95,7 +95,7 @@ AStarFinder.prototype.findPath = function(startX, startY, endX, endY, grid) { // get the distance between current node and the neighbor // and calculate the next g score - ng = node.g + ((x - node.x === 0 || y - node.y === 0) ? 1 : SQRT2); + ng = node.g + neighbor.cost + ((x - node.x === 0 || y - node.y === 0) ? 1 : SQRT2); // check if the neighbor has not been inspected yet, or // can be reached with smaller cost from the current node diff --git a/src/finders/BiAStarFinder.js b/src/finders/BiAStarFinder.js index 49159478..ba94e360 100644 --- a/src/finders/BiAStarFinder.js +++ b/src/finders/BiAStarFinder.js @@ -103,7 +103,7 @@ BiAStarFinder.prototype.findPath = function(startX, startY, endX, endY, grid) { // get the distance between current node and the neighbor // and calculate the next g score - ng = node.g + ((x - node.x === 0 || y - node.y === 0) ? 1 : SQRT2); + ng = node.g + neighbor.cost + ((x - node.x === 0 || y - node.y === 0) ? 1 : SQRT2); // check if the neighbor has not been inspected yet, or // can be reached with smaller cost from the current node @@ -147,7 +147,7 @@ BiAStarFinder.prototype.findPath = function(startX, startY, endX, endY, grid) { // get the distance between current node and the neighbor // and calculate the next g score - ng = node.g + ((x - node.x === 0 || y - node.y === 0) ? 1 : SQRT2); + ng = node.g + neighbor.cost + ((x - node.x === 0 || y - node.y === 0) ? 1 : SQRT2); // check if the neighbor has not been inspected yet, or // can be reached with smaller cost from the current node diff --git a/test/PathTest.js b/test/PathTest.js index 3cc4de68..47a8171f 100644 --- a/test/PathTest.js +++ b/test/PathTest.js @@ -8,20 +8,26 @@ var scenarios = require('./PathTestScenarios'); function pathTest(opt) { var name = opt.name, finder = opt.finder, - optimal = opt.optimal; + optimal = opt.optimal, + useCost = opt.useCost; + describe(name, function() { var startX, startY, endX, endY, grid, expectedLength, - width, height, matrix, path, i, scen; + width, height, matrix, costs, path, i, scen; var test = (function() { var testId = 0; - return function(startX, startY, endX, endY, grid, expectedLength) { + return function(startX, startY, endX, endY, grid, expectedLength, expectedCostLength) { it('should solve maze '+ ++testId, function() { path = finder.findPath(startX, startY, endX, endY, grid); if (optimal) { + if (useCost && expectedCostLength !== undefined) { + path.length.should.equal(expectedCostLength); + } else { path.length.should.equal(expectedLength); + } } else { path[0].should.eql([startX, startY]); path[path.length - 1].should.eql([endX, endY]); @@ -35,16 +41,17 @@ function pathTest(opt) { scen = scenarios[i]; matrix = scen.matrix; + costs = useCost ? scen.costs : undefined; height = matrix.length; - width = matrix[0].length; - - grid = new PF.Grid(width, height, matrix); + width = matrix[0].length; + grid = new PF.Grid(width, height, matrix, costs); test( scen.startX, scen.startY, scen.endX, scen.endY, grid, - scen.expectedLength + scen.expectedLength, + scen.expectedCostLength ); } }); @@ -61,52 +68,73 @@ function pathTests(tests) { pathTests({ name: 'AStar', finder: new PF.AStarFinder(), - optimal: true + optimal: true, + useCost: false +}, { + name: 'AStar Cost', + finder: new PF.AStarFinder(), + optimal: true, + useCost: true }, { name: 'BreadthFirst', finder: new PF.BreadthFirstFinder(), - optimal: true + optimal: true, + useCost: false }, { name: 'Dijkstra', finder: new PF.DijkstraFinder(), - optimal: true + optimal: true, + useCost: false +}, { + name: 'Dijkstra Cost', + finder: new PF.DijkstraFinder(), + optimal: true, + useCost: true }, { name: 'BiBreadthFirst', finder: new PF.BiBreadthFirstFinder(), - optimal: true + optimal: true, + useCost: false }, { name: 'BiDijkstra', finder: new PF.BiDijkstraFinder(), - optimal: true + optimal: true, + useCost: false }); // finders NOT guaranteed to find the shortest path pathTests({ name: 'BiAStar', finder: new PF.BiAStarFinder(), - optimal: false + optimal: false, + useCost: false }, { name: 'BestFirst', finder: new PF.BestFirstFinder(), - optimal: false + optimal: false, + useCost: false }, { name: 'BiBestFirst', finder: new PF.BiBestFirstFinder(), - optimal: false + optimal: false, + useCost: false }, { name: 'IDAStar', finder: new PF.IDAStarFinder(), - optimal: false + optimal: false, + useCost: false }, { name: 'JPFMoveDiagonallyIfAtMostOneObstacle', finder: new PF.JumpPointFinder({ diagonalMovement: PF.DiagonalMovement.IfAtMostOneObstacle }), - optimal: false + optimal: false, + useCost: false }, { name: 'JPFNeverMoveDiagonally', finder: new PF.JumpPointFinder({ diagonalMovement: PF.DiagonalMovement.Never }), - optimal: false + optimal: false, + useCost: false }); diff --git a/test/PathTestScenarios.js b/test/PathTestScenarios.js index 63476f61..71b5b1cf 100644 --- a/test/PathTestScenarios.js +++ b/test/PathTestScenarios.js @@ -8,6 +8,24 @@ module.exports = [ [1, 0]], expectedLength: 3, }, + { + startX: 0, + startY: 0, + endX: 4, + endY: 4, + matrix: [[0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]], + costs: [[0, 0, 0, 0, 0], + [9, 9, 9, 9, 0], + [0, 0, 0, 0, 0], + [0, 9, 9, 9, 9], + [0, 0, 0, 0, 0]], + expectedLength: 9, + expectedCostLength: 17, + }, { startX: 1, startY: 1,