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:
@@ -40,6 +40,7 @@
|
||||
import std/[os, json, random, math, sequtils, sets]
|
||||
import std/strutils except fromHex # `fromHex` would clash with color.fromHex
|
||||
import gun_harness/gun_interface
|
||||
import movements/ram_decision
|
||||
# Private-field access: include (do NOT import) the shipped mover.
|
||||
include movements/the_floor_is_lava
|
||||
|
||||
@@ -1479,6 +1480,109 @@ proc testJ154() =
|
||||
check "j154: clearing the knob restores today's behaviour exactly",
|
||||
TfilHoldMaxTicks == 0
|
||||
|
||||
proc testJ160() =
|
||||
## j160 — the energy-reserve FIRING FLOOR (TR_RAM_FLOOR_ENERGY) and the
|
||||
## ENEMY-EXHAUSTION ram trigger (TR_RAM_ENEMY_ENERGY). Both default 0.0 =
|
||||
## today's behaviour. Pure logic only; no bot, no server.
|
||||
const F = 5.0
|
||||
|
||||
# 1. DEFAULT PARITY, floor: with the knob unset the floor blocks NOTHING over
|
||||
# a grid that includes every measured low-energy region (<=5: 9.4% of
|
||||
# ticks in the 8612-round closed-loop corpus, p01 self energy = 0.2).
|
||||
var blocked = 0
|
||||
for e in [-1.0, 0.0, 0.1, 1.0, 2.5, 5.0, 7.0, 20.0, 46.0, 100.0, 120.0]:
|
||||
if fireFloorBlocks(0.0, e): inc blocked
|
||||
check "j160: TR_RAM_FLOOR_ENERGY unset (=0) suppresses fire on NO input, " &
|
||||
"so the default path is byte-for-byte today's (" & $blocked & " blocked)",
|
||||
blocked == 0
|
||||
|
||||
# 2. DEFAULT PARITY, trigger: with the knob unset the reason over a grid is
|
||||
# the PRE-j160 result — the new arm is unreachable, and every old arm still
|
||||
# returns exactly what it returned before.
|
||||
var newArm, mismatch: int
|
||||
for dist in [10.0, 100.0, 299.0, 301.0, 500.0]:
|
||||
for se in [0.5, 5.0, 19.0, 21.0, 60.0, 100.0]:
|
||||
for ee in [0.0, 1.0, 5.0, 19.9, 20.0, 40.0, 100.0]:
|
||||
let inp = RamInputs(dist: dist, selfEnergy: se, enemyEnergy: ee)
|
||||
let got = ramTrigger(inp)
|
||||
if got == rrExhausted: inc newArm
|
||||
# the pre-j160 body, verbatim
|
||||
var want: RamReason = rrNone
|
||||
if ee > 0.0:
|
||||
if dist < RamFinisherDist and ee < RamFinisherEnergy and se > ee: want = rrFinisher
|
||||
elif se < RamDesperationEnergy and ee < RamDesperationEnergy and dist < RamDesperationDist: want = rrDesperation
|
||||
if got != want: inc mismatch
|
||||
check "j160: TR_RAM_ENEMY_ENERGY unset (=0) makes the exhaustion arm " &
|
||||
"unreachable (" & $newArm & " hits) and leaves the old finisher / " &
|
||||
"desperation verdicts identical (" & $mismatch & " mismatches over 210 " &
|
||||
"input combinations)",
|
||||
newArm == 0 and mismatch == 0
|
||||
|
||||
# 3. the floor suppresses AT the threshold, not above it.
|
||||
check "j160: the floor blocks AT the threshold (self == 5.0 <= floor 5.0)",
|
||||
fireFloorBlocks(F, 5.0)
|
||||
check "j160: the floor blocks just below it and not just above it — one " &
|
||||
"tick of hysteresis, no dead band",
|
||||
fireFloorBlocks(F, 4.999) and not fireFloorBlocks(F, 5.001)
|
||||
|
||||
# 4. it never suppresses while we are healthy, at ANY floor setting.
|
||||
var healthy = 0
|
||||
for floor in [0.5, 1.0, 5.0, 20.0, 25.0, 40.0]:
|
||||
for e in [floor, 46.0, 60.0, 100.0, 120.0]:
|
||||
if e > floor and fireFloorBlocks(floor, e): inc healthy
|
||||
check "j160: the floor NEVER blocks above its own threshold — healthy energy " &
|
||||
"fires for every floor/energy pair (" & $healthy & " violations)",
|
||||
healthy == 0
|
||||
|
||||
# 5. the trigger switches to ram EXACTLY at the tolerance, no earlier.
|
||||
let inp2 = RamInputs(dist: 100.0, selfEnergy: 60.0, enemyEnergy: 10.0)
|
||||
check "j160: the exhaustion trigger fires EXACTLY at TR_RAM_ENEMY_ENERGY " &
|
||||
"(enemy 10.0 <= tol 10.0) and not one tick above (10.001)",
|
||||
ramTrigger(inp2, enemyEnergyTol = 10.0) == rrExhausted and
|
||||
ramTrigger(RamInputs(dist: 100.0, selfEnergy: 60.0, enemyEnergy: 10.001),
|
||||
enemyEnergyTol = 10.0) != rrExhausted
|
||||
|
||||
# 6. the surplus guard survives: 0.6/contact is applied to BOTH bots, so we
|
||||
# only ram an exhausted enemy while WE hold the surplus.
|
||||
check "j160: the exhaustion trigger keeps the finisher's energy-surplus " &
|
||||
"guard — an exhausted enemy while WE are lower is a ram we lose",
|
||||
ramTrigger(RamInputs(dist: 250.0, selfEnergy: 2.0, enemyEnergy: 3.0),
|
||||
enemyEnergyTol = 10.0) != rrExhausted
|
||||
|
||||
# 7. it is the finisher's own shape: the existing range guard still applies.
|
||||
check "j160: the exhaustion trigger keeps the finisher's 300px range guard " &
|
||||
"(enemy exhausted at 301px is not a ram)",
|
||||
ramTrigger(RamInputs(dist: 301.0, selfEnergy: 60.0, enemyEnergy: 3.0),
|
||||
enemyEnergyTol = 10.0) != rrExhausted
|
||||
|
||||
# 8. COMPOSITION: ramming WINS. The floor is the reserve FOR the ram, so once
|
||||
# the ram is engaged the reserve is being spent, not held. No starvation:
|
||||
# the floor alone can never make us unable to close.
|
||||
check "j160: RAMMING wins the conflict — at self energy 0.1 (below any " &
|
||||
"sane floor) an engaged ram is never floor-blocked, so the two " &
|
||||
"compose instead of deadlocking each other",
|
||||
fireFloorBlocks(F, 0.1, ramming = true) == false and
|
||||
fireFloorBlocks(F, 0.1, ramming = false) == true
|
||||
|
||||
# 9. and the floor can never be engaged at all without self energy being
|
||||
# genuinely low — the guard the owner asked for, stated as a property.
|
||||
var unsafe = 0
|
||||
for floor in [0.5, 5.0, 20.0, 25.0]:
|
||||
for e in [0.0, 1.0, 10.0, 25.0, 50.0, 100.0]:
|
||||
if fireFloorBlocks(floor, e, ramming = false) and e > floor: inc unsafe
|
||||
check "j160: the floor is a LOW-ENERGY guard only — it can never suppress " &
|
||||
"fire while we are healthy, in any configuration (" & $unsafe & ")",
|
||||
unsafe == 0
|
||||
|
||||
# 10. both knobs together: the exhausted trigger still fires while the floor
|
||||
# is at full strength, and the floor still holds when no ram is engaged.
|
||||
check "j160: both knobs ON compose — exhaustion (enemy 3, us 60) still " &
|
||||
"ram-bypasses the floor, and a non-ramming low-energy tick still holds",
|
||||
fireFloorBlocks(F, 3.0, ramming = false) and
|
||||
ramTrigger(RamInputs(dist: 100.0, selfEnergy: 60.0, enemyEnergy: 3.0),
|
||||
enemyEnergyTol = 10.0) == rrExhausted and
|
||||
not fireFloorBlocks(F, 3.0, ramming = true)
|
||||
|
||||
# ── driver ───────────────────────────────────────────────────────────────────
|
||||
|
||||
testDefaultParity()
|
||||
@@ -1494,6 +1598,7 @@ when declared(loadTfilCommitEnv):
|
||||
testJ150()
|
||||
testJ154()
|
||||
testJ153()
|
||||
testJ160()
|
||||
|
||||
if failures > 0:
|
||||
echo "\n", failures, " check(s) FAILED"
|
||||
|
||||
Reference in New Issue
Block a user