diff --git a/plan/candidates.md b/plan/candidates.md index 7f4eacf..5073274 100644 --- a/plan/candidates.md +++ b/plan/candidates.md @@ -802,9 +802,15 @@ vector is a feature change with its own control run, and has not been made. preferring them, either the new states are bad or the scorer cannot tell them apart, and both are findings. Remove them once a run shows the generated states winning, and record the share at which they were removed. - Counted: `PlanTally::verb_share`, printed every round. **First real match, - `mirror-lance` seed 1: the four verbs won 26 of 31 movement decisions, 84%.** - The verbs stay until that turns over + Counted: `PlanTally::verb_share`, counted when an order is played. **First + real match, `mirror-lance` seed 1: the four verbs won 26 of 31 movement + decisions, 84%; 32 of 37, 86%, once enemies became envelopes.** The verbs + stay until that turns over +- [x] **Enemies are an envelope, not a point.** `M` is an unmoved enemy's own + reachable set and a moved one's placed hex, decided by `Unit::done`, and + the plan is rebuilt per decision so the envelope collapses as the phase + runs. Tests: `unit::tests::an_enemy_envelope_collapses_to_a_point_when_it_moves` + and `tests::a_settled_enemy_changes_the_plan_key` - [ ] `examples/pathfind.rs`: states/sec and `L x M` pairs/sec, open terrain against heavy forest, recorded in [PERFORMANCE.md](../docs/PERFORMANCE.md) in its own style @@ -819,11 +825,13 @@ vector is a feature change with its own control run, and has not been made. weights, against Princess. It completed: victory on round 9, 60 of 66 decisions answered, 0 defaulted, 6 illegal. Princess won. -- **Time is not the problem.** A whole planning cycle - every unit of a lance - swept, scored, surfaced, and the lance reconciled - ran in 2 to 8 ms, mean 4. - The offline 79 ms was six views against enemy *reachable sets*; in a match - every enemy is one `Presence`, so `M = 1` per enemy and the sweep is `L x N`. - Largest single unit seen: 106 states, 848 exchanges. +- **Time is not the problem** - but the 2 to 8 ms first reported was a + degenerate case and must not be quoted. That run built every enemy as a single + `Presence`, so `M = 1` always, the sweep was `L x N`, and the whole + uncertainty model - the power mean over `M`, the risk exponent, the paired + joint outcome - collapsed to a point and did nothing. See + [Enemies are an envelope](#enemies-are-an-envelope-not-a-point) for the fix + and the real numbers. - **The menu is capped and the cap binds.** 5 to 16 candidates a unit, 11.5 mean. - **The four verbs won 26 of 31 movement decisions, 84%.** That is the number the removal step is waiting on. Either the surfaced states are worse than a @@ -841,6 +849,63 @@ answered, 0 defaulted, 6 illegal. Princess won. because two of them rarely name one hex. +## Enemies are an envelope, not a point + +The first wiring built every enemy as `vec![Presence::placed(unit)]`. One +position each, unconditionally, so `M = 1` always. That is not a cheap +approximation of the model; it is the model switched off. The power mean over +`M`, the risk exponent spanning minimax to expectation, the paired joint outcome +at a single enemy position - each one operates on `M`, and each one is the +identity on a set of size one. + +**`M` is now decided by `Entity.isDone()`**, on the wire as `Unit::done` and +emitted by `bridge/sds/Observation.java`. Read by disassembly rather than from +the name: the server clears it for every entity in +`TWGameManager.resetEntityPhase` at the top of each phase and sets it in +`endCurrentTurn`, so inside the movement phase it reads "has taken its turn". + +- **Not yet moved** - `M` is that enemy's own reachable set, from its own start, + its own walk MP and its own `maxElevationChange`, blocked by every other unit + on the board. The same construction the offline scenes use. +- **Moved, or destroyed** - `M` is its placed hex, as before. + +**The plan is rebuilt per decision, not per round.** Movement alternates by +initiative, so the set of settled enemies grows between our own decisions, and +a plan made at the top of the round would hand our last mover the uncertainty +our first mover faced. `Bot::plan` is keyed on `(round, settled enemies)` and +`Bot::settled` is that set; `movement` gets a fresh observation from the host +for every decision, so the key is read off current state rather than a cached +round. The order tally moved with it: which generator won is recorded on the +standing order and counted when the order is *played*, so a rebuilt plan cannot +count the same unit's turn twice. + +**`mirror-lance` seed 1, `sds one --sds-seat North --candidates`**, against +Princess, hand-authored weights. Victory on round 11, 82 of 82 decisions +answered, 0 defaulted, 0 illegal. + +- **Per-decision wall time: 61 ms mean, 191 ms worst.** Broken out by how many + enemies had still to move: 4 unmoved 104 ms mean / 191 ms worst, 3 unmoved + 86 / 144, 2 unmoved 54 / 107, 1 unmoved 34 / 58, 0 unmoved 8 / 11. The + expensive case is exactly the one that should be expensive. +- **`sum(M)` ran 4 to 302, mean 126.** Round 1 in decision order: 226, 202, 172, + 125. Round 2: 302, 216, 63, 4. The collapse is visible as data. +- **The LOS cache does not fall over as `M` grows.** 93% hit at four unmoved + enemies and 55k questions asked, 96% at three, 99% at two. `AttackInfo` holds + hexes and heights and no unit identity, so sweeping an envelope asks about + hexes already seen while scoring our own stands. +- **The four verbs won 32 of 37 movement orders, 86%**, against 84% under + `M = 1`. A wider `M` did not make the generated states win more. + +**The run-to-run spread is large and is not this change.** Four runs of the same +scenario and seed ended on rounds 9, 11, 13 and 11 with 3, 14, 13 and 0 illegal +movement orders. Two of those were before the plan was rebuilt per decision, so +the harness is not reproducible for a single match and no single run's illegal +count says anything. The same-hex collisions have a known cause - the sweep +blocks on where units *are* and nothing deconflicts where they are *going* - and +a rebuild is triggered by an enemy settling, not by one of ours moving, so it +does not fix them. + + ## Heat: reported, not budgeted `VolleyOutcome` reported expected damage, `p_kill`, `p_mission_kill`, expected @@ -1062,8 +1127,10 @@ minimax ranking puts blind hexes first by construction; the mean does not, at 1 of 24. The render says so on the page. Fixing it is a scoring change and belongs to whatever chooses between the two lists, not to the estimator. -**Move order.** With `M = 1` for a unit that has already moved, the last mover -has near perfect information and the first is guessing. `hierarchy` already +**Move order.** `M = 1` for a unit that has already moved, so the last mover has +near perfect information and the first is guessing. That asymmetry is now +measured rather than assumed - `sum(M)` over round 1 of one match ran +226, 202, 172, 125 across our four units in decision order. `hierarchy` already carries "move order is the harness's, not the bot's" as an open item; under this design it stops being tidiness and becomes a tactical lever.