@excaliburjs/plugin-pathfinding
Version:
excalibur-pathfinding provides ability to use A* and Dijkstra's Algorithm for pathfinding and with Excalibur.js
377 lines • 15.8 kB
JavaScript
import { TileMap } from "excalibur";
export class ExcaliburGraph {
duration = 0;
starttime = 0;
endtime = 0;
key = false;
nodes = new Map();
edges = new Map();
addNode(node) {
this.nodes.set(typeof node.id === "number" ? node.id.toString() : node.id, node);
}
addEdge(edge, bidirectional = false) {
this.edges.set(edge.name, edge);
if (bidirectional) {
let newEdge = { name: `${edge.name}_reverse`, from: edge.to, to: edge.from };
if (edge.value)
Object.assign(newEdge, { value: edge.value });
this.addEdge(newEdge);
}
}
resetGraph() {
this.nodes = new Map();
this.edges = new Map();
}
addTileMap(tilemap, diagonal = false) {
tilemap.tiles.forEach((tile, index) => {
if (!tile.collider) {
this.addNode({ id: `${index}`, value: 0 });
}
});
tilemap.tiles.forEach((tile, index) => {
if (tilemap.tiles[index].collider)
return;
let neighbors = [];
if (tilemap.tiles[index - 1] && index % tilemap.cols != 0)
neighbors.push({ index: index - 1, diagonal: false });
if (tilemap.tiles[index + 1] && index % tilemap.cols != tilemap.cols - 1)
neighbors.push({ index: index + 1, diagonal: false });
if (tilemap.tiles[index - tilemap.cols])
neighbors.push({ index: index - tilemap.cols, diagonal: false });
if (tilemap.tiles[index + tilemap.cols])
neighbors.push({ index: index + tilemap.cols, diagonal: false });
if (diagonal) {
if (tilemap.tiles[index - tilemap.cols - 1] && index % tilemap.cols != 0)
neighbors.push({ index: index - tilemap.cols - 1, diagonal: true });
if (tilemap.tiles[index - tilemap.cols + 1] && index % tilemap.cols != tilemap.cols - 1)
neighbors.push({ index: index - tilemap.cols + 1, diagonal: true });
if (tilemap.tiles[index + tilemap.cols - 1] && index % tilemap.cols != 0)
neighbors.push({ index: index + tilemap.cols - 1, diagonal: true });
if (tilemap.tiles[index + tilemap.cols + 1] && index % tilemap.cols != tilemap.cols - 1)
neighbors.push({ index: index + tilemap.cols + 1, diagonal: true });
}
neighbors.forEach(neighbor => {
if (tilemap.tiles[neighbor.index].collider != true) {
this.addEdge({
name: `${index}_${neighbor.index}`,
from: this.nodes.get(`${index}`),
to: this.nodes.get(`${neighbor.index}`),
value: neighbor.diagonal ? 1.41 : 1,
});
}
});
});
}
getNodes() {
return this.nodes;
}
getEdges() {
return this.edges;
}
getAdjacentNodes(node) {
const adjacentNodes = this.getAdjacentEdges(node).map(edge => edge.to);
const uniqueNames = Array.from(new Set(adjacentNodes.map(node => node.id)));
return uniqueNames.map(name => this.nodes.get(typeof name === "number" ? name.toString() : name));
}
getAdjacentEdges(node) {
return [...this.edges.values()].filter(edge => edge.from === node).map(edge => edge);
}
getAdjacentEdgesTo(node) {
return [...this.edges.values()].filter(edge => edge.to === node).map(edge => edge);
}
bfs(startnode, endnode) {
this.starttime = performance.now();
const queue = [startnode];
const visited = new Set();
while (queue.length > 0) {
const current = queue.shift();
if (!visited.has(current)) {
visited.add(current);
queue.push(...this.getAdjacentNodes(current));
}
if (current === endnode) {
this.endtime = performance.now() - this.starttime;
this.duration = this.endtime;
return true;
}
}
this.endtime = performance.now() - this.starttime;
this.duration = this.endtime;
return false;
}
dfs(startnode, endnode, visited = new Set()) {
this.starttime = performance.now();
const stack = [startnode];
visited.add(startnode);
const adjacentNodes = this.getAdjacentNodes(startnode);
for (const node of adjacentNodes) {
if (node === endnode) {
return true;
}
if (!visited.has(node)) {
if (this.dfs(node, endnode, visited)) {
this.endtime = performance.now() - this.starttime;
this.duration = this.endtime;
return true;
}
}
}
this.endtime = performance.now() - this.starttime;
this.duration = this.endtime;
return false;
}
dijkstra(sourcenode) {
this.starttime = performance.now();
const visited = [];
const unvisited = [];
const resultArray = [];
this.nodes.forEach(node => unvisited.push(node));
this.nodes.forEach(node => resultArray.push({ node, distance: Infinity, previous: null }));
const startingNodeIndex = resultArray.findIndex(node => node.node === sourcenode);
if (startingNodeIndex === -1)
return [];
resultArray[startingNodeIndex].distance = 0;
visited.push(sourcenode);
unvisited.splice(unvisited.indexOf(sourcenode), 1);
let current = sourcenode;
let currentEdges = this.getAdjacentEdges(current);
for (let i = 0; i < currentEdges.length; i++) {
const edge = currentEdges[i];
const index = resultArray.findIndex(node => node.node === edge.to);
if (startingNodeIndex === -1)
return [];
resultArray[index].distance = edge.value;
resultArray[index].previous = current;
}
while (unvisited.length > 0) {
let listOfAvailableNodes = [];
let listofAvailableEntries = [];
listofAvailableEntries = resultArray.filter(node => unvisited.includes(node.node));
listOfAvailableNodes = listofAvailableEntries.map(node => node.node);
let lowestDistance = Infinity;
let lowestDistanceIndex = -1;
if (listOfAvailableNodes.length > 0) {
for (let i = 0; i < listOfAvailableNodes.length; i++) {
const unVisitedNode = listOfAvailableNodes[i];
let index = resultArray.findIndex(node => node.node === unVisitedNode);
if (resultArray[index].distance < lowestDistance) {
lowestDistance = resultArray[index].distance;
lowestDistanceIndex = index;
}
}
}
else {
lowestDistance = Infinity;
lowestDistanceIndex = -1;
for (let i = 0; i < unvisited.length; i++) {
const unVisitedNode = unvisited[i];
let index = resultArray.findIndex(node => node.node === unVisitedNode);
if (resultArray[index].distance < lowestDistance) {
lowestDistance = resultArray[index].distance;
lowestDistanceIndex = index;
}
}
}
if (lowestDistanceIndex === -1) {
break;
}
current = resultArray[lowestDistanceIndex].node;
currentEdges = this.getAdjacentEdges(current);
currentEdges = currentEdges.filter(edge => {
return !visited.includes(edge.from) && !visited.includes(edge.to);
});
visited.push(current);
unvisited.splice(unvisited.indexOf(current), 1);
for (let i = 0; i < currentEdges.length; i++) {
const edge = currentEdges[i];
const index = resultArray.findIndex(node => node.node === edge.to);
const previousIndex = resultArray.findIndex(node => node.node === edge.from);
const previousDistance = resultArray[previousIndex].distance;
const cumDistance = (previousDistance + edge.value);
if (cumDistance < resultArray[index].distance) {
resultArray[index].distance = cumDistance;
resultArray[index].previous = current;
}
}
}
this.endtime = performance.now() - this.starttime;
this.duration = this.endtime;
return resultArray;
}
shortestPath(startnode, endnode) {
let dAnalysis = this.dijkstra(startnode);
let path = [];
let current = endnode;
while (current != null) {
path.push(current);
current = dAnalysis.find(node => node.node == current)?.previous;
if (current == null) {
break;
}
}
path.reverse();
return path;
}
}
export class ExcaliburAStar {
tilemap = {
cols: 0,
rows: 0,
};
grid = [];
currentNode = null;
currentIndex = 0;
checkedNodes = [];
openNodes = [];
startnode = null;
endnode = null;
goalReached = false;
path = [];
duration = 0;
starttime = 0;
endtime = 0;
constructor(tilemap) {
this.tilemap.cols = tilemap instanceof TileMap ? tilemap.columns : tilemap.cols;
this.tilemap.rows = tilemap.rows;
let tileIndex = 0;
for (const tile of tilemap.tiles) {
if (tilemap instanceof TileMap) {
this.grid.push({
id: tileIndex,
collider: tile.solid ? true : false,
gCost: 0,
hCost: 0,
fCost: 0,
x: tileIndex % this.tilemap.cols,
y: Math.floor(tileIndex / this.tilemap.cols),
checked: false,
parent: null,
});
tileIndex++;
}
else {
this.grid.push({
id: tileIndex,
collider: tile.collider ? true : false,
gCost: 0,
hCost: 0,
fCost: 0,
x: tileIndex % this.tilemap.cols,
y: Math.floor(tileIndex / this.tilemap.cols),
checked: false,
parent: null,
});
tileIndex++;
}
}
}
setCost() {
if (this.startnode === null || this.endnode === null)
return;
if (this.grid.length === 0)
return;
for (const tile of this.grid) {
tile.gCost = this.getGCost(tile, this.startnode);
tile.hCost = this.getHCost(tile, this.endnode);
tile.fCost = this.getFCost(tile);
}
}
astar(sourcenode, endnode, diagonal = false) {
this.starttime = performance.now();
this.startnode = sourcenode;
this.endnode = endnode;
this.goalReached = false;
this.checkedNodes = [];
this.openNodes = [];
this.setCost();
this.openNodes.push(this.startnode);
while (this.openNodes.length > 0) {
this.currentNode = this.openNodes[0];
this.currentIndex = 0;
for (const node of this.openNodes) {
if (node.fCost < this.currentNode.fCost) {
this.currentNode = node;
this.currentIndex = this.openNodes.indexOf(node);
}
}
this.openNodes.splice(this.currentIndex, 1);
this.checkedNodes.push(this.currentNode);
if (this.currentNode === this.endnode) {
this.path = [];
do {
this.path.push(this.currentNode);
this.currentNode = this.currentNode.parent;
} while (this.currentNode !== this.startnode);
this.path = this.path.reverse();
this.goalReached = true;
this.endtime = performance.now();
this.duration = this.endtime - this.starttime;
return this.path;
}
const neighbors = this.getNeighbors(this.currentNode, diagonal);
for (const neighbor of neighbors) {
if (!this.checkedNodes.includes(neighbor)) {
if (!this.openNodes.includes(neighbor) && !neighbor.collider) {
neighbor.parent = this.currentNode;
this.openNodes.push(neighbor);
}
}
}
}
this.endtime = performance.now();
this.duration = this.endtime - this.starttime;
return [];
}
getNeighbors(node, diagonal) {
const neighbors = [];
const index = typeof node.id === "string" ? parseInt(node.id) : node.id;
if (this.grid[index - 1] && index % this.tilemap.cols != 0)
neighbors.push(this.grid[index - 1]);
if (this.grid[index + 1] && index % this.tilemap.cols != this.tilemap.cols - 1)
neighbors.push(this.grid[index + 1]);
if (this.grid[index - this.tilemap.cols])
neighbors.push(this.grid[index - this.tilemap.cols]);
if (this.grid[index + this.tilemap.cols])
neighbors.push(this.grid[index + this.tilemap.cols]);
if (diagonal) {
if (this.grid[index - this.tilemap.cols - 1] && index % this.tilemap.cols != 0)
neighbors.push(this.grid[index - this.tilemap.cols - 1]);
if (this.grid[index - this.tilemap.cols + 1] && index % this.tilemap.cols != this.tilemap.cols - 1)
neighbors.push(this.grid[index - this.tilemap.cols + 1]);
if (this.grid[index + this.tilemap.cols - 1] && index % this.tilemap.cols != 0)
neighbors.push(this.grid[index + this.tilemap.cols - 1]);
if (this.grid[index + this.tilemap.cols + 1] && index % this.tilemap.cols != this.tilemap.cols - 1)
neighbors.push(this.grid[index + this.tilemap.cols + 1]);
}
return neighbors;
}
getNodeByIndex(index) {
return this.grid[index];
}
getNodeByCoord(x, y) {
return this.grid[y * this.tilemap.cols + x];
}
getGCost(node, startnode) {
return Math.abs(node.x - startnode.x) + Math.abs(node.y - startnode.y);
}
getHCost(node, endnode) {
return Math.abs(node.x - endnode.x) + Math.abs(node.y - endnode.y);
}
getFCost(node) {
return node.gCost + node.hCost;
}
resetGrid() {
for (const tile of this.grid) {
tile.checked = false;
tile.gCost = 0;
tile.hCost = 0;
tile.fCost = 0;
}
this.checkedNodes = [];
this.startnode = null;
this.endnode = null;
this.goalReached = false;
this.duration = 0;
}
}
//# sourceMappingURL=excalibur-pathfinding.js.map