126 lines
4.0 KiB
JavaScript
126 lines
4.0 KiB
JavaScript
// A* over a hex grid, independent of units and terrain. The caller describes
|
|
// the graph with callbacks so the algorithm stays pure and testable:
|
|
//
|
|
// neighbours(coords) -> [{x,y}, ...]
|
|
// canEnter(coords, isGoal) -> bool
|
|
// stepCost(from, to) -> number
|
|
// heuristic(from, to) -> number
|
|
//
|
|
// canEnter is told whether the tile is the destination, so a caller can allow
|
|
// entering a tile it would otherwise route around (a hostile unit or city) only
|
|
// as the very goal. findPath returns the cells from start to goal inclusive, or
|
|
// [] when the goal cannot be entered or is unreachable.
|
|
|
|
import { key } from "./hex.js";
|
|
|
|
class MinHeap {
|
|
constructor() {
|
|
this.keys = [];
|
|
this.values = [];
|
|
}
|
|
|
|
isEmpty() {
|
|
return this.keys.length === 0;
|
|
}
|
|
|
|
push(keyValue, value) {
|
|
this.keys.push(keyValue);
|
|
this.values.push(value);
|
|
let index = this.keys.length - 1;
|
|
while (index > 0) {
|
|
const parent = (index - 1) >> 1;
|
|
if (this.keys[parent] <= this.keys[index]) break;
|
|
this._swap(parent, index);
|
|
index = parent;
|
|
}
|
|
}
|
|
|
|
pop() {
|
|
const value = this.values[0];
|
|
const last = this.keys.length - 1;
|
|
this.keys[0] = this.keys[last];
|
|
this.values[0] = this.values[last];
|
|
this.keys.pop();
|
|
this.values.pop();
|
|
let index = 0;
|
|
const count = this.keys.length;
|
|
while (true) {
|
|
let smallest = index;
|
|
const left = (index << 1) + 1;
|
|
const right = left + 1;
|
|
if (left < count && this.keys[left] < this.keys[smallest]) smallest = left;
|
|
if (right < count && this.keys[right] < this.keys[smallest]) smallest = right;
|
|
if (smallest === index) break;
|
|
this._swap(index, smallest);
|
|
index = smallest;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
_swap(a, b) {
|
|
const k = this.keys[a];
|
|
this.keys[a] = this.keys[b];
|
|
this.keys[b] = k;
|
|
const v = this.values[a];
|
|
this.values[a] = this.values[b];
|
|
this.values[b] = v;
|
|
}
|
|
}
|
|
|
|
export class HexPathfinder {
|
|
findPath(start, goal, neighbours, canEnter, stepCost, heuristic) {
|
|
const search = this.findPathIter(start, goal, neighbours, canEnter, stepCost, heuristic);
|
|
let step = search.next();
|
|
while (!step.done) step = search.next();
|
|
return step.value;
|
|
}
|
|
|
|
// The same search exposed as an iterator, suspending after every expanded
|
|
// tile. A caller on the main thread can drive it a few tiles at a time and
|
|
// stay responsive instead of freezing on one long search.
|
|
*findPathIter(start, goal, neighbours, canEnter, stepCost, heuristic) {
|
|
const startKey = key(start.x, start.y);
|
|
const goalKey = key(goal.x, goal.y);
|
|
if (startKey === goalKey || !canEnter(goal, true)) return [];
|
|
const open = new MinHeap();
|
|
const cameFrom = new Map();
|
|
const gScore = new Map();
|
|
const closed = new Set();
|
|
gScore.set(startKey, 0);
|
|
open.push(heuristic(start, goal), start);
|
|
while (!open.isEmpty()) {
|
|
const current = open.pop();
|
|
const currentKey = key(current.x, current.y);
|
|
if (!closed.has(currentKey)) {
|
|
if (currentKey === goalKey) return reconstruct(cameFrom, current);
|
|
closed.add(currentKey);
|
|
for (const neighbour of neighbours(current)) {
|
|
const neighbourKey = key(neighbour.x, neighbour.y);
|
|
if (closed.has(neighbourKey)) continue;
|
|
if (!canEnter(neighbour, neighbourKey === goalKey)) continue;
|
|
const tentative = (gScore.has(currentKey) ? gScore.get(currentKey) : Infinity) +
|
|
stepCost(current, neighbour);
|
|
if (tentative < (gScore.has(neighbourKey) ? gScore.get(neighbourKey) : Infinity)) {
|
|
cameFrom.set(neighbourKey, current);
|
|
gScore.set(neighbourKey, tentative);
|
|
open.push(tentative + heuristic(neighbour, goal), neighbour);
|
|
}
|
|
}
|
|
}
|
|
yield;
|
|
}
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function reconstruct(cameFrom, current) {
|
|
const path = [current];
|
|
let cursor = key(current.x, current.y);
|
|
while (cameFrom.has(cursor)) {
|
|
current = cameFrom.get(cursor);
|
|
path.unshift(current);
|
|
cursor = key(current.x, current.y);
|
|
}
|
|
return path;
|
|
}
|