eb74f9b2e3
Follows the diagnosis that proactive straight-line ramming CANNOT work: both bots have MAX_SPEED=8, so a pursuit cannot catch an evading equal-speed opponent. Measured over 49 rounds per arm, opportunity -> contact was **0/6** (base), 0/40 (ring), 0/12 (ringhot). The only proactive conversion in the whole corpus came from a FINISHER, and only because a <20-energy DrussGT stops fleeing (that episode closed at 6-8 px/tick). Opportunity episodes never got below ~80px; one ran the full 60-tick duration cap and closed only 198->171px; a perfectly aligned full-speed one closed 195->114px then plateaued. CHANGES - **Finisher-only default.** `finisher` (<20 energy, dist<300, we are healthier) and the rare `desperation` (both <5, dist<150) are kept; `opportunity` and the speculative `plan` are OFF. Both are env-reenableable with no rebuild: `TR_RAM_OPPORTUNITY=1` (tune via TR_RAM_OPP_DIST/MARGIN) and `TR_RAM_PLAN=1`. Justification: it removes 100+ non-converting episodes per fixture at zero measured loss (oldram vs base was p=0.69, damage 279 vs 284, survival 17/49 vs 16/49) - and each of those episodes spent up to 60 ticks driving STRAIGHT at the enemy, abandoning the mover's dodging and disrupting aim. - **`desperation` KEPT** deliberately: it is cheap and rare, fires only when both bots are nearly dead at short range (a coin-flip where 0.6 contact can decide it), and it is not the refuted straight-line pursuit. - **THE BULLET-RAIN ABORT WAS DEAD CODE AND IS NOW FIXED.** `onHitByBullet` accumulated raw bullet FIREPOWER while `TR_RAM_ABORT_DMG = 0.5` was documented as a DAMAGE rate - so the bar was implicitly "sum of power > 7.5 over 15 turns" and the maximum rate ever observed was 0.27. It now accumulates REAL ENERGY via a `bulletDamage(power)` helper matching the server's `4p` / `6p-2` formula, and `TR_RAM_ABORT_DMG` defaults to **2.0 energy/turn** (~30 HP over 15 turns): "abort an in-progress ram if we take > 2.0 energy per turn". Same effective bar for normal firepower, and it can now actually fire - the live run reports `dmgRate=1.07/turn` where the old units said 0.27. - **`ramStuckTicks` REMOVED.** It required `dist < 5px`; contact occurs at ~36px (two 18px radii) and position rewind prevents getting closer, so it could never increment. Only the 60-tick duration cap can now self-end a ram. LIVENESS (measured, default config, vs a charging Java RamFire, 3 rounds): default -> `[ram] ON reason=finisher` x3, `reason=opportunity` x0 TR_RAM_OPPORTUNITY=1 -> `reason=opportunity` x4, `reason=finisher` x2 So the opportunity states DID occur and are suppressed by the new default - the removal is real, not an arm that never fires. A line also read `[ram] OFF reason=duration dmgRate=1.07/turn`, confirming the new energy units. Adds docs/ramming_negative_result.md (70 lines) recording the question, the five diagnostic answers, the geometric reason, the finisher exception, the two dead code paths, and an explicit "do not re-attempt a proactive straight-line ram; if point-blank forcing is ever wanted it is an INTERCEPTION/cornering movement problem" note - the same pattern that stopped the corpse bug recurring. Guards: test_ram_decision 40 (was 28), test_gun_harness 39, test_vbullet_metric 11, test_power_selection 3, test_adaptive_radar 41, test_tfil_ring_weights 24, test_power_policy 26, test_rack_membership 48, test_selector_tiebreak 19, test_tm_pattern_registration 20, test_vbullet_admit_gate 12, acceptance_offline_vs_online 12/12. ModularBot compiles. Honest note: the abort-threshold fix is a real (tiny) behaviour change, NOT measured-neutral - it only bites while a finisher ram is under sustained fire, which is exactly the user's stated wish. The finisher-only removal itself is measured-neutral per the given A/B.
73 lines
4.1 KiB
Markdown
73 lines
4.1 KiB
Markdown
# The three unmeasured features, finally A/B'd — and a methodology correction
|
||
|
||
Five arms × 7 runs × 7 rounds (35 real-DrusGT bridge battles, 8 concurrent), ONE frozen binary
|
||
built from `git archive HEAD` at `185a32e` (includes the shipped Pattern-only rack), env knobs
|
||
only, server-side event sidecar ground truth, exact two-sided permutation tests.
|
||
|
||
| arm | real % | dmg/run | survival (rounds won) | per-run range | p vs base |
|
||
|---|---:|---:|---:|---|---:|
|
||
| `base` (shipped) | 10.61 | 284.1 | **16/49** (32.7%) | 8.71–12.18 | — |
|
||
| `nopower` (policy off) | 7.88 | 247.1 | 9/49 (18.4%) | 6.51–9.52 | **0.0012** |
|
||
| `oldram` (old gates) | 10.78 | 279.4 | 17/49 (34.7%) | 9.67–11.43 | 0.6888 |
|
||
| `ring` (`tfil_ring`) | **20.28** | 267.4 | **6/49** (12.2%) | 15.70–23.36 | **0.0006** |
|
||
| `ringhot` (ring, original heat) | 11.73 | 285.3 | 15/49 (30.6%) | 10.39–12.45 | 0.0303 |
|
||
|
||
Every round ends with exactly one death (0 timeouts), so **survival = round win**.
|
||
|
||
## Verdicts
|
||
|
||
**1. Power policy — HELPS. Keep.** With it off, real hit rate drops 10.61 → 7.88 (p=0.0012),
|
||
damage/run drops 284 → 247, and FEWER rounds are won (16 → 9). The cap trades per-shot damage for
|
||
many more shots and a higher per-shot rate, and that trade is a clear win.
|
||
|
||
**2. Proactive ramming (200px / +15 energy gates) — INDISTINGUISHABLE.** `oldram` vs `base`:
|
||
p=0.69, damage 279 vs 284, survival 17/49 vs 16/49 (p=1.0), ram contacts 1 vs 2. The lever is
|
||
provably LIVE (6 `opportunity` ON events vs **0** under the old 50px/+30 gates, and `base` reached
|
||
<40px on 10 ticks vs 0) — but it converts to essentially no extra collisions and no measurable
|
||
outcome change. Safe to keep; it is not earning its keep, and reverting it is equally defensible.
|
||
**Follow-up:** a log-replay diagnosis later showed `opportunity -> contact` was **0/59** and the
|
||
gate never got below ~80px, so the default is now **finisher-only** and the proactive trigger is
|
||
opt-in (`TR_RAM_OPPORTUNITY=1`). See `docs/ramming_negative_result.md`.
|
||
|
||
**3. Range-weighted ring mover — HURTS the objective. Do NOT ship.** See below; this is the
|
||
important one.
|
||
|
||
## METHODOLOGY CORRECTION: "real hit rate is the only ground truth" is UNSAFE for movement
|
||
|
||
The ring arm has the best hit rate of anything measured all night — **20.28% vs 10.61% (+9.67pp,
|
||
p=0.0006, non-overlapping ranges)**. Read alone, that says "ship it immediately".
|
||
|
||
It is a confound. The ring halves engagement range (median **460 → 240 px**), which halves round
|
||
length (**1542 → 636 ticks**) and shots (**4466 → 1834**). So:
|
||
|
||
- **damage/run is FLAT**: 284 → 267
|
||
- **survival / round-win more than halves**: 16/49 → **6/49** (p=0.0122, i.e. base survives
|
||
significantly more)
|
||
- the bot dies ~2.4× faster
|
||
|
||
It is a **glass cannon**: the same damage dealt, twice as many deaths. The hit-rate gain is a
|
||
geometric artefact of fighting closer, not an improvement.
|
||
|
||
**Rule for movement arms: hit rate alone INVERTS the verdict.** A movement change alters range,
|
||
shots fired and round length simultaneously, so the objective metrics are **damage/run and
|
||
round-win rate (survival)**. Report all three. (For gun/selection arms, where range and round
|
||
length are held fixed, real hit rate remains the right ground truth.)
|
||
|
||
## Heat taming is the knob that moves the tradeoff
|
||
|
||
Tamed heat (`corridor 5 / wall 10`) is what lets the range weighting pull the bot to ~240px
|
||
(20.28% / 6 wins). The original heat (`20/30`) keeps it at ~400px (11.73% / 15 wins, only p=0.0303
|
||
above base). So heat taming buys hit rate at the cost of survival — **a knob to keep conservative**,
|
||
and the reason the ring is not the default.
|
||
|
||
Nothing to revert: the shipped movement is already `tfil`, and the ring is opt-in.
|
||
|
||
## Caveats
|
||
|
||
- **Only adversary: the real DrussGT jar** (the shim only hosts `jk.mega.DrussGT`). The ring-vs-winning
|
||
result should be re-checked on other bots before any final decision.
|
||
- `base` (10.61%) is consistent with the committed `onlyPattern` result (9.99%), validating the
|
||
frozen binary and pipeline.
|
||
- The observability envs (`TR_POWER_LOG`, `TR_RAM_LOG`, `TR_MOVEMENT_LOG`) were enabled uniformly
|
||
across all arms; they gate `echo` only and cannot alter decisions.
|