UNPKG

@ziagl/tiled-map-path-finder

Version:

A path finder library for 2 dimensional maps.

273 lines 12.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PathFinder = void 0; const honeycomb_grid_1 = require("honeycomb-grid"); const Tile_1 = require("./models/Tile"); const tiled_map_utils_1 = require("@ziagl/tiled-map-utils"); class PathFinder { MAXLOOPS = 10000; _map = []; _map_columns = 0; _map_rows = 0; _map_layers = 0; _grid; _hexSetting; _hexDefinition; constructor(map, rows, columns) { for (let i = 0; i < map.length; ++i) { this._map.push(tiled_map_utils_1.Utils.convertTo2DArray(map[i], rows, columns)); } this._map_columns = columns; this._map_rows = rows; this._map_layers = map.length; // initilize grid and definition to convert offset -> cube coordinates this._grid = new honeycomb_grid_1.Grid(Tile_1.Tile, (0, honeycomb_grid_1.rectangle)({ width: this._map_columns, height: this._map_rows })); this._hexSetting = { offset: -1, orientation: honeycomb_grid_1.Orientation.POINTY }; this._hexDefinition = (0, honeycomb_grid_1.defineHex)(this._hexSetting); } /** * computes path with lowest costs from start to end (A* algorithm) * @param start start coordinates * @param end end coordinates * @param layerIndex layer index * @returns path as cube coordinates or empty path if no path was found or layer is out of bounds */ computePath(start, end, layerIndex) { // early exit if layer is out of bounds if (layerIndex < 0 || layerIndex >= this._map_layers) { return []; } // initilize grid this._grid = new honeycomb_grid_1.Grid(Tile_1.Tile, (0, honeycomb_grid_1.rectangle)({ width: this._map_columns, height: this._map_rows })); let path = []; // initialize AStar let openList = []; let closedList = []; let tile = new Tile_1.Tile(); tile.coordinates = start; tile.movementCost = 0; tile.estimatedMovementCost = this.calculateDistance(start, end); openList.push(tile); // compute AStar algorithm let pathFound = false; let loopMax = this.MAXLOOPS; do { // remove tile from open list const tile = openList.pop(); // add it to closed list closedList.push(tile); // if tile is end, break if (tile.coordinates.q == end.q && tile.coordinates.r == end.r) { pathFound = true; break; } // if start tile is not passable, break if (this.movementCosts(tile.coordinates, layerIndex) == 0) { pathFound = false; break; } // get neighbors walkable neighbors let neighbors = tiled_map_utils_1.Utils.neighbors(this._grid, tile.coordinates); let walkableNeighbors = tiled_map_utils_1.Utils.walkableNeighbors(neighbors, this._map[layerIndex]); // for every walkable neighbor walkableNeighbors.forEach((neighbor) => { // if neighbor is in closed list, skip it if (closedList.find((t) => t.coordinates.q == neighbor.coordinates.q && t.coordinates.r == neighbor.coordinates.r) != undefined) { return; } // if neighbor is not in open list, add it if (openList.find((t) => t.coordinates.q == neighbor.coordinates.q && t.coordinates.r == neighbor.coordinates.r) == undefined) { const tileMovementCost = this.movementCosts(neighbor.coordinates, layerIndex); neighbor.movementCost = tile.movementCost + tileMovementCost; neighbor.estimatedMovementCost = this.calculateDistance(neighbor.coordinates, end); neighbor.sum = neighbor.movementCost + neighbor.estimatedMovementCost; openList.unshift(neighbor); } // if neighbor is in open list and has a lower cost, update it else { let existing = openList.find((t) => t.coordinates.q == neighbor.coordinates.q && t.coordinates.r == neighbor.coordinates.r); const tileMovementCost = this.movementCosts(neighbor.coordinates, layerIndex); if (existing != undefined && existing.movementCost > tile.movementCost + tileMovementCost) { const tileMovementCost = this.movementCosts(neighbor.coordinates, layerIndex); existing.movementCost = tile.movementCost + tileMovementCost; existing.estimatedMovementCost = this.calculateDistance(neighbor.coordinates, end); existing.sum = existing.movementCost + existing.estimatedMovementCost; } } }); --loopMax; } while (openList.length > 0 && pathFound == false && loopMax > 0); // reconstruct path if (pathFound == true) { let current = closedList.pop(); loopMax = this.MAXLOOPS; while (current != undefined && loopMax > 0) { // add end coordinates path.push(current.coordinates); // if start is reached end loop if (current.coordinates.q == start.q && current.coordinates.r == start.r) { // stop if start is reached current = undefined; } else { const neighbors = tiled_map_utils_1.Utils.neighbors(this._grid, current.coordinates); const walkableNeighbors = tiled_map_utils_1.Utils.walkableNeighbors(neighbors, this._map[layerIndex]); tiled_map_utils_1.Utils.shuffle(walkableNeighbors); for (const neighbor of walkableNeighbors) { const nextTile = closedList.find((t) => t.coordinates.q == neighbor.coordinates.q && t.coordinates.r == neighbor.coordinates.r); if (nextTile != undefined) { if (neighbor.movementCost < current?.movementCost) { current = nextTile; } } } } --loopMax; } } return path.reverse(); } /** * same as computePath, but with interface for offset coordinates * @param start start coordinates * @param end end coordinates * @param layerIndex layer index * @returns path as offset coordinates or empty path if no path was found or layer is out of bounds */ computePathOffsetCoordinates(start, end, layerIndex) { let path = []; // convert offset input coordinates to cube coordinates const startCube = (0, honeycomb_grid_1.offsetToCube)(this._hexSetting, { col: start.x, row: start.y }); const endCube = (0, honeycomb_grid_1.offsetToCube)(this._hexSetting, { col: end.x, row: end.y }); // call compute path method const computedPath = this.computePath(startCube, endCube, layerIndex); // convert resulting path back to output offset coordinates if (computedPath.length > 0) { computedPath.forEach((coord) => { path.push(this.cubeToOffset(coord)); }); } return path; } /** * returns all tiles that are in range * @param start start coordinates * @param maxcost maximum cost * @param layerIndex layer index * @returns reachable tiles as cube coordinates or empty path if layer is out of bounds */ reachableTiles(start, maxcost, layerIndex) { // early exit if layer is out of bounds if (layerIndex < 0 || layerIndex >= this._map_layers) { return []; } // initilize grid this._grid = new honeycomb_grid_1.Grid(Tile_1.Tile, (0, honeycomb_grid_1.rectangle)({ width: this._map_columns, height: this._map_rows })); let reachableTiles = []; // initialize let openList = []; let closedList = []; let tile = new Tile_1.Tile(); tile.coordinates = start; tile.movementCost = 0; openList.push(tile); // compute let loopMax = this.MAXLOOPS; do { // remove tile from open list const tile = openList.pop(); // add it to closed list closedList.push(tile); // if start tile is not passable, break if (this.movementCosts(tile.coordinates, layerIndex) == 0) { break; } // get neighbors walkable neighbors let neighbors = tiled_map_utils_1.Utils.neighbors(this._grid, tile.coordinates); let walkableNeighbors = tiled_map_utils_1.Utils.walkableNeighbors(neighbors, this._map[layerIndex]); // for every walkable neighbor walkableNeighbors.forEach((neighbor) => { // if neighbor is in closed list, skip it if (closedList.find((t) => t.coordinates.q == neighbor.coordinates.q && t.coordinates.r == neighbor.coordinates.r) != undefined) { return; } // if neighbor is not in open list, add it if (openList.find((t) => t.coordinates.q == neighbor.coordinates.q && t.coordinates.r == neighbor.coordinates.r) == undefined) { const tileMovementCost = this.movementCosts(neighbor.coordinates, layerIndex); neighbor.movementCost = tile.movementCost + tileMovementCost; if (tile.movementCost < maxcost) { openList.unshift(neighbor); } } }); --loopMax; } while (openList.length > 0 && loopMax > 0); // fill reachable tiles closedList.forEach((tile) => { reachableTiles.push(tile.coordinates); }); return reachableTiles; } /** * Returns coordinates of all neighbors of a given base tile. * Minimum 2 (map edges), maximum 6. * @param base coordinates of a tile on this map * @returns list of cubecoordinates of all neighbors */ neighborTiles(base) { return tiled_map_utils_1.Utils.neighbors(this._grid, base).map((tile) => tile.coordinates); } /** * Converts cube coordinates to offset coordinates * @param coordinate cube coordinates (q, r, s) * @returns offset coordinates (x, y) */ cubeToOffset(coordinate) { const hex = new this._hexDefinition([coordinate.q, coordinate.r]); const offset = (0, honeycomb_grid_1.hexToOffset)(hex); return { x: offset.col, y: offset.row }; } /** * Converts offset coordinates to cube coordinates * @param coordinate offset coordinates (x, y) * @returns cube coordinates (q, r, s) */ offsetToCube(coordinate) { return (0, honeycomb_grid_1.offsetToCube)(this._hexSetting, { col: coordinate.x, row: coordinate.y }); } /** * print map structured (one row as one line) * @returns map as string */ print() { let response = ''; for (let i = 0; i < this._map_columns; ++i) { const row = this._map[i]; response += row?.join(' '); if (i < this._map_columns - 1) { response += '\n'; } } return response; } /** * print map unstructured * @returns map as string **/ print_unstructured() { return this._map.flat().join(' '); } // calculates Manhattan distance calculateDistance(start, end) { const lineBetween = (0, honeycomb_grid_1.line)({ start: [start.q, start.r], stop: [end.q, end.r] }); return this._grid.traverse(lineBetween).size; } // get movement costs for a given tile movementCosts(coordinates, layerIndex) { const hex = new this._hexDefinition([coordinates.q, coordinates.r]); const offset = (0, honeycomb_grid_1.hexToOffset)(hex); return this._map[layerIndex]?.[offset.row]?.[offset.col]; } } exports.PathFinder = PathFinder; //# sourceMappingURL=PathFinder.js.map