Skip to content

Added support for non uniform node cost #100

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/contributor-guide/authors.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Juan Pablo Canepa <https://github.com/jpcanepa><br>
Mat Gadd <https://github.com/Drarok><br>
Murilo Pereira <https://github.com/mpereira><br>
Nathan Witmer <https://github.com/zerowidth><br>
Paul Robello <https://github.com/paulrobello><br>
rafaelcastrocouto <https://github.com/rafaelcastrocouto><br>
Raminder Singh <https://github.com/imor><br>
Ricardo Tomasi <https://github.com/ricardobeat><br>
Expand Down
48 changes: 42 additions & 6 deletions src/core/Grid.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ var DiagonalMovement = require('./DiagonalMovement');
* @param {number} height Number of rows of the grid.
* @param {Array.<Array.<(number|boolean)>>} [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.<Array.<(number)>>} [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') {
Expand All @@ -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);
}

/**
Expand All @@ -44,9 +47,11 @@ function Grid(width_or_matrix, height, matrix) {
* @param {number} height
* @param {Array.<Array.<number|boolean>>} [matrix] - A 0-1 matrix representing
* the walkable status of the nodes.
* @param {Array.<Array.<number>>} [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;
Expand All @@ -58,7 +63,6 @@ Grid.prototype._buildNodes = function(width, height, matrix) {
}
}


if (matrix === undefined) {
return nodes;
}
Expand All @@ -67,13 +71,20 @@ 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]) {
// 0, false, null will be walkable
// while others will be un-walkable
nodes[i][j].walkable = false;
}
if (costs !== undefined) {
nodes[i][j].cost=costs[i][j];
}
}
}

Expand All @@ -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.
Expand All @@ -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.
*
Expand Down Expand Up @@ -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);
}
}

Expand Down
8 changes: 7 additions & 1 deletion src/core/Node.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
2 changes: 1 addition & 1 deletion src/finders/AStarFinder.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/finders/BiAStarFinder.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
64 changes: 46 additions & 18 deletions test/PathTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand All @@ -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
);
}
});
Expand All @@ -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
});
18 changes: 18 additions & 0 deletions test/PathTestScenarios.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down