UNPKG

@excaliburjs/plugin-pathfinding

Version:

excalibur-pathfinding provides ability to use A* and Dijkstra's Algorithm for pathfinding and with Excalibur.js

491 lines (424 loc) 16.6 kB
import { Loadable, Tile, TileMap } from "excalibur"; export type GraphNode = { id: number | string; value?: any; }; export type aStarNode = { id: number | string; collider: boolean; gCost: number; hCost: number; fCost: number; x: number; y: number; checked: boolean; parent: aStarNode | null; }; export type Edge = { name: string; from: GraphNode; to: GraphNode; value?: number; }; export type GraphTile = { name?: string; index?: number; coordinates?: { x: number; y: number }; data?: any; collider?: boolean; }; export type GraphTileMap = { name: string; tiles: Array<GraphTile>; rows: number; cols: number; }; export class ExcaliburGraph { duration: number = 0; starttime: number = 0; endtime: number = 0; key: boolean = false; nodes: Map<string, GraphNode> = new Map(); edges: Map<string, Edge> = new Map(); addNode(node: GraphNode) { this.nodes.set(typeof node.id === "number" ? node.id.toString() : node.id, node); } addEdge(edge: 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: GraphTileMap, diagonal = false) { tilemap.tiles.forEach((tile, index) => { //tile.collider === true ? this.addNode({ name: `${index}`, value: 1 }) : this.addNode({ name: `${index}`, value: 0 }); if (!tile.collider) { this.addNode({ id: `${index}`, value: 0 }); } }); //loop over tilemap and find each tile's neighbors //add edges to the adjacency list tilemap.tiles.forEach((tile, index) => { //get neighbors if (tilemap.tiles[index].collider) return; type Neighbors = { index: number; diagonal: boolean; }; let neighbors: 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 }); } //check value for 1 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: GraphNode): GraphNode[] { 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: GraphNode) { return [...this.edges.values()].filter(edge => edge.from === node).map(edge => edge); } getAdjacentEdgesTo(node: GraphNode) { return [...this.edges.values()].filter(edge => edge.to === node).map(edge => edge); } bfs(startnode: GraphNode, endnode: GraphNode): boolean { 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: GraphNode, endnode: GraphNode, visited = new Set()): boolean { 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: GraphNode): Array<{ node: GraphNode; distance: number; previous: GraphNode | null }> { this.starttime = performance.now(); const visited: GraphNode[] = []; const unvisited: GraphNode[] = []; const resultArray: Array<{ node: GraphNode; distance: number; previous: GraphNode | null }> = []; //fill unvisited this.nodes.forEach(node => unvisited.push(node)); //fill resultArray this.nodes.forEach(node => resultArray.push({ node, distance: Infinity, previous: null })); //start with starting node //add starting node to result array 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); //update result array with distances, which is edge values 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 as number; resultArray[index].previous = current; } while (unvisited.length > 0) { //get list of unvisited available nodes let listOfAvailableNodes: GraphNode[] = []; let listofAvailableEntries: Array<{ node: GraphNode; distance: number; previous: GraphNode | null }> = []; listofAvailableEntries = resultArray.filter(node => unvisited.includes(node.node)); listOfAvailableNodes = listofAvailableEntries.map(node => node.node); //loop through available nodes and find lowest distance to sourcenode 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 { //manage exception //choose node from unvisited list that has lowest distance to source node 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; } } } // add test for -1 index representing unreachable node if (lowestDistanceIndex === -1) { break; } current = resultArray[lowestDistanceIndex].node; currentEdges = this.getAdjacentEdges(current); //remove visited from currentEdges currentEdges = currentEdges.filter(edge => { return !visited.includes(edge.from) && !visited.includes(edge.to); }); visited.push(current); unvisited.splice(unvisited.indexOf(current), 1); //update result array with distances, which is edge values for (let i = 0; i < currentEdges.length; i++) { const edge = currentEdges[i]; const index = resultArray.findIndex(node => node.node === edge.to); //update cumulative distances const previousIndex = resultArray.findIndex(node => node.node === edge.from); const previousDistance = resultArray[previousIndex].distance; const cumDistance = (previousDistance + edge.value!) as number; 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: GraphNode, endnode: GraphNode): GraphNode[] { let dAnalysis = this.dijkstra(startnode); //iterate through dAnalysis to plot shortest path to endnode let path: GraphNode[] = []; let current: GraphNode | null | undefined = 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: aStarNode[] = []; currentNode: aStarNode | null = null; currentIndex = 0; checkedNodes: aStarNode[] = []; openNodes: aStarNode[] = []; startnode: aStarNode | null = null; endnode: aStarNode | null = null; goalReached: boolean = false; path: aStarNode[] = []; duration: number = 0; starttime: number = 0; endtime: number = 0; constructor(tilemap: TileMap | GraphTileMap) { 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 as 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 as GraphTile).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: aStarNode, endnode: aStarNode, diagonal = false): aStarNode[] { 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; // get lowest fCost in openNodes for (const node of this.openNodes) { if (node.fCost < this.currentNode.fCost) { this.currentNode = node; this.currentIndex = this.openNodes.indexOf(node); } } //remove current node from openNodes this.openNodes.splice(this.currentIndex, 1); //add to checked this.checkedNodes.push(this.currentNode); if (this.currentNode === this.endnode) { this.path = []; do { this.path.push(this.currentNode as aStarNode); this.currentNode = (this.currentNode as aStarNode).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: aStarNode, diagonal: boolean): aStarNode[] { const neighbors: aStarNode[] = []; const index: number = typeof node.id === "string" ? parseInt(node.id) : node.id; //get neighbors //left/right tiles 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]); //up/down tiles 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 (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: number): aStarNode { return this.grid[index]; } getNodeByCoord(x: number, y: number): aStarNode { return this.grid[y * this.tilemap.cols + x]; } getGCost(node: aStarNode, startnode: aStarNode): number { return Math.abs(node.x - startnode.x) + Math.abs(node.y - startnode.y); } getHCost(node: aStarNode, endnode: aStarNode): number { return Math.abs(node.x - endnode.x) + Math.abs(node.y - endnode.y); } getFCost(node: aStarNode): number { 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; } }