Files
OpenFrontIO/src/core/pathfinding/SerialAStar.ts
T
evanpelle af451be606 improve astar perf (#1268)
## Description:

Created test that has astar pathfind from top left to bottom right of
giant world map.

* Before these changes: took ~950ms
* replaced queue with fastqueue library: ~600ms
* Changes heuristic to be more greedy (1.1 * dist => 2 * dist): ~90ms

Resulting in a roughly 10x improvement.

Other paths also saw improvements as well, although not as dramatic.

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
- [x] I understand that submitting code with bugs that could have been
caught through manual testing blocks releases and new features for all
contributors

## Please put your Discord username so you can be contacted if a bug or
regression is found:

evan
2025-06-26 12:52:31 -07:00

190 lines
5.8 KiB
TypeScript

import FastPriorityQueue from "fastpriorityqueue";
import { AStar, PathFindResultType } from "./AStar";
/**
* Implement this interface with your graph to find paths with A*
*/
export interface GraphAdapter<NodeType> {
neighbors(node: NodeType): NodeType[];
cost(node: NodeType): number;
position(node: NodeType): { x: number; y: number };
isTraversable(from: NodeType, to: NodeType): boolean;
}
export class SerialAStar<NodeType> implements AStar<NodeType> {
private fwdOpenSet: FastPriorityQueue<{
tile: NodeType;
fScore: number;
}>;
private bwdOpenSet: FastPriorityQueue<{
tile: NodeType;
fScore: number;
}>;
private fwdCameFrom = new Map<NodeType, NodeType>();
private bwdCameFrom = new Map<NodeType, NodeType>();
private fwdGScore = new Map<NodeType, number>();
private bwdGScore = new Map<NodeType, number>();
private meetingPoint: NodeType | null = null;
public completed = false;
private sources: NodeType[];
private closestSource: NodeType;
constructor(
src: NodeType | NodeType[],
private dst: NodeType,
private iterations: number,
private maxTries: number,
private graph: GraphAdapter<NodeType>,
private directionChangePenalty: number = 0,
) {
this.fwdOpenSet = new FastPriorityQueue((a, b) => a.fScore < b.fScore);
this.bwdOpenSet = new FastPriorityQueue((a, b) => a.fScore < b.fScore);
this.sources = Array.isArray(src) ? src : [src];
this.closestSource = this.findClosestSource(dst);
// Initialize forward search with source point(s)
this.sources.forEach((startPoint) => {
this.fwdGScore.set(startPoint, 0);
this.fwdOpenSet.add({
tile: startPoint,
fScore: this.heuristic(startPoint, dst),
});
});
// Initialize backward search from destination
this.bwdGScore.set(dst, 0);
this.bwdOpenSet.add({
tile: dst,
fScore: this.heuristic(dst, this.findClosestSource(dst)),
});
}
private findClosestSource(tile: NodeType): NodeType {
return this.sources.reduce((closest, source) =>
this.heuristic(tile, source) < this.heuristic(tile, closest)
? source
: closest,
);
}
compute(): PathFindResultType {
if (this.completed) return PathFindResultType.Completed;
this.maxTries -= 1;
let iterations = this.iterations;
while (!this.fwdOpenSet.isEmpty() && !this.bwdOpenSet.isEmpty()) {
iterations--;
if (iterations <= 0) {
if (this.maxTries <= 0) {
return PathFindResultType.PathNotFound;
}
return PathFindResultType.Pending;
}
// Process forward search
const fwdCurrent = this.fwdOpenSet.poll()!.tile;
// Check if we've found a meeting point
if (this.bwdGScore.has(fwdCurrent)) {
this.meetingPoint = fwdCurrent;
this.completed = true;
return PathFindResultType.Completed;
}
this.expandNode(fwdCurrent, true);
// Process backward search
const bwdCurrent = this.bwdOpenSet.poll()!.tile;
// Check if we've found a meeting point
if (this.fwdGScore.has(bwdCurrent)) {
this.meetingPoint = bwdCurrent;
this.completed = true;
return PathFindResultType.Completed;
}
this.expandNode(bwdCurrent, false);
}
return this.completed
? PathFindResultType.Completed
: PathFindResultType.PathNotFound;
}
private expandNode(current: NodeType, isForward: boolean) {
for (const neighbor of this.graph.neighbors(current)) {
if (
neighbor !== (isForward ? this.dst : this.closestSource) &&
!this.graph.isTraversable(current, neighbor)
)
continue;
const gScore = isForward ? this.fwdGScore : this.bwdGScore;
const openSet = isForward ? this.fwdOpenSet : this.bwdOpenSet;
const cameFrom = isForward ? this.fwdCameFrom : this.bwdCameFrom;
const tentativeGScore = gScore.get(current)! + this.graph.cost(neighbor);
let penalty = 0;
// With a direction change penalty, the path will get as straight as possible
if (this.directionChangePenalty > 0) {
const prev = cameFrom.get(current);
if (prev) {
const prevDir = this.getDirection(prev, current);
const newDir = this.getDirection(current, neighbor);
if (prevDir !== newDir) {
penalty = this.directionChangePenalty;
}
}
}
const totalG = tentativeGScore + penalty;
if (!gScore.has(neighbor) || totalG < gScore.get(neighbor)!) {
cameFrom.set(neighbor, current);
gScore.set(neighbor, totalG);
const fScore =
totalG +
this.heuristic(neighbor, isForward ? this.dst : this.closestSource);
openSet.add({ tile: neighbor, fScore: fScore });
}
}
}
private heuristic(a: NodeType, b: NodeType): number {
const posA = this.graph.position(a);
const posB = this.graph.position(b);
return 2 * (Math.abs(posA.x - posB.x) + Math.abs(posA.y - posB.y));
}
private getDirection(from: NodeType, to: NodeType): string {
const fromPos = this.graph.position(from);
const toPos = this.graph.position(to);
const dx = toPos.x - fromPos.x;
const dy = toPos.y - fromPos.y;
return `${Math.sign(dx)},${Math.sign(dy)}`;
}
public reconstructPath(): NodeType[] {
if (!this.meetingPoint) return [];
// Reconstruct path from start to meeting point
const fwdPath: NodeType[] = [this.meetingPoint];
let current = this.meetingPoint;
while (this.fwdCameFrom.has(current)) {
current = this.fwdCameFrom.get(current)!;
fwdPath.unshift(current);
}
// Reconstruct path from meeting point to goal
current = this.meetingPoint;
while (this.bwdCameFrom.has(current)) {
current = this.bwdCameFrom.get(current)!;
fwdPath.push(current);
}
return fwdPath;
}
}