Files
SirRoboGarage/docs/feature_ab_results.md
T
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

4.1 KiB
Raw Blame History

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.