Files
OpenFrontIO/docs/MultiSourceAnyTargetBFS.md
T
scamiv 65ca00d54f Implement MultiSourceAnyTargetBFS for efficient boat routing
- Add MultiSourceAnyTargetBFS algorithm for water-based pathfinding
- Replace "guess landing tile, then pathfind" with single multi-source search
2025-12-26 22:32:16 +01:00

138 lines
5.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# MultiSourceAnyTargetBFS (boats) — design notes
## Goal
Replace the current “guess a landing tile, then pathfind” approach with a single **multi-source, any-target** search that:
- Prefers landings **near the user click** (destination selection stays “near click”).
- Finds the **best source+target pair** in one run (no retries / staged hacks).
- Is fast enough for a hot path by doing **one search per boat launch**, not per tick.
This is the standard “virtual super-source + virtual super-target” idea:
- Imagine a `START` node connected to every source `S` with cost 0.
- Imagine every target `D` connected to an `END` node with cost 0.
- Run shortest-path from `START` to `END`.
We do not build those nodes; we seed the queue with all sources and stop when we pop a target.
## Scope / assumptions
- Boat routing runs on a **water-only graph**.
- For now, all edges are **equal cost**, so we use **BFS**.
- Optional “pretty” modes (diagonals or smoothing) must be behind a toggle so the default stays cheap.
If we later add non-uniform costs (e.g., currents, danger zones, traffic), the same API can switch to
Dijkstra/A* by supplying a cost function; BFS remains the fast-path when cost is constant.
## Inputs and outputs
### Inputs
- `sources`: candidate attacker spawn shores (or their adjacent water tiles).
- `targets`: candidate defender landing shores near click (or their adjacent water tiles).
- `neighbors(node)`: 4-neighbor (or optional 8-neighbor) adjacency function.
- `passable(node)` or `isTraversable(from,to)`: boat constraint (water-only).
- `maxNodes` / `maxRadius` / `maxSteps` (optional): guardrails for worst-case expansions.
### Output
`{ source, target, path } | PathNotFound`
- `source`: which source won.
- `target`: which target was reached first (with correct BFS semantics).
- `path`: full route (list of tiles) to persist on the unit/execution.
## Target selection (“near click”, but bounded)
Keep the “landing near click” behavior by constructing a **small target set**:
- Collect defender shore tiles around the clicked tile and/or on the defender border.
- Sort by Manhattan distance to click.
- Keep the first `K` (cap, e.g. `K=50..200`) or `min(K, floor(shoreCount * 0.05))` with a hard max.
- Preferably filter to the same connected water component as the attacker (ocean vs a specific lake),
otherwise BFS will waste time exploring an impossible component.
This turns “any destination” into a precise, controllable set.
## Source selection (bounded)
Sources can be large (player border can be huge). For performance:
- Prefer “spawnable shore tiles” (owned + shore + valid spawn rules).
- Cap/summarize: best-by-distance-to-click, extremal tiles, plus uniform sampling.
- If we already know the boats actual spawn (e.g. UI precomputes), sources can be a singleton.
## Core algorithm (unweighted)
### Correct early-exit rule
When using BFS:
- Early-exit only when a target node is **dequeued** (popped), not when first discovered.
Dequeue guarantees minimal distance.
### Multi-source seeding
Initialize BFS frontier with all sources:
- `dist[source] = 0`
- `prev[source] = -1`
- `startOf[source] = source`
- enqueue all sources
When expanding neighbors:
- if unseen, set `prev[neighbor] = node`, set `startOf[neighbor] = startOf[node]`, enqueue.
When dequeuing:
- if `node` is in `targets`, stop and reconstruct by walking `prev[]`.
### Data structures (hot-path friendly)
Avoid maps/sets in the inner loop:
- `visitedStamp: Uint32Array(numTiles)` + `stampCounter` (no clearing per query).
- `prev: Int32Array(numTiles)` (or `Int32Array` only for seen nodes using a compact list).
- `queue: Int32Array(numTiles)` with head/tail indices (ring buffer).
- `targetsStamp: Uint32Array(numTiles)` (mark target membership once per query via stamping).
- `startOf: Int32Array(numTiles)` if we need “which source won” without reconstructing first step.
Allocate these once and reuse.
## Integration points (boat launch only)
- On boat launch (intent/execution init), compute:
1) `targets` near click
2) `sources` (spawn candidates)
3) `path = MultiSourceAnyTargetBFS(sources, targets)`
- Persist `path` and only advance an index in `tick()`.
- Recompute only on meaningful topology changes (rare) or if the current path is invalidated.
Default: do not “find path… then find path…”.
## Diagonals / smoothing (optional)
Two mutually exclusive options:
1) **8-neighbor BFS** (“king moves”, Chebyshev) with **no corner cutting**:
- allow diagonal move only if both adjacent orthogonal tiles are water.
- still unweighted BFS (diagonal cost == orthogonal cost), so the metric becomes Chebyshev.
2) **4-neighbor BFS + smoothing pass**:
- keep BFS cheap/correct,
- do a short post-process that removes zigzags if there is line-of-sight over water.
Default recommendation for boats: option (1) enabled, with no-corner-cutting enabled.
## Failure modes and guardrails
- If `targets` is empty → return `PathNotFound` early.
- If `sources` is empty → return `PathNotFound` early.
- If BFS exceeds a configured budget (`maxNodes`) → return `PathNotFound` (and optionally fall back).
## What this intentionally does NOT do
- No staged “find path… then adjust/retry” heuristics.
- No per-tick pathfinding.
- No bidirectional multi-target tricks (too easy to get subtly wrong).