#!/usr/bin/env node /** * Compare pathfinding adapters side-by-side * * Usage: * npx tsx tests/pathfinding/benchmark/compare.ts * npx tsx tests/pathfinding/benchmark/compare.ts --synthetic * * 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 { 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 npx tsx tests/pathfinding/benchmark/compare.ts --synthetic Arguments: Name of the scenario (default: "default") 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 and 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); });