Files
OpenFrontIO/docs/LocalCorridorWidening.md
scamiv aa09240d40 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.
2025-12-27 19:49:34 +01:00

4.1 KiB
Raw Permalink Blame History

Local corridor widening (adaptive coarse-to-fine water pathfinding, adaptive constraint relaxation)

Goal: keep the coarse corridor win, but avoid the current “corridor fails → global full-res BFS” cliff.

Local widening behaves like a cheap BSP refinement:

  • start with a narrow corridor (fast)
  • if it fails, expand only where it matters (still fast)
  • only as a last resort, drop the mask entirely

This is intended to be a generic wrapper around MultiSourceAnyTargetBFS (used by transport/trade/warship).

Inputs / outputs

Inputs:

  • fineMap: GameMap
  • coarseMap: GameMap (typically map16x)
  • seedNodes[], seedOrigins[] (multi-source)
  • targets[] (any-target)
  • bfsOpts (king moves, no-corner-cutting, etc.)
  • initial corridor radius r0, max attempts k

Output:

  • { source, target, path } like MultiSourceAnyTargetBFSResult, or null

Baseline (what we have today)

  1. Coarse BFS to get coarsePath
  2. Corridor = inflate coarsePath by radius r
  3. Fine BFS restricted by corridor mask
  4. If fail: widen radius globally or fall back to unrestricted fine BFS

Problem: a tiny lie in the coarse map (optimistic water) can cause step (4) to explode to “search the whole ocean”.

Local widening: two practical variants

Variant A (chosen): widen around visited coarse regions

If fine BFS fails inside the corridor, we already know where it was trying.

Implementation sketch:

  1. Build initial corridor mask: allowedCoarse[coarseCell] = true.
  2. Run fine BFS with allowedMask = coarse corridor.
  3. If it succeeds: done.
  4. If it fails:
    • compute visitedCoarse: all coarse cells that were actually visited by fine BFS
    • expand corridor by 1 ring around visitedCoarse (Chebyshev ring, since king moves)
    • retry fine BFS
  5. Repeat up to k times.
  6. If still no path: unrestricted fine BFS fallback (correctness guardrail).

Clarification: widening is cumulative. Each failed attempt expands around that attempts visitedCoarse, and newly allowed coarse cells stay allowed across subsequent attempts (via the same allowedCoarseStamp).

Why it works:

  • you only “pay more” near the constriction you hit
  • open-ocean cells that were never approached dont get unlocked

What “visitedCoarse” means (cheaply):

  • while expanding fine BFS, map fineTile -> coarseCell (precomputed fineToCoarse[])
  • stamp visitedCoarseStamp[coarseCell] = stamp when the BFS pops/visits a tile

How to expand by one ring:

  • for each coarse cell in visitedCoarse, mark its 8 neighbors as allowed
  • use stamps, not Set, to avoid allocations

Variant B (not chosen): widen only along the coarse path segment you reached

Similar, but tighter:

  • intersect visitedCoarse with the original coarsePath (or the prefix thats reachable)
  • widen only around that subset

This can be even cheaper on huge corridors, but is easier to get wrong (requires careful “prefix” reasoning).

Hot-path constraints (dont regress perf)

  • No per-call allocations in the inner BFS loop.
  • Use stamp arrays:
    • allowedCoarseStamp[coarseCell]
    • visitedCoarseStamp[coarseCell]
  • Reuse MultiSourceAnyTargetBFS instances via WeakMap<GameMap, MultiSourceAnyTargetBFS>.
  • Keep attempt count small (k = 2..4).

Correctness guardrails

  • Coarse map is approximate: coarse success never guarantees fine success.
  • Local widening can still miss a path if the corridor is too wrong; thats fine:
    • always end with an unrestricted fine BFS fallback
  • Preserve current move rules:
    • king moves (8-neighbor)
    • no-corner-cutting

Suggested defaults

  • r0 = 1..2 coarse cells (start tight)
  • k = 3 (initial + 2 widen steps)
  • widen step = +1 ring around visitedCoarse
  • final fallback = unrestricted fine BFS

Where this plugs in

Replace the current “attempt loop that only increases radius globally” inside coarse-to-fine helper with:

  • attempt loop driven by visitedCoarse
  • optional “global radius bump” as a last attempt before full fallback

This keeps the interface identical for all callsites (transport/trade/warship), but makes “tight corridor” failures cheap.