Files
SirRoboGarage/docs/ramming_negative_result.md
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.2 KiB

Proactive ramming: a negative result (do not re-attempt straight-line pursuit)

Question. The bot declined visible ram opportunities ("it could jump over the enemy and shred it"). The proactive opportunity gate was relaxed to fire from a closable range (dist < 200, energy lead > 15, was dist < 50). Did it convert? No — a log-replay diagnosis (no new battles), 49 rounds/arm: it fires, never reaches contact.

The five measurements

# Diagnostic Result
1 opportunity -> contact 0/6 (base), 0/40 (ring), 0/12 (ringhot), 0/1 (nopower)
2 abort reasons triggerGone 94/108 (87%), duration 14, stuck 0, bulletRain 0
3 closest approach in an opportunity episode never below ~80 px; base closest median 126 px
4 ticks spent during ram <40: 10, 40-80: 16, 80-120: 60, 120-200: 165, >=200: 85
5 A/B, oldram vs base p=0.69, damage 279 vs 284, survival 17/49 vs 16/49 (p=1.0)

The only proactive conversion in the corpus came from a finisher (enemy at 16 -> 1 energy), not an opportunity. Two smoking guns: a base episode ran the FULL 60-tick duration cap and closed only 198 -> 171 px; a perfectly aligned (delta ~= 0 deg), full-speed-8 episode closed 195 -> 114 px then PLATEAUED.

Why it cannot work (the geometric reason)

Both bots have MAX_SPEED = 8. A straight-line pursuit can never catch an evading equal-speed opponent: every turn spent turning is a turn the evader pulls away, and any juke re-opens the gap. Closing requires interception (cutting off the escape path) or cornering, not chasing. That is a movement problem, and it is disproportionate to 0.6 damage per contact (one-shot; NOT 0.6/turn). Corroboration: cornering was independently refuted (wall-adjacent enemies are less predictable; closest approach to DrussGT in 15 rounds was 118.7 px). Both routes to point-blank have now failed for the same underlying reason.

The one exception: the finisher

The finisher (enemy < 20 energy, dist < 300, we are healthier) DOES convert, because a <20-energy DrussGT stops fleeing: that episode closed at 6-8 px/tick and reached contact. It is kept and is now the default gate. The rare desperation case (both < 5 energy, dist < 150) is kept as a cheap last-ditch play. opportunity and plan are OFF by default.

Re-enable for re-testing: TR_RAM_OPPORTUNITY=1 (with TR_RAM_OPP_DIST, TR_RAM_OPP_MARGIN) restores the proactive gate; TR_RAM_PLAN=1 restores the change-of-plan trigger. Verified live: the same ModularBot-vs-RamFire battle logs 0 opportunity ON by default and 4 opportunity ON with TR_RAM_OPPORTUNITY=1.

The two dead code paths, dealt with

  1. bulletRain abort — units fixed. onHitByBullet accumulated bullet firepower, while TR_RAM_ABORT_DMG = 0.5/turn implied a damage rate. The server's damage is calcBulletDamage(p) = 4p (plus 2*(p-1) above 1), so the abort was ~4x too insensitive and never fired (max observed dmgRate 0.27 in all 108 OFF lines). It now accumulates real energy via bulletDamage() and the threshold is TR_RAM_ABORT_DMG = 2.0 energy/turn over 15 turns (~30 HP) — the same effective bar for normal firepower, now in correct units. The live run reports dmgRate=1.07/turn (was 0.27) and does not abort the finisher.
  2. ramStuckTicks — removed. It required dist < 5 px on consecutive ticks, but two 18 px radii make contact occur at ~36 px and collision resolution rewinds positions — so the closest approach is ~36 px and the counter could never increment (diagnostic: stuck 0). Removed rather than documented as protection it cannot provide; the 60-tick duration cap is the only self-abort.

Do not re-attempt a proactive straight-line ram

Do not re-relax the opportunity gate, add "get closer" logic to satisfy it, or add a new chase trigger. Any future attempt at forcing point-blank must be an interception/cornering movement problem (predict the escape path and occupy it), and must clear the same opportunity -> contact bar above. Proactive ramming is worth at most 0.6 damage per contact and measured zero outcome change.