mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-28 20:44:17 +00:00
Add mask-expanding for adaptive corridor refinement
- Introduced a new `MaskExpanding.md` documentation detailing the mask-expanding BFS approach, which enhances pathfinding by allowing corridor expansion without restarting the search. - Updated `CoarseToFineWaterPath` to utilize the new mask-expanding strategy, improving efficiency by resuming searches instead of clearing state upon corridor tightness. - Enhanced `MultiSourceAnyTargetBFS` with a new method to support mask expansion, allowing for dynamic adjustment of allowed regions during BFS execution. - Implemented data structures and core loop adjustments to facilitate both fast and optimal variants of the mask-expanding approach, ensuring soundness and performance improvements in pathfinding. - Suggested milestones for future enhancements and optimizations in corridor repair and pathfinding strategies.
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
# Mask-expanding BFS (adaptive corridor refinement, no restart)
|
||||
|
||||
Purpose: keep the “coarse corridor” win, but avoid repeated **restart + re-walk** churn when the corridor is too tight.
|
||||
|
||||
This is the “performance-first” sibling of:
|
||||
- `docs/CoarseToFine.md` (coarse corridor + safe fallback)
|
||||
- `docs/LocalCorridorWidening.md` (visited-driven local relaxation)
|
||||
|
||||
Key idea: run one fine-res search; if the queue exhausts because the corridor is too restrictive, **expand the mask and keep going** without clearing the fine BFS state.
|
||||
|
||||
## What changes (vs restart-based local widening)
|
||||
|
||||
Today (A2-style):
|
||||
- attempt = run fine BFS inside current mask
|
||||
- on failure: widen mask, **restart** fine BFS (visited/prev cleared via new stamp)
|
||||
|
||||
No-restart:
|
||||
- run fine BFS inside current mask
|
||||
- on queue empty: widen mask, **resume** fine BFS (keep visited/prev/queue state)
|
||||
|
||||
This avoids re-enqueueing and re-walking large already-explored areas on “almost works” corridors.
|
||||
|
||||
## Correctness note (don’t hand-wave)
|
||||
|
||||
Naively resuming a FIFO BFS after expanding the allowed set can change shortest-path guarantees, because newly-allowed tiles might introduce shorter routes to areas you already visited.
|
||||
|
||||
Invariant: once a fine tile is marked visited, it is never “unvisited” again — mask expansion only enables additional neighbors/regions and never invalidates already-visited tiles. This is why the fast variant remains sound (valid path) and why we can justify not clearing `visitedStamp` when expanding the mask.
|
||||
|
||||
Two viable interpretations:
|
||||
|
||||
1) **Fast variant (good enough for corridor repair):**
|
||||
- accept that expanding the mask mid-run can produce a path that is not strictly shortest in the *final* expanded region
|
||||
- still produces a valid path and is often much faster
|
||||
|
||||
2) **Optimal variant (paper-grade):**
|
||||
- track `dist[tile]` (or level) and allow “relaxation” when new tiles become allowed
|
||||
- process newly enabled nodes in non-decreasing distance (Dial/bucket queue or heap)
|
||||
- guarantees shortest path in the final allowed region
|
||||
|
||||
For OpenFront boats, the fast variant may already be acceptable because the corridor is a heuristic bound anyway; if we care about strict optimality, use the optimal variant.
|
||||
|
||||
This doc describes the implementation in a way that supports either, with minimal extra plumbing.
|
||||
|
||||
## Groundwork we already have
|
||||
|
||||
From existing coarse-to-fine:
|
||||
- `fineToCoarse: Uint32Array` mapping
|
||||
- `allowedMask` as `(tileToRegion, regionStamp, stamp)` using stamps
|
||||
|
||||
From `LocalCorridorWidening` implementation:
|
||||
- `visitedMaskOut` to collect “which coarse regions were actually explored” in a failed attempt
|
||||
- widening by 1-ring around visited coarse regions, cumulative
|
||||
|
||||
We reuse that exact widening rule; the only change is: don’t restart the fine search.
|
||||
|
||||
## Data structures (recommended)
|
||||
|
||||
Inside `MultiSourceAnyTargetBFS` (or a sibling specialized class):
|
||||
- `visitedStamp[tile]` (already exists)
|
||||
- `prev[tile]` (already exists)
|
||||
- `startOf[tile]` (already exists)
|
||||
- `queue[]`, `head`, `tail` (already exists)
|
||||
|
||||
Additionally for the optimal variant:
|
||||
- `dist[tile]: Int32Array` (init -1; set on visit)
|
||||
- `deferred[]` or a bucket/heap for nodes that become enabled later
|
||||
|
||||
Mask tracking:
|
||||
- `allowedCoarseStamp[coarseCell]` (cumulative allowed regions)
|
||||
- `visitedCoarseStamp[coarseCell]` (per-expansion snapshot; used to widen)
|
||||
|
||||
## Core loop (fast variant)
|
||||
|
||||
Pseudocode:
|
||||
|
||||
1) Build initial allowed mask from coarse spine corridor (`r0`).
|
||||
2) Seed BFS queue from fine seedNodes filtered by allowed mask.
|
||||
3) Run BFS:
|
||||
- when exploring neighbors:
|
||||
- if neighbor is blocked by allowed mask: **skip** (but see below)
|
||||
- otherwise visit/enqueue as usual
|
||||
4) When `head == tail` (queue empty):
|
||||
- widen `allowedCoarseStamp` by 1 ring around `visitedCoarseStamp` from the last phase
|
||||
- **activate newly enabled frontier nodes**:
|
||||
- for each visited fine tile, re-check its neighbors that were previously mask-blocked
|
||||
- enqueue any newly-allowed, unvisited neighbors
|
||||
- continue BFS
|
||||
5) Stop when a target is dequeued.
|
||||
|
||||
The only missing piece is “activate newly enabled frontier nodes” efficiently.
|
||||
|
||||
### Frontier activation strategies
|
||||
|
||||
**Strategy F1 (simple, may be OK):**
|
||||
- keep an `Int32Array deferredTiles` of “neighbor candidates that were blocked by mask”
|
||||
- when mask widens, scan deferredTiles and enqueue those that are now allowed
|
||||
- keep deferredTiles deduped via a stamp array to avoid blowup
|
||||
|
||||
**Strategy F2 (faster, more code):**
|
||||
- maintain a per-coarse-region list of deferred fine tiles
|
||||
- when a coarse region becomes allowed, enqueue only tiles in that region’s list
|
||||
|
||||
F2 is the “hot path” answer; F1 is the “get it working + measure” answer.
|
||||
|
||||
## Core loop (optimal variant)
|
||||
|
||||
If we want shortest paths in the final expanded region, treat mask-expansion as adding nodes that can introduce shorter routes.
|
||||
|
||||
Minimal way:
|
||||
- maintain `dist[tile]`
|
||||
- when a neighbor becomes newly allowed:
|
||||
- `nd = dist[cur] + 1`
|
||||
- if `dist[neighbor] == -1 || nd < dist[neighbor]`:
|
||||
- update `dist`, `prev`, `startOf`
|
||||
- push neighbor into a structure processed in increasing `dist`
|
||||
|
||||
Implementation options:
|
||||
- bucket queue (Dial) since edge weights are 1
|
||||
- binary heap (slower constants, simpler reasoning)
|
||||
|
||||
## Where this plugs in
|
||||
|
||||
Implement as a variant of the current coarse-to-fine helper:
|
||||
|
||||
- `findWaterPathFromSeedsMaskExpanding(...)` (fineMap + coarseMap + opts)
|
||||
- reuse the same `allowedMask` and the same visited-driven widening rule
|
||||
- keep the same guardrail: if expansions exceed `k`, fall back to unrestricted fine BFS
|
||||
|
||||
This is a stepping stone to Spine & Portals:
|
||||
- This improves “corridor repair” without restarts
|
||||
- Spine & Portals changes the big-O by avoiding fine search over open water entirely
|
||||
|
||||
## Suggested milestones
|
||||
|
||||
1) Implement the fast variant with deferred frontier list (F1) and measure.
|
||||
2) If it helps but still shows spikes, upgrade frontier activation to F2.
|
||||
3) If strict optimality is needed, implement the optimal variant (dist + buckets/heap).
|
||||
|
||||
## Note: depth-gated BFS (future)
|
||||
|
||||
We can reuse the same “monotonic relaxation” idea for “prefer deep water” by adding a second passability mask like `gm.magnitude(tile) >= minDepth` and relaxing `minDepth` only if needed. This stays BFS-friendly (still unweighted), but changes the objective to “deepest-possible path, then shortest within that depth”; if we need strict shortest for the relaxed threshold, restart per-threshold or use the optimal semantics.
|
||||
|
||||
## BSP-ish note
|
||||
|
||||
Both mask expansion and depth-gating are “BSP-ish” in the same sense: they incrementally relax constraints (expand the allowed subset) without invalidating already explored space. This makes the search behave more like progressive partition refinement than a single global floodfill, even though we are not literally constructing a BSP tree.
|
||||
@@ -154,8 +154,9 @@ function widenAllowedByVisitedRing(
|
||||
allowed: number,
|
||||
visitedStamp: Uint32Array,
|
||||
visited: number,
|
||||
): boolean {
|
||||
let widened = false;
|
||||
outNewlyAllowed: Int32Array,
|
||||
): number {
|
||||
let count = 0;
|
||||
for (let y = 0; y < coarseHeight; y++) {
|
||||
const row = y * coarseWidth;
|
||||
for (let x = 0; x < coarseWidth; x++) {
|
||||
@@ -171,12 +172,12 @@ function widenAllowedByVisitedRing(
|
||||
const n = nRow + xx;
|
||||
if (allowedStamp[n] === allowed) continue;
|
||||
allowedStamp[n] = allowed;
|
||||
widened = true;
|
||||
outNewlyAllowed[count++] = n;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return widened;
|
||||
return count;
|
||||
}
|
||||
|
||||
export function findWaterPathFromSeedsCoarseToFine(
|
||||
@@ -269,7 +270,8 @@ export function findWaterPathFromSeedsCoarseToFine(
|
||||
);
|
||||
}
|
||||
|
||||
const corridorRadius0 = Math.max(0, coarseToFine.corridorRadius ?? 2);
|
||||
// Start tight (radius 0) and rely on local widening + final fallback for robustness.
|
||||
const corridorRadius0 = Math.max(0, coarseToFine.corridorRadius ?? 0);
|
||||
const maxAttempts = Math.max(1, coarseToFine.maxAttempts ?? 3);
|
||||
|
||||
// Allowed corridor stamp is stable across attempts (widening is cumulative).
|
||||
@@ -285,44 +287,50 @@ export function findWaterPathFromSeedsCoarseToFine(
|
||||
);
|
||||
|
||||
const visitedSet = getVisitedStampSet(coarseMap);
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||
const visited = nextStamp(visitedSet);
|
||||
let expansionsLeft = maxAttempts - 1;
|
||||
const visitedMask = {
|
||||
tileToRegion: mapping.fineToCoarse,
|
||||
regionStamp: visitedSet.data,
|
||||
stamp: nextStamp(visitedSet),
|
||||
};
|
||||
|
||||
const refined = fineBfs.findWaterPathFromSeeds(
|
||||
fineMap,
|
||||
seedNodes,
|
||||
seedOrigins,
|
||||
targets,
|
||||
{
|
||||
...bfsOpts,
|
||||
allowedMask: {
|
||||
tileToRegion: mapping.fineToCoarse,
|
||||
regionStamp: allowedSet.data,
|
||||
stamp: allowed,
|
||||
},
|
||||
visitedMaskOut: {
|
||||
tileToRegion: mapping.fineToCoarse,
|
||||
regionStamp: visitedSet.data,
|
||||
stamp: visited,
|
||||
},
|
||||
const refined = fineBfs.findWaterPathFromSeedsMaskExpanding(
|
||||
fineMap,
|
||||
seedNodes,
|
||||
seedOrigins,
|
||||
targets,
|
||||
{
|
||||
...bfsOpts,
|
||||
allowedMask: {
|
||||
tileToRegion: mapping.fineToCoarse,
|
||||
regionStamp: allowedSet.data,
|
||||
stamp: allowed,
|
||||
},
|
||||
);
|
||||
if (refined !== null) return refined;
|
||||
visitedMaskOut: visitedMask,
|
||||
},
|
||||
(outNewlyAllowed) => {
|
||||
if (expansionsLeft <= 0) return 0;
|
||||
|
||||
if (attempt === maxAttempts - 1) break;
|
||||
// Expand by 1 ring around the coarse regions actually visited in the most recent phase.
|
||||
// Widening is cumulative (newly allowed regions stay allowed).
|
||||
const newCount = widenAllowedByVisitedRing(
|
||||
coarseWidth,
|
||||
coarseHeight,
|
||||
allowedSet.data,
|
||||
allowed,
|
||||
visitedSet.data,
|
||||
visitedMask.stamp,
|
||||
outNewlyAllowed,
|
||||
);
|
||||
expansionsLeft--;
|
||||
if (newCount <= 0) return 0;
|
||||
|
||||
// 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;
|
||||
}
|
||||
// Reset visited coarse tracking for the next phase.
|
||||
visitedMask.stamp = nextStamp(visitedSet);
|
||||
return newCount;
|
||||
},
|
||||
);
|
||||
if (refined !== null) return refined;
|
||||
|
||||
// Final fallback: unrestricted fine BFS.
|
||||
return fineBfs.findWaterPathFromSeeds(
|
||||
|
||||
@@ -48,6 +48,16 @@ export class MultiSourceAnyTargetBFS {
|
||||
private readonly startOf: Int32Array;
|
||||
private readonly queue: Int32Array;
|
||||
|
||||
// Scratch for mask-expanding searches (allocated lazily).
|
||||
private deferredStamp?: Uint32Array;
|
||||
private deferredPrev?: Int32Array;
|
||||
private deferredNext?: Int32Array;
|
||||
private deferredRegionHead?: Int32Array;
|
||||
private deferredRegionTouched?: Int32Array;
|
||||
private deferredRegionTouchedCount = 0;
|
||||
private deferredRegionsSize = 0;
|
||||
private newlyAllowedRegions?: Int32Array;
|
||||
|
||||
constructor(numTiles: number) {
|
||||
this.visitedStamp = new Uint32Array(numTiles);
|
||||
this.targetStamp = new Uint32Array(numTiles);
|
||||
@@ -270,6 +280,282 @@ export class MultiSourceAnyTargetBFS {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like `findWaterPathFromSeeds`, but supports expanding `opts.allowedMask` without restarting.
|
||||
*
|
||||
* When the queue exhausts, calls `onQueueEmpty(outNewlyAllowedRegions)`:
|
||||
* - the callback should widen the allowed mask in-place and return how many coarse regions were newly allowed
|
||||
* - if it returns 0, the search stops and returns null
|
||||
*
|
||||
* This is the "mask-expanding BFS" fast variant: it is sound (finds a valid path if one exists
|
||||
* under the eventually allowed regions), but it is not guaranteed to be shortest under the final
|
||||
* expanded region set.
|
||||
*/
|
||||
findWaterPathFromSeedsMaskExpanding(
|
||||
gm: GameMap,
|
||||
seedNodes: readonly TileRef[],
|
||||
seedOrigins: readonly TileRef[],
|
||||
targets: readonly TileRef[],
|
||||
opts: MultiSourceAnyTargetBFSOptions,
|
||||
onQueueEmpty: (outNewlyAllowedRegions: Int32Array) => number,
|
||||
): MultiSourceAnyTargetBFSResult | null {
|
||||
if (seedNodes.length === 0 || targets.length === 0) return null;
|
||||
|
||||
const allowed = opts.allowedMask;
|
||||
if (!allowed) {
|
||||
return this.findWaterPathFromSeeds(
|
||||
gm,
|
||||
seedNodes,
|
||||
seedOrigins,
|
||||
targets,
|
||||
opts,
|
||||
);
|
||||
}
|
||||
|
||||
this.ensureMaskExpandingScratch(allowed.regionStamp.length);
|
||||
|
||||
const deferredStamp = this.deferredStamp!;
|
||||
const deferredPrev = this.deferredPrev!;
|
||||
const deferredNext = this.deferredNext!;
|
||||
const regionHead = this.deferredRegionHead!;
|
||||
const touched = this.deferredRegionTouched!;
|
||||
const outNewRegions = this.newlyAllowedRegions!;
|
||||
|
||||
const stamp = this.nextStamp();
|
||||
|
||||
for (const t of targets) {
|
||||
if (t >= 0 && t < this.targetStamp.length) {
|
||||
this.targetStamp[t] = stamp;
|
||||
}
|
||||
}
|
||||
|
||||
const w = gm.width();
|
||||
const h = gm.height();
|
||||
const lastRowStart = (h - 1) * w;
|
||||
|
||||
let head = 0;
|
||||
let tail = 0;
|
||||
|
||||
const visitedOut = opts.visitedMaskOut;
|
||||
|
||||
const count = Math.min(seedNodes.length, seedOrigins.length);
|
||||
for (let i = 0; i < count; i++) {
|
||||
const node = seedNodes[i]!;
|
||||
const origin = seedOrigins[i]!;
|
||||
if (node < 0 || node >= this.visitedStamp.length) continue;
|
||||
if (
|
||||
allowed.regionStamp[allowed.tileToRegion[node]!] !== allowed.stamp
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (!gm.isWater(node)) continue;
|
||||
if (this.visitedStamp[node] === stamp) continue;
|
||||
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;
|
||||
}
|
||||
|
||||
if (tail === 0) return null;
|
||||
|
||||
const kingMoves = opts.kingMoves ?? true;
|
||||
const noCornerCutting = opts.noCornerCutting ?? true;
|
||||
|
||||
const defer = (tile: TileRef, from: TileRef) => {
|
||||
if (tile < 0 || tile >= deferredStamp.length) return;
|
||||
if (deferredStamp[tile] === stamp) return;
|
||||
const region = allowed.tileToRegion[tile]!;
|
||||
deferredStamp[tile] = stamp;
|
||||
deferredPrev[tile] = from;
|
||||
deferredNext[tile] = regionHead[region]!;
|
||||
if (regionHead[region] === -1) {
|
||||
touched[this.deferredRegionTouchedCount++] = region;
|
||||
}
|
||||
regionHead[region] = tile;
|
||||
};
|
||||
|
||||
const activateNewRegions = (newCount: number) => {
|
||||
for (let i = 0; i < newCount; i++) {
|
||||
const region = outNewRegions[i]!;
|
||||
let tile = regionHead[region]!;
|
||||
regionHead[region] = -1;
|
||||
while (tile !== -1) {
|
||||
const next = deferredNext[tile]!;
|
||||
if (
|
||||
this.visitedStamp[tile] !== stamp &&
|
||||
allowed.regionStamp[allowed.tileToRegion[tile]!] === allowed.stamp
|
||||
) {
|
||||
// Deferred tiles are always water (we only defer after gm.isWater check),
|
||||
// so we can skip re-checking gm.isWater here.
|
||||
this.visit(tile, deferredPrev[tile]! as TileRef, stamp, visitedOut);
|
||||
this.queue[tail++] = tile;
|
||||
}
|
||||
tile = next;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (;;) {
|
||||
while (head < tail) {
|
||||
const node = this.queue[head++] as TileRef;
|
||||
|
||||
if (this.targetStamp[node] === stamp) {
|
||||
this.resetTouchedRegions(regionHead, touched);
|
||||
return {
|
||||
source: this.startOf[node] as TileRef,
|
||||
target: node,
|
||||
path: this.reconstructPath(node),
|
||||
};
|
||||
}
|
||||
|
||||
const x = gm.x(node);
|
||||
|
||||
// Orthogonal neighbors
|
||||
if (node >= w) {
|
||||
const n = node - w;
|
||||
if (gm.isWater(n) && this.visitedStamp[n] !== stamp) {
|
||||
if (
|
||||
allowed.regionStamp[allowed.tileToRegion[n]!] !== allowed.stamp
|
||||
) {
|
||||
defer(n, node);
|
||||
} else {
|
||||
this.visit(n, node, stamp, visitedOut);
|
||||
this.queue[tail++] = n;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (node < lastRowStart) {
|
||||
const s = node + w;
|
||||
if (gm.isWater(s) && this.visitedStamp[s] !== stamp) {
|
||||
if (
|
||||
allowed.regionStamp[allowed.tileToRegion[s]!] !== allowed.stamp
|
||||
) {
|
||||
defer(s, node);
|
||||
} else {
|
||||
this.visit(s, node, stamp, visitedOut);
|
||||
this.queue[tail++] = s;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (x !== 0) {
|
||||
const wv = node - 1;
|
||||
if (gm.isWater(wv) && this.visitedStamp[wv] !== stamp) {
|
||||
if (
|
||||
allowed.regionStamp[allowed.tileToRegion[wv]!] !== allowed.stamp
|
||||
) {
|
||||
defer(wv, node);
|
||||
} else {
|
||||
this.visit(wv, node, stamp, visitedOut);
|
||||
this.queue[tail++] = wv;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (x !== w - 1) {
|
||||
const ev = node + 1;
|
||||
if (gm.isWater(ev) && this.visitedStamp[ev] !== stamp) {
|
||||
if (
|
||||
allowed.regionStamp[allowed.tileToRegion[ev]!] !== allowed.stamp
|
||||
) {
|
||||
defer(ev, node);
|
||||
} else {
|
||||
this.visit(ev, node, stamp, visitedOut);
|
||||
this.queue[tail++] = ev;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!kingMoves) continue;
|
||||
|
||||
// Diagonals (king moves). With noCornerCutting, forbid squeezing past land corners.
|
||||
if (node >= w && x !== 0) {
|
||||
const nw = node - w - 1;
|
||||
if (
|
||||
gm.isWater(nw) &&
|
||||
(!noCornerCutting ||
|
||||
(gm.isWater(node - w) && gm.isWater(node - 1))) &&
|
||||
this.visitedStamp[nw] !== stamp
|
||||
) {
|
||||
if (
|
||||
allowed.regionStamp[allowed.tileToRegion[nw]!] !== allowed.stamp
|
||||
) {
|
||||
defer(nw, node);
|
||||
} else {
|
||||
this.visit(nw, node, stamp, visitedOut);
|
||||
this.queue[tail++] = nw;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (node >= w && x !== w - 1) {
|
||||
const ne = node - w + 1;
|
||||
if (
|
||||
gm.isWater(ne) &&
|
||||
(!noCornerCutting ||
|
||||
(gm.isWater(node - w) && gm.isWater(node + 1))) &&
|
||||
this.visitedStamp[ne] !== stamp
|
||||
) {
|
||||
if (
|
||||
allowed.regionStamp[allowed.tileToRegion[ne]!] !== allowed.stamp
|
||||
) {
|
||||
defer(ne, node);
|
||||
} else {
|
||||
this.visit(ne, node, stamp, visitedOut);
|
||||
this.queue[tail++] = ne;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (node < lastRowStart && x !== 0) {
|
||||
const sw = node + w - 1;
|
||||
if (
|
||||
gm.isWater(sw) &&
|
||||
(!noCornerCutting ||
|
||||
(gm.isWater(node + w) && gm.isWater(node - 1))) &&
|
||||
this.visitedStamp[sw] !== stamp
|
||||
) {
|
||||
if (
|
||||
allowed.regionStamp[allowed.tileToRegion[sw]!] !== allowed.stamp
|
||||
) {
|
||||
defer(sw, node);
|
||||
} else {
|
||||
this.visit(sw, node, stamp, visitedOut);
|
||||
this.queue[tail++] = sw;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (node < lastRowStart && x !== w - 1) {
|
||||
const se = node + w + 1;
|
||||
if (
|
||||
gm.isWater(se) &&
|
||||
(!noCornerCutting ||
|
||||
(gm.isWater(node + w) && gm.isWater(node + 1))) &&
|
||||
this.visitedStamp[se] !== stamp
|
||||
) {
|
||||
if (
|
||||
allowed.regionStamp[allowed.tileToRegion[se]!] !== allowed.stamp
|
||||
) {
|
||||
defer(se, node);
|
||||
} else {
|
||||
this.visit(se, node, stamp, visitedOut);
|
||||
this.queue[tail++] = se;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Queue exhausted under current mask.
|
||||
const newCount = onQueueEmpty(outNewRegions);
|
||||
if (newCount <= 0) break;
|
||||
activateNewRegions(newCount);
|
||||
// If expansion didn't actually yield any new reachable nodes, we'll loop back and exhaust again.
|
||||
}
|
||||
|
||||
this.resetTouchedRegions(regionHead, touched);
|
||||
return null;
|
||||
}
|
||||
|
||||
private visit(
|
||||
node: TileRef,
|
||||
from: TileRef,
|
||||
@@ -300,4 +586,28 @@ export class MultiSourceAnyTargetBFS {
|
||||
this.stamp = next === 0 ? 1 : next;
|
||||
return this.stamp;
|
||||
}
|
||||
|
||||
private ensureMaskExpandingScratch(regionCount: number) {
|
||||
if (!this.deferredStamp) {
|
||||
const n = this.visitedStamp.length;
|
||||
this.deferredStamp = new Uint32Array(n);
|
||||
this.deferredPrev = new Int32Array(n);
|
||||
this.deferredNext = new Int32Array(n);
|
||||
}
|
||||
if (!this.deferredRegionHead || this.deferredRegionsSize !== regionCount) {
|
||||
this.deferredRegionsSize = regionCount;
|
||||
this.deferredRegionHead = new Int32Array(regionCount);
|
||||
this.deferredRegionHead.fill(-1);
|
||||
this.deferredRegionTouched = new Int32Array(regionCount);
|
||||
this.newlyAllowedRegions = new Int32Array(regionCount);
|
||||
}
|
||||
this.deferredRegionTouchedCount = 0;
|
||||
}
|
||||
|
||||
private resetTouchedRegions(regionHead: Int32Array, touched: Int32Array) {
|
||||
for (let i = 0; i < this.deferredRegionTouchedCount; i++) {
|
||||
regionHead[touched[i]!] = -1;
|
||||
}
|
||||
this.deferredRegionTouchedCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user