Audience: Teammates + judges. This is the doc you read if someone asks "how does your agent actually decide where to look?". Top-down: starts with the question "where are survivors likely?" and drills into every layer that answers it.


1. The Core Question

Survivors are not uniformly distributed. Our whole search strategy is built around one probabilistic model of where they're likely to be:

"Given a damaged urban disaster, a cell's survivor probability is proportional to how built-up and broken that cell was."

Which translates into a fixed per-terrain weight:

Terrain Weight Rationale
hazard (collapsed urban) 7 Highest survivor density — trapped people, collapsed buildings
city (intact urban) 5 Still populated, but fewer trapped survivors than hazard
forest 2 Sparse, hikers / isolated individuals
flat (open ground) 1 Baseline
lake 0 Impassable

(Source: backend/simulation.py:21TERRAIN_SCAN_WEIGHT)

Every downstream decision — which cell to scan, which zone to assign, which drone to redirect — is a transformation of this single table.


2. Strategy Cascade (one diagram)

flowchart TB
    T["Terrain map<br/>hazard / city / forest / flat / lake"] --> P["Probability map<br/>per-cell survivor weight<br/>normalised to sum=1"]
    P --> Z["Zone score<br/>Σ probability over<br/>unscanned cells of zone"]
    Z --> C["Commander priority_map<br/>LLM output<br/>(or rule-derived in RULES mode)"]
    C --> M["Pilot menu<br/>per-drone: battery-affordable<br/>top-6 options, tag-annotated"]
    M --> D["Pilot decision<br/>LLM or WeightedPlanner"]
    D --> SP["Sweep path<br/>terrain-tiered zig-zag<br/>hazard-first, BFS detour"]
    SP --> SC["Scan<br/>thermal bloom detection"]
    SC -.->|survivor found| P
    SC -.->|zone done| Z

Everything above the dotted arrows is feed-forward planning. The dotted arrows are the feedback loops — a successful scan updates the probability map (boosts adjacent zones x1.5) and rezeroes the scanned cell, so the next cycle re-ranks from fresh evidence.


3. Layer 1 — Cell-Level Probability Map

File: backend/simulation.py::_init_probability_map

for y in range(GRID_H):
    for x in range(GRID_W):
        if self.is_inaccessible(x, y):   # lake / off-grid
            row.append(0.0)
        else:
            w = float(TERRAIN_SCAN_WEIGHT.get(self.zone.terrain_types[y][x], 1))
            row.append(w)
            total += w
# Normalise so the whole grid sums to 1
for y, x: weights[y][x] /= total

Each cell gets a probability mass, not a probability density. Summing over any region gives that region's fractional survivor likelihood.

Two dynamic updates

Event Update rule Code
Cell scanned probability_map[y][x] = 0.0 update_probability_after_scan
Survivor found in cell (x, y) Nearby adjacent unscanned cells gain small boosts: +0.02 for 1-cell neighbours, +0.01 for 2-cell neighbours update_probability_after_scan
Survivor found anywhere in a zone Every unscanned cell in adjacent zones gets ×1.5 probability boost boost_adjacent_zones — fires once per find