UNPKG

@evilkiwi/astar

Version:

Synchronous A* pathfinding for TypeScript

212 lines (206 loc) 6.61 kB
'use strict'; 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