mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-13 17:39:18 +00:00
Optimize here and there
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { GameMap, TileRef } from "../../game/GameMap";
|
||||
import { PathFinder } from "../types";
|
||||
import { BucketQueue, PriorityQueue } from "./PriorityQueue";
|
||||
import { MinHeap, PriorityQueue } from "./PriorityQueue";
|
||||
|
||||
const LAND_BIT = 7; // Bit 7 in terrain indicates land
|
||||
const MAGNITUDE_MASK = 0x1f;
|
||||
@@ -45,11 +45,7 @@ export class AStarWater implements PathFinder<number> {
|
||||
this.gScore = new Uint32Array(this.numNodes);
|
||||
this.cameFrom = new Int32Array(this.numNodes);
|
||||
|
||||
// Account for scaled costs + tie-breaker headroom
|
||||
const maxDim = map.width() + map.height();
|
||||
const maxF =
|
||||
(this.heuristicWeight + 1) * BASE_COST * maxDim + COST_SCALE * maxDim;
|
||||
this.queue = new BucketQueue(maxF);
|
||||
this.queue = new MinHeap(this.numNodes);
|
||||
}
|
||||
|
||||
findPath(start: number | number[], goal: number): number[] | null {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { GameMap, TileRef } from "../../game/GameMap";
|
||||
import { PathFinder } from "../types";
|
||||
import { BucketQueue } from "./PriorityQueue";
|
||||
import { MinHeap } from "./PriorityQueue";
|
||||
|
||||
const LAND_BIT = 7;
|
||||
const MAGNITUDE_MASK = 0x1f;
|
||||
@@ -33,7 +33,7 @@ export class AStarWaterBounded implements PathFinder<number> {
|
||||
private readonly gScoreStamp: Uint32Array;
|
||||
private readonly gScore: Uint32Array;
|
||||
private readonly cameFrom: Int32Array;
|
||||
private readonly queue: BucketQueue;
|
||||
private readonly queue: MinHeap;
|
||||
private readonly terrain: Uint8Array;
|
||||
private readonly mapWidth: number;
|
||||
private readonly heuristicWeight: number;
|
||||
@@ -54,11 +54,7 @@ export class AStarWaterBounded implements PathFinder<number> {
|
||||
this.gScore = new Uint32Array(maxSearchArea);
|
||||
this.cameFrom = new Int32Array(maxSearchArea);
|
||||
|
||||
const maxDim = Math.ceil(Math.sqrt(maxSearchArea));
|
||||
// Account for scaled costs + tie-breaker headroom
|
||||
const maxF =
|
||||
(this.heuristicWeight + 1) * BASE_COST * maxDim * 2 + COST_SCALE * maxDim;
|
||||
this.queue = new BucketQueue(maxF);
|
||||
this.queue = new MinHeap(maxSearchArea * 4);
|
||||
}
|
||||
|
||||
findPath(start: number | number[], goal: number): number[] | null {
|
||||
@@ -209,6 +205,8 @@ export class AStarWaterBounded implements PathFinder<number> {
|
||||
closedStamp[neighborLocal] !== stamp &&
|
||||
(neighbor === goal || (neighborTerrain & landMask) === 0)
|
||||
) {
|
||||
const ny = currentY - 1;
|
||||
const distToGoal = Math.abs(currentX - goalX) + Math.abs(ny - goalY);
|
||||
const magnitude = neighborTerrain & MAGNITUDE_MASK;
|
||||
const cost = BASE_COST + getMagnitudePenalty(magnitude);
|
||||
const tentativeG = currentG + cost;
|
||||
@@ -219,11 +217,7 @@ export class AStarWaterBounded implements PathFinder<number> {
|
||||
cameFrom[neighborLocal] = currentLocal;
|
||||
gScore[neighborLocal] = tentativeG;
|
||||
gScoreStamp[neighborLocal] = stamp;
|
||||
const ny = currentY - 1;
|
||||
const h =
|
||||
weight *
|
||||
BASE_COST *
|
||||
(Math.abs(currentX - goalX) + Math.abs(ny - goalY));
|
||||
const h = weight * BASE_COST * distToGoal;
|
||||
const f = tentativeG + h + crossTieBreaker(currentX, ny);
|
||||
queue.push(neighborLocal, f);
|
||||
}
|
||||
@@ -238,6 +232,8 @@ export class AStarWaterBounded implements PathFinder<number> {
|
||||
closedStamp[neighborLocal] !== stamp &&
|
||||
(neighbor === goal || (neighborTerrain & landMask) === 0)
|
||||
) {
|
||||
const ny = currentY + 1;
|
||||
const distToGoal = Math.abs(currentX - goalX) + Math.abs(ny - goalY);
|
||||
const magnitude = neighborTerrain & MAGNITUDE_MASK;
|
||||
const cost = BASE_COST + getMagnitudePenalty(magnitude);
|
||||
const tentativeG = currentG + cost;
|
||||
@@ -248,11 +244,7 @@ export class AStarWaterBounded implements PathFinder<number> {
|
||||
cameFrom[neighborLocal] = currentLocal;
|
||||
gScore[neighborLocal] = tentativeG;
|
||||
gScoreStamp[neighborLocal] = stamp;
|
||||
const ny = currentY + 1;
|
||||
const h =
|
||||
weight *
|
||||
BASE_COST *
|
||||
(Math.abs(currentX - goalX) + Math.abs(ny - goalY));
|
||||
const h = weight * BASE_COST * distToGoal;
|
||||
const f = tentativeG + h + crossTieBreaker(currentX, ny);
|
||||
queue.push(neighborLocal, f);
|
||||
}
|
||||
@@ -267,6 +259,8 @@ export class AStarWaterBounded implements PathFinder<number> {
|
||||
closedStamp[neighborLocal] !== stamp &&
|
||||
(neighbor === goal || (neighborTerrain & landMask) === 0)
|
||||
) {
|
||||
const nx = currentX - 1;
|
||||
const distToGoal = Math.abs(nx - goalX) + Math.abs(currentY - goalY);
|
||||
const magnitude = neighborTerrain & MAGNITUDE_MASK;
|
||||
const cost = BASE_COST + getMagnitudePenalty(magnitude);
|
||||
const tentativeG = currentG + cost;
|
||||
@@ -277,11 +271,7 @@ export class AStarWaterBounded implements PathFinder<number> {
|
||||
cameFrom[neighborLocal] = currentLocal;
|
||||
gScore[neighborLocal] = tentativeG;
|
||||
gScoreStamp[neighborLocal] = stamp;
|
||||
const nx = currentX - 1;
|
||||
const h =
|
||||
weight *
|
||||
BASE_COST *
|
||||
(Math.abs(nx - goalX) + Math.abs(currentY - goalY));
|
||||
const h = weight * BASE_COST * distToGoal;
|
||||
const f = tentativeG + h + crossTieBreaker(nx, currentY);
|
||||
queue.push(neighborLocal, f);
|
||||
}
|
||||
@@ -296,6 +286,8 @@ export class AStarWaterBounded implements PathFinder<number> {
|
||||
closedStamp[neighborLocal] !== stamp &&
|
||||
(neighbor === goal || (neighborTerrain & landMask) === 0)
|
||||
) {
|
||||
const nx = currentX + 1;
|
||||
const distToGoal = Math.abs(nx - goalX) + Math.abs(currentY - goalY);
|
||||
const magnitude = neighborTerrain & MAGNITUDE_MASK;
|
||||
const cost = BASE_COST + getMagnitudePenalty(magnitude);
|
||||
const tentativeG = currentG + cost;
|
||||
@@ -306,11 +298,7 @@ export class AStarWaterBounded implements PathFinder<number> {
|
||||
cameFrom[neighborLocal] = currentLocal;
|
||||
gScore[neighborLocal] = tentativeG;
|
||||
gScoreStamp[neighborLocal] = stamp;
|
||||
const nx = currentX + 1;
|
||||
const h =
|
||||
weight *
|
||||
BASE_COST *
|
||||
(Math.abs(nx - goalX) + Math.abs(currentY - goalY));
|
||||
const h = weight * BASE_COST * distToGoal;
|
||||
const f = tentativeG + h + crossTieBreaker(nx, currentY);
|
||||
queue.push(neighborLocal, f);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ export class AStarWaterHierarchical implements PathFinder<number> {
|
||||
private abstractAStar: AbstractGraphAStar;
|
||||
private localAStar: AStarWaterBounded;
|
||||
private localAStarMultiCluster: AStarWaterBounded;
|
||||
private localAStarShortPath: AStarWaterBounded;
|
||||
private sourceResolver: SourceResolver;
|
||||
|
||||
constructor(
|
||||
@@ -41,6 +42,11 @@ export class AStarWaterHierarchical implements PathFinder<number> {
|
||||
maxMultiClusterNodes,
|
||||
);
|
||||
|
||||
// BoundedAStar for short path multi-source (120 + 2*10 padding = 140)
|
||||
const shortPathSize = 140;
|
||||
const maxShortPathNodes = shortPathSize * shortPathSize;
|
||||
this.localAStarShortPath = new AStarWaterBounded(map, maxShortPathNodes);
|
||||
|
||||
// SourceResolver for multi-source search
|
||||
this.sourceResolver = new SourceResolver(this.map, this.graph);
|
||||
}
|
||||
@@ -62,6 +68,10 @@ export class AStarWaterHierarchical implements PathFinder<number> {
|
||||
sources: TileRef[],
|
||||
target: TileRef,
|
||||
): TileRef[] | null {
|
||||
// Early exit: try bounded A* for sources close to target
|
||||
const shortPath = this.tryShortPathMultiSource(sources, target);
|
||||
if (shortPath) return shortPath;
|
||||
|
||||
// 1. Resolve target to abstract node
|
||||
const targetNode = this.sourceResolver.resolveTarget(target);
|
||||
if (!targetNode) return null;
|
||||
@@ -82,6 +92,44 @@ export class AStarWaterHierarchical implements PathFinder<number> {
|
||||
return this.findPathSingle(winningSource, target);
|
||||
}
|
||||
|
||||
private tryShortPathMultiSource(
|
||||
sources: TileRef[],
|
||||
target: TileRef,
|
||||
): TileRef[] | null {
|
||||
const SHORT_PATH_THRESHOLD = 120;
|
||||
const PADDING = 10;
|
||||
|
||||
const candidates = sources.filter(
|
||||
(s) => this.map.manhattanDist(s, target) <= SHORT_PATH_THRESHOLD,
|
||||
);
|
||||
if (candidates.length === 0) return null;
|
||||
|
||||
const toX = this.map.x(target);
|
||||
const toY = this.map.y(target);
|
||||
let minX = toX,
|
||||
maxX = toX,
|
||||
minY = toY,
|
||||
maxY = toY;
|
||||
|
||||
for (const s of candidates) {
|
||||
const sx = this.map.x(s);
|
||||
const sy = this.map.y(s);
|
||||
minX = Math.min(minX, sx);
|
||||
maxX = Math.max(maxX, sx);
|
||||
minY = Math.min(minY, sy);
|
||||
maxY = Math.max(maxY, sy);
|
||||
}
|
||||
|
||||
const bounds = {
|
||||
minX: Math.max(0, minX - PADDING),
|
||||
maxX: Math.min(this.map.width() - 1, maxX + PADDING),
|
||||
minY: Math.max(0, minY - PADDING),
|
||||
maxY: Math.min(this.map.height() - 1, maxY + PADDING),
|
||||
};
|
||||
|
||||
return this.localAStarShortPath.searchBounded(candidates, target, bounds);
|
||||
}
|
||||
|
||||
findPathSingle(from: TileRef, to: TileRef): TileRef[] | null {
|
||||
const dist = this.map.manhattanDist(from, to);
|
||||
|
||||
|
||||
@@ -94,6 +94,8 @@ export class MinHeap implements PriorityQueue {
|
||||
export class BucketQueue implements PriorityQueue {
|
||||
private buckets: Int32Array[];
|
||||
private bucketSizes: Int32Array;
|
||||
private bucketStamp: Uint32Array;
|
||||
private stamp = 0;
|
||||
private minBucket: number;
|
||||
private maxBucket: number;
|
||||
private size: number;
|
||||
@@ -102,6 +104,7 @@ export class BucketQueue implements PriorityQueue {
|
||||
this.maxBucket = maxPriority + 1;
|
||||
this.buckets = new Array(this.maxBucket);
|
||||
this.bucketSizes = new Int32Array(this.maxBucket);
|
||||
this.bucketStamp = new Uint32Array(this.maxBucket);
|
||||
this.minBucket = this.maxBucket;
|
||||
this.size = 0;
|
||||
}
|
||||
@@ -113,7 +116,9 @@ export class BucketQueue implements PriorityQueue {
|
||||
this.buckets[bucket] = new Int32Array(64);
|
||||
}
|
||||
|
||||
const size = this.bucketSizes[bucket];
|
||||
const size =
|
||||
this.bucketStamp[bucket] === this.stamp ? this.bucketSizes[bucket] : 0;
|
||||
|
||||
if (size >= this.buckets[bucket].length) {
|
||||
const newBucket = new Int32Array(this.buckets[bucket].length * 2);
|
||||
newBucket.set(this.buckets[bucket]);
|
||||
@@ -121,7 +126,8 @@ export class BucketQueue implements PriorityQueue {
|
||||
}
|
||||
|
||||
this.buckets[bucket][size] = node;
|
||||
this.bucketSizes[bucket]++;
|
||||
this.bucketSizes[bucket] = size + 1;
|
||||
this.bucketStamp[bucket] = this.stamp;
|
||||
this.size++;
|
||||
|
||||
if (bucket < this.minBucket) {
|
||||
@@ -131,11 +137,13 @@ export class BucketQueue implements PriorityQueue {
|
||||
|
||||
pop(): number {
|
||||
while (this.minBucket < this.maxBucket) {
|
||||
const size = this.bucketSizes[this.minBucket];
|
||||
if (size > 0) {
|
||||
this.bucketSizes[this.minBucket]--;
|
||||
this.size--;
|
||||
return this.buckets[this.minBucket][size - 1];
|
||||
if (this.bucketStamp[this.minBucket] === this.stamp) {
|
||||
const size = this.bucketSizes[this.minBucket];
|
||||
if (size > 0) {
|
||||
this.bucketSizes[this.minBucket]--;
|
||||
this.size--;
|
||||
return this.buckets[this.minBucket][size - 1];
|
||||
}
|
||||
}
|
||||
this.minBucket++;
|
||||
}
|
||||
@@ -147,7 +155,11 @@ export class BucketQueue implements PriorityQueue {
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.bucketSizes.fill(0);
|
||||
this.stamp++;
|
||||
if (this.stamp > 0xffffffff) {
|
||||
this.bucketStamp.fill(0);
|
||||
this.stamp = 1;
|
||||
}
|
||||
this.minBucket = this.maxBucket;
|
||||
this.size = 0;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,27 @@
|
||||
import { Game, Player, TerraNullius } from "../../game/Game";
|
||||
import { TileRef } from "../../game/GameMap";
|
||||
import { DebugSpan } from "../../utilities/DebugSpan";
|
||||
import { PathFinding } from "../PathFinder";
|
||||
import { AStarWaterBounded } from "../algorithms/AStar.WaterBounded";
|
||||
|
||||
type Owner = Player | TerraNullius;
|
||||
|
||||
const REFINE_MAX_SEARCH_AREA = 100 * 100;
|
||||
|
||||
export class SpatialQuery {
|
||||
private boundedAStar: AStarWaterBounded | null = null;
|
||||
|
||||
constructor(private game: Game) {}
|
||||
|
||||
private getBoundedAStar(): AStarWaterBounded {
|
||||
this.boundedAStar ??= new AStarWaterBounded(
|
||||
this.game.map(),
|
||||
REFINE_MAX_SEARCH_AREA,
|
||||
);
|
||||
|
||||
return this.boundedAStar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find nearest tile matching predicate using BFS traversal.
|
||||
* Uses Manhattan distance filter, ignores terrain barriers.
|
||||
@@ -64,30 +79,34 @@ export class SpatialQuery {
|
||||
* Returns null for terra nullius (no borderTiles).
|
||||
*/
|
||||
closestShoreByWater(owner: Owner, target: TileRef): TileRef | null {
|
||||
if (!owner.isPlayer()) return null;
|
||||
return DebugSpan.wrap("SpatialQuery.closestShoreByWater", () => {
|
||||
if (!owner.isPlayer()) return null;
|
||||
|
||||
const gm = this.game;
|
||||
const player = owner as Player;
|
||||
const gm = this.game;
|
||||
const player = owner as Player;
|
||||
|
||||
// Target must be water or shore (land adjacent to water)
|
||||
if (!gm.isWater(target) && !gm.isShore(target)) return null;
|
||||
// Target must be water or shore (land adjacent to water)
|
||||
if (!gm.isWater(target) && !gm.isShore(target)) return null;
|
||||
|
||||
const targetComponent = gm.getWaterComponent(target);
|
||||
if (targetComponent === null) return null;
|
||||
const targetComponent = gm.getWaterComponent(target);
|
||||
if (targetComponent === null) return null;
|
||||
|
||||
const isValidTile = (t: TileRef) => {
|
||||
if (!gm.isShore(t) || !gm.isLand(t)) return false;
|
||||
const tComponent = gm.getWaterComponent(t);
|
||||
return tComponent === targetComponent;
|
||||
};
|
||||
const isValidTile = (t: TileRef) => {
|
||||
if (!gm.isShore(t) || !gm.isLand(t)) return false;
|
||||
const tComponent = gm.getWaterComponent(t);
|
||||
return tComponent === targetComponent;
|
||||
};
|
||||
|
||||
const shores = Array.from(player.borderTiles()).filter(isValidTile);
|
||||
if (shores.length === 0) return null;
|
||||
const shores = Array.from(player.borderTiles()).filter(isValidTile);
|
||||
if (shores.length === 0) return null;
|
||||
|
||||
const path = PathFinding.Water(gm).findPath(shores, target);
|
||||
if (!path || path.length === 0) return null;
|
||||
const path = PathFinding.Water(gm).findPath(shores, target);
|
||||
if (!path || path.length === 0) return null;
|
||||
|
||||
return this.refineStartTile(path, shores, gm);
|
||||
return DebugSpan.wrap("SpatialQuery.refineStartTile", () =>
|
||||
this.refineStartTile(path, shores, gm),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private refineStartTile(
|
||||
@@ -95,8 +114,10 @@ export class SpatialQuery {
|
||||
shores: TileRef[],
|
||||
gm: Game,
|
||||
): TileRef {
|
||||
const CANDIDATE_RADIUS = 10;
|
||||
const WAYPOINT_DIST = 20;
|
||||
const CANDIDATE_RADIUS = 20;
|
||||
const MIN_WAYPOINT_DIST = 50;
|
||||
const MAX_WAYPOINT_DIST = 200;
|
||||
const PADDING = 10;
|
||||
|
||||
const bestTile = path[0];
|
||||
const map = gm.map();
|
||||
@@ -107,13 +128,72 @@ export class SpatialQuery {
|
||||
|
||||
if (candidates.length <= 1) return bestTile;
|
||||
|
||||
const waypointIdx = Math.min(WAYPOINT_DIST, path.length - 1);
|
||||
const waypoint = path[waypointIdx];
|
||||
// Precompute candidate bounds
|
||||
let candMinX = map.x(candidates[0]);
|
||||
let candMaxX = candMinX;
|
||||
let candMinY = map.y(candidates[0]);
|
||||
let candMaxY = candMinY;
|
||||
|
||||
const refinedPath = PathFinding.WaterSimple(gm).findPath(
|
||||
for (let i = 1; i < candidates.length; i++) {
|
||||
const sx = map.x(candidates[i]);
|
||||
const sy = map.y(candidates[i]);
|
||||
candMinX = Math.min(candMinX, sx);
|
||||
candMaxX = Math.max(candMaxX, sx);
|
||||
candMinY = Math.min(candMinY, sy);
|
||||
candMaxY = Math.max(candMaxY, sy);
|
||||
}
|
||||
|
||||
// Binary search for furthest waypoint that keeps bounds within limit
|
||||
let lo = MIN_WAYPOINT_DIST;
|
||||
let hi = Math.min(MAX_WAYPOINT_DIST, path.length - 1);
|
||||
let bestWaypointIdx = lo;
|
||||
|
||||
for (let i = 0; i < 5 && lo <= hi; i++) {
|
||||
const mid = (lo + hi) >> 1;
|
||||
const wp = path[mid];
|
||||
const wpX = map.x(wp);
|
||||
const wpY = map.y(wp);
|
||||
|
||||
const minX = Math.min(candMinX, wpX) - PADDING;
|
||||
const maxX = Math.max(candMaxX, wpX) + PADDING;
|
||||
const minY = Math.min(candMinY, wpY) - PADDING;
|
||||
const maxY = Math.max(candMaxY, wpY) + PADDING;
|
||||
|
||||
const area = (maxX - minX + 1) * (maxY - minY + 1);
|
||||
if (area <= REFINE_MAX_SEARCH_AREA) {
|
||||
bestWaypointIdx = mid;
|
||||
lo = mid + 1;
|
||||
} else {
|
||||
hi = mid - 1;
|
||||
}
|
||||
}
|
||||
|
||||
const waypoint = path[bestWaypointIdx];
|
||||
const wpX = map.x(waypoint);
|
||||
const wpY = map.y(waypoint);
|
||||
|
||||
const bounds = {
|
||||
minX: Math.max(0, Math.min(candMinX, wpX) - PADDING),
|
||||
maxX: Math.min(map.width() - 1, Math.max(candMaxX, wpX) + PADDING),
|
||||
minY: Math.max(0, Math.min(candMinY, wpY) - PADDING),
|
||||
maxY: Math.min(map.height() - 1, Math.max(candMaxY, wpY) + PADDING),
|
||||
};
|
||||
|
||||
const boundsArea =
|
||||
(bounds.maxX - bounds.minX + 1) * (bounds.maxY - bounds.minY + 1);
|
||||
if (boundsArea > REFINE_MAX_SEARCH_AREA) return bestTile;
|
||||
|
||||
const refinedPath = this.getBoundedAStar().searchBounded(
|
||||
candidates,
|
||||
waypoint,
|
||||
bounds,
|
||||
);
|
||||
|
||||
DebugSpan.set("$candidates", () => candidates);
|
||||
DebugSpan.set("$refinedPath", () => refinedPath);
|
||||
DebugSpan.set("$originalBestTile", () => bestTile);
|
||||
DebugSpan.set("$newBestTile", () => refinedPath?.[0] ?? bestTile);
|
||||
|
||||
return refinedPath?.[0] ?? bestTile;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,9 @@ export class SmoothingWaterTransformer implements PathFinder<TileRef> {
|
||||
this.refineEndpoints(smoothed),
|
||||
);
|
||||
|
||||
// Pass 3: LOS smoothing again (refinement may create new shortcut opportunities)
|
||||
smoothed = DebugSpan.wrap("smoother:los2", () => this.losSmooth(smoothed));
|
||||
|
||||
return smoothed;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user