Energy economy: the cliff becomes a SLOPE, plus a finishing cap. 11% less energy.
The user's request: "when our bot is low OR enemy is low, it is useless to use high power instead low fast bullets have more chances to finish the enemy. Let's do a math slope: starting from some health down, the power goes down with it." 1. ENERGY SLOPE (`TR_POWER_ENERGY_*`), replacing the old hard step at 50 energy: cap = ENERGY_MAX at/above ENERGY_HI, ENERGY_MIN at/below ENERGY_LO, LINEAR in power between, clamped. Defaults HI=80 LO=20 MIN=0.5 MAX=3.0, so no cap >=80, 0.5 at <=20, and e.g. E=65 -> 2.375, E=50 -> 1.75, E=35 -> 1.125. Rationale: bullet speed is 20-3p, so lower power = FASTER bullet (less lead error, higher hit chance), fires more often (10+2p) and drains slower (p/shot). E[dE] = p(3P-1) => break-even hit probability is 1/3 INDEPENDENT of power, and our measured rates are 5-27%, far below it. 2. FINISHING CAP (`TR_POWER_FINISH_KILL`, default ON): cap power at the SMALLEST bullet that still removes the enemy's remaining energy - `E<=4 -> p=E/4` (min 0.1), `4<E<=16 -> p=(E+2)/6`, `E>16 -> no cap`. Rationale, and it makes the user's instinct stronger than a heuristic: server 1.3.1 caps the damage SCORE at the energy ACTUALLY REMOVED, so overkill is WASTED damage AND ~6x the energy for ZERO extra score. Damage is 4p (p<=1) / 6p-2 (p>1). Both are min-composed with the existing far/below-average caps, may only LOWER power (exhaustively tested), and are exempt while ramming. `TR_POWER_POLICY=0` still returns the uncapped control exactly. MEASURED ENERGY SAVING (offline replay of the DrussGT fixtures, 28,797 ticks): arm shots energy meanP E/1k ticks vs cliff control(uncapped) 1913 4646 2.43 161.4 -90.2% cliff (today) 2363 2443 1.03 84.8 0.0% slope 2404 2178 0.91 75.6 ** 10.9% LESS ** slope+finish 2404 2167 0.90 75.3 ** 11.3% LESS ** So the slope spends ~11% less energy than the cliff AND fires slightly MORE shots (2404 vs 2363) - both directions at once. HONEST NOTE on the finishing rule's reach here: ticks where the enemy is low (0 < E <= 16) are only 2252/28797 = 7.8% of these fixtures, so finishing adds just ~11 energy of saving against DrussGT. It matters in CLOSER fights, not this one. Verification: test_power_policy 58 (was 26) in BOTH the default and TR_POWER_POLICY=0 control arms - slope at E=100/80/65/50/35/20/5, powerToKill across E=0.1..100, the inverse-cover property for E<=16, monotonicity, ram exemption, and an exhaustive sweep proving power <= preference. Guards: test_gun_harness 39, test_vbullet_metric 11, test_power_selection 3, test_adaptive_radar 41, test_tfil_ring_weights 24, test_ram_decision 40, test_rack_membership 48, test_selector_tiebreak 19, test_tm_pattern_registration 20, test_vbullet_admit_gate 12. acceptance 12/12 PASS. ModularBot compiles release. Adds `common_libs/tests/measure_power_policy.nim` (the energy/histogram tool) and updates docs/env_reference.md for the new `energySlope|finishKill` log reasons. NOT MEASURED: the battle/hit-rate effect. The offline figures use the fixture shooter's energy as a proxy, open-loop; the RELATIVE saving is the meaningful part.
This commit is contained in:
@@ -1,10 +1,16 @@
|
||||
## Unit guard for the energy-aware power policy (TR_POWER_*).
|
||||
##
|
||||
## The policy is a CAP on the gun's own preferred bin: at long range or low
|
||||
## energy it trades single-hit damage for a faster bullet and more shots, and it
|
||||
## withholds power 3.0 unless the gun's own chances for the chosen bin are above
|
||||
## that gun's average. It must NEVER raise power, and `TR_POWER_POLICY=0` must
|
||||
## reproduce the uncapped preference exactly (the control arm).
|
||||
## The policy is a CAP on the gun's own preferred bin. Every rule can only LOWER
|
||||
## power; `power = min(preference, min(all caps))`. The two energy-economy rules
|
||||
## under test here are:
|
||||
##
|
||||
## * the ENERGY SLOPE — a linear cap on OUR energy (`energySlopeCap`), replacing
|
||||
## the old hard cliff at `TR_POWER_LOW_ENERGY` (50);
|
||||
## * the FINISHING SLOPE — cap power at the smallest bullet that still removes
|
||||
## the ENEMY's remaining energy (`powerToKill`), because server 1.3.1 caps the
|
||||
## `bulletDamage` score at the energy actually removed, so overkill is wasted.
|
||||
##
|
||||
## `TR_POWER_POLICY=0` must reproduce the uncapped preference exactly (control).
|
||||
##
|
||||
## Pure: no Java, no battle. Run:
|
||||
## nim c -r common_libs/tests/test_power_policy.nim
|
||||
@@ -20,6 +26,8 @@ proc check(name: string, ok: bool) =
|
||||
if ok: echo "PASS: ", name
|
||||
else: echo "FAIL: ", name; inc failures
|
||||
|
||||
proc close(a, b: float, eps = 1e-9): bool = abs(a - b) < eps
|
||||
|
||||
proc recordHit(fw: var FitnessWindow, hit: bool) =
|
||||
fw.hits[fw.head] = hit
|
||||
fw.head = (fw.head + 1) mod WindowSize
|
||||
@@ -32,7 +40,92 @@ proc seedWindow(t: var VirtualTracker, targetId, gunId, binIdx, hits, misses: in
|
||||
for _ in 0..<hits: recordHit(fw[], true)
|
||||
for _ in 0..<misses: recordHit(fw[], false)
|
||||
|
||||
# ── pure cap core ────────────────────────────────────────────────────────────
|
||||
# ── CHANGE 1: the energy slope (pure formula) ────────────────────────────────
|
||||
# Defaults: capMax 3.0 at/above 80, capMin 0.5 at/below 20, linear between.
|
||||
|
||||
proc testEnergySlope() =
|
||||
# Above / at HI: no cap from this rule.
|
||||
check "energy slope: selfE=100 -> cap 3.0 (no cap)",
|
||||
close(energySlopeCap(100.0, 80.0, 20.0, 0.5, 3.0), 3.0)
|
||||
check "energy slope: selfE=80 (HI) -> cap 3.0",
|
||||
close(energySlopeCap(80.0, 80.0, 20.0, 0.5, 3.0), 3.0)
|
||||
# At/below LO: the floor cap.
|
||||
check "energy slope: selfE=20 (LO) -> cap 0.5",
|
||||
close(energySlopeCap(20.0, 80.0, 20.0, 0.5, 3.0), 0.5)
|
||||
check "energy slope: selfE=5 -> cap 0.5 (clamped at LO)",
|
||||
close(energySlopeCap(5.0, 80.0, 20.0, 0.5, 3.0), 0.5)
|
||||
# Linear interpolation in POWER.
|
||||
check "energy slope: selfE=50 -> cap 1.75 (halfway 0.5..3.0)",
|
||||
close(energySlopeCap(50.0, 80.0, 20.0, 0.5, 3.0), 1.75)
|
||||
check "energy slope: selfE=65 -> cap 2.375 (t=0.75)",
|
||||
close(energySlopeCap(65.0, 80.0, 20.0, 0.5, 3.0), 2.375)
|
||||
check "energy slope: selfE=35 -> cap 1.125 (t=0.25)",
|
||||
close(energySlopeCap(35.0, 80.0, 20.0, 0.5, 3.0), 1.125)
|
||||
# A non-default top: the interpolation endpoint is capMax, not hardcoded 3.0.
|
||||
check "energy slope: custom capMax=2.0 at HI",
|
||||
close(energySlopeCap(80.0, 80.0, 20.0, 0.5, 2.0), 2.0)
|
||||
|
||||
proc testEnergySlopeMonotone() =
|
||||
# The cap must be non-decreasing in our energy (more energy never caps lower).
|
||||
var mono = true
|
||||
var prev = -1.0
|
||||
for i in 0..20:
|
||||
let e = i.float * 5.0
|
||||
let c = energySlopeCap(e, 80.0, 20.0, 0.5, 3.0)
|
||||
if c < prev - 1e-12: mono = false
|
||||
prev = c
|
||||
check "energy slope: cap is non-decreasing in self energy", mono
|
||||
|
||||
# ── CHANGE 2: the finishing slope (pure formula) ─────────────────────────────
|
||||
|
||||
proc testPowerToKill() =
|
||||
# E <= 4: p = E/4, clamped to >= 0.1.
|
||||
check "finish: E=0.1 -> p=0.1 (clamp)",
|
||||
close(powerToKill(0.1), 0.1)
|
||||
check "finish: E=0.4 -> p=0.1 (clamp)",
|
||||
close(powerToKill(0.4), 0.1)
|
||||
check "finish: E=1 -> p=0.25",
|
||||
close(powerToKill(1.0), 0.25)
|
||||
check "finish: E=2 -> p=0.5",
|
||||
close(powerToKill(2.0), 0.5)
|
||||
check "finish: E=4 -> p=1.0 (branch seam)",
|
||||
close(powerToKill(4.0), 1.0)
|
||||
# E > 4: p = (E+2)/6.
|
||||
check "finish: E=6 -> p=4/3",
|
||||
close(powerToKill(6.0), 4.0 / 3.0)
|
||||
check "finish: E=10 -> p=2.0",
|
||||
close(powerToKill(10.0), 2.0)
|
||||
check "finish: E=16 -> p=3.0",
|
||||
close(powerToKill(16.0), 3.0)
|
||||
# E > 16: no cap (p=3.0 removes only 16).
|
||||
check "finish: E=17 -> p=3.0 (no cap)",
|
||||
close(powerToKill(17.0), 3.0)
|
||||
check "finish: E=20 -> p=3.0 (no cap)",
|
||||
close(powerToKill(20.0), 3.0)
|
||||
check "finish: E=100 -> p=3.0 (no cap)",
|
||||
close(powerToKill(100.0), 3.0)
|
||||
|
||||
proc testPowerToKillCovers() =
|
||||
# The formula must be a TRUE inverse for the range it can cover (E <= 16);
|
||||
# above 16 even p=3.0 removes only 16, so there is no cap to find.
|
||||
var covers = true
|
||||
for i in 0..159:
|
||||
let e = 0.1 + i.float * 0.1 # 0.1 .. 16.0
|
||||
if bulletDamageAtPower(powerToKill(e)) < e - 1e-9: covers = false
|
||||
check "finish: powerToKill(E) always removes at least E (E in 0.1..16.0)",
|
||||
covers
|
||||
|
||||
proc testPowerToKillMonotone() =
|
||||
var mono = true
|
||||
var prev = -1.0
|
||||
for i in 0..200:
|
||||
let e = i.float * 0.1
|
||||
let p = powerToKill(e)
|
||||
if p < prev - 1e-12: mono = false
|
||||
prev = p
|
||||
check "finish: powerToKill is non-decreasing in enemy energy", mono
|
||||
|
||||
# ── the combined cap core ────────────────────────────────────────────────────
|
||||
|
||||
proc testFar() =
|
||||
let d = applyPowerPolicy(preferredPower = 3.0, dist = 250.0, selfEnergy = 100.0,
|
||||
@@ -40,17 +133,50 @@ proc testFar() =
|
||||
check "distance > TR_POWER_FAR_DIST -> cap 1.0 (far)",
|
||||
d.power == 1.0 and d.cap == 1.0 and d.reason == prFar
|
||||
|
||||
proc testLowEnergy() =
|
||||
let d = applyPowerPolicy(3.0, 100.0, 30.0, 0.9, 0.1, false, enabled = true)
|
||||
check "self energy < TR_POWER_LOW_ENERGY -> cap 1.0 (lowEnergy)",
|
||||
d.power == 1.0 and d.cap == 1.0 and d.reason == prLowEnergy
|
||||
proc testEnergySlopeCap() =
|
||||
# Close, above average, healthy enemy: only the energy slope binds.
|
||||
let d = applyPowerPolicy(3.0, 100.0, 50.0, 0.9, 0.1, false,
|
||||
enemyEnergy = 100.0, enabled = true)
|
||||
check "selfE=50 -> cap 1.75 (energySlope)",
|
||||
close(d.power, 1.75) and close(d.cap, 1.75) and d.reason == prEnergySlope
|
||||
let e = applyPowerPolicy(3.0, 100.0, 20.0, 0.9, 0.1, false,
|
||||
enemyEnergy = 100.0, enabled = true)
|
||||
check "selfE=20 -> cap 0.5 (energySlope)",
|
||||
close(e.power, 0.5) and e.reason == prEnergySlope
|
||||
let f = applyPowerPolicy(3.0, 100.0, 80.0, 0.9, 0.1, false,
|
||||
enemyEnergy = 100.0, enabled = true)
|
||||
check "selfE=80 -> no energy cap, full power", f.power == 3.0
|
||||
|
||||
proc testFinishingCap() =
|
||||
# Close, healthy self, above average, low enemy: only finishing binds.
|
||||
let d = applyPowerPolicy(3.0, 100.0, 100.0, 0.9, 0.1, false,
|
||||
enemyEnergy = 2.0, enabled = true, finishKill = true)
|
||||
check "enemyE=2 -> smallest killing bullet p=0.5 (finishKill)",
|
||||
close(d.power, 0.5) and close(d.cap, 0.5) and d.reason == prFinishKill
|
||||
let e = applyPowerPolicy(3.0, 100.0, 100.0, 0.9, 0.1, false,
|
||||
enemyEnergy = 10.0, enabled = true, finishKill = true)
|
||||
check "enemyE=10 -> p=2.0 (finishKill)",
|
||||
close(e.power, 2.0) and e.reason == prFinishKill
|
||||
let f = applyPowerPolicy(3.0, 100.0, 100.0, 0.9, 0.1, false,
|
||||
enemyEnergy = 16.0, enabled = true, finishKill = true)
|
||||
check "enemyE=16 -> p=3.0 (finish cap does not bite)", f.power == 3.0
|
||||
let g = applyPowerPolicy(3.0, 100.0, 100.0, 0.9, 0.1, false,
|
||||
enemyEnergy = 20.0, enabled = true, finishKill = true)
|
||||
check "enemyE=20 -> no finish cap", g.power == 3.0
|
||||
# Disabled finishing leaves power alone.
|
||||
let h = applyPowerPolicy(3.0, 100.0, 100.0, 0.9, 0.1, false,
|
||||
enemyEnergy = 2.0, enabled = true, finishKill = false)
|
||||
check "enemyE=2 with finishKill=false -> uncapped", h.power == 3.0
|
||||
# A dead/unknown target (energy 0) must NOT collapse power to 0.1.
|
||||
let i = applyPowerPolicy(3.0, 100.0, 100.0, 0.9, 0.1, false,
|
||||
enemyEnergy = 0.0, enabled = true, finishKill = true)
|
||||
check "enemyE=0 (dead) -> finish rule skipped, no collapse", i.power == 3.0
|
||||
|
||||
proc testBelowAverage() =
|
||||
# pEst == pRef is "not above average": withhold power 3.0 -> cap 2.0.
|
||||
let d = applyPowerPolicy(3.0, 100.0, 100.0, 0.2, 0.2, false, enabled = true)
|
||||
check "chances not above average -> cap 2.0 (belowAvg)",
|
||||
d.power == 2.0 and d.cap == 2.0 and d.reason == prBelowAvg
|
||||
# Strictly below also caps.
|
||||
let e = applyPowerPolicy(3.0, 100.0, 100.0, 0.1, 0.2, false, enabled = true)
|
||||
check "chances strictly below average -> cap 2.0 (belowAvg)",
|
||||
e.power == 2.0 and e.reason == prBelowAvg
|
||||
@@ -61,36 +187,58 @@ proc testAboveAverageFull() =
|
||||
d.power == 3.0 and d.cap == 3.0 and d.reason == prFull
|
||||
|
||||
proc testRamExempt() =
|
||||
# Far, low energy, no chance data: still full power because we are ramming.
|
||||
let d = applyPowerPolicy(3.0, 500.0, 5.0, 0.0, 0.9, true)
|
||||
check "ramming exempts the caps even far + low energy",
|
||||
# Far, low self energy, low enemy energy, no chance data: still full power.
|
||||
let d = applyPowerPolicy(3.0, 500.0, 5.0, 0.0, 0.9, true,
|
||||
enemyEnergy = 1.0, enabled = true, finishKill = true)
|
||||
check "ramming exempts far + energy slope + finishing",
|
||||
d.power == 3.0 and d.reason == prRam
|
||||
# Ram beats far even with the policy on.
|
||||
let e = applyPowerPolicy(3.0, 500.0, 5.0, 0.0, 0.9, true, enabled = true)
|
||||
check "ram exemption takes precedence over the far cap", e.power == 3.0
|
||||
|
||||
proc testCapNeverRaises() =
|
||||
# Preferred below every cap must pass through untouched.
|
||||
let a = applyPowerPolicy(1.0, 500.0, 5.0, 0.0, 0.9, false, enabled = true)
|
||||
check "far cap does not raise a preferred p=1.0", a.power == 1.0
|
||||
let b = applyPowerPolicy(1.5, 100.0, 100.0, 0.1, 0.2, false, enabled = true)
|
||||
check "mid cap does not raise a preferred p=1.5", b.power == 1.5
|
||||
let c = applyPowerPolicy(1.5, 100.0, 100.0, 0.9, 0.2, false, enabled = true)
|
||||
check "full cap does not raise a preferred p=1.5", c.power == 1.5
|
||||
# Caps may LOWER a preference (the new slope does exactly that here) but must
|
||||
# never raise it: power is always min(preference, cap).
|
||||
let a = applyPowerPolicy(1.0, 500.0, 5.0, 0.0, 0.9, false,
|
||||
enemyEnergy = 1.0, enabled = true, finishKill = true)
|
||||
check "all caps never raise a preferred p=1.0 (power = min(pref, cap))",
|
||||
a.power <= 1.0 and close(a.power, min(1.0, a.cap))
|
||||
let b = applyPowerPolicy(1.5, 100.0, 100.0, 0.1, 0.2, false,
|
||||
enemyEnergy = 20.0, enabled = true, finishKill = true)
|
||||
check "belowAvg cap does not raise a preferred p=1.5", b.power == 1.5
|
||||
# Exhaustive: for every cap combination, power <= preference.
|
||||
var never = true
|
||||
for pref in [0.5, 1.0, 1.5, 2.0, 3.0]:
|
||||
for selfE in [5.0, 20.0, 50.0, 80.0, 100.0]:
|
||||
for enemyE in [0.0, 0.5, 2.0, 10.0, 20.0]:
|
||||
for d in [50.0, 250.0]:
|
||||
let r = applyPowerPolicy(pref, d, selfE, 0.1, 0.2, false,
|
||||
enemyEnergy = enemyE, enabled = true,
|
||||
finishKill = true)
|
||||
if r.power > pref + 1e-12: never = false
|
||||
check "cap never raises power (exhaustive sweep)", never
|
||||
|
||||
proc testPrecedence() =
|
||||
# far beats low energy, and low energy beats belowAvg.
|
||||
let a = applyPowerPolicy(3.0, 500.0, 5.0, 0.0, 0.9, false, enabled = true)
|
||||
check "far takes precedence over low energy", a.reason == prFar
|
||||
let b = applyPowerPolicy(3.0, 100.0, 5.0, 0.0, 0.9, false, enabled = true)
|
||||
check "low energy takes precedence over belowAvg", b.reason == prLowEnergy
|
||||
# far (1.0) beats the energy slope (1.75 at selfE=50).
|
||||
let a = applyPowerPolicy(3.0, 500.0, 50.0, 0.0, 0.9, false,
|
||||
enemyEnergy = 100.0, enabled = true)
|
||||
check "far takes precedence over the energy slope", a.reason == prFar
|
||||
# the energy slope (1.75) beats belowAvg (2.0).
|
||||
let b = applyPowerPolicy(3.0, 100.0, 50.0, 0.0, 0.9, false,
|
||||
enemyEnergy = 100.0, enabled = true)
|
||||
check "energy slope takes precedence over belowAvg",
|
||||
b.reason == prEnergySlope
|
||||
# finishing (0.5 at enemyE=2) is smaller than far (1.0), so it wins.
|
||||
let c = applyPowerPolicy(3.0, 500.0, 100.0, 0.9, 0.1, false,
|
||||
enemyEnergy = 2.0, enabled = true, finishKill = true)
|
||||
check "finishing beats far when it is the smaller cap",
|
||||
close(c.power, 0.5) and c.reason == prFinishKill
|
||||
|
||||
proc testDisabledUncapped() =
|
||||
# enabled=false is the code path TR_POWER_POLICY=0 drives.
|
||||
for p in [1.0, 1.5, 2.0, 3.0]:
|
||||
let d = applyPowerPolicy(p, 500.0, 5.0, 0.0, 0.9, false, enabled = false)
|
||||
let d = applyPowerPolicy(p, 500.0, 5.0, 0.0, 0.9, false,
|
||||
enemyEnergy = 1.0, enabled = false, finishKill = true)
|
||||
check fmt"policy off reproduces the uncapped preference p={p:.1f}",
|
||||
d.power == p and d.cap == 3.0
|
||||
d.power == p and d.cap == 3.0 and d.reason == prFull
|
||||
|
||||
proc testEnvFlag() =
|
||||
# The flag is read once at module init, so the active branch is selected by
|
||||
@@ -98,9 +246,13 @@ proc testEnvFlag() =
|
||||
if PowerPolicyEnabled:
|
||||
check "TR_POWER_POLICY default (on): far shot is capped to 1.0",
|
||||
applyPowerPolicy(3.0, 300.0, 100.0, 0.5, 0.2, false).power == 1.0
|
||||
check "TR_POWER_POLICY default (on): low self energy is capped by the slope",
|
||||
applyPowerPolicy(3.0, 100.0, 20.0, 0.5, 0.2, false).power == 0.5
|
||||
else:
|
||||
check "TR_POWER_POLICY=0: far shot is NOT capped (control arm)",
|
||||
applyPowerPolicy(3.0, 300.0, 100.0, 0.5, 0.2, false).power == 3.0
|
||||
check "TR_POWER_POLICY=0: low self energy is NOT capped (control arm)",
|
||||
applyPowerPolicy(3.0, 100.0, 20.0, 0.5, 0.2, false).power == 3.0
|
||||
|
||||
proc testBinIndex() =
|
||||
check "binIndexForPower maps the shipped bins exactly",
|
||||
@@ -112,7 +264,8 @@ proc testBinIndex() =
|
||||
proc testReasonNames() =
|
||||
check "reason names match the documented log vocabulary",
|
||||
powerReasonName(prFar) == "far" and
|
||||
powerReasonName(prLowEnergy) == "lowEnergy" and
|
||||
powerReasonName(prEnergySlope) == "energySlope" and
|
||||
powerReasonName(prFinishKill) == "finishKill" and
|
||||
powerReasonName(prBelowAvg) == "belowAvg" and
|
||||
powerReasonName(prFull) == "full" and
|
||||
powerReasonName(prRam) == "ram"
|
||||
@@ -129,7 +282,8 @@ proc testTrackerAboveAverage() =
|
||||
seedWindow(t, 7, 0, 2, 10, 90)
|
||||
seedWindow(t, 7, 0, 3, 50, 50)
|
||||
let (g, _, p, d) = t.selectShotPolicy(7, tick = 0, dist = 100.0,
|
||||
selfEnergy = 100.0, ramming = false)
|
||||
selfEnergy = 100.0, enemyEnergy = 100.0,
|
||||
ramming = false)
|
||||
if PowerPolicyEnabled:
|
||||
check "tracker: above-average bin + close + healthy -> power 3.0",
|
||||
g == 0 and p == 3.0 and d.reason == prFull
|
||||
@@ -137,36 +291,61 @@ proc testTrackerAboveAverage() =
|
||||
check "tracker (control): above-average bin still fires power 3.0",
|
||||
g == 0 and p == 3.0
|
||||
|
||||
proc testTrackerEnergySlope() =
|
||||
var t = initTracker(1)
|
||||
seedWindow(t, 7, 0, 3, 50, 50)
|
||||
let (_, _, p, d) = t.selectShotPolicy(7, tick = 0, dist = 100.0,
|
||||
selfEnergy = 50.0, enemyEnergy = 100.0,
|
||||
ramming = false)
|
||||
if PowerPolicyEnabled:
|
||||
check "tracker: selfE=50 -> power 1.75 (energySlope)",
|
||||
close(p, 1.75) and d.reason == prEnergySlope
|
||||
else:
|
||||
check "tracker (control): selfE=50 keeps the uncapped preference (3.0)",
|
||||
p == 3.0
|
||||
|
||||
proc testTrackerFinishing() =
|
||||
var t = initTracker(1)
|
||||
seedWindow(t, 7, 0, 3, 50, 50)
|
||||
let (_, _, p, d) = t.selectShotPolicy(7, tick = 0, dist = 100.0,
|
||||
selfEnergy = 100.0, enemyEnergy = 2.0,
|
||||
ramming = false)
|
||||
if PowerPolicyEnabled and PowerFinishKill:
|
||||
check "tracker: enemyE=2 -> power 0.5 (finishKill)",
|
||||
close(p, 0.5) and d.reason == prFinishKill
|
||||
else:
|
||||
check "tracker (control/finish off): enemyE=2 keeps preference (3.0)",
|
||||
p == 3.0
|
||||
|
||||
proc testTrackerBelowAverage() =
|
||||
var t = initTracker(1)
|
||||
for b in 0..<len(PowerBins):
|
||||
seedWindow(t, 7, 0, b, 20, 80) # all bins equal: chosen bin not above mean
|
||||
let (_, _, p, d) = t.selectShotPolicy(7, tick = 0, dist = 100.0,
|
||||
selfEnergy = 100.0, ramming = false)
|
||||
selfEnergy = 100.0, enemyEnergy = 100.0,
|
||||
ramming = false)
|
||||
if PowerPolicyEnabled:
|
||||
check "tracker: flat gun -> cap 2.0 (belowAvg)", p == 2.0 and d.reason == prBelowAvg
|
||||
else:
|
||||
check "tracker (control): flat gun keeps its uncapped preference (power 3.0)",
|
||||
p == 3.0
|
||||
|
||||
proc testTrackerFarAndLowEnergy() =
|
||||
proc testTrackerFar() =
|
||||
var t = initTracker(1)
|
||||
seedWindow(t, 7, 0, 3, 50, 50)
|
||||
let (_, _, pFar, dFar) = t.selectShotPolicy(7, 0, dist = 250.0,
|
||||
selfEnergy = 100.0, ramming = false)
|
||||
let (_, _, pLow, dLow) = t.selectShotPolicy(7, 0, dist = 100.0,
|
||||
selfEnergy = 30.0, ramming = false)
|
||||
selfEnergy = 100.0, enemyEnergy = 100.0,
|
||||
ramming = false)
|
||||
if PowerPolicyEnabled:
|
||||
check "tracker: far -> power 1.0", pFar == 1.0 and dFar.reason == prFar
|
||||
check "tracker: low energy -> power 1.0", pLow == 1.0 and dLow.reason == prLowEnergy
|
||||
else:
|
||||
check "tracker (control): far does NOT cap (uncapped preference)", pFar == 3.0
|
||||
check "tracker (control): low energy does NOT cap (uncapped preference)", pLow == 3.0
|
||||
|
||||
proc testTrackerColdAndEmptyBin() =
|
||||
var t = initTracker(1)
|
||||
let (_, _, pCold, dCold) = t.selectShotPolicy(7, 0, dist = 100.0,
|
||||
selfEnergy = 100.0, ramming = false)
|
||||
selfEnergy = 100.0, enemyEnergy = 100.0,
|
||||
ramming = false)
|
||||
if PowerPolicyEnabled:
|
||||
# Cold gun: no data at all -> pEst <= pRef vacuously -> mid cap, but the
|
||||
# preferred bin is already 1.0, so the fired power stays 1.0.
|
||||
@@ -179,13 +358,20 @@ proc testTrackerRamExempt() =
|
||||
var t = initTracker(1)
|
||||
seedWindow(t, 7, 0, 3, 50, 50)
|
||||
let (_, _, p, d) = t.selectShotPolicy(7, 0, dist = 500.0,
|
||||
selfEnergy = 5.0, ramming = true)
|
||||
selfEnergy = 5.0, enemyEnergy = 1.0,
|
||||
ramming = true)
|
||||
check "tracker: ramming exempts the caps", p == 3.0 and d.reason == prRam
|
||||
|
||||
# ── driver ───────────────────────────────────────────────────────────────────
|
||||
|
||||
testEnergySlope()
|
||||
testEnergySlopeMonotone()
|
||||
testPowerToKill()
|
||||
testPowerToKillCovers()
|
||||
testPowerToKillMonotone()
|
||||
testFar()
|
||||
testLowEnergy()
|
||||
testEnergySlopeCap()
|
||||
testFinishingCap()
|
||||
testBelowAverage()
|
||||
testAboveAverageFull()
|
||||
testRamExempt()
|
||||
@@ -196,8 +382,10 @@ testEnvFlag()
|
||||
testBinIndex()
|
||||
testReasonNames()
|
||||
testTrackerAboveAverage()
|
||||
testTrackerEnergySlope()
|
||||
testTrackerFinishing()
|
||||
testTrackerBelowAverage()
|
||||
testTrackerFarAndLowEnergy()
|
||||
testTrackerFar()
|
||||
testTrackerColdAndEmptyBin()
|
||||
testTrackerRamExempt()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user