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:
@@ -193,6 +193,7 @@ proc shouldFire*(currentGunDir, targetAngle, gunHeat, distPx: float): bool =
|
||||
|
||||
proc selectShotPolicy*(t: var VirtualTracker, targetId = -1, tick = 0,
|
||||
dist = 0.0, selfEnergy = 100.0,
|
||||
enemyEnergy = 100.0,
|
||||
ramming = false,
|
||||
rackMode: RackMode = rm1v1,
|
||||
membership: openArray[RackMembership] = []
|
||||
@@ -200,10 +201,11 @@ proc selectShotPolicy*(t: var VirtualTracker, targetId = -1, tick = 0,
|
||||
## `selectShot` plus the energy-aware power-policy decision, so a caller can
|
||||
## log the cap and its reason (see `applyPowerPolicy` in virtual_bullets).
|
||||
##
|
||||
## `dist` is the current distance (px) to the target and `selfEnergy` our own
|
||||
## energy; `ramming` exempts the caps (the movement code's `shouldRam` is the
|
||||
## single source of truth). The policy is applied identically wherever this is
|
||||
## called, so live and any offline caller cannot diverge.
|
||||
## `dist` is the current distance (px) to the target, `selfEnergy` our own
|
||||
## energy and `enemyEnergy` the target's remaining energy (drives the
|
||||
## finishing cap); `ramming` exempts the caps (the movement code's `shouldRam`
|
||||
## is the single source of truth). The policy is applied identically wherever
|
||||
## this is called, so live and any offline caller cannot diverge.
|
||||
##
|
||||
## `rackMode` is the server-truth enemy-count mode (`rackMode`); `membership`
|
||||
## is the process-wide `TR_RACK_*` table, passed by the live bot. An empty
|
||||
@@ -221,21 +223,25 @@ proc selectShotPolicy*(t: var VirtualTracker, targetId = -1, tick = 0,
|
||||
let pEst =
|
||||
if fit[gunId].bins[prefBin].count == 0: pRef
|
||||
else: fit[gunId].bins[prefBin].hitRate()
|
||||
let dec = applyPowerPolicy(preferred, dist, selfEnergy, pEst, pRef, ramming)
|
||||
let dec = applyPowerPolicy(preferred, dist, selfEnergy, pEst, pRef, ramming,
|
||||
enemyEnergy = enemyEnergy)
|
||||
result = (gunId, binIndexForPower(dec.power), dec.power, dec)
|
||||
|
||||
proc selectShot*(t: var VirtualTracker, targetId = -1, tick = 0,
|
||||
dist = 0.0, selfEnergy = 100.0,
|
||||
enemyEnergy = 100.0,
|
||||
ramming = false,
|
||||
rackMode: RackMode = rm1v1,
|
||||
membership: openArray[RackMembership] = []): (GunId, int, float) =
|
||||
## Returns (gunId, powerBinIdx, power) — the shot to take this tick.
|
||||
## Pass targetId to pick the best gun for that specific enemy. `tick` drives
|
||||
## the minimum-dwell hysteresis (see `selectGun`). `dist`/`selfEnergy`/`ramming`
|
||||
## feed the energy-aware power cap (`TR_POWER_POLICY`); defaults keep every
|
||||
## existing caller compiling, and `TR_POWER_POLICY=0` reproduces the uncapped
|
||||
## `bestPower` preference. Use `selectShotPolicy` when the cap/reason is needed.
|
||||
## the minimum-dwell hysteresis (see `selectGun`). `dist`/`selfEnergy`/
|
||||
## `enemyEnergy`/`ramming` feed the energy-aware power cap (`TR_POWER_POLICY`);
|
||||
## defaults keep every existing caller compiling, and `TR_POWER_POLICY=0`
|
||||
## reproduces the uncapped `bestPower` preference. Use `selectShotPolicy` when
|
||||
## the cap/reason is needed.
|
||||
let (gunId, binIdx, power, _) =
|
||||
t.selectShotPolicy(targetId, tick, dist, selfEnergy, ramming,
|
||||
t.selectShotPolicy(targetId, tick, dist, selfEnergy,
|
||||
enemyEnergy = enemyEnergy, ramming = ramming,
|
||||
rackMode = rackMode, membership = membership)
|
||||
result = (gunId, binIdx, power)
|
||||
|
||||
Reference in New Issue
Block a user