mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-04 19:26:09 +00:00
Pathfinding Refactor pt. 2 (#2866)
## Playtest https://pf-pt-2.openfront.dev/ ## Pathfinding Refactor pt. 2 <img width="1536" height="1024" alt="image" src="https://github.com/user-attachments/assets/9477958e-54b7-4c83-b317-ba789e809e9e" /> This is a follow-up to a previous PR introducing pathfinding changes. This time, it introduces a complete refactor of `pathfinding` directory and breakdown into composable pieces. ### Unified PathFinder interface `PathFinder<T>` and `SteppingPathFinder<T>` are introduced to unify **all** pathfinding across the application. First one exposes complete path, while stepping variant allows the callee to iterate over the path by calling `.next`. All pathfinders share this one common interface, which makes them easy to use in any scenario - `PathFinding.Water(game).search(from, to)`. `SteppingPathFinder<T>` extends `PathFinder<T>` with an ability to iterate over the path. It handles caching, storing current index and invalidation. This allows the units to not care about the inner workings of the pathfinder and just call `pf.next(current, target)` and receive instructions on what to do next. ### Common entry point All pathfinders are now exposed from common `PathFinding` entrypoint: - `PathFinding.Water` - `PathFinding.Rail` - `PathFinding.Stations` - `PathFinding.Rail` Additional entry point is introduced for pathfinders which need to work both in the worker, but also on the frontend, which lacks `Game` interface. Currently only `UniversalPathFinding.Parabola` is available. ### Spatial Query New module has been introduced close to `pathfinding` - `SpatialQuery`. It aims to resolve any questions game may have about finding tiles meeting criteria. Currently `SpatialQuery.closestShore(player, target)` and `SpatialQuery.closestShoreByWater(player, target)` are available - they help answering questions about naval invasion: "What is the best landing location from user's click?" and "Which our tile should be used to launch the transport ship?". Under the hood they use very similar mechanics to pathfinding, so it felt right to put them close by. ### Modular architecture Pathfinders now support transformers: `MiniMapTransformer`, `ShoreCoercingTransformer`, `ComponentCheckTransformer`, `SmoothingTransformer`. Transformers functions like a middleware in the pathfinding chain. They wrap around the pathfinder and provide additional functionality. This allows the pathfinder to focus on actually finding the path instead of doing unrelated things. Example chain for simple (A*) water pathfinding: ```ts static WaterSimple(game: Game): SteppingPathFinder<TileRef> { const miniMap = game.miniMap(); const pf = new AStarWater(miniMap); return PathFinderBuilder.create(pf) .wrap((pf) => new ShoreCoercingTransformer(pf, miniMap)) .wrap((pf) => new MiniMapTransformer(pf, game.map(), miniMap)) .buildWithStepper(tileStepperConfig(game)); } ``` The Pathfinder - here `AStarWater` - does not care about the conversion between minimap and main map tiles. It also does not care if the source or destination is a land tile. The transformers take care of that. The pathfinder gets a set of valid coordinates and produces the path - that's it. Modular approach makes working on a particular set of utilities much easier - for example map upscaling is handled consistently across all pathfinders. Additionally, the pathfinders are not tied to the particular map resolution used. Pass them a different map and they will work the same. ### Algorithms Algorithms used are neatly organized inside `src/core/pathfinding/algorithms`. They are prefixed with the algorithm name and suffixed with the use case. File without suffix exposes generic version ready to traverse any graph with adapters. Specialized versions either use an adapter or inline logic when performance is critical - using adapters leads to 20-30% performance loss. The directory includes `A*` and `BFS` but also other useful utils, such as `AbstractGraph` used to generate... an abstract graph on top of the tile map and `ConnectedComponents` helping to identify whether two tiles are connected by a path without actually computing the path. ### Playground The playground have been updated with new algorithms, including tweaked very greedy `A*`. <img width="2175" height="1424" alt="image" src="https://github.com/user-attachments/assets/1f833651-0024-4299-bf86-882f5368358c" /> ### Tests Yeah, there are some, a little too many if I say so myself. But there are no useless tests. I had to ensure refactored code works somehow reliably. This PR comes with trust me bro guarantee, but I would appreciate someone confirming **naval invasions, nukes (esp. MIRV) and warships**. ### Discord `moleole` GL & HF
This commit is contained in:
committed by
GitHub
parent
bcec4ad758
commit
0e3ced3bfa
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Compare pathfinding adapters side-by-side
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx tests/pathfinding/benchmark/compare.ts <scenario> <adapters>
|
||||
* npx tsx tests/pathfinding/benchmark/compare.ts --synthetic <map-name> <adapters>
|
||||
*
|
||||
* Examples:
|
||||
* npx tsx tests/pathfinding/benchmark/compare.ts default hpa,a.baseline
|
||||
* npx tsx tests/pathfinding/benchmark/compare.ts --synthetic giantworldmap hpa,hpa.cached,a.full
|
||||
*/
|
||||
|
||||
import {
|
||||
type BenchmarkResult,
|
||||
calculateStats,
|
||||
getAdapter,
|
||||
getScenario,
|
||||
measureExecutionTime,
|
||||
measurePathLength,
|
||||
} from "../utils";
|
||||
|
||||
interface AdapterResults {
|
||||
adapter: string;
|
||||
initTime: number;
|
||||
totalTime: number;
|
||||
totalDistance: number;
|
||||
successfulRoutes: number;
|
||||
totalRoutes: number;
|
||||
}
|
||||
|
||||
const DEFAULT_ITERATIONS = 1;
|
||||
|
||||
async function runBenchmark(
|
||||
scenarioName: string,
|
||||
adapterName: string,
|
||||
): Promise<AdapterResults> {
|
||||
const { game, routes, initTime } = await getScenario(
|
||||
scenarioName,
|
||||
adapterName,
|
||||
);
|
||||
const adapter = getAdapter(game, adapterName);
|
||||
|
||||
const results: BenchmarkResult[] = [];
|
||||
|
||||
// Measure path lengths
|
||||
for (const route of routes) {
|
||||
const pathLength = measurePathLength(adapter, route);
|
||||
results.push({ route: route.name, pathLength, executionTime: null });
|
||||
}
|
||||
|
||||
// Measure execution times
|
||||
for (const route of routes) {
|
||||
const result = results.find((r) => r.route === route.name);
|
||||
if (result && result.pathLength !== null) {
|
||||
const execTime = measureExecutionTime(adapter, route, DEFAULT_ITERATIONS);
|
||||
result.executionTime = execTime;
|
||||
}
|
||||
}
|
||||
|
||||
const stats = calculateStats(results);
|
||||
|
||||
return {
|
||||
adapter: adapterName,
|
||||
initTime,
|
||||
totalTime: stats.totalTime,
|
||||
totalDistance: stats.totalDistance,
|
||||
successfulRoutes: stats.successfulRoutes,
|
||||
totalRoutes: stats.totalRoutes,
|
||||
};
|
||||
}
|
||||
|
||||
const TABLE_HEADERS = [
|
||||
"Adapter",
|
||||
"Init (ms)",
|
||||
"Path (ms)",
|
||||
"Distance",
|
||||
"Routes",
|
||||
];
|
||||
|
||||
const TABLE_WIDTHS = [20, 12, 12, 12, 10];
|
||||
|
||||
function printTableHeader(scenarioName: string) {
|
||||
console.log(`\nResults: ${scenarioName}`);
|
||||
console.log("=".repeat(70));
|
||||
console.log(TABLE_HEADERS.map((h, i) => h.padEnd(TABLE_WIDTHS[i])).join(" "));
|
||||
console.log("-".repeat(70));
|
||||
}
|
||||
|
||||
function printTableRow(r: AdapterResults) {
|
||||
const row = [
|
||||
r.adapter,
|
||||
r.initTime.toFixed(2),
|
||||
r.totalTime.toFixed(2),
|
||||
r.totalDistance.toString(),
|
||||
`${r.successfulRoutes}/${r.totalRoutes}`,
|
||||
];
|
||||
console.log(row.map((c, i) => c.padEnd(TABLE_WIDTHS[i])).join(" "));
|
||||
}
|
||||
|
||||
function printTableFooter() {
|
||||
console.log("-".repeat(70));
|
||||
}
|
||||
|
||||
function printUsage() {
|
||||
console.log(`
|
||||
Usage:
|
||||
npx tsx tests/pathfinding/benchmark/compare.ts <scenario> <adapters>
|
||||
npx tsx tests/pathfinding/benchmark/compare.ts --synthetic <map-name> <adapters>
|
||||
|
||||
Arguments:
|
||||
<scenario> Name of the scenario (default: "default")
|
||||
<adapters> Comma-separated list of adapters to compare (e.g., "hpa,a.baseline")
|
||||
|
||||
Examples:
|
||||
npx tsx tests/pathfinding/benchmark/compare.ts default hpa,a.baseline
|
||||
npx tsx tests/pathfinding/benchmark/compare.ts --synthetic giantworldmap hpa,hpa.cached,a.full
|
||||
|
||||
Available adapters:
|
||||
a.baseline - A* on minimap (inlined)
|
||||
a.generic - A* on minimap (adapter)
|
||||
a.full - A* on full map
|
||||
hpa - Hierarchical pathfinding (no cache)
|
||||
hpa.cached - Hierarchical pathfinding (with cache)
|
||||
`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.includes("--help") || args.includes("-h")) {
|
||||
printUsage();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const isSynthetic = args.includes("--synthetic");
|
||||
const nonFlagArgs = args.filter((arg) => !arg.startsWith("--"));
|
||||
|
||||
if (nonFlagArgs.length < 2) {
|
||||
console.error("Error: requires <scenario> and <adapters> arguments");
|
||||
printUsage();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const scenarioArg = nonFlagArgs[0];
|
||||
const adaptersArg = nonFlagArgs[1];
|
||||
const adapters = adaptersArg.split(",").map((a) => a.trim());
|
||||
|
||||
if (adapters.length < 1) {
|
||||
console.error("Error: at least one adapter required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const scenarioName = isSynthetic ? `synthetic/${scenarioArg}` : scenarioArg;
|
||||
|
||||
console.log(
|
||||
`Comparing ${adapters.length} adapters on scenario: ${scenarioName}`,
|
||||
);
|
||||
console.log(`Adapters: ${adapters.join(", ")}`);
|
||||
console.log("");
|
||||
|
||||
printTableHeader(scenarioName);
|
||||
|
||||
for (const adapter of adapters) {
|
||||
try {
|
||||
const result = await runBenchmark(scenarioName, adapter);
|
||||
printTableRow(result);
|
||||
} catch (error) {
|
||||
console.log(`${adapter.padEnd(TABLE_WIDTHS[0])} FAILED: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
printTableFooter();
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("Fatal error:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -2,10 +2,13 @@ import { readdirSync, readFileSync } from "fs";
|
||||
import { dirname, join } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { Game } from "../../../../src/core/game/Game.js";
|
||||
import { TileRef } from "../../../../src/core/game/GameMap.js";
|
||||
import { NavMesh } from "../../../../src/core/pathfinding/navmesh/NavMesh.js";
|
||||
import { AStarWaterHierarchical } from "../../../../src/core/pathfinding/algorithms/AStar.WaterHierarchical.js";
|
||||
import { setupFromPath } from "../../utils.js";
|
||||
|
||||
// Available comparison adapters
|
||||
// Note: "hpa" runs same algorithm without debug overhead for fair timing comparison
|
||||
export const COMPARISON_ADAPTERS = ["hpa", "a.baseline", "a.generic", "a.full"];
|
||||
|
||||
export interface MapInfo {
|
||||
name: string;
|
||||
displayName: string;
|
||||
@@ -13,7 +16,7 @@ export interface MapInfo {
|
||||
|
||||
export interface MapCache {
|
||||
game: Game;
|
||||
navMesh: NavMesh;
|
||||
hpaStar: AStarWaterHierarchical;
|
||||
}
|
||||
|
||||
const cache = new Map<string, MapCache>();
|
||||
@@ -114,13 +117,20 @@ export async function loadMap(mapName: string): Promise<MapCache> {
|
||||
const mapsDir = getMapsDirectory();
|
||||
|
||||
// Use the existing setupFromPath utility to load the map
|
||||
const game = await setupFromPath(mapsDir, mapName);
|
||||
const game = await setupFromPath(mapsDir, mapName, { disableNavMesh: false });
|
||||
|
||||
// Initialize NavMesh
|
||||
const navMesh = new NavMesh(game, { cachePaths: config.cachePaths });
|
||||
navMesh.initialize();
|
||||
// Get pre-built graph from game
|
||||
const graph = game.miniWaterGraph();
|
||||
if (!graph) {
|
||||
throw new Error(`No water graph available for map: ${mapName}`);
|
||||
}
|
||||
|
||||
const cacheEntry: MapCache = { game, navMesh };
|
||||
// Initialize AStarWaterHierarchical with minimap and graph
|
||||
const hpaStar = new AStarWaterHierarchical(game.miniMap(), graph, {
|
||||
cachePaths: config.cachePaths,
|
||||
});
|
||||
|
||||
const cacheEntry: MapCache = { game, hpaStar };
|
||||
|
||||
// Store in cache
|
||||
cache.set(mapName, cacheEntry);
|
||||
@@ -132,7 +142,7 @@ export async function loadMap(mapName: string): Promise<MapCache> {
|
||||
* Get map metadata for client
|
||||
*/
|
||||
export async function getMapMetadata(mapName: string) {
|
||||
const { game, navMesh } = await loadMap(mapName);
|
||||
const { game, hpaStar } = await loadMap(mapName);
|
||||
|
||||
// Extract map data
|
||||
const mapData: number[] = [];
|
||||
@@ -143,65 +153,48 @@ export async function getMapMetadata(mapName: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Extract static graph data from NavMesh
|
||||
// Extract static graph data from GameMapHPAStar
|
||||
// Access internal graph via type casting (test code only)
|
||||
const graph = (hpaStar as any).graph;
|
||||
const miniMap = game.miniMap();
|
||||
const navMeshGraph = (navMesh as any).graph;
|
||||
|
||||
// Convert gateways from Map to array
|
||||
const gatewaysArray = Array.from(navMeshGraph.gateways.values());
|
||||
const allGateways = gatewaysArray.map((gw: any) => ({
|
||||
id: gw.id,
|
||||
x: miniMap.x(gw.tile),
|
||||
y: miniMap.y(gw.tile),
|
||||
// Convert nodes to client format
|
||||
const allNodes = graph.getAllNodes().map((node: any) => ({
|
||||
id: node.id,
|
||||
x: miniMap.x(node.tile),
|
||||
y: miniMap.y(node.tile),
|
||||
}));
|
||||
|
||||
// Create a lookup map from gateway ID to gateway for edge conversion
|
||||
const gatewayById = new Map(gatewaysArray.map((gw: any) => [gw.id, gw]));
|
||||
// Convert edges to client format
|
||||
const edges: Array<{
|
||||
fromId: number;
|
||||
toId: number;
|
||||
from: number[];
|
||||
to: number[];
|
||||
cost: number;
|
||||
}> = [];
|
||||
for (let i = 0; i < graph.edgeCount; i++) {
|
||||
const edge = graph.getEdge(i);
|
||||
if (!edge) continue;
|
||||
|
||||
// Convert edges from Map<gatewayId, Edge[]> to flat array
|
||||
// The edges Map has gateway IDs as keys, and arrays of edges as values
|
||||
const allEdges: any[] = [];
|
||||
for (const edgeArray of navMeshGraph.edges.values()) {
|
||||
allEdges.push(...edgeArray);
|
||||
const nodeA = graph.getNode(edge.nodeA);
|
||||
const nodeB = graph.getNode(edge.nodeB);
|
||||
if (!nodeA || !nodeB) continue;
|
||||
|
||||
edges.push({
|
||||
fromId: edge.nodeA,
|
||||
toId: edge.nodeB,
|
||||
from: [miniMap.x(nodeA.tile) * 2, miniMap.y(nodeA.tile) * 2],
|
||||
to: [miniMap.x(nodeB.tile) * 2, miniMap.y(nodeB.tile) * 2],
|
||||
cost: edge.cost,
|
||||
});
|
||||
}
|
||||
|
||||
// Deduplicate edges (they're bidirectional, so each edge appears twice)
|
||||
const seenEdges = new Set<string>();
|
||||
const edges = allEdges
|
||||
.filter((edge: any) => {
|
||||
const edgeKey =
|
||||
edge.from < edge.to
|
||||
? `${edge.from}-${edge.to}`
|
||||
: `${edge.to}-${edge.from}`;
|
||||
if (seenEdges.has(edgeKey)) return false;
|
||||
seenEdges.add(edgeKey);
|
||||
return true;
|
||||
})
|
||||
.map((edge: any) => {
|
||||
const fromGateway = gatewayById.get(edge.from);
|
||||
const toGateway = gatewayById.get(edge.to);
|
||||
|
||||
return {
|
||||
fromId: edge.from,
|
||||
toId: edge.to,
|
||||
from: fromGateway
|
||||
? [miniMap.x(fromGateway.tile) * 2, miniMap.y(fromGateway.tile) * 2]
|
||||
: [0, 0],
|
||||
to: toGateway
|
||||
? [miniMap.x(toGateway.tile) * 2, miniMap.y(toGateway.tile) * 2]
|
||||
: [0, 0],
|
||||
cost: edge.cost,
|
||||
path: edge.path
|
||||
? edge.path.map((tile: TileRef) => [game.x(tile), game.y(tile)])
|
||||
: null,
|
||||
};
|
||||
});
|
||||
|
||||
console.log(
|
||||
`Map ${mapName}: ${allGateways.length} gateways, ${edges.length} edges`,
|
||||
`Map ${mapName}: ${allNodes.length} nodes, ${edges.length} edges`,
|
||||
);
|
||||
|
||||
const sectorSize = navMeshGraph.sectorSize;
|
||||
const clusterSize = graph.clusterSize;
|
||||
|
||||
return {
|
||||
name: mapName,
|
||||
@@ -209,10 +202,11 @@ export async function getMapMetadata(mapName: string) {
|
||||
height: game.height(),
|
||||
mapData,
|
||||
graphDebug: {
|
||||
allGateways,
|
||||
allNodes,
|
||||
edges,
|
||||
sectorSize,
|
||||
clusterSize,
|
||||
},
|
||||
adapters: COMPARISON_ADAPTERS,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,39 +1,64 @@
|
||||
import { TileRef } from "../../../../src/core/game/GameMap.js";
|
||||
import { MiniAStarAdapter } from "../../../../src/core/pathfinding/adapters/MiniAStarAdapter.js";
|
||||
import { loadMap } from "./maps.js";
|
||||
import { AStarWaterHierarchical } from "../../../../src/core/pathfinding/algorithms/AStar.WaterHierarchical.js";
|
||||
import { BresenhamSmoothingTransformer } from "../../../../src/core/pathfinding/smoothing/BresenhamPathSmoother.js";
|
||||
import { ComponentCheckTransformer } from "../../../../src/core/pathfinding/transformers/ComponentCheckTransformer.js";
|
||||
import { MiniMapTransformer } from "../../../../src/core/pathfinding/transformers/MiniMapTransformer.js";
|
||||
import { ShoreCoercingTransformer } from "../../../../src/core/pathfinding/transformers/ShoreCoercingTransformer.js";
|
||||
import {
|
||||
PathFinder,
|
||||
SteppingPathFinder,
|
||||
} from "../../../../src/core/pathfinding/types.js";
|
||||
import { getAdapter } from "../../utils.js";
|
||||
import { COMPARISON_ADAPTERS, loadMap } from "./maps.js";
|
||||
|
||||
interface PathfindingOptions {
|
||||
includePfMini?: boolean;
|
||||
includeNavMesh?: boolean;
|
||||
}
|
||||
|
||||
interface NavMeshResult {
|
||||
// Primary result with debug info
|
||||
interface PrimaryResult {
|
||||
path: Array<[number, number]> | null;
|
||||
initialPath: Array<[number, number]> | null;
|
||||
gateways: Array<[number, number]> | null;
|
||||
timings: any;
|
||||
length: number;
|
||||
time: number;
|
||||
debug: {
|
||||
nodePath: Array<[number, number]> | null;
|
||||
initialPath: Array<[number, number]> | null;
|
||||
timings: Record<string, number>;
|
||||
};
|
||||
}
|
||||
|
||||
interface PfMiniResult {
|
||||
// Comparison result (path + timing only)
|
||||
interface ComparisonResult {
|
||||
adapter: string;
|
||||
path: Array<[number, number]> | null;
|
||||
length: number;
|
||||
time: number;
|
||||
}
|
||||
|
||||
// Cache pathfinding adapters per map
|
||||
const pfMiniCache = new Map<string, MiniAStarAdapter>();
|
||||
export interface PathfindResult {
|
||||
primary: PrimaryResult;
|
||||
comparisons: ComparisonResult[];
|
||||
}
|
||||
|
||||
// Cache adapters per map
|
||||
const adapterCache = new Map<
|
||||
string,
|
||||
Map<string, SteppingPathFinder<TileRef>>
|
||||
>();
|
||||
|
||||
/**
|
||||
* Get or create MiniAStar adapter for a map
|
||||
* Get or create an adapter for a map
|
||||
*/
|
||||
function getPfMiniAdapter(mapName: string, game: any): MiniAStarAdapter {
|
||||
if (!pfMiniCache.has(mapName)) {
|
||||
const adapter = new MiniAStarAdapter(game, { waterPath: true });
|
||||
pfMiniCache.set(mapName, adapter);
|
||||
function getOrCreateAdapter(
|
||||
mapName: string,
|
||||
adapterName: string,
|
||||
game: any,
|
||||
): SteppingPathFinder<TileRef> {
|
||||
if (!adapterCache.has(mapName)) {
|
||||
adapterCache.set(mapName, new Map());
|
||||
}
|
||||
return pfMiniCache.get(mapName)!;
|
||||
const mapAdapters = adapterCache.get(mapName)!;
|
||||
|
||||
if (!mapAdapters.has(adapterName)) {
|
||||
mapAdapters.set(adapterName, getAdapter(game, adapterName));
|
||||
}
|
||||
return mapAdapters.get(adapterName)!;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,110 +73,177 @@ function pathToCoords(
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute pathfinding between two points
|
||||
* Build the full transformer chain like PathFinding.Water() does
|
||||
*/
|
||||
export async function computePath(
|
||||
mapName: string,
|
||||
from: [number, number],
|
||||
to: [number, number],
|
||||
options: PathfindingOptions = {},
|
||||
): Promise<NavMeshResult> {
|
||||
const { game, navMesh: navMeshAdapter } = await loadMap(mapName);
|
||||
function buildWrappedPathFinder(
|
||||
hpaStar: AStarWaterHierarchical,
|
||||
game: any,
|
||||
graph: any,
|
||||
): PathFinder<TileRef> {
|
||||
const miniMap = game.miniMap();
|
||||
const componentCheckFn = (t: TileRef) => graph.getComponentId(t);
|
||||
|
||||
// Convert coordinates to TileRefs
|
||||
const fromRef = game.ref(from[0], from[1]);
|
||||
const toRef = game.ref(to[0], to[1]);
|
||||
// Chain: hpaStar -> ComponentCheck -> Bresenham -> ShoreCoercing -> MiniMap
|
||||
const withComponentCheck = new ComponentCheckTransformer(
|
||||
hpaStar,
|
||||
componentCheckFn,
|
||||
);
|
||||
const withSmoothing = new BresenhamSmoothingTransformer(
|
||||
withComponentCheck,
|
||||
miniMap,
|
||||
);
|
||||
const withShoreCoercing = new ShoreCoercingTransformer(
|
||||
withSmoothing,
|
||||
miniMap,
|
||||
);
|
||||
const withMiniMap = new MiniMapTransformer(withShoreCoercing, game, miniMap);
|
||||
|
||||
// Validate that both points are water tiles
|
||||
if (!game.isWater(fromRef)) {
|
||||
throw new Error(`Start point (${from[0]}, ${from[1]}) is not water`);
|
||||
}
|
||||
if (!game.isWater(toRef)) {
|
||||
throw new Error(`End point (${to[0]}, ${to[1]}) is not water`);
|
||||
}
|
||||
|
||||
// Compute NavMesh path
|
||||
const navMeshPath = navMeshAdapter.findPath(fromRef, toRef, true);
|
||||
const path = pathToCoords(navMeshPath, game);
|
||||
return withMiniMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute primary path using AStarWaterHierarchical with debug info
|
||||
* Uses the same transformer chain as PathFinding.Water()
|
||||
*/
|
||||
function computePrimaryPath(
|
||||
hpaStar: AStarWaterHierarchical,
|
||||
game: any,
|
||||
graph: any,
|
||||
fromRef: TileRef,
|
||||
toRef: TileRef,
|
||||
): PrimaryResult {
|
||||
const miniMap = game.miniMap();
|
||||
|
||||
// Extract debug info
|
||||
let gateways: Array<[number, number]> | null = null;
|
||||
// Build wrapped pathfinder with all transformers
|
||||
const wrappedPf = buildWrappedPathFinder(hpaStar, game, graph);
|
||||
|
||||
// Enable debug mode to capture internal state
|
||||
hpaStar.debugMode = true;
|
||||
|
||||
const start = performance.now();
|
||||
const path = wrappedPf.findPath(fromRef, toRef);
|
||||
const time = performance.now() - start;
|
||||
|
||||
const debugInfo = hpaStar.debugInfo;
|
||||
|
||||
// Convert node path (miniMap coords) to full map coords
|
||||
let nodePath: Array<[number, number]> | null = null;
|
||||
if (debugInfo?.nodePath) {
|
||||
nodePath = debugInfo.nodePath.map((tile: TileRef) => {
|
||||
const x = miniMap.x(tile) * 2;
|
||||
const y = miniMap.y(tile) * 2;
|
||||
return [x, y] as [number, number];
|
||||
});
|
||||
}
|
||||
|
||||
// Convert initialPath (miniMap TileRefs) to full map coords
|
||||
let initialPath: Array<[number, number]> | null = null;
|
||||
let timings: any = {};
|
||||
|
||||
if (navMeshAdapter.debugInfo) {
|
||||
// Convert gatewayPath (TileRefs on miniMap) to full map coordinates
|
||||
if (navMeshAdapter.debugInfo.gatewayPath) {
|
||||
gateways = navMeshAdapter.debugInfo.gatewayPath.map((tile: TileRef) => {
|
||||
const x = miniMap.x(tile) * 2;
|
||||
const y = miniMap.y(tile) * 2;
|
||||
return [x, y] as [number, number];
|
||||
});
|
||||
}
|
||||
|
||||
// Convert initial path
|
||||
if (navMeshAdapter.debugInfo.initialPath) {
|
||||
initialPath = navMeshAdapter.debugInfo.initialPath.map(
|
||||
(tile: TileRef) => [game.x(tile), game.y(tile)] as [number, number],
|
||||
);
|
||||
}
|
||||
|
||||
timings = navMeshAdapter.debugInfo.timings || {};
|
||||
if (debugInfo?.initialPath) {
|
||||
initialPath = debugInfo.initialPath.map((tile: TileRef) => {
|
||||
const x = miniMap.x(tile) * 2;
|
||||
const y = miniMap.y(tile) * 2;
|
||||
return [x, y] as [number, number];
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
path,
|
||||
initialPath,
|
||||
gateways,
|
||||
timings,
|
||||
path: pathToCoords(path, game),
|
||||
length: path ? path.length : 0,
|
||||
time: timings.total ?? 0,
|
||||
time,
|
||||
debug: {
|
||||
nodePath,
|
||||
initialPath,
|
||||
timings: debugInfo?.timings ?? {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute only PathFinder.Mini path
|
||||
* Compute comparison path using adapter
|
||||
*/
|
||||
export async function computePfMiniPath(
|
||||
mapName: string,
|
||||
from: [number, number],
|
||||
to: [number, number],
|
||||
): Promise<PfMiniResult> {
|
||||
const { game } = await loadMap(mapName);
|
||||
|
||||
// Convert coordinates to TileRefs
|
||||
const fromRef = game.ref(from[0], from[1]);
|
||||
const toRef = game.ref(to[0], to[1]);
|
||||
|
||||
// Validate that both points are water tiles
|
||||
if (!game.isWater(fromRef)) {
|
||||
throw new Error(`Start point (${from[0]}, ${from[1]}) is not water`);
|
||||
}
|
||||
if (!game.isWater(toRef)) {
|
||||
throw new Error(`End point (${to[0]}, ${to[1]}) is not water`);
|
||||
}
|
||||
|
||||
// Compute PathFinder.Mini path
|
||||
const pfMiniAdapter = getPfMiniAdapter(mapName, game);
|
||||
const pfMiniStart = performance.now();
|
||||
const pfMiniPath = pfMiniAdapter.findPath(fromRef, toRef);
|
||||
const pfMiniEnd = performance.now();
|
||||
|
||||
const path = pathToCoords(pfMiniPath, game);
|
||||
const time = pfMiniEnd - pfMiniStart;
|
||||
function computeComparisonPath(
|
||||
adapter: SteppingPathFinder<TileRef>,
|
||||
game: any,
|
||||
fromRef: TileRef,
|
||||
toRef: TileRef,
|
||||
adapterName: string,
|
||||
): ComparisonResult {
|
||||
const start = performance.now();
|
||||
const path = adapter.findPath(fromRef, toRef);
|
||||
const time = performance.now() - start;
|
||||
|
||||
return {
|
||||
path,
|
||||
adapter: adapterName,
|
||||
path: pathToCoords(path, game),
|
||||
length: path ? path.length : 0,
|
||||
time,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute pathfinding between two points
|
||||
*/
|
||||
export async function computePath(
|
||||
mapName: string,
|
||||
from: [number, number],
|
||||
to: [number, number],
|
||||
options: { adapters?: string[] } = {},
|
||||
): Promise<PathfindResult> {
|
||||
const { game, hpaStar } = await loadMap(mapName);
|
||||
const graph = game.miniWaterGraph();
|
||||
|
||||
// Convert coordinates to TileRefs
|
||||
const fromRef = game.ref(from[0], from[1]);
|
||||
const toRef = game.ref(to[0], to[1]);
|
||||
|
||||
// Validate that both points are water tiles
|
||||
if (!game.isWater(fromRef)) {
|
||||
throw new Error(`Start point (${from[0]}, ${from[1]}) is not water`);
|
||||
}
|
||||
if (!game.isWater(toRef)) {
|
||||
throw new Error(`End point (${to[0]}, ${to[1]}) is not water`);
|
||||
}
|
||||
|
||||
// Compute primary path (HPA* with debug)
|
||||
const primary = computePrimaryPath(hpaStar, game, graph, fromRef, toRef);
|
||||
|
||||
// Compute comparison paths
|
||||
const selectedAdapters = options.adapters ?? COMPARISON_ADAPTERS;
|
||||
const comparisons: ComparisonResult[] = [];
|
||||
|
||||
for (const adapterName of selectedAdapters) {
|
||||
if (!COMPARISON_ADAPTERS.includes(adapterName)) {
|
||||
console.warn(`Unknown adapter: ${adapterName}, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const adapter = getOrCreateAdapter(mapName, adapterName, game);
|
||||
const result = computeComparisonPath(
|
||||
adapter,
|
||||
game,
|
||||
fromRef,
|
||||
toRef,
|
||||
adapterName,
|
||||
);
|
||||
comparisons.push(result);
|
||||
} catch (error) {
|
||||
console.error(`Error with adapter ${adapterName}:`, error);
|
||||
comparisons.push({
|
||||
adapter: adapterName,
|
||||
path: null,
|
||||
length: 0,
|
||||
time: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { primary, comparisons };
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear pathfinding adapter caches
|
||||
*/
|
||||
export function clearAdapterCaches() {
|
||||
pfMiniCache.clear();
|
||||
adapterCache.clear();
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -129,13 +129,13 @@
|
||||
<button class="toggle-button" id="showInitialPath" data-active="false">
|
||||
Initial Path
|
||||
</button>
|
||||
<button class="toggle-button" id="showUsedGateways" data-active="false">
|
||||
Used Gateways
|
||||
<button class="toggle-button" id="showUsedNodes" data-active="false">
|
||||
Used Nodes
|
||||
</button>
|
||||
</div>
|
||||
<div class="debug-panel-row">
|
||||
<button class="toggle-button" id="showGateways" data-active="false">
|
||||
Gateways
|
||||
<button class="toggle-button" id="showNodes" data-active="false">
|
||||
Nodes
|
||||
</button>
|
||||
<button class="toggle-button" id="showSectorGrid" data-active="false">
|
||||
Sectors
|
||||
@@ -166,75 +166,42 @@
|
||||
<div class="timing-label">
|
||||
<button
|
||||
class="refresh-icon"
|
||||
id="refreshNavMesh"
|
||||
title="Recompute NavMesh path"
|
||||
id="refreshHpa"
|
||||
title="Recompute HPA* path"
|
||||
>
|
||||
<span>↻</span>
|
||||
</button>
|
||||
NavMesh <span class="timing-label-detail" id="navMeshTiles"></span>
|
||||
HPA* <span class="timing-label-detail" id="hpaTiles"></span>
|
||||
</div>
|
||||
<div class="timing-value-large" id="navMeshTime">—</div>
|
||||
<div class="timing-value-large" id="hpaTime">—</div>
|
||||
|
||||
<div class="timing-breakdown" id="timingBreakdown">
|
||||
<div class="timing-item" id="timingEarlyExit" style="display: none">
|
||||
<span class="timing-name">Early Exit:</span>
|
||||
<span class="timing-value" id="timingEarlyExitValue">—</span>
|
||||
</div>
|
||||
<div class="timing-item" id="timingFindNodes" style="display: none">
|
||||
<span class="timing-name">Find Nodes:</span>
|
||||
<span class="timing-value" id="timingFindNodesValue">—</span>
|
||||
</div>
|
||||
<div
|
||||
class="timing-item"
|
||||
id="timingFindGateways"
|
||||
id="timingAbstractPath"
|
||||
style="display: none"
|
||||
>
|
||||
<span class="timing-name">Find Gateways:</span>
|
||||
<span class="timing-value" id="timingFindGatewaysValue">—</span>
|
||||
</div>
|
||||
<div class="timing-item" id="timingGatewayPath" style="display: none">
|
||||
<span class="timing-name">Gateway Path:</span>
|
||||
<span class="timing-value" id="timingGatewayPathValue">—</span>
|
||||
<span class="timing-name">Abstract Path:</span>
|
||||
<span class="timing-value" id="timingAbstractPathValue">—</span>
|
||||
</div>
|
||||
<div class="timing-item" id="timingInitialPath" style="display: none">
|
||||
<span class="timing-name">Initial Path:</span>
|
||||
<span class="timing-value" id="timingInitialPathValue">—</span>
|
||||
</div>
|
||||
<div class="timing-item" id="timingSmoothPath" style="display: none">
|
||||
<span class="timing-name">Smooth Path:</span>
|
||||
<span class="timing-value" id="timingSmoothPathValue">—</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="timing-section" id="pfMiniRequestSection">
|
||||
<button
|
||||
id="requestPfMini"
|
||||
class="timing-button"
|
||||
title="PathFinder.Mini is slow (50-1800ms per path). Click to compare."
|
||||
disabled
|
||||
>
|
||||
Request PathFinder.Mini
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="timing-section"
|
||||
id="pfMiniTimingSection"
|
||||
style="display: none"
|
||||
>
|
||||
<div class="timing-label">
|
||||
<button
|
||||
class="refresh-icon"
|
||||
id="refreshPfMini"
|
||||
title="Recompute PF.Mini path"
|
||||
>
|
||||
<span>↻</span>
|
||||
</button>
|
||||
PF.Mini <span class="timing-label-detail" id="pfMiniTiles"></span>
|
||||
</div>
|
||||
<div class="timing-value-large" id="pfMiniTime">—</div>
|
||||
</div>
|
||||
|
||||
<div class="timing-section" id="speedupSection" style="display: none">
|
||||
<div class="timing-label">Speedup</div>
|
||||
<div class="timing-value-speedup" id="speedupValue">—</div>
|
||||
<div class="timing-section" id="comparisonsSection" style="display: none">
|
||||
<div class="timing-label">Comparisons</div>
|
||||
<div id="comparisonsContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -256,13 +223,9 @@
|
||||
></div>
|
||||
<span>End Point</span>
|
||||
</div>
|
||||
<div class="legend-item" id="pfMiniLegend" style="display: none">
|
||||
<div class="legend-color" style="background: #ffaa00"></div>
|
||||
<span>PathFinder.Mini</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background: #00ffff"></div>
|
||||
<span>NavMesh</span>
|
||||
<span>HPA*</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background: #ff00ff"></div>
|
||||
@@ -273,7 +236,7 @@
|
||||
class="legend-color"
|
||||
style="background: #ffff00; height: 8px"
|
||||
></div>
|
||||
<span>Used Gateways</span>
|
||||
<span>Used Nodes</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div
|
||||
@@ -285,7 +248,7 @@
|
||||
border-radius: 50%;
|
||||
"
|
||||
></div>
|
||||
<span>Gateways</span>
|
||||
<span>Nodes</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div
|
||||
|
||||
@@ -500,6 +500,67 @@ canvas {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
/* Comparison rows */
|
||||
.comparison-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 6px 8px;
|
||||
margin: 0 -8px;
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid #333;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.comparison-row:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.comparison-row.active {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.comparison-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.comp-color {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 2px;
|
||||
margin-right: 8px;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.4;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.comparison-row.active .comp-color {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.comp-name {
|
||||
color: #aaa;
|
||||
flex: 1;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.comp-tiles {
|
||||
font-family: monospace;
|
||||
color: #888;
|
||||
width: 50px;
|
||||
text-align: right;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.comp-time {
|
||||
font-family: monospace;
|
||||
color: #f5f5f5;
|
||||
width: 60px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Legend panel (right side) */
|
||||
.legend-panel {
|
||||
position: fixed;
|
||||
|
||||
@@ -8,11 +8,7 @@ import {
|
||||
listMaps,
|
||||
setConfig,
|
||||
} from "./api/maps.js";
|
||||
import {
|
||||
clearAdapterCaches,
|
||||
computePath,
|
||||
computePfMiniPath,
|
||||
} from "./api/pathfinding.js";
|
||||
import { clearAdapterCaches, computePath } from "./api/pathfinding.js";
|
||||
|
||||
// Parse command-line arguments
|
||||
const args = process.argv.slice(2);
|
||||
@@ -112,12 +108,18 @@ app.get("/api/maps/:name/thumbnail", (req: Request, res: Response) => {
|
||||
* map: string,
|
||||
* from: [x, y],
|
||||
* to: [x, y],
|
||||
* includePfMini?: boolean
|
||||
* adapters?: string[] // Optional: which comparison adapters to run
|
||||
* }
|
||||
*
|
||||
* Response:
|
||||
* {
|
||||
* primary: { path, length, time, debug: { nodePath, initialPath, timings } },
|
||||
* comparisons: [{ adapter, path, length, time }, ...]
|
||||
* }
|
||||
*/
|
||||
app.post("/api/pathfind", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { map, from, to, includePfMini } = req.body;
|
||||
const { map, from, to, adapters } = req.body;
|
||||
|
||||
// Validate request
|
||||
if (!map || !from || !to) {
|
||||
@@ -144,7 +146,7 @@ app.post("/api/pathfind", async (req: Request, res: Response) => {
|
||||
map,
|
||||
from as [number, number],
|
||||
to as [number, number],
|
||||
{ includePfMini: !!includePfMini },
|
||||
{ adapters },
|
||||
);
|
||||
|
||||
res.json(result);
|
||||
@@ -165,66 +167,6 @@ app.post("/api/pathfind", async (req: Request, res: Response) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/pathfind-pfmini
|
||||
* Compute only PathFinder.Mini path
|
||||
*
|
||||
* Request body:
|
||||
* {
|
||||
* map: string,
|
||||
* from: [x, y],
|
||||
* to: [x, y]
|
||||
* }
|
||||
*/
|
||||
app.post("/api/pathfind-pfmini", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { map, from, to } = req.body;
|
||||
|
||||
// Validate request
|
||||
if (!map || !from || !to) {
|
||||
return res.status(400).json({
|
||||
error: "Invalid request",
|
||||
message: "Missing required fields: map, from, to",
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!Array.isArray(from) ||
|
||||
from.length !== 2 ||
|
||||
!Array.isArray(to) ||
|
||||
to.length !== 2
|
||||
) {
|
||||
return res.status(400).json({
|
||||
error: "Invalid coordinates",
|
||||
message: "from and to must be [x, y] coordinate arrays",
|
||||
});
|
||||
}
|
||||
|
||||
// Compute PF.Mini path only
|
||||
const result = await computePfMiniPath(
|
||||
map,
|
||||
from as [number, number],
|
||||
to as [number, number],
|
||||
);
|
||||
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
console.error("Error computing PF.Mini path:", error);
|
||||
|
||||
if (error instanceof Error && error.message.includes("is not water")) {
|
||||
res.status(400).json({
|
||||
error: "Invalid coordinates",
|
||||
message: error.message,
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
error: "Failed to compute PF.Mini path",
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/cache/clear
|
||||
* Clear all caches (useful for development)
|
||||
|
||||
+58
-16
@@ -17,10 +17,19 @@ import {
|
||||
MapManifest,
|
||||
} from "../../src/core/game/TerrainMapLoader";
|
||||
import { UserSettings } from "../../src/core/game/UserSettings";
|
||||
import { NavMesh } from "../../src/core/pathfinding/navmesh/NavMesh";
|
||||
import { PathFinder, PathFinders } from "../../src/core/pathfinding/PathFinder";
|
||||
import { AStarWater } from "../../src/core/pathfinding/algorithms/AStar.Water";
|
||||
import { AStarWaterHierarchical } from "../../src/core/pathfinding/algorithms/AStar.WaterHierarchical";
|
||||
import { PathFinding } from "../../src/core/pathfinding/PathFinder";
|
||||
import { PathFinderBuilder } from "../../src/core/pathfinding/PathFinderBuilder";
|
||||
import { StepperConfig } from "../../src/core/pathfinding/PathFinderStepper";
|
||||
import { MiniMapTransformer } from "../../src/core/pathfinding/transformers/MiniMapTransformer";
|
||||
import {
|
||||
PathStatus,
|
||||
SteppingPathFinder,
|
||||
} from "../../src/core/pathfinding/types";
|
||||
import { GameConfig } from "../../src/core/Schemas";
|
||||
import { TestConfig } from "../util/TestConfig";
|
||||
|
||||
export type BenchmarkRoute = {
|
||||
name: string;
|
||||
from: TileRef;
|
||||
@@ -42,25 +51,58 @@ export type BenchmarkSummary = {
|
||||
avgTime: number;
|
||||
};
|
||||
|
||||
export function getAdapter(game: Game, name: string): PathFinder {
|
||||
function tileStepperConfig(game: Game): StepperConfig<TileRef> {
|
||||
return {
|
||||
equals: (a, b) => a === b,
|
||||
distance: (a, b) => game.manhattanDist(a, b),
|
||||
preCheck: (from, to) =>
|
||||
typeof from !== "number" ||
|
||||
typeof to !== "number" ||
|
||||
!game.isValidRef(from) ||
|
||||
!game.isValidRef(to)
|
||||
? { status: PathStatus.NOT_FOUND }
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function getAdapter(
|
||||
game: Game,
|
||||
name: string,
|
||||
): SteppingPathFinder<TileRef> {
|
||||
switch (name) {
|
||||
case "legacy":
|
||||
return PathFinders.WaterLegacy(game, {
|
||||
iterations: 500_000,
|
||||
maxTries: 50,
|
||||
});
|
||||
case "a.baseline": {
|
||||
return PathFinderBuilder.create(new AStarWater(game.miniMap()))
|
||||
.wrap((pf) => new MiniMapTransformer(pf, game, game.miniMap()))
|
||||
.buildWithStepper(tileStepperConfig(game));
|
||||
}
|
||||
case "a.generic": {
|
||||
// Same as baseline - uses AStarWater on minimap
|
||||
return PathFinderBuilder.create(new AStarWater(game.miniMap()))
|
||||
.wrap((pf) => new MiniMapTransformer(pf, game, game.miniMap()))
|
||||
.buildWithStepper(tileStepperConfig(game));
|
||||
}
|
||||
case "a.full": {
|
||||
return PathFinderBuilder.create(
|
||||
new AStarWater(game.map()),
|
||||
).buildWithStepper(tileStepperConfig(game));
|
||||
}
|
||||
case "hpa": {
|
||||
// Recreate NavMesh without cache, this approach was chosen
|
||||
// Recreate AStarWaterHierarchical without cache, this approach was chosen
|
||||
// over adding cache toggles to the existing game instance
|
||||
// to avoid adding side effect from benchmark to the game
|
||||
const navMesh = new NavMesh(game, { cachePaths: false });
|
||||
navMesh.initialize();
|
||||
(game as any)._navMesh = navMesh;
|
||||
const graph = game.miniWaterGraph();
|
||||
if (!graph) {
|
||||
throw new Error("miniWaterGraph not available");
|
||||
}
|
||||
const hpa = new AStarWaterHierarchical(game.miniMap(), graph, {
|
||||
cachePaths: false,
|
||||
});
|
||||
(game as any)._miniWaterHPA = hpa;
|
||||
|
||||
return PathFinders.Water(game);
|
||||
return PathFinding.Water(game);
|
||||
}
|
||||
case "hpa.cached":
|
||||
return PathFinders.Water(game);
|
||||
return PathFinding.Water(game);
|
||||
default:
|
||||
throw new Error(`Unknown pathfinding adapter: ${name}`);
|
||||
}
|
||||
@@ -102,7 +144,7 @@ export async function getScenario(
|
||||
}
|
||||
|
||||
export function measurePathLength(
|
||||
adapter: PathFinder,
|
||||
adapter: SteppingPathFinder<TileRef>,
|
||||
route: BenchmarkRoute,
|
||||
): number | null {
|
||||
const path = adapter.findPath(route.from, route.to);
|
||||
@@ -117,7 +159,7 @@ export function measureTime<T>(fn: () => T): { result: T; time: number } {
|
||||
}
|
||||
|
||||
export function measureExecutionTime(
|
||||
adapter: PathFinder,
|
||||
adapter: SteppingPathFinder<TileRef>,
|
||||
route: BenchmarkRoute,
|
||||
executions: number = 1,
|
||||
): number | null {
|
||||
|
||||
Reference in New Issue
Block a user