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.
This commit is contained in:
@@ -178,7 +178,6 @@ type
|
||||
ringMover: TFILRingModule
|
||||
rammer: RammerModule
|
||||
isRamming: bool
|
||||
ramStuckTicks: int
|
||||
ramDurationTicks: int
|
||||
ramCooldownTicks: int
|
||||
ramDmgWindow: array[RamDamageWindow, float] ## per-turn incoming damage
|
||||
@@ -560,7 +559,10 @@ method onBulletHitBullet*(bot: ModularBot, e: BulletHitBulletEvent) =
|
||||
bot.ringMover.removeBulletNear(e.bullet.x, e.bullet.y)
|
||||
|
||||
method onHitByBullet*(bot: ModularBot, e: HitByBulletEvent) =
|
||||
bot.ramDmgAccum += e.bullet.power # feeds the ram bullet-rain abort window
|
||||
# Feeds the ram bullet-rain abort window. Accumulate REAL ENERGY (the server's
|
||||
# 4p/6p-2 damage), not raw firepower, so `damageRatePerTurn` is a real
|
||||
# energy/turn rate (see movements/ram_decision.bulletDamage).
|
||||
bot.ramDmgAccum += bulletDamage(e.bullet.power)
|
||||
bot.moveTracker.registerHit(e.bullet.power, e.bullet.direction, getX(), getY())
|
||||
|
||||
method onRoundEnded*(bot: ModularBot, e: RoundEndedEventForBot) =
|
||||
@@ -719,7 +721,6 @@ method onRoundStarted*(bot: ModularBot, e: RoundStartedEvent) =
|
||||
# next round cannot inherit the previous round's overfit net.
|
||||
bot.tmPattern.resetLearning()
|
||||
bot.isRamming = false
|
||||
bot.ramStuckTicks = 0
|
||||
bot.ramDurationTicks = 0
|
||||
bot.ramCooldownTicks = 0
|
||||
for i in 0 ..< RamDamageWindow: bot.ramDmgWindow[i] = 0.0
|
||||
@@ -875,7 +876,7 @@ method run*(bot: ModularBot) =
|
||||
|
||||
# Ram decision — harness decides, not the movement module (uses fresh lastState).
|
||||
# Triggers + abort live in the pure `ram_decision` module so they can be
|
||||
# swept/unit-tested; the cooldown/duration/stuck machinery stays here.
|
||||
# swept/unit-tested; the cooldown/duration machinery stays here.
|
||||
let ws = bot.lastState
|
||||
let ramDist = hypot(ws.enemyX - ws.selfX, ws.enemyY - ws.selfY)
|
||||
let ramDmgRate = damageRatePerTurn(bot.ramDmgWindow)
|
||||
@@ -896,13 +897,11 @@ method run*(bot: ModularBot) =
|
||||
|
||||
if shouldRam and not bot.isRamming:
|
||||
bot.isRamming = true
|
||||
bot.ramStuckTicks = 0
|
||||
bot.ramDurationTicks = 0
|
||||
elif abortRam and bot.isRamming:
|
||||
# Bullet rain: drop the ram, return to the normal range band, KEEP the
|
||||
# target. The cooldown stops an immediate re-trigger while fire persists.
|
||||
bot.isRamming = false
|
||||
bot.ramStuckTicks = 0
|
||||
bot.ramDurationTicks = 0
|
||||
bot.ramCooldownTicks = 30
|
||||
bot.logRamChange(false, rrNone, ramDist, ws, ramDmgRate, "bulletRain")
|
||||
@@ -910,23 +909,15 @@ method run*(bot: ModularBot) =
|
||||
bot.isRamming = false
|
||||
bot.logRamChange(false, rrNone, ramDist, ws, ramDmgRate, "triggerGone")
|
||||
|
||||
# Only the 60-tick duration cap can end a ram by itself. The old `stuck`
|
||||
# counter required dist < 5px on consecutive ticks, but contact happens at
|
||||
# ~36px (two 18px radii) and positions rewind, so it could never increment;
|
||||
# it has been removed (see docs/ramming_negative_result.md).
|
||||
if bot.isRamming:
|
||||
inc bot.ramDurationTicks
|
||||
if ramDist < 5.0:
|
||||
inc bot.ramStuckTicks
|
||||
else:
|
||||
bot.ramStuckTicks = 0
|
||||
if bot.ramStuckTicks > 10:
|
||||
if bot.ramDurationTicks > 60:
|
||||
bot.isRamming = false
|
||||
bot.currentTargetId = -1
|
||||
bot.ramStuckTicks = 0
|
||||
bot.ramDurationTicks = 0
|
||||
bot.ramCooldownTicks = 30
|
||||
bot.logRamChange(false, rrNone, ramDist, ws, ramDmgRate, "stuck")
|
||||
elif bot.ramDurationTicks > 60:
|
||||
bot.isRamming = false
|
||||
bot.currentTargetId = -1
|
||||
bot.ramStuckTicks = 0
|
||||
bot.ramDurationTicks = 0
|
||||
bot.ramCooldownTicks = 30
|
||||
bot.logRamChange(false, rrNone, ramDist, ws, ramDmgRate, "duration")
|
||||
|
||||
Reference in New Issue
Block a user