- Introduced a new `CoarseToFineWaterPath` module to enhance pathfinding efficiency for boats by utilizing a coarse map to guide fine path searches. - Implemented a two-stage pathfinding approach: a coarse search on a low-resolution map followed by a refined search on the full-resolution map. - Updated `TransportShipExecution` and `TransportShipUtils` to leverage the new coarse-to-fine pathfinding method, improving routing decisions and handling of retreat paths. - Added tests to validate the new pathfinding functionality and ensure robustness in various scenarios.
3.2 KiB
Coarse-to-fine pathfinding (boats) — notes
Why
Full-res water BFS is optimal and simple, but the “ocean case” can still expand a lot of tiles. Coarse-to-fine is the next lever: do a cheap solve on a low-res map to guide / bound the expensive solve.
Do we already have low-res maps?
Yes. The terrain loader already ships multiple resolutions per map:
manifest.map+map.bin(full res)manifest.map4x+map4x.bin(coarser)manifest.map16x+map16x.bin(even coarser)
At runtime we already load both:
gameMap: full res for normal games (ormap4xfor compact games)miniGameMap: lower res (map4xfor normal games, ormap16xfor compact games)
So we can prototype coarse-to-fine without extending mapgen first.
Core idea (don’t overthink it)
Stage 1 (coarse):
- Run the same multi-source/any-target search on
miniGameMap(BFS, water-only, king-moves if desired). - Result is a coarse path (or just a coarse distance field).
Stage 2 (refine):
- Run full-res BFS on
gameMap, but restricted by what stage 1 learned (a “corridor”) or guided by a coarse heuristic.
Important: the coarse map is an approximation. It must never be allowed to make the final path invalid. If the refine stage fails inside the corridor, fall back to full-res BFS.
Option A: Coarse corridor (usually the biggest win)
- Map fine tiles → coarse cells by integer scaling:
scaleX = gameMap.width / miniGameMap.widthscaleY = gameMap.height / miniGameMap.height
- Solve on coarse, get a coarse cell path.
- Inflate that path into a corridor:
- include all coarse cells within radius
rof the coarse path (e.g.r = 1..3)(Manhattan or Chebyshev radius depending on move rules)
- include all coarse cells within radius
- Refine on full-res with a fast mask:
passableFine(tile) = gm.isWater(tile) && corridorMask[coarseOf(tile)]
- If no path found, retry without the corridor (or inflate
rand retry once).
Notes:
- If the low-res generation is “optimistic” (water if any child tile is water), the coarse path can cut across land. Inflation + fallback is what keeps this safe.
Option B: Coarse heuristic for A*
If we ever move from BFS → A* on full-res, a cheap heuristic is:
- Precompute
coarseDist[coarseCell]by BFS onminiGameMapseeded from coarse targets. - Use
h(tile) = coarseDist[coarseOf(tile)] * min(scaleX, scaleY)
If the coarse map is “more passable” than the fine map (typical for minimaps), coarseDist tends to underestimate,
which is admissible (safe) but not always very tight.
Where component IDs fit
Water-component IDs are still a free early reject:
WaterComponents.tsalready precomputes IDs perGameMapinstance.- Do the same check on
miniGameMapif useful, but full-res component filtering already prevents the worst “wrong ocean” searches.
Practical next steps (incremental)
- Add a coarse route helper that mirrors the existing API but runs on
miniGameMap. - Implement corridor masking + refine fallback as a generic helper (so transport/trade/warship can all share it).
- Measure: expansions + ms, before/after, on worst-case oceans.
- Only then decide if mapgen needs a better “navmap” (e.g. conservative water, coastline preservation, etc.).