Add local corridor widening for adaptive pathfinding

- Introduced a new approach to local corridor widening in the `CoarseToFineWaterPath` module, allowing for more efficient pathfinding by expanding the search corridor based on visited coarse regions.
- Implemented a mechanism to avoid global radius increases, enhancing performance and reducing unnecessary searches.
- Updated `MultiSourceAnyTargetBFS` to support marking visited coarse regions during BFS, facilitating the local widening process.
- Adjusted parameters for maximum attempts and corridor radius to optimize pathfinding behavior.
This commit is contained in:
scamiv
2025-12-27 19:49:34 +01:00
parent 368f5c5900
commit aa09240d40
3 changed files with 225 additions and 33 deletions
+78 -16
View File
@@ -91,6 +91,16 @@ function getStampSet(gm: GameMap): StampSet {
stampSetCache.set(gm, set);
return set;
}
// Separate stamp array for "visited coarse regions" marking to avoid clobbering the allowed corridor stamp.
const visitedStampSetCache = new WeakMap<GameMap, StampSet>();
function getVisitedStampSet(gm: GameMap): StampSet {
const cached = visitedStampSetCache.get(gm);
if (cached) return cached;
const set: StampSet = { stamp: 1, data: new Uint32Array(gm.width() * gm.height()) };
visitedStampSetCache.set(gm, set);
return set;
}
function nextStamp(set: StampSet): number {
const next = (set.stamp + 1) >>> 0;
set.stamp = next === 0 ? 1 : next;
@@ -137,6 +147,38 @@ function markCoarseCorridor(
}
}
function widenAllowedByVisitedRing(
coarseWidth: number,
coarseHeight: number,
allowedStamp: Uint32Array,
allowed: number,
visitedStamp: Uint32Array,
visited: number,
): boolean {
let widened = false;
for (let y = 0; y < coarseHeight; y++) {
const row = y * coarseWidth;
for (let x = 0; x < coarseWidth; x++) {
const idx = row + x;
if (visitedStamp[idx] !== visited) continue;
const y0 = Math.max(0, y - 1);
const y1 = Math.min(coarseHeight - 1, y + 1);
const x0 = Math.max(0, x - 1);
const x1 = Math.min(coarseWidth - 1, x + 1);
for (let yy = y0; yy <= y1; yy++) {
const nRow = yy * coarseWidth;
for (let xx = x0; xx <= x1; xx++) {
const n = nRow + xx;
if (allowedStamp[n] === allowed) continue;
allowedStamp[n] = allowed;
widened = true;
}
}
}
}
return widened;
}
export function findWaterPathFromSeedsCoarseToFine(
fineMap: GameMap,
seedNodes: readonly TileRef[],
@@ -228,20 +270,23 @@ export function findWaterPathFromSeedsCoarseToFine(
}
const corridorRadius0 = Math.max(0, coarseToFine.corridorRadius ?? 2);
const maxAttempts = Math.max(1, coarseToFine.maxAttempts ?? 2);
const radiusMultiplier = Math.max(1, coarseToFine.radiusMultiplier ?? 2);
const maxAttempts = Math.max(1, coarseToFine.maxAttempts ?? 3);
const corridorSet = getStampSet(coarseMap);
for (let attempt = 0, radius = corridorRadius0; attempt < maxAttempts; attempt++) {
const corridorStamp = nextStamp(corridorSet);
markCoarseCorridor(
coarseWidth,
coarseHeight,
corridorSet.data,
corridorStamp,
coarseResult.path,
radius,
);
// Allowed corridor stamp is stable across attempts (widening is cumulative).
const allowedSet = getStampSet(coarseMap);
const allowed = nextStamp(allowedSet);
markCoarseCorridor(
coarseWidth,
coarseHeight,
allowedSet.data,
allowed,
coarseResult.path,
corridorRadius0,
);
const visitedSet = getVisitedStampSet(coarseMap);
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const visited = nextStamp(visitedSet);
const refined = fineBfs.findWaterPathFromSeeds(
fineMap,
@@ -252,14 +297,31 @@ export function findWaterPathFromSeedsCoarseToFine(
...bfsOpts,
allowedMask: {
tileToRegion: mapping.fineToCoarse,
regionStamp: corridorSet.data,
stamp: corridorStamp,
regionStamp: allowedSet.data,
stamp: allowed,
},
visitedMaskOut: {
tileToRegion: mapping.fineToCoarse,
regionStamp: visitedSet.data,
stamp: visited,
},
},
);
if (refined !== null) return refined;
radius *= radiusMultiplier;
if (attempt === maxAttempts - 1) break;
// Local corridor widening: expand by 1 ring around the coarse regions actually visited
// in this failed attempt. Widening is cumulative (newly allowed regions stay allowed).
const widened = widenAllowedByVisitedRing(
coarseWidth,
coarseHeight,
allowedSet.data,
allowed,
visitedSet.data,
visited,
);
if (!widened) break;
}
// Final fallback: unrestricted fine BFS.
+40 -17
View File
@@ -20,6 +20,17 @@ export type MultiSourceAnyTargetBFSOptions = {
regionStamp: Uint32Array;
stamp: number;
};
/**
* Optional region marking output.
*
* Intended for local corridor widening: during BFS, mark which coarse regions were
* actually visited (cheap stamp write, allocation-free).
*/
visitedMaskOut?: {
tileToRegion: Uint32Array;
regionStamp: Uint32Array;
stamp: number;
};
};
/**
@@ -79,6 +90,7 @@ export class MultiSourceAnyTargetBFS {
let tail = 0;
const allowed = opts.allowedMask;
const visitedOut = opts.visitedMaskOut;
const count = Math.min(seedNodes.length, seedOrigins.length);
for (let i = 0; i < count; i++) {
@@ -96,6 +108,9 @@ export class MultiSourceAnyTargetBFS {
this.visitedStamp[node] = stamp;
this.prev[node] = -1;
this.startOf[node] = origin;
if (visitedOut) {
visitedOut.regionStamp[visitedOut.tileToRegion[node]!] = visitedOut.stamp;
}
this.queue[tail++] = node;
}
@@ -127,8 +142,8 @@ export class MultiSourceAnyTargetBFS {
) {
// skip
} else {
this.visit(n, node, stamp);
this.queue[tail++] = n;
this.visit(n, node, stamp, visitedOut);
this.queue[tail++] = n;
}
}
}
@@ -141,8 +156,8 @@ export class MultiSourceAnyTargetBFS {
) {
// skip
} else {
this.visit(s, node, stamp);
this.queue[tail++] = s;
this.visit(s, node, stamp, visitedOut);
this.queue[tail++] = s;
}
}
}
@@ -155,8 +170,8 @@ export class MultiSourceAnyTargetBFS {
) {
// skip
} else {
this.visit(wv, node, stamp);
this.queue[tail++] = wv;
this.visit(wv, node, stamp, visitedOut);
this.queue[tail++] = wv;
}
}
}
@@ -169,8 +184,8 @@ export class MultiSourceAnyTargetBFS {
) {
// skip
} else {
this.visit(ev, node, stamp);
this.queue[tail++] = ev;
this.visit(ev, node, stamp, visitedOut);
this.queue[tail++] = ev;
}
}
}
@@ -191,8 +206,8 @@ export class MultiSourceAnyTargetBFS {
) {
// skip
} else {
this.visit(nw, node, stamp);
this.queue[tail++] = nw;
this.visit(nw, node, stamp, visitedOut);
this.queue[tail++] = nw;
}
}
}
@@ -209,8 +224,8 @@ export class MultiSourceAnyTargetBFS {
) {
// skip
} else {
this.visit(ne, node, stamp);
this.queue[tail++] = ne;
this.visit(ne, node, stamp, visitedOut);
this.queue[tail++] = ne;
}
}
}
@@ -227,8 +242,8 @@ export class MultiSourceAnyTargetBFS {
) {
// skip
} else {
this.visit(sw, node, stamp);
this.queue[tail++] = sw;
this.visit(sw, node, stamp, visitedOut);
this.queue[tail++] = sw;
}
}
}
@@ -245,8 +260,8 @@ export class MultiSourceAnyTargetBFS {
) {
// skip
} else {
this.visit(se, node, stamp);
this.queue[tail++] = se;
this.visit(se, node, stamp, visitedOut);
this.queue[tail++] = se;
}
}
}
@@ -255,10 +270,18 @@ export class MultiSourceAnyTargetBFS {
return null;
}
private visit(node: TileRef, from: TileRef, stamp: number) {
private visit(
node: TileRef,
from: TileRef,
stamp: number,
visitedOut: MultiSourceAnyTargetBFSOptions["visitedMaskOut"],
) {
this.visitedStamp[node] = stamp;
this.prev[node] = from;
this.startOf[node] = this.startOf[from];
if (visitedOut) {
visitedOut.regionStamp[visitedOut.tileToRegion[node]!] = visitedOut.stamp;
}
}
private reconstructPath(target: TileRef): TileRef[] {