interface Position { x: number y: number } export interface Node extends Position { parent?: Node g: number h: number f: number } /** * A* pathfinding algorithm. */ export class AStar { private static readonly ORTHOGONAL_DIRECTIONS: Position[] = [ { x: 0, y: -1 }, { x: 0, y: 1 }, { x: -1, y: 0 }, { x: 1, y: 0 } ] private static readonly DIAGONAL_DIRECTIONS: Position[] = [ { x: -1, y: -1 }, { x: -1, y: 1 }, { x: 1, y: -1 }, { x: 1, y: 1 } ] /** * Finds the shortest path from start to end on the given grid. * @param start - Start position. * @param end - End position. * @param grid - The grid representing the map (0 = open space, 1 = obstacle). * @param allowDiagonal - Whether diagonal movements are allowed. * @returns Array of `Node` representing the path from start to end. */ static findPath(start: Position, end: Position, grid: number[][], allowDiagonal: boolean = false): Node[] { const openList: Node[] = [] const closedSet = new Set() const startNode: Node = { ...start, g: 0, h: 0, f: 0 } openList.push(startNode) while (openList.length > 0) { const currentNode = this.getLowestFScoreNode(openList) if (this.isEndNode(currentNode, end)) { return this.reconstructPath(currentNode) } this.removeNodeFromOpenList(openList, currentNode) closedSet.add(this.nodeToString(currentNode)) for (const neighbor of this.getValidNeighbors(currentNode, grid, end, allowDiagonal)) { if (closedSet.has(this.nodeToString(neighbor))) continue const tentativeGScore = currentNode.g + this.getDistance(currentNode, neighbor) if (!this.isInOpenList(openList, neighbor) || tentativeGScore < neighbor.g) { neighbor.parent = currentNode neighbor.g = tentativeGScore neighbor.h = this.heuristic(neighbor, end) neighbor.f = neighbor.g + neighbor.h if (!this.isInOpenList(openList, neighbor)) { openList.push(neighbor) } } } } return [] // No path found } private static getLowestFScoreNode(nodes: Node[]): Node { return nodes.reduce((min, node) => (node.f < min.f ? node : min)) } private static isEndNode(node: Node, end: Position): boolean { return node.x === end.x && node.y === end.y } private static removeNodeFromOpenList(openList: Node[], node: Node): void { const index = openList.findIndex((n) => n.x === node.x && n.y === node.y) if (index !== -1) openList.splice(index, 1) } private static nodeToString(node: Position): string { return `${node.x},${node.y}` } private static getValidNeighbors(node: Node, grid: number[][], end: Position, allowDiagonal: boolean): Node[] { const directions = allowDiagonal ? [...this.ORTHOGONAL_DIRECTIONS, ...this.DIAGONAL_DIRECTIONS] : this.ORTHOGONAL_DIRECTIONS return directions .map((dir) => ({ x: node.x + dir.x, y: node.y + dir.y, g: 0, h: 0, f: 0 })) .filter((pos) => this.isValidPosition(pos, grid, end)) } private static isValidPosition(pos: Position, grid: number[][], end: Position): boolean { const { x, y } = pos return x >= 0 && y >= 0 && x < grid.length && y < grid[0].length && (grid[y][x] === 0 || (x === end.x && y === end.y)) } private static isInOpenList(openList: Node[], node: Position): boolean { return openList.some((n) => n.x === node.x && n.y === node.y) } private static getDistance(a: Position, b: Position): number { const dx = Math.abs(a.x - b.x) const dy = Math.abs(a.y - b.y) return Math.sqrt(dx * dx + dy * dy) } private static heuristic(node: Position, goal: Position): number { return this.getDistance(node, goal) } private static reconstructPath(endNode: Node): Node[] { const path: Node[] = [] let currentNode: Node | undefined = endNode while (currentNode) { path.unshift(currentNode) currentNode = currentNode.parent } return path } }