From 69e422d35e0d6d8229d4a1abec494d27158150c8 Mon Sep 17 00:00:00 2001 From: scamiv <6170744+scamiv@users.noreply.github.com> Date: Sat, 27 Dec 2025 14:22:59 +0100 Subject: [PATCH] Refactor MultiSourceAnyTargetBFS documentation for clarity and performance - Updated the design notes to clarify the multi-source, any-target BFS approach for boat routing on a water-only grid. - Simplified the explanation of the algorithm, focusing on the use of a virtual super-source and super-target. - Enhanced the API description, detailing the return value structure and movement model. - Added performance optimizations, including the use of typed arrays and precomputation of water component IDs to improve routing efficiency. --- docs/MultiSourceAnyTargetBFS.md | 150 +++++++------------------------- 1 file changed, 31 insertions(+), 119 deletions(-) diff --git a/docs/MultiSourceAnyTargetBFS.md b/docs/MultiSourceAnyTargetBFS.md index 822a69a51..ac50e6bf0 100644 --- a/docs/MultiSourceAnyTargetBFS.md +++ b/docs/MultiSourceAnyTargetBFS.md @@ -1,137 +1,49 @@ # MultiSourceAnyTargetBFS (boats) — design notes -## Goal +## What we’re doing -Replace the current “guess a landing tile, then pathfind” approach with a single **multi-source, any-target** search that: +Boat routing is now a single, sound **multi-source / any-target** shortest-path solve on a **water-only** grid. +For equal cost edges, that’s just **BFS**. -- 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. +The mental model is the classic “virtual super-source / super-target” trick: -This is the standard “virtual super-source + virtual super-target” idea: +- Pretend there is a `START` node with 0-cost edges to every source `S`. +- Pretend every target `D` has a 0-cost edge to an `END` node. +- Run shortest path `START → END`. -- 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 don’t build those nodes: we seed the queue with all sources, and we stop when we **dequeue** any target. -We do not build those nodes; we seed the queue with all sources and stop when we pop a target. +## API shape -## Scope / assumptions +Return value: `{ source, target, path } | null` -- 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. +- `source`: which seed-origin won (useful when seeds are water-adjacent tiles but the origin is a shore tile). +- `target`: the water tile we reached (usually adjacent to the chosen landing shore). +- `path`: water path as a list of `TileRef` to cache and replay (no per-tick pathfinding). -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. +## Movement model (boats) -## Inputs and outputs +- Traversal: `gm.isWater(tile)` only. +- Neighbors: **king moves** (8-neighbor / Chebyshev) by default. +- No-corner-cutting: diagonal is only allowed if both touching orthogonals are water. +- All moves cost 1, so BFS is optimal. -### Inputs +## Performance wins (the non-negotiables) -- `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. +- Use typed arrays + stamps (no `Set`/`Map` in the inner loop). +- **Precompute water-body component IDs** once per map instance (`WaterComponents.ts`) and filter sources/targets: + - If source component ≠ target component, skip it (ocean vs lake becomes O(1) reject). + - This makes “impossible” routes cheap and lets us delete hacky visited-count early exits. -### Output +## How this integrates (transport/trade/warships) -`{ source, target, path } | PathNotFound` +- Destination selection stays “near click”: pick a bounded set of landing shore candidates, then convert to adjacent water targets. +- Source selection stays bounded (sampling/extrema/etc), then convert to adjacent water seeds. +- Compute the route once on launch/init, cache `path`, and only advance an index during ticks. +- Retreat follows the existing cached path backwards (no recompute). -- `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. +## Current known waste -## Target selection (“near click”, but bounded) +If the client pre-queries and the server recomputes, you will see two searches for a single action. +We should ensure we only do one solve per user action (exact mechanism TBD: remove the pre-query, or reuse/publish the computed route). -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 boat’s 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).