// 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, from) -> 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, and which tile the step came from, so a caller can forbid // crossing from one denied tile straight into another. 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, current)) 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; }