@evilkiwi/astar
Version:
Synchronous A* pathfinding for TypeScript
212 lines (206 loc) • 6.61 kB
JavaScript
;
const diagonal = (from, to) => {
const d1 = Math.abs(to[0] - from[0]);
const d2 = Math.abs(to[1] - from[1]);
const D2 = Math.sqrt(2);
const D = 1;
return D * (d1 + d2) + (D2 - 2 * D) * Math.min(d1, d2);
};
const manhattan = (from, to) => {
return Math.abs(to[0] - from[0]) + Math.abs(to[1] - from[1]);
};
var heuristics = /*#__PURE__*/Object.freeze({
__proto__: null,
diagonal: diagonal,
manhattan: manhattan
});
function search(options) {
const heuristic = options.heuristic ?? 'diagonal';
const cutCorners = options.cutCorners ?? true;
const stepHeight = options.stepHeight ?? 1;
const diagonal = options.diagonal ?? false;
const closed = [vectorId(options.from)];
const end = vectorId(options.to);
let path = null;
let open = [];
if (!Array.isArray(options.grid)) {
throw new Error('non-array grid provided');
}
if (!options.grid.length || !Array.isArray(options.grid[0])) {
throw new Error('2 dimensional grid array required');
}
const maxX = options.grid[0].length;
const maxY = options.grid.length;
const tileCache = {};
function tile(vector) {
const id = vectorId(vector);
if (tileCache[id]) {
return tileCache[id];
}
const rawValue = options.grid[vector[1]]?.[vector[0]];
if (rawValue === undefined) {
throw new Error('Grid value is undefined');
}
let value;
if (typeof rawValue === 'number') {
value = {
elevation: Math.max(0, rawValue),
isLegal: rawValue !== -1,
};
}
else {
value = {
...rawValue,
isLegal: rawValue.isLegal ?? rawValue.elevation !== -1,
};
}
tileCache[id] = value;
return value;
}
function canUse([cell, neighbors], origin) {
return (!isIllegal(cell, origin) &&
closed.indexOf(vectorId(cell)) === -1 &&
(neighbors === null ||
cutCorners ||
(!isIllegal(neighbors[0], origin, 0) && !isIllegal(neighbors[1], origin, 0))));
}
function isIllegal(cell, origin, step = stepHeight) {
return (cell[0] < 0 ||
cell[1] < 0 ||
cell[0] >= maxX ||
cell[1] >= maxY ||
(!tile(cell).isLegal &&
(tile(cell).validAsDestination !== true || cell[0] !== options.to[0] || cell[1] !== options.to[1])) ||
(!(origin[0] === options.from[0] && origin[1] === options.from[1] && !tile(options.from).isLegal && !tile(options.from).validAsDestination) &&
(tile(cell).elevation - tile(origin).elevation > step || tile(cell).elevation - tile(origin).elevation < -step)));
}
function traverse(from) {
const { tiles, total } = neighbors(from[0], diagonal);
for (let i = 0; i < total; i++) {
const neighbor = tiles[i];
if (!neighbor) {
continue;
}
const name = vectorId(neighbor[0]);
if (canUse(neighbor, from[0])) {
const existing = open.find((item) => vectorId(item[0]) === name);
const currentScore = score({
current: neighbor[0],
parent: from,
goal: options.to,
heuristic,
});
if (existing && currentScore.f < existing[1].f) {
const existingName = vectorId(existing[0]);
open = open.map((existingTile) => {
if (vectorId(existingTile[0]) === existingName) {
existingTile[1] = currentScore;
existingTile[2] = from;
}
return existingTile;
});
}
else if (!existing) {
open.push([neighbor[0], currentScore, from]);
}
}
}
open = open.sort((a, b) => asc(a[1].f, b[1].f));
}
traverse([
options.from,
{
g: 0,
h: 0,
f: 0,
},
null,
]);
while (open.length > 0) {
const bestScore = open.shift();
if (bestScore) {
const [vector] = bestScore;
const name = vectorId(vector);
closed.push(name);
if (name === end) {
path = calculatePath(bestScore);
open = [];
continue;
}
traverse(bestScore);
}
}
return path;
}
function calculatePath(result) {
let current = result;
const path = [];
while (current !== null) {
path.push(current[0]);
current = current[2];
}
path.reverse();
return path;
}
function neighbors(vector, diagonals = false) {
const tiles = [];
tiles.push([[vector[0] - 1, vector[1]], null]);
tiles.push([[vector[0] + 1, vector[1]], null]);
tiles.push([[vector[0], vector[1] - 1], null]);
tiles.push([[vector[0], vector[1] + 1], null]);
if (diagonals) {
tiles.push([
[vector[0] - 1, vector[1] - 1],
[
[vector[0], vector[1] - 1],
[vector[0] - 1, vector[1]],
],
]);
tiles.push([
[vector[0] + 1, vector[1] + 1],
[
[vector[0], vector[1] + 1],
[vector[0] + 1, vector[1]],
],
]);
tiles.push([
[vector[0] + 1, vector[1] - 1],
[
[vector[0], vector[1] - 1],
[vector[0] + 1, vector[1]],
],
]);
tiles.push([
[vector[0] - 1, vector[1] + 1],
[
[vector[0], vector[1] + 1],
[vector[0] - 1, vector[1]],
],
]);
}
return { tiles, total: tiles.length };
}
function score(options) {
const g = options.parent[1].g + 1;
const h = heuristics[options.heuristic](options.current, options.goal);
return { g, h, f: g + h };
}
function vectorId(vector) {
return `${vector[0]},${vector[1]}`;
}
function asc(a, b) {
if (a > b) {
return 1;
}
if (a < b) {
return -1;
}
return 0;
}
exports.asc = asc;
exports.calculatePath = calculatePath;
exports.neighbors = neighbors;
exports.score = score;
exports.search = search;
exports.vectorId = vectorId;
//# sourceMappingURL=index.cjs.map