ramming: make the decision PROACTIVE, with a bullet-rain abort
The user watched 1v1 and melee runs and saw ram opportunities arise that the bot
declined: "there were moments where the bot could jump over the enemy and shred
it but shot it down instead."
DIAGNOSIS - a chicken-and-egg loop. `ramOpportunity` required dist < 50px, but
an offline measurement over 15 rounds vs DrussGT found the closest approach was
118.7px and the <50px trigger had NEVER fired: the mover has no reason to close,
so the trigger waited for a proximity nothing created. The MECHANISM to close
already existed (the ring mover expresses a ram as band=(0,50)); what was
missing was a decision that fires at a range the bot can actually close from.
Changes:
- opportunity gate relaxed: dist 50 -> TR_RAM_OPP_DIST (200), energy margin
+30 -> TR_RAM_OPP_MARGIN (15). Both env-tunable, no rebuild needed.
- New pure module `common_libs/movements/ram_decision.nim` holding the trigger
and abort logic (no battle/API deps), so it is unit-testable.
- BULLET-RAIN ABORT (the user asked for this earlier): `onHitByBullet` now
accumulates `e.bullet.power` into a 15-turn ring; damageRatePerTurn = sum/15;
an in-progress ram aborts when rate > TR_RAM_ABORT_DMG (0.5/turn). On abort:
isRamming=false, cooldown 30, TARGET KEPT, and the mover returns to the normal
range band. It never stops the bot.
- Opt-in, DEFAULT-OFF `plan` trigger for "change of plan when the gun duel is
failing" (dist<250, margin+20, selected gun's pooled virtual rate < 0.05).
Left off because a cold gun reads 0.0 and would qualify - speculative.
- `TR_RAM_LOG=1` change-gated line: `[ram] ON reason=opportunity dist=143
selfE=78 enemyE=41 cap=3.0 band=[0,50]` / `[ram] OFF reason=bulletRain`.
- Existing cooldown/duration/stuck machinery untouched (stuck>10 or duration>60
-> abort + cooldown 30). A refactor bug that briefly DROPPED the
`ramCooldownTicks == 0` gate was caught and fixed.
Trigger set (first match wins): finisher (dist<300, enemy<20, we are healthier);
opportunity (dist<200, we lead by 15+); desperation (both <5, dist<150); plan
(off). All require enemy>0, a valid target, and no cooldown.
Proof the intent now fires at a closable distance (28/28 unit checks):
PASS: opportunity fires at dist 143 with a 37-energy lead <- the exact case
PASS: old gate (dist<50, margin+30) does NOT fire at 143 <- the old bug
PASS: fires at 199px / does NOT fire at 201px
+ margin, finisher priority, desperation, plan on/off, window mean, abort
threshold checks.
HONEST FRAMING: ram damage is 0.6 per CONTACT EVENT, one-shot (collision
resolution rewinds positions so contacts do not stream) - small next to a p=3.0
bullet hit (16). The payoff is that point-blank forces hit probability toward 1,
so heavy bullets stop missing and E[dE]=p(3P-1) turns positive above P=1/3; ram
damage also scores 2.0/point (highest in the game) and a ram kill carries a 0.30
bonus vs 0.20. So this is "force the fight to point-blank", not "the ram shreds
them". Base rate is rare (2 collisions in the whole fixture corpus).
Guards: test_ram_decision 28 (new), test_power_policy 26, test_gun_harness 39,
test_vbullet_metric 11, test_power_selection 3, test_adaptive_radar 41,
test_tfil_ring_weights 24. Compiles (release).
UNVERIFIED: the live effect. No A/B has run, and whether the mover actually
reaches contact is unproven.
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
## Pure unit guard for the ram decision (`movements/ram_decision.nim`).
|
||||
##
|
||||
## NO battle, NO Java, NO server. Run with:
|
||||
## nim c -r common_libs/tests/test_ram_decision.nim
|
||||
##
|
||||
## These pin the two contracts the proactive-ram change depends on:
|
||||
## 1. the opportunity trigger can fire at a distance the mover can actually
|
||||
## close FROM (the old `dist < 50` gate could not — the closest measured
|
||||
## approach vs DrussGT was 118.7px, so it never fired);
|
||||
## 2. the bullet-rain abort only applies to an IN-PROGRESS ram and is driven by
|
||||
## the windowed incoming-damage rate.
|
||||
|
||||
import std/math
|
||||
import movements/ram_decision
|
||||
|
||||
var failures = 0
|
||||
proc check(name: string, ok: bool) =
|
||||
if ok: echo "PASS: ", name
|
||||
else: echo "FAIL: ", name; inc failures
|
||||
|
||||
# ── 1. the shipped defaults are the RELAXED gates ────────────────────────────
|
||||
|
||||
proc testDefaults() =
|
||||
check "default opportunity distance is relaxed to 200 (was 50)",
|
||||
DefaultRamOppDist == 200.0
|
||||
check "default opportunity margin is relaxed to 15 (was 30)",
|
||||
DefaultRamOppMargin == 15.0
|
||||
check "default bullet-rain abort threshold is 0.5/turn",
|
||||
DefaultRamAbortDmg == 0.5
|
||||
check "change-of-plan hit-rate bar defaults to 0.05",
|
||||
DefaultRamPlanHitRate == 0.05
|
||||
|
||||
# ── 2. opportunity fires at a closable distance (the user's scenario) ────────
|
||||
|
||||
proc testOpportunityAtClosableRange() =
|
||||
# The exact shape the diagnosis used: dist 143, we are clearly healthier.
|
||||
let inp = RamInputs(dist: 143.0, selfEnergy: 78.0, enemyEnergy: 41.0, gunHitRate: 0.0)
|
||||
check "opportunity fires at dist 143 with a 37-energy lead",
|
||||
ramTrigger(inp) == rrOpportunity
|
||||
# Same state under the OLD gate must NOT fire: this is the chicken-and-egg.
|
||||
check "old gate (dist<50, margin+30) does NOT fire at dist 143",
|
||||
ramTrigger(inp, oppDist = 50.0, oppMargin = 30.0) == rrNone
|
||||
# Just inside the new distance edge fires; just outside does not.
|
||||
check "opportunity fires at 199px", ramTrigger(inp, oppDist = 200.0) == rrOpportunity
|
||||
check "opportunity does NOT fire at 201px",
|
||||
ramTrigger(RamInputs(dist: 201.0, selfEnergy: 78.0, enemyEnergy: 41.0)) == rrNone
|
||||
|
||||
# ── 3. the energy margin is env-tunable and enforced ─────────────────────────
|
||||
|
||||
proc testMargin() =
|
||||
# 9-energy lead is below the default 15 margin, so no opportunity...
|
||||
let thin = RamInputs(dist: 143.0, selfEnergy: 50.0, enemyEnergy: 41.0)
|
||||
check "9-energy lead does NOT trigger at the default margin",
|
||||
ramTrigger(thin) == rrNone
|
||||
# ...but a looser margin (sweepable via TR_RAM_OPP_MARGIN) lets it fire.
|
||||
check "9-energy lead fires with a 5-energy margin",
|
||||
ramTrigger(thin, oppMargin = 5.0) == rrOpportunity
|
||||
# A stricter margin vetoes a lead that the default accepts.
|
||||
check "37-energy lead vetoed by a 40-energy margin",
|
||||
ramTrigger(RamInputs(dist: 143.0, selfEnergy: 78.0, enemyEnergy: 41.0),
|
||||
oppMargin = 40.0) == rrNone
|
||||
|
||||
# ── 4. finisher + desperation keep working and take priority ─────────────────
|
||||
|
||||
proc testFinisherAndDesperation() =
|
||||
# Finisher: enemy nearly dead, we are healthier, proactive distance.
|
||||
check "finisher fires at dist 250 against a 12-energy enemy",
|
||||
ramTrigger(RamInputs(dist: 250.0, selfEnergy: 60.0, enemyEnergy: 12.0)) == rrFinisher
|
||||
# Finisher outranks opportunity when both match.
|
||||
check "finisher takes priority over opportunity",
|
||||
ramTrigger(RamInputs(dist: 40.0, selfEnergy: 80.0, enemyEnergy: 12.0)) == rrFinisher
|
||||
# Desperation: both nearly dead, short range.
|
||||
check "desperation fires when both are nearly dead",
|
||||
ramTrigger(RamInputs(dist: 100.0, selfEnergy: 3.0, enemyEnergy: 3.0)) == rrDesperation
|
||||
# Dead enemy never triggers anything.
|
||||
check "zero enemy energy never triggers",
|
||||
ramTrigger(RamInputs(dist: 20.0, selfEnergy: 100.0, enemyEnergy: 0.0)) == rrNone
|
||||
|
||||
# ── 5. change-of-plan trigger is opt-in and needs a failing gun ──────────────
|
||||
|
||||
proc testPlanTrigger() =
|
||||
# dist 220 is OUTSIDE the opportunity gate (200) but inside the plan gate
|
||||
# (250), so only the change-of-plan trigger can match here.
|
||||
let inp = RamInputs(dist: 220.0, selfEnergy: 80.0, enemyEnergy: 50.0, gunHitRate: 0.01)
|
||||
# Disabled (the default): the plan trigger must NOT fire.
|
||||
check "plan trigger does not fire while disabled",
|
||||
ramTrigger(inp, planEnabled = false) == rrNone
|
||||
# Enabled + poor gun hit rate: fires.
|
||||
check "plan trigger fires when enabled and the gun rate is poor",
|
||||
ramTrigger(inp, planEnabled = true) == rrPlan
|
||||
# Enabled + good gun hit rate: does not fire.
|
||||
check "plan trigger does not fire when the gun rate is healthy",
|
||||
ramTrigger(RamInputs(dist: 220.0, selfEnergy: 80.0, enemyEnergy: 50.0,
|
||||
gunHitRate: 0.30),
|
||||
planEnabled = true) == rrNone
|
||||
# Enabled but not out-energising enough: does not fire.
|
||||
check "plan trigger does not fire without the energy lead",
|
||||
ramTrigger(RamInputs(dist: 220.0, selfEnergy: 55.0, enemyEnergy: 50.0,
|
||||
gunHitRate: 0.01),
|
||||
planEnabled = true) == rrNone
|
||||
|
||||
# ── 6. the incoming-damage window ────────────────────────────────────────────
|
||||
|
||||
proc testDamageRate() =
|
||||
check "empty window -> 0.0/turn", damageRatePerTurn(newSeq[float]()) == 0.0
|
||||
check "all-zero window -> 0.0/turn", damageRatePerTurn([0.0, 0.0, 0.0]) == 0.0
|
||||
check "window mean is exact",
|
||||
abs(damageRatePerTurn([0.0, 0.6, 0.0, 1.2, 0.0]) - 0.36) < 1e-12
|
||||
check "one hit in a 15-turn window reads low (0.04/turn)",
|
||||
abs(damageRatePerTurn([0.6, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
||||
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) - 0.04) < 1e-12
|
||||
|
||||
proc testAbort() =
|
||||
check "no abort before the ram has started",
|
||||
not shouldAbortRam(ramming = false, dmgRate = 5.0)
|
||||
check "no abort at exactly the threshold",
|
||||
not shouldAbortRam(ramming = true, dmgRate = 0.5)
|
||||
check "abort above the threshold",
|
||||
shouldAbortRam(ramming = true, dmgRate = 0.51)
|
||||
check "threshold is overridable",
|
||||
shouldAbortRam(ramming = true, dmgRate = 0.51, abortDmg = 0.6) == false and
|
||||
shouldAbortRam(ramming = true, dmgRate = 0.61, abortDmg = 0.6) == true
|
||||
|
||||
proc testReasonNames() =
|
||||
check "reason names match the log vocabulary",
|
||||
reasonName(rrNone) == "none" and reasonName(rrFinisher) == "finisher" and
|
||||
reasonName(rrOpportunity) == "opportunity" and
|
||||
reasonName(rrDesperation) == "desperation" and reasonName(rrPlan) == "plan"
|
||||
|
||||
# ── driver ───────────────────────────────────────────────────────────────────
|
||||
|
||||
testDefaults()
|
||||
testOpportunityAtClosableRange()
|
||||
testMargin()
|
||||
testFinisherAndDesperation()
|
||||
testPlanTrigger()
|
||||
testDamageRate()
|
||||
testAbort()
|
||||
testReasonNames()
|
||||
|
||||
if failures > 0:
|
||||
echo "\n", failures, " check(s) FAILED"
|
||||
quit(1)
|
||||
echo "\nAll ram-decision checks passed."
|
||||
Reference in New Issue
Block a user