diff --git a/plan/candidates.md b/plan/candidates.md index 264d7f2..3250fd7 100644 --- a/plan/candidates.md +++ b/plan/candidates.md @@ -89,6 +89,10 @@ through expectation (`p = 1`, they move without regard to us). Same operator characterful rather than arbitrary - a cautious commander runs a lower `p`, so it belongs to [doctrine](doctrine.md). +**The generator no longer runs that collapse.** See "The collapse moved out of +the generator" below: `score_stands` reports the joint outcome at every position +and `rank` is a caller that collapses it. + Over `N`, which enemy: **offence and defence aggregate differently.** - Offence takes the *best* target, or the best few: we fire at one thing. @@ -188,6 +192,9 @@ good offence and bad defence, and a single operator over `N` cannot say so. summed incoming. Test: a state good against one enemy and exposed to three scores high on one and low on the other. `stands::score_stands`, and the five volley outputs stay five through both aggregations +- [x] Keep the two channels paired at the position that produced them, and move + the collapse to the caller. `score_stands` reports per-position outcomes, + `rank` collapses them. See "The collapse moved out of the generator" - [x] Emit **every scored state** as a proposal. The unit does not prune: see "The unit does not prune" below. `stands::Ranking` carries every state scored, and the non-dominated set and the two top-*K* lists beside it as @@ -536,6 +543,58 @@ a force's real exposure is an allocation problem whose worst case is concentration on one unit. Nothing here may add these numbers up, and the fix is force-level - see [hierarchy](hierarchy.md) and [strategies](strategies.md). +### The collapse moved out of the generator + +`score_stands` used to collapse over `M` **once per channel**: damage dealt +aggregated over `M`, damage taken aggregated over `M`, the two independent. That +is incoherent below `p = 1`. The enemy occupies one position, and that position +settles both channels; minimaxing them apart describes two adversaries in two +hexes at once - one moving to minimise our offence, another simultaneously +moving to maximise our exposure - and the worst case it reports cannot happen. + +It also explains a number the facts work turned up. Of the pairs reading +`deal 0.00` on `open`, **203 of 497 are "firing, but the collapse says zero"**: +some member of `M` zeroes our offence, and the damage taken printed beside it +came from a different member. All 203 are now readable, up to 30.25 expected +damage at the position the collapse threw away; the same holds for 52 of 52 on +`forest` and 43 of 43 on `water`. The counts were 200 of 494 before the +attacker's own movement modifier was charged to the to-hit, which moved every +damage figure. + +**What the record carries.** One entry per `(stand, enemy, position)` pair: +which position it is, and each direction as either the prune that stopped it - +no line, arc blocked, out of bracket - or a volley, with the five outputs and +the arc the damage landed in. The two directions of an entry are read at the +same position, which is the correlation the old collapse destroyed. The per-arc +split is now recorded where it happens and averaged on the way out, rather than +being the only thing that survived. + +**Sparse.** A pair that fired in neither direction stores nothing; its reason is +in the four-way partition that was already being counted. Measured on the corpus +boards at 52 B an entry: `open` 31,878 pairs, 40.2% of them storing nothing, +968 KB of entries against 1,619 KB dense; `forest` 61.4% sparse and 38 KB; +`water` 44.7% sparse and 193 KB. Scaled to a 16v16 turn - 151 stands, 16 +enemies, 144 positions - that is about 10.9 MB a unit, which is **eight times** +the 1.3 MB this was estimated at. Nothing has yet needed sixteen of those alive +at once, but that is the number to watch, and shrinking an entry means dropping +outputs that the five-outputs rule says stay. + +**Wall clock is unchanged**, and cheaper wherever more than one exponent is +read. One exponent over the whole `examples/stands` run, twenty runs averaged: +`open` 44.5 to 43.8 ms, `water` 14.6 to 14.0 ms, `forest` 6.66 to 5.91 ms. +Three exponents, which is what the example actually prints, now share one sweep +instead of running three: `open` 111 to 57.5 ms, `water` 29.0 to 18.0 ms, +`forest` 11.2 to 7.5 ms. + +**Damage taken is per enemy now**, symmetric with damage dealt. +`StandScore::taken_from` is the mirror of `per_target` and `defence` is its sum, +which is the shape the limitation above asks for: a unit's exposure is a +conditional, and the allocation is the force's to make. + +`power_mean`, `robust_mean` and `Sense` are unchanged and stay as library +functions. What changed is who calls them, and that the call is visible at the +call site. + ### What this exposed There is no bridge from a `Stance` to a `Weights` anywhere in the tree.