j160 (default-off): the energy-reserve FIRING FLOOR + ENEMY-EXHAUSTION ram trigger
TR_RAM_FLOOR_ENERGY (0.0 = off): at/below this self energy we start no NEW shot, holding back the reserve for a final ram exchange. Justified by the only energy gain in the game being +3*power per bullet hit LANDED, so not firing denies the enemy its only refill. Blocks only NEW shots (gunHeat already gates committed ones) and is bypassed while ramming. TR_RAM_ENEMY_ENERGY (0.0 = off): last-scanned enemy energy <= this -> ram mode. Enemy energy IS observable (ScannedBotEvent.energy, schemas.nim:306), 1-8 ticks stale. This is the shipped finisher with its energy tolerance promoted to a knob, keeping the self>enemy surplus guard because RAM_DAMAGE 0.6 applies to BOTH bots on every contact tick. Open-loop measurement (measure_ramfloor_energy, 8149 recordings / 29871 rounds / 33.8M ticks): 'both low' is COMMON (10.4% of ticks below 20, 15.1% below 25) but neither side goes low first (enemy 52.7% / us 47.3%), and the owner's literal trigger - enemy so low it cannot fire (energy <= 1.95) - is only 2.5% of ticks, 1.1% while we are healthy. Guards 136 -> 147 in test_tfil_commit_env.nim, all green. A/B PRE-REGISTERED in docs/ram_floor_exhaustion_ab.md and NOT RUN.
This commit is contained in:
@@ -46,7 +46,29 @@
|
||||
## TR_RAM_PLAN_MARGIN default 20.0 change-of-plan energy advantage
|
||||
## TR_RAM_PLAN_HITRATE default 0.05 selected gun's pooled virtual hit rate
|
||||
## below which the gun duel counts as failing
|
||||
## TR_RAM_LOG=1 emit one change-gated `[ram]` line
|
||||
## ## TR_RAM_LOG=1 emit one change-gated `[ram]` line
|
||||
## TR_RAM_FLOOR_ENERGY default 0.0 FIRING FLOOR (j160). At or below this
|
||||
## self energy we stop firing to keep a
|
||||
## ram reserve. 0 = off = today's behaviour.
|
||||
## TR_RAM_ENEMY_ENERGY default 0.0 ENEMY-EXHAUSTION trigger (j160). The
|
||||
## last-scanned enemy energy <= this ->
|
||||
## ram mode. 0 = off.
|
||||
##
|
||||
## ── j160: the energy-reserve + exhaustion policy ───────────────────────────
|
||||
## Energy NEVER regenerates and has no cap; the only gain in the whole game is
|
||||
## `+3 * power` per bullet hit LANDED (server `rules.kt`). So not firing denies
|
||||
## the enemy its only refill AND keeps our ram reserve intact — the two halves
|
||||
## of the policy are the same bet.
|
||||
##
|
||||
## Floor sizing: one likely return hit (`bulletDamage(1.0)` = 4.0) plus two
|
||||
## 0.1-power shots (0.1 each) is 4.2. The knob DEFAULT stays 0.0 so the default
|
||||
## path is byte-identical; the operator sets 5-ish.
|
||||
##
|
||||
## The exhaustion trigger is the FINISHER with the energy tolerance promoted to
|
||||
## an operator knob. It deliberately KEEPS the finisher's own
|
||||
## `selfEnergy > enemyEnergy` surplus guard: `RAM_DAMAGE 0.6` is applied to BOTH
|
||||
## bots on every contact tick, so a head-on contact is a symmetric bleed decided
|
||||
## by who walks in with the surplus.
|
||||
|
||||
import std/[os, strutils]
|
||||
|
||||
@@ -95,10 +117,15 @@ let RamPlanDist* = getEnvFloat("TR_RAM_PLAN_DIST", DefaultRamPlanDist)
|
||||
let RamPlanMargin* = getEnvFloat("TR_RAM_PLAN_MARGIN", DefaultRamPlanMargin)
|
||||
let RamPlanHitRate* = getEnvFloat("TR_RAM_PLAN_HITRATE", DefaultRamPlanHitRate)
|
||||
let RamLog* = existsEnv("TR_RAM_LOG")
|
||||
## j160. 0.0 = off on BOTH knobs, which is the shipped behaviour.
|
||||
let RamFloorEnergy* = getEnvFloat("TR_RAM_FLOOR_ENERGY", 0.0)
|
||||
let RamEnemyEnergy* = getEnvFloat("TR_RAM_ENEMY_ENERGY", 0.0)
|
||||
|
||||
type
|
||||
RamReason* = enum
|
||||
rrNone ## no trigger fires
|
||||
rrExhausted ## j160: last-scanned enemy energy <= TR_RAM_ENEMY_ENERGY
|
||||
## and we hold the surplus (it is out of ammo, we are not)
|
||||
rrFinisher ## enemy < 20 energy, we are healthier, dist < 300
|
||||
rrOpportunity ## we clearly out-energise and are close enough to close
|
||||
rrDesperation ## both nearly dead, short range
|
||||
@@ -131,7 +158,8 @@ proc ramTrigger*(inp: RamInputs,
|
||||
planEnabled = RamPlanEnabled,
|
||||
planDist = RamPlanDist,
|
||||
planMargin = RamPlanMargin,
|
||||
planHitRate = RamPlanHitRate): RamReason =
|
||||
planHitRate = RamPlanHitRate,
|
||||
enemyEnergyTol = RamEnemyEnergy): RamReason =
|
||||
## Pure trigger evaluation. Returns the FIRST matching reason in priority
|
||||
## order, or `rrNone`. Cooldown/duration/abort are deliberately NOT here — the
|
||||
## caller composes those, so this function has no state and is unit-testable.
|
||||
@@ -143,6 +171,13 @@ proc ramTrigger*(inp: RamInputs,
|
||||
## `desperation` and `finisher` are kept: they are rare, short-range, and the
|
||||
## finisher is the only measured conversion. `plan` remains opt-in and off.
|
||||
if inp.enemyEnergy <= 0.0: return rrNone
|
||||
# j160 exhaustion trigger. Checked FIRST so the operator-set tolerance wins
|
||||
# the label when it is set; it is the finisher's own shape (same surplus and
|
||||
# distance guards) with the 20.0 energy tolerance promoted to a knob. With
|
||||
# `enemyEnergyTol = 0.0` (the default) this arm can never fire.
|
||||
if enemyEnergyTol > 0.0 and inp.enemyEnergy <= enemyEnergyTol and
|
||||
inp.dist < RamFinisherDist and inp.selfEnergy > inp.enemyEnergy:
|
||||
return rrExhausted
|
||||
if inp.dist < RamFinisherDist and inp.enemyEnergy < RamFinisherEnergy and
|
||||
inp.selfEnergy > inp.enemyEnergy:
|
||||
return rrFinisher
|
||||
@@ -158,9 +193,24 @@ proc ramTrigger*(inp: RamInputs,
|
||||
return rrPlan
|
||||
rrNone
|
||||
|
||||
proc fireFloorBlocks*(floor, selfEnergy: float, ramming = false): bool =
|
||||
## j160 FIRING FLOOR. True when the reserve is thin enough that we must not
|
||||
## commit a NEW shot. `floor = 0.0` (the default) disables the floor entirely
|
||||
## and returns false for every input, so the default path is unchanged.
|
||||
##
|
||||
## `ramming` WINS over the floor: once ram mode is engaged the duel is over,
|
||||
## so the reserve is being spent on the contact, not held for it. This is the
|
||||
## same exemption `ramming` already gets in `applyPowerPolicy`.
|
||||
##
|
||||
## The floor blocks only NEW shots. A bullet already in the air (gun heat > 0)
|
||||
## is untouched — `shouldFire` already gates on `gunHeat <= 0.0`, so there is
|
||||
## no committed shot for the floor to suppress or cancel.
|
||||
not ramming and floor > 0.0 and selfEnergy <= floor
|
||||
|
||||
proc reasonName*(r: RamReason): string =
|
||||
case r
|
||||
of rrNone: "none"
|
||||
of rrExhausted: "exhausted"
|
||||
of rrFinisher: "finisher"
|
||||
of rrOpportunity: "opportunity"
|
||||
of rrDesperation: "desperation"
|
||||
|
||||
Reference in New Issue
Block a user