mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 12:00:44 +00:00
Pathfinding Refinement (#2878)
# Pathfinding pt. 3 ## Description: This PR introduces final change to the pathfinding - path refinement. It optimizes Line of Sight refinement by searching with for the best tile with a binary search instead of linearly. And then spends the recovered budget on better refinement of the first and last 50 tiles of the journey - the place where user is most likely to look at. Additionally this PR re-introduces magnitude check and makes the ships prefer sailing close to the coast, but not too close. ## 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 ## What? | Before | After | | :--- | :--- | | <img width="1097" height="1117" alt="image" src="https://github.com/user-attachments/assets/4a0b300d-10ef-4151-b6dc-33acfb49f992" /> | <img width="1093" height="1119" alt="image" src="https://github.com/user-attachments/assets/cf81c515-c145-40f4-91e5-a4353986907b" /> | | <img width="1096" height="1129" alt="image" src="https://github.com/user-attachments/assets/21b46bce-f961-4259-88f6-fe4a66180270" /> | <img width="1098" height="1126" alt="image" src="https://github.com/user-attachments/assets/d92587d1-e6b6-4353-b4a4-1efe71bca43d" /> | ## Performance There is actually a severe performance impact of these changes. The path initial path takes almost 2x as long to generate - this is because pre processing can only do so much if the initial path is ugly. Luckily in real gameplay we only need to do this calculation once per edge, so the actual observed performance impact should be much smaller. Cache FTW. | | No Cache | Cache | | :--- | :--- | :--- | | Before | 277.04ms | 208.58ms | | After | 498.34ms | 264.27ms | ## DebugSpan Small utility, it allows any code to be easily instrumented for performance. The idea is the same as with [OTEL Spans](https://opentelemetry.io/docs/concepts/signals/traces/). Produce a span, create sub-spans, measure whatever you need. Works only when `globalThis.__DEBUG_SPAN_ENABLED__ === true`, otherwise no-op. Cool stuff, try it out: ```ts // Convenient wrapper, small performance impact return DebugSpan.wrap('add', () => a + b) // Synchronous API, basically free DebugSpan.start('work') work() DebugSpan.end() // Create sub spans DebugSpan.wrap('complex', () => { const aPlusB = DebugSpan.wrap('add', () => a + b) DebugSpan.set('additionResult', () => aPlusB) // Store data return aPlusB * c }) // Access spans, data and timing const span = DebugSpan.getLast() const compelxSpan = DebugSpan.getLast('complex') console.log(complexSpan.duration, complexSpan.data['additionResult']) ``` These are virtually free and can be enabled on-demand **in production** and available in the devtools. Under the hood devtools integration is just a wrapper around [Performance API](https://developer.mozilla.org/en-US/docs/Web/API/Performance_API). For clarity data keys not prefixed by `$` are omitted from the integration. Every key prefixed with `$` must be fully JSON serializable. <img width="977" height="799" alt="image" src="https://github.com/user-attachments/assets/b4d43506-1639-4f78-a611-30e61de12a07" />
This commit is contained in:
committed by
GitHub
parent
35b7213c5c
commit
85def73bd9
@@ -2,39 +2,40 @@ import { readdirSync, readFileSync } from "fs";
|
||||
import { dirname, join } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { Game } from "../../../../src/core/game/Game.js";
|
||||
import { AStarWaterHierarchical } from "../../../../src/core/pathfinding/algorithms/AStar.WaterHierarchical.js";
|
||||
import { DebugSpan } from "../../../../src/core/utilities/DebugSpan.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"];
|
||||
// Note: "hpa.cached" runs same algorithm without debug overhead for fair timing comparison
|
||||
export const COMPARISON_ADAPTERS = [
|
||||
"hpa.cached",
|
||||
"hpa",
|
||||
"a.baseline",
|
||||
"a.generic",
|
||||
"a.full",
|
||||
];
|
||||
|
||||
export interface MapInfo {
|
||||
name: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
export interface GraphBuildData {
|
||||
nodes: any[];
|
||||
edges: any[];
|
||||
nodesCount: number;
|
||||
edgesCount: number;
|
||||
clustersCount: number;
|
||||
buildTime: number;
|
||||
}
|
||||
|
||||
export interface MapCache {
|
||||
game: Game;
|
||||
hpaStar: AStarWaterHierarchical;
|
||||
graphBuildData: GraphBuildData | null;
|
||||
}
|
||||
|
||||
const cache = new Map<string, MapCache>();
|
||||
|
||||
/**
|
||||
* Global configuration for map loading
|
||||
*/
|
||||
let config = {
|
||||
cachePaths: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Set configuration options
|
||||
*/
|
||||
export function setConfig(options: { cachePaths?: boolean }) {
|
||||
config = { ...config, ...options };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resources/maps directory path
|
||||
*/
|
||||
@@ -105,6 +106,25 @@ export function listMaps(): MapInfo[] {
|
||||
return maps.sort((a, b) => a.displayName.localeCompare(b.displayName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract graph build data from DebugSpan
|
||||
*/
|
||||
function extractGraphBuildData(): GraphBuildData | null {
|
||||
const span = DebugSpan.getLastSpan();
|
||||
if (!span || span.name !== "AbstractGraphBuilder:build") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
nodes: (span.data.nodes as any[]) || [],
|
||||
edges: (span.data.edges as any[]) || [],
|
||||
nodesCount: (span.data.nodesCount as number) || 0,
|
||||
edgesCount: (span.data.edgesCount as number) || 0,
|
||||
clustersCount: (span.data.clustersCount as number) || 0,
|
||||
buildTime: span.duration || 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a map from cache or disk
|
||||
*/
|
||||
@@ -116,21 +136,17 @@ export async function loadMap(mapName: string): Promise<MapCache> {
|
||||
|
||||
const mapsDir = getMapsDirectory();
|
||||
|
||||
// Enable DebugSpan to capture graph build data
|
||||
DebugSpan.enable();
|
||||
|
||||
// Use the existing setupFromPath utility to load the map
|
||||
const game = await setupFromPath(mapsDir, mapName, { disableNavMesh: false });
|
||||
|
||||
// Get pre-built graph from game
|
||||
const graph = game.miniWaterGraph();
|
||||
if (!graph) {
|
||||
throw new Error(`No water graph available for map: ${mapName}`);
|
||||
}
|
||||
// Capture graph build data from DebugSpan
|
||||
const graphBuildData = extractGraphBuildData();
|
||||
DebugSpan.disable();
|
||||
|
||||
// Initialize AStarWaterHierarchical with minimap and graph
|
||||
const hpaStar = new AStarWaterHierarchical(game.miniMap(), graph, {
|
||||
cachePaths: config.cachePaths,
|
||||
});
|
||||
|
||||
const cacheEntry: MapCache = { game, hpaStar };
|
||||
const cacheEntry: MapCache = { game, graphBuildData };
|
||||
|
||||
// Store in cache
|
||||
cache.set(mapName, cacheEntry);
|
||||
@@ -142,7 +158,7 @@ export async function loadMap(mapName: string): Promise<MapCache> {
|
||||
* Get map metadata for client
|
||||
*/
|
||||
export async function getMapMetadata(mapName: string) {
|
||||
const { game, hpaStar } = await loadMap(mapName);
|
||||
const { game, graphBuildData } = await loadMap(mapName);
|
||||
|
||||
// Extract map data
|
||||
const mapData: number[] = [];
|
||||
@@ -153,49 +169,84 @@ export async function getMapMetadata(mapName: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Extract static graph data from GameMapHPAStar
|
||||
// Access internal graph via type casting (test code only)
|
||||
const graph = (hpaStar as any).graph;
|
||||
const graph = game.miniWaterGraph();
|
||||
const miniMap = game.miniMap();
|
||||
const clusterSize = graph?.clusterSize ?? 0;
|
||||
|
||||
// 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),
|
||||
}));
|
||||
|
||||
// Convert edges to client format
|
||||
const edges: Array<{
|
||||
// Use graphBuildData from DebugSpan if available, otherwise fall back to direct access
|
||||
let allNodes: Array<{ id: number; x: number; y: number }>;
|
||||
let 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;
|
||||
}>;
|
||||
|
||||
const nodeA = graph.getNode(edge.nodeA);
|
||||
const nodeB = graph.getNode(edge.nodeB);
|
||||
if (!nodeA || !nodeB) continue;
|
||||
if (graphBuildData) {
|
||||
// Convert nodes from DebugSpan data (AbstractNode format)
|
||||
allNodes = graphBuildData.nodes.map((node: any) => ({
|
||||
id: node.id,
|
||||
x: miniMap.x(node.tile),
|
||||
y: miniMap.y(node.tile),
|
||||
}));
|
||||
|
||||
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,
|
||||
// Convert edges from DebugSpan data (AbstractEdge format)
|
||||
edges = graphBuildData.edges.map((edge: any) => {
|
||||
const nodeA = graphBuildData.nodes.find((n: any) => n.id === edge.nodeA);
|
||||
const nodeB = graphBuildData.nodes.find((n: any) => n.id === edge.nodeB);
|
||||
return {
|
||||
fromId: edge.nodeA,
|
||||
toId: edge.nodeB,
|
||||
from: nodeA
|
||||
? [miniMap.x(nodeA.tile) * 2, miniMap.y(nodeA.tile) * 2]
|
||||
: [0, 0],
|
||||
to: nodeB
|
||||
? [miniMap.x(nodeB.tile) * 2, miniMap.y(nodeB.tile) * 2]
|
||||
: [0, 0],
|
||||
cost: edge.cost,
|
||||
};
|
||||
});
|
||||
|
||||
console.log(
|
||||
`Map ${mapName}: ${allNodes.length} nodes, ${edges.length} edges (from DebugSpan, built in ${graphBuildData.buildTime.toFixed(2)}ms)`,
|
||||
);
|
||||
} else if (graph) {
|
||||
// Fallback: extract directly from graph
|
||||
allNodes = graph.getAllNodes().map((node: any) => ({
|
||||
id: node.id,
|
||||
x: miniMap.x(node.tile),
|
||||
y: miniMap.y(node.tile),
|
||||
}));
|
||||
|
||||
edges = [];
|
||||
for (let i = 0; i < graph.edgeCount; i++) {
|
||||
const edge = graph.getEdge(i);
|
||||
if (!edge) continue;
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Map ${mapName}: ${allNodes.length} nodes, ${edges.length} edges (fallback)`,
|
||||
);
|
||||
} else {
|
||||
// No graph available
|
||||
allNodes = [];
|
||||
edges = [];
|
||||
console.log(`Map ${mapName}: no graph available`);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Map ${mapName}: ${allNodes.length} nodes, ${edges.length} edges`,
|
||||
);
|
||||
|
||||
const clusterSize = graph.clusterSize;
|
||||
|
||||
return {
|
||||
name: mapName,
|
||||
width: game.width(),
|
||||
@@ -205,6 +256,7 @@ export async function getMapMetadata(mapName: string) {
|
||||
allNodes,
|
||||
edges,
|
||||
clusterSize,
|
||||
buildTime: graphBuildData?.buildTime,
|
||||
},
|
||||
adapters: COMPARISON_ADAPTERS,
|
||||
};
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import { TileRef } from "../../../../src/core/game/GameMap.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 { PathFinding } from "../../../../src/core/pathfinding/PathFinder.js";
|
||||
import { SteppingPathFinder } from "../../../../src/core/pathfinding/types.js";
|
||||
import { DebugSpan } from "../../../../src/core/utilities/DebugSpan.js";
|
||||
import { getAdapter } from "../../utils.js";
|
||||
import { COMPARISON_ADAPTERS, loadMap } from "./maps.js";
|
||||
|
||||
@@ -19,6 +13,7 @@ interface PrimaryResult {
|
||||
debug: {
|
||||
nodePath: Array<[number, number]> | null;
|
||||
initialPath: Array<[number, number]> | null;
|
||||
cachedSegmentsUsed: number | null;
|
||||
timings: Record<string, number>;
|
||||
};
|
||||
}
|
||||
@@ -73,63 +68,54 @@ function pathToCoords(
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the full transformer chain like PathFinding.Water() does
|
||||
* Extract timings from DebugSpan hierarchy
|
||||
* Flattens nested spans into { spanName: duration } format
|
||||
*/
|
||||
function buildWrappedPathFinder(
|
||||
hpaStar: AStarWaterHierarchical,
|
||||
game: any,
|
||||
graph: any,
|
||||
): PathFinder<TileRef> {
|
||||
const miniMap = game.miniMap();
|
||||
const componentCheckFn = (t: TileRef) => graph.getComponentId(t);
|
||||
function extractTimings(span: {
|
||||
name: string;
|
||||
duration?: number;
|
||||
children: any[];
|
||||
}): Record<string, number> {
|
||||
const timings: Record<string, number> = {};
|
||||
|
||||
// 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);
|
||||
if (span.duration !== undefined) {
|
||||
timings[span.name] = span.duration;
|
||||
}
|
||||
|
||||
return withMiniMap;
|
||||
for (const child of span.children) {
|
||||
Object.assign(timings, extractTimings(child));
|
||||
}
|
||||
|
||||
return timings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute primary path using AStarWaterHierarchical with debug info
|
||||
* Uses the same transformer chain as PathFinding.Water()
|
||||
* Compute primary path using PathFinding.Water with debug info
|
||||
*/
|
||||
function computePrimaryPath(
|
||||
hpaStar: AStarWaterHierarchical,
|
||||
game: any,
|
||||
graph: any,
|
||||
fromRef: TileRef,
|
||||
toRef: TileRef,
|
||||
): PrimaryResult {
|
||||
const miniMap = game.miniMap();
|
||||
|
||||
// Build wrapped pathfinder with all transformers
|
||||
const wrappedPf = buildWrappedPathFinder(hpaStar, game, graph);
|
||||
// Use standard PathFinding.Water
|
||||
const pf = PathFinding.Water(game);
|
||||
|
||||
// Enable debug mode to capture internal state
|
||||
hpaStar.debugMode = true;
|
||||
// Enable DebugSpan to capture internal state
|
||||
DebugSpan.enable();
|
||||
|
||||
const start = performance.now();
|
||||
const path = wrappedPf.findPath(fromRef, toRef);
|
||||
const time = performance.now() - start;
|
||||
const path = pf.findPath(fromRef, toRef);
|
||||
|
||||
const debugInfo = hpaStar.debugInfo;
|
||||
// Get span data and disable
|
||||
const span = DebugSpan.getLastSpan();
|
||||
DebugSpan.disable();
|
||||
|
||||
// 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 spanNodePath = span?.data?.nodePath as TileRef[] | undefined;
|
||||
if (spanNodePath) {
|
||||
nodePath = spanNodePath.map((tile: TileRef) => {
|
||||
const x = miniMap.x(tile) * 2;
|
||||
const y = miniMap.y(tile) * 2;
|
||||
return [x, y] as [number, number];
|
||||
@@ -138,22 +124,32 @@ function computePrimaryPath(
|
||||
|
||||
// Convert initialPath (miniMap TileRefs) to full map coords
|
||||
let initialPath: Array<[number, number]> | null = null;
|
||||
if (debugInfo?.initialPath) {
|
||||
initialPath = debugInfo.initialPath.map((tile: TileRef) => {
|
||||
const spanInitialPath = span?.data?.initialPath as TileRef[] | undefined;
|
||||
if (spanInitialPath) {
|
||||
initialPath = spanInitialPath.map((tile: TileRef) => {
|
||||
const x = miniMap.x(tile) * 2;
|
||||
const y = miniMap.y(tile) * 2;
|
||||
return [x, y] as [number, number];
|
||||
});
|
||||
}
|
||||
|
||||
let cachedSegmentsUsed: number | null = null;
|
||||
if (span?.data?.cachedSegmentsUsed !== undefined) {
|
||||
cachedSegmentsUsed = span.data.cachedSegmentsUsed as number;
|
||||
}
|
||||
|
||||
// Extract timings from span hierarchy
|
||||
const timings = span ? extractTimings(span) : {};
|
||||
|
||||
return {
|
||||
path: pathToCoords(path, game),
|
||||
length: path ? path.length : 0,
|
||||
time,
|
||||
time: timings["hpa:findPath"] || 0,
|
||||
debug: {
|
||||
nodePath,
|
||||
initialPath,
|
||||
timings: debugInfo?.timings ?? {},
|
||||
cachedSegmentsUsed,
|
||||
timings,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -189,8 +185,7 @@ export async function computePath(
|
||||
to: [number, number],
|
||||
options: { adapters?: string[] } = {},
|
||||
): Promise<PathfindResult> {
|
||||
const { game, hpaStar } = await loadMap(mapName);
|
||||
const graph = game.miniWaterGraph();
|
||||
const { game } = await loadMap(mapName);
|
||||
|
||||
// Convert coordinates to TileRefs
|
||||
const fromRef = game.ref(from[0], from[1]);
|
||||
@@ -204,8 +199,8 @@ export async function computePath(
|
||||
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 primary path (PathFinding.Water with debug)
|
||||
const primary = computePrimaryPath(game, fromRef, toRef);
|
||||
|
||||
// Compute comparison paths
|
||||
const selectedAdapters = options.adapters ?? COMPARISON_ADAPTERS;
|
||||
|
||||
@@ -20,6 +20,7 @@ const state = {
|
||||
|
||||
// Colors for comparison paths
|
||||
const COMPARISON_COLORS = {
|
||||
"hpa.cached": "#00ffff", // cyan
|
||||
hpa: "#ff8800", // orange
|
||||
"a.baseline": "#ff00ff", // magenta
|
||||
"a.generic": "#88ff00", // lime
|
||||
@@ -814,20 +815,6 @@ function updatePathInfo(result) {
|
||||
function updateTimingsPanel(result) {
|
||||
const primary = result.primary;
|
||||
const timings = primary && primary.debug ? primary.debug.timings : {};
|
||||
|
||||
// Use timings.total (excludes debug overhead) instead of raw time
|
||||
const hpaTime = timings.total || 0;
|
||||
|
||||
// Show HPA* time and path length (or 0.00 in light gray if no data)
|
||||
const hpaTimeEl = document.getElementById("hpaTime");
|
||||
if (hpaTime > 0) {
|
||||
hpaTimeEl.textContent = `${hpaTime.toFixed(2)}ms`;
|
||||
hpaTimeEl.classList.remove("faded");
|
||||
} else {
|
||||
hpaTimeEl.textContent = "0.00ms";
|
||||
hpaTimeEl.classList.add("faded");
|
||||
}
|
||||
|
||||
const hpaTilesEl = document.getElementById("hpaTiles");
|
||||
if (primary && primary.length > 0) {
|
||||
hpaTilesEl.textContent = `- ${primary.length} tiles`;
|
||||
@@ -840,8 +827,9 @@ function updateTimingsPanel(result) {
|
||||
const earlyExitEl = document.getElementById("timingEarlyExit");
|
||||
const earlyExitValueEl = document.getElementById("timingEarlyExitValue");
|
||||
earlyExitEl.style.display = "flex";
|
||||
if (timings.earlyExitLocalPath !== undefined) {
|
||||
earlyExitValueEl.textContent = `${timings.earlyExitLocalPath.toFixed(2)}ms`;
|
||||
const earlyExitTime = timings["earlyExit"];
|
||||
if (earlyExitTime !== undefined) {
|
||||
earlyExitValueEl.textContent = `${earlyExitTime.toFixed(2)}ms`;
|
||||
earlyExitValueEl.style.color = "#f5f5f5";
|
||||
} else {
|
||||
earlyExitValueEl.textContent = "—";
|
||||
@@ -852,8 +840,9 @@ function updateTimingsPanel(result) {
|
||||
const findNodesEl = document.getElementById("timingFindNodes");
|
||||
const findNodesValueEl = document.getElementById("timingFindNodesValue");
|
||||
findNodesEl.style.display = "flex";
|
||||
if (timings.findNodes !== undefined) {
|
||||
findNodesValueEl.textContent = `${timings.findNodes.toFixed(2)}ms`;
|
||||
const nodeLookupTime = timings["nodeLookup"];
|
||||
if (nodeLookupTime !== undefined) {
|
||||
findNodesValueEl.textContent = `${nodeLookupTime.toFixed(2)}ms`;
|
||||
findNodesValueEl.style.color = "#f5f5f5";
|
||||
} else {
|
||||
findNodesValueEl.textContent = "—";
|
||||
@@ -866,8 +855,9 @@ function updateTimingsPanel(result) {
|
||||
"timingAbstractPathValue",
|
||||
);
|
||||
abstractPathEl.style.display = "flex";
|
||||
if (timings.findAbstractPath !== undefined) {
|
||||
abstractPathValueEl.textContent = `${timings.findAbstractPath.toFixed(2)}ms`;
|
||||
const abstractPathTime = timings["abstractPath"];
|
||||
if (abstractPathTime !== undefined) {
|
||||
abstractPathValueEl.textContent = `${abstractPathTime.toFixed(2)}ms`;
|
||||
abstractPathValueEl.style.color = "#f5f5f5";
|
||||
} else {
|
||||
abstractPathValueEl.textContent = "—";
|
||||
@@ -878,14 +868,28 @@ function updateTimingsPanel(result) {
|
||||
const initialPathEl = document.getElementById("timingInitialPath");
|
||||
const initialPathValueEl = document.getElementById("timingInitialPathValue");
|
||||
initialPathEl.style.display = "flex";
|
||||
if (timings.buildInitialPath !== undefined) {
|
||||
initialPathValueEl.textContent = `${timings.buildInitialPath.toFixed(2)}ms`;
|
||||
const initialPathTime = timings["initialPath"];
|
||||
if (initialPathTime !== undefined) {
|
||||
initialPathValueEl.textContent = `${initialPathTime.toFixed(2)}ms`;
|
||||
initialPathValueEl.style.color = "#f5f5f5";
|
||||
} else {
|
||||
initialPathValueEl.textContent = "—";
|
||||
initialPathValueEl.style.color = "#666";
|
||||
}
|
||||
|
||||
// Smooth Path
|
||||
const smoothPathEl = document.getElementById("timingSmoothPath");
|
||||
const smoothPathValueEl = document.getElementById("timingSmoothPathValue");
|
||||
smoothPathEl.style.display = "flex";
|
||||
const smoothPathTime = timings["smoothingTransformer"];
|
||||
if (smoothPathTime !== undefined) {
|
||||
smoothPathValueEl.textContent = `${smoothPathTime.toFixed(2)}ms`;
|
||||
smoothPathValueEl.style.color = "#f5f5f5";
|
||||
} else {
|
||||
smoothPathValueEl.textContent = "—";
|
||||
smoothPathValueEl.style.color = "#666";
|
||||
}
|
||||
|
||||
// Show comparisons section
|
||||
const comparisonsSection = document.getElementById("comparisonsSection");
|
||||
const comparisonsContainer = document.getElementById("comparisonsContainer");
|
||||
@@ -905,6 +909,23 @@ function updateTimingsPanel(result) {
|
||||
}
|
||||
}
|
||||
|
||||
// Use total span time from DebugSpan
|
||||
let hpaTime = timings["findPath"] || 0;
|
||||
|
||||
if (compMap["hpa.cached"]) {
|
||||
hpaTime = compMap["hpa.cached"].time;
|
||||
}
|
||||
|
||||
// Show HPA* time and path length (or 0.00 in light gray if no data)
|
||||
const hpaTimeEl = document.getElementById("hpaTime");
|
||||
if (hpaTime > 0) {
|
||||
hpaTimeEl.textContent = `${hpaTime.toFixed(2)}ms`;
|
||||
hpaTimeEl.classList.remove("faded");
|
||||
} else {
|
||||
hpaTimeEl.textContent = "0.00ms";
|
||||
hpaTimeEl.classList.add("faded");
|
||||
}
|
||||
|
||||
// Find fastest time overall (including HPA*) when we have data
|
||||
const compTimes = result.comparisons
|
||||
? result.comparisons.map((c) => c.time).filter((t) => t > 0)
|
||||
|
||||
@@ -196,6 +196,10 @@
|
||||
<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>
|
||||
|
||||
|
||||
@@ -522,6 +522,10 @@ canvas {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.comparison-row.active .comp-name {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.comparison-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
@@ -6,20 +6,9 @@ import {
|
||||
clearCache as clearMapCache,
|
||||
getMapMetadata,
|
||||
listMaps,
|
||||
setConfig,
|
||||
} from "./api/maps.js";
|
||||
import { clearAdapterCaches, computePath } from "./api/pathfinding.js";
|
||||
|
||||
// Parse command-line arguments
|
||||
const args = process.argv.slice(2);
|
||||
const noCache = args.includes("--no-cache");
|
||||
|
||||
// Configure map loading
|
||||
if (noCache) {
|
||||
setConfig({ cachePaths: false });
|
||||
console.log("Path caching disabled (--no-cache)");
|
||||
}
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT ?? 5555;
|
||||
|
||||
@@ -203,9 +192,6 @@ app.listen(PORT, () => {
|
||||
|
||||
Server running at: http://localhost:${PORT}
|
||||
|
||||
Configuration:
|
||||
- Path caching: ${noCache ? "disabled" : "enabled"}
|
||||
|
||||
Press Ctrl+C to stop
|
||||
`);
|
||||
});
|
||||
|
||||
+18
-10
@@ -10,7 +10,7 @@ import {
|
||||
GameType,
|
||||
PlayerInfo,
|
||||
} from "../../src/core/game/Game";
|
||||
import { createGame } from "../../src/core/game/GameImpl";
|
||||
import { createGame, GameImpl } from "../../src/core/game/GameImpl";
|
||||
import { TileRef } from "../../src/core/game/GameMap";
|
||||
import {
|
||||
genTerrainFromBin,
|
||||
@@ -90,16 +90,24 @@ export function getAdapter(
|
||||
// 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 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 PathFinding.Water(game);
|
||||
const originalGame = game as any;
|
||||
const clonedGame = new GameImpl(
|
||||
originalGame._humans,
|
||||
originalGame._nations,
|
||||
originalGame._map,
|
||||
originalGame.miniGameMap,
|
||||
originalGame._config,
|
||||
originalGame._stats,
|
||||
);
|
||||
|
||||
(clonedGame as any)._miniWaterHPA = new AStarWaterHierarchical(
|
||||
clonedGame.miniMap(),
|
||||
(clonedGame as any)._miniWaterGraph!,
|
||||
{ cachePaths: false },
|
||||
);
|
||||
|
||||
return PathFinding.Water(clonedGame);
|
||||
}
|
||||
case "hpa.cached":
|
||||
return PathFinding.Water(game);
|
||||
|
||||
Reference in New Issue
Block a user