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
This commit is contained in:
evanpelle
2025-06-26 12:52:31 -07:00
committed by GitHub
parent 1b1ed9bf3f
commit af451be606
12 changed files with 415 additions and 16 deletions
+1
View File
@@ -8,3 +8,4 @@ resources/images/.DS_Store
resources/.DS_Store
.env*
.DS_Store
.clinic/
+7 -1
View File
@@ -14,7 +14,13 @@ export default {
"ts-jest",
{
useESM: true,
tsconfig: "tsconfig.jest.json",
tsconfig: {
target: "ES2020",
module: "es2022",
moduleResolution: "node",
experimentalDecorators: true,
types: ["jest", "node"],
},
},
],
},
+34
View File
@@ -34,6 +34,7 @@
"dotenv": "^16.5.0",
"express": "^4.21.1",
"express-rate-limit": "^7.5.0",
"fastpriorityqueue": "^0.7.5",
"google-auth-library": "^9.14.0",
"googleapis": "^143.0.0",
"hammerjs": "^2.0.8",
@@ -68,6 +69,7 @@
"@babel/preset-typescript": "^7.24.7",
"@eslint/compat": "^1.2.7",
"@eslint/js": "^9.21.0",
"@types/benchmark": "^2.1.5",
"@types/chai": "^4.3.17",
"@types/d3": "^7.4.3",
"@types/jest": "^30.0.0",
@@ -79,6 +81,7 @@
"@types/systeminformation": "^3.23.1",
"@types/ws": "^8.5.11",
"autoprefixer": "^10.4.20",
"benchmark": "^2.1.4",
"binary-base64-loader": "^1.0.0",
"canvas": "^3.1.0",
"chai": "^5.1.1",
@@ -8925,6 +8928,13 @@
"@babel/types": "^7.20.7"
}
},
"node_modules/@types/benchmark": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@types/benchmark/-/benchmark-2.1.5.tgz",
"integrity": "sha512-cKio2eFB3v7qmKcvIHLUMw/dIx/8bhWPuzpzRT4unCPRTD8VdA9Zb0afxpcxOqR4PixRS7yT42FqGS8BYL8g1w==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/body-parser": {
"version": "1.19.6",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
@@ -10912,6 +10922,17 @@
"integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==",
"license": "MIT"
},
"node_modules/benchmark": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz",
"integrity": "sha512-l9MlfN4M1K/H2fbhfMy3B7vJd6AGKJVQn2h6Sg/Yx+KckoUA7ewS5Vv6TjSq18ooE1kS9hhAlQRH3AkXIh/aOQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"lodash": "^4.17.4",
"platform": "^1.3.3"
}
},
"node_modules/big.js": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
@@ -14011,6 +14032,12 @@
"node": ">= 4.9.1"
}
},
"node_modules/fastpriorityqueue": {
"version": "0.7.5",
"resolved": "https://registry.npmjs.org/fastpriorityqueue/-/fastpriorityqueue-0.7.5.tgz",
"integrity": "sha512-3Pa0n9gwy8yIbEsT3m2j/E9DXgWvvjfiZjjqcJ+AdNKTAlVMIuFYrYG5Y3RHEM8O6cwv9hOpOWY/NaMfywoQVA==",
"license": "Apache-2.0"
},
"node_modules/fastq": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
@@ -19518,6 +19545,13 @@
"node": ">=8"
}
},
"node_modules/platform": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
"integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==",
"dev": true,
"license": "MIT"
},
"node_modules/pngjs": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz",
+4
View File
@@ -9,6 +9,7 @@
"dev": "cross-env GAME_ENV=dev concurrently \"npm run start:client\" \"npm run start:server-dev\"",
"tunnel": "npm run build-prod && npm run start:server",
"test": "jest",
"perf": "npx tsx tests/perf/*.ts",
"test:coverage": "jest --coverage",
"format": "prettier --ignore-unknown --write .",
"lint": "eslint",
@@ -27,6 +28,7 @@
"@babel/preset-typescript": "^7.24.7",
"@eslint/compat": "^1.2.7",
"@eslint/js": "^9.21.0",
"@types/benchmark": "^2.1.5",
"@types/chai": "^4.3.17",
"@types/d3": "^7.4.3",
"@types/jest": "^30.0.0",
@@ -38,6 +40,7 @@
"@types/systeminformation": "^3.23.1",
"@types/ws": "^8.5.11",
"autoprefixer": "^10.4.20",
"benchmark": "^2.1.4",
"binary-base64-loader": "^1.0.0",
"canvas": "^3.1.0",
"chai": "^5.1.1",
@@ -105,6 +108,7 @@
"dotenv": "^16.5.0",
"express": "^4.21.1",
"express-rate-limit": "^7.5.0",
"fastpriorityqueue": "^0.7.5",
"google-auth-library": "^9.14.0",
"googleapis": "^143.0.0",
"hammerjs": "^2.0.8",
+11 -11
View File
@@ -1,4 +1,4 @@
import { PriorityQueue } from "@datastructures-js/priority-queue";
import FastPriorityQueue from "fastpriorityqueue";
import { AStar, PathFindResultType } from "./AStar";
/**
@@ -12,11 +12,11 @@ export interface GraphAdapter<NodeType> {
}
export class SerialAStar<NodeType> implements AStar<NodeType> {
private fwdOpenSet: PriorityQueue<{
private fwdOpenSet: FastPriorityQueue<{
tile: NodeType;
fScore: number;
}>;
private bwdOpenSet: PriorityQueue<{
private bwdOpenSet: FastPriorityQueue<{
tile: NodeType;
fScore: number;
}>;
@@ -39,15 +39,15 @@ export class SerialAStar<NodeType> implements AStar<NodeType> {
private graph: GraphAdapter<NodeType>,
private directionChangePenalty: number = 0,
) {
this.fwdOpenSet = new PriorityQueue((a, b) => a.fScore - b.fScore);
this.bwdOpenSet = new PriorityQueue((a, b) => a.fScore - b.fScore);
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.enqueue({
this.fwdOpenSet.add({
tile: startPoint,
fScore: this.heuristic(startPoint, dst),
});
@@ -55,7 +55,7 @@ export class SerialAStar<NodeType> implements AStar<NodeType> {
// Initialize backward search from destination
this.bwdGScore.set(dst, 0);
this.bwdOpenSet.enqueue({
this.bwdOpenSet.add({
tile: dst,
fScore: this.heuristic(dst, this.findClosestSource(dst)),
});
@@ -85,7 +85,7 @@ export class SerialAStar<NodeType> implements AStar<NodeType> {
}
// Process forward search
const fwdCurrent = this.fwdOpenSet.dequeue()!.tile;
const fwdCurrent = this.fwdOpenSet.poll()!.tile;
// Check if we've found a meeting point
if (this.bwdGScore.has(fwdCurrent)) {
@@ -96,7 +96,7 @@ export class SerialAStar<NodeType> implements AStar<NodeType> {
this.expandNode(fwdCurrent, true);
// Process backward search
const bwdCurrent = this.bwdOpenSet.dequeue()!.tile;
const bwdCurrent = this.bwdOpenSet.poll()!.tile;
// Check if we've found a meeting point
if (this.fwdGScore.has(bwdCurrent)) {
@@ -145,7 +145,7 @@ export class SerialAStar<NodeType> implements AStar<NodeType> {
const fScore =
totalG +
this.heuristic(neighbor, isForward ? this.dst : this.closestSource);
openSet.enqueue({ tile: neighbor, fScore: fScore });
openSet.add({ tile: neighbor, fScore: fScore });
}
}
}
@@ -153,7 +153,7 @@ export class SerialAStar<NodeType> implements AStar<NodeType> {
private heuristic(a: NodeType, b: NodeType): number {
const posA = this.graph.position(a);
const posB = this.graph.position(b);
return 1.1 * (Math.abs(posA.x - posB.x) + Math.abs(posA.y - posB.y));
return 2 * (Math.abs(posA.x - posB.x) + Math.abs(posA.y - posB.y));
}
private getDirection(from: NodeType, to: NodeType): string {
+36
View File
@@ -0,0 +1,36 @@
import Benchmark from "benchmark";
import { dirname } from "path";
import { fileURLToPath } from "url";
import { PathFinder } from "../../src/core/pathfinding/PathFinding";
import { setup } from "../util/Setup";
const game = await setup(
"giantworldmap",
{},
[],
dirname(fileURLToPath(import.meta.url)),
);
new Benchmark.Suite()
.add("top-left-to-bottom-right", () => {
PathFinder.Mini(game, 10_000_000_000, true, 1).nextTile(
game.ref(0, 0),
game.ref(4077, 1929),
);
})
.add("hawaii to svalbard", () => {
PathFinder.Mini(game, 10_000_000_000, true, 1).nextTile(
game.ref(186, 800),
game.ref(2205, 52),
);
})
.add("black sea to california", () => {
PathFinder.Mini(game, 10_000_000_000, true, 1).nextTile(
game.ref(2349, 455),
game.ref(511, 536),
);
})
.on("cycle", (event: any) => {
console.log(String(event.target));
})
.run({ async: true });
+14
View File
@@ -0,0 +1,14 @@
{
"map": {
"height": 1948,
"num_land_tiles": 2544294,
"width": 4110
},
"mini_map": {
"height": 974,
"num_land_tiles": 618860,
"width": 2055
},
"name": "Giant World Map",
"nations": []
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

+4 -3
View File
@@ -23,21 +23,22 @@ export async function setup(
mapName: string,
_gameConfig: Partial<GameConfig> = {},
humans: PlayerInfo[] = [],
currentDir: string = __dirname,
): Promise<Game> {
// Suppress console.debug for tests.
console.debug = () => {};
// Simple binary file loading using fs.readFileSync()
const mapBinPath = path.join(
__dirname,
currentDir,
`../testdata/maps/${mapName}/map.bin`,
);
const miniMapBinPath = path.join(
__dirname,
currentDir,
`../testdata/maps/${mapName}/mini_map.bin`,
);
const manifestPath = path.join(
__dirname,
currentDir,
`../testdata/maps/${mapName}/manifest.json`,
);
+2 -1
View File
@@ -1,10 +1,11 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"experimentalDecorators": true,
"types": ["jest", "node"]
},
"include": ["tests/**/*"]
"include": ["tests/**/*", "src/**/*"]
}