Commit Graph

2 Commits

Author SHA1 Message Date
SirStone eb74f9b2e3 Ram: finisher-only by default, and the bullet-rain abort now measures real energy
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.
2026-09-22 08:14:03 +02:00
SirStone bfdcdf8919 The three unmeasured features, A/B'd - and a methodology correction I had wrong
Five arms x 7 runs x 7 rounds (35 real-DrusGT battles, 8 concurrent), one frozen
binary from git archive HEAD at 185a32e (includes the shipped Pattern-only rack),
env knobs only, server-side event sidecar, exact two-sided permutation tests.

  arm                   real %   dmg/run   survival(rounds won)   p vs base
  base (shipped)         10.61    284.1     16/49 (32.7%)          --
  nopower (policy off)    7.88    247.1      9/49 (18.4%)          0.0012
  oldram (old gates)     10.78    279.4     17/49 (34.7%)          0.6888
  ring (tfil_ring)       20.28    267.4      6/49 (12.2%)          0.0006
  ringhot (orig heat)    11.73    285.3     15/49 (30.6%)          0.0303
Every round ends with exactly one death (0 timeouts), so survival = round win.

1. POWER POLICY HELPS - KEEP. Turning it off drops real hit rate 10.61 -> 7.88
   (p=0.0012), damage/run 284 -> 247, and wins FEWER rounds (16 -> 9). The cap
   trades per-shot damage for many more shots and a higher per-shot rate; that
   trade is a clear win. This was shipped on unit tests alone until now.

2. PROACTIVE RAMMING - 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 live
   (6 `opportunity` ON events vs 0 under the old 50px/+30 gates; base reached
   <40px on 10 ticks vs 0) but converts to essentially no extra collisions and no
   measurable outcome change. Safe to keep, but it is not earning its keep and
   reverting it is equally defensible.

3. RING MOVER HURTS THE OBJECTIVE - DO NOT SHIP. And this is the important one.

*** METHODOLOGY CORRECTION - I HAD THIS WRONG ALL NIGHT ***
The ring arm has the BEST hit rate of anything measured tonight: 20.28% vs 10.61%
(+9.67pp, p=0.0006, non-overlapping). Read alone it says "ship it immediately".
It is a CONFOUND. The ring halves engagement range (median 460 -> 240px), which
halves round length (1542 -> 636 ticks) and shots (4466 -> 1834). So damage/run is
FLAT (284 -> 267) while survival/round-win MORE THAN HALVES (16/49 -> 6/49,
p=0.0122). It is a GLASS CANNON: same damage dealt, twice as many deaths. The
hit-rate gain is a geometric artefact of fighting closer, not an improvement.
** 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. I had been enforcing the hit-rate-only rule
without qualification; it is now qualified.

HEAT TAMING is the knob that moves the tradeoff: tamed (corridor 5 / wall 10)
lets the range weighting pull to ~240px (20.28% / 6 wins); original (20/30) keeps
it at ~400px (11.73% / 15 wins). 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 recorded: only adversary is the real DrussGT jar (the shim hosts only
jk.mega.DrussGT), so the ring-vs-winning result needs re-checking elsewhere; and
base (10.61%) is consistent with the committed onlyPattern result (9.99%),
validating the frozen binary and pipeline.

Adds docs/feature_ab_results.md. No source files changed by this job.
2026-09-22 02:34:05 +02:00