j134 fire fix: share ONE fire_tracker across tfil/ring/strafe/learned/surf (TR_FIRE_FIX, default on); env-gated TR_FIRE_DIAG alignment trace

This commit is contained in:
2026-09-26 12:40:19 +02:00
parent bd57db6d63
commit 6ad5d99922
11 changed files with 435 additions and 214 deletions
+37 -57
View File
@@ -182,6 +182,7 @@ from std/strutils import parseFloat, parseInt, strip, toLowerAscii
import std/strformat
import gun_harness/gun_interface
import movement_harness/movement_interface
import movement_harness/fire_tracker
import robocode_tankroyale_botapi/graphics
import robocode_tankroyale_botapi/color
# j105/j106 reuse: the exported time-indexed heat helpers + pillar globals.
@@ -351,8 +352,15 @@ var
## subtracts it.
## MEASURED on 70 recorded battles (67065 true enemy fires): catches
## 98.888% of enemy fires with the knob OFF and 100.000% with it ON.
## Default ON; `TR_STRAFE_FIRE_FIX=0` restores the shipped detector exactly.
## j134: the detector now lives ONCE in `movement_harness/fire_tracker.nim`
## and every mover calls it. This flag is STRAFE's local gate: it is ON only
## when BOTH `TR_STRAFE_FIRE_FIX` and the global `TR_FIRE_FIX` are on, so
## either knob set to an off value restores the shipped detector exactly.
StrafeFireFix*: bool = true
## TEMPORARY j134 diagnostic (TASK B): when `TR_FIRE_DIAG` is set, print one
## `[firediag] READ tick=…` line per enemy energy reading so the event/reading
## tick alignment can be checked live. OFF by default; observability only.
StrafeFireDiag*: bool = false
## GUI: draw the full lava field (every non-zero tile, value-labelled) the
## way TFIL does. Default ON; `TR_STRAFE_HEAT_GRID=0` hides the field so the
## strafe overlays can be read on their own.
@@ -386,7 +394,9 @@ proc loadStrafeEnv*() =
StrafeWallBias = max(0.0, min(1.0, getEnvFloat("TR_STRAFE_WALL_BIAS", DefaultStrafeWallBias)))
StrafeWallSafe = max(0.0, getEnvFloat("TR_STRAFE_WALL_SAFE", DefaultStrafeWallSafe))
StrafeEscape = getEnvBool("TR_STRAFE_ESCAPE", true)
StrafeFireFix = getEnvBool("TR_STRAFE_FIRE_FIX", true)
StrafeFireFix = getEnvBool("TR_STRAFE_FIRE_FIX", true) and
getEnvBool("TR_FIRE_FIX", true)
StrafeFireDiag = existsEnv("TR_FIRE_DIAG")
StrafeHeatGrid = getEnvBool("TR_STRAFE_HEAT_GRID", true)
loadStrafeHeatEnv()
@@ -419,7 +429,6 @@ type
arenaWidth*, arenaHeight*: float
lava: seq[float]
bullets: seq[TrackedBullet]
prevEnergy: seq[tuple[id: int, energy: float]]
# ── decision state ──
targetX*, targetY*: float ## chosen tile centre (world coords)
targetValid*: bool
@@ -444,11 +453,8 @@ type
wallEscapePicks*: int ## picks forced inward by the all-hot escape
escapeModeTicks*: int ## ticks the escape bearing was in effect
lastMode*: string ## "pick" | "fallback" | "escape" | "radial"
# ── fire-detection fix (j133): event-fed energy-delta corrections ──
hitBonusPending: float ## 3 * power of enemy bullets that hit us this tick
dealtPending: float ## damage our bullets dealt to the enemy this tick
fireFixSplitWaves*: int ## waves emitted by splitting a too-large drop
fireFixCorrectedTicks*: int ## ticks whose drop was non-trivially corrected
# ── fire detection (j134): the shared enemy-fire tracker ──
fire: FireTracker ## ONE detector for every mover (movement_harness/fire_tracker)
# ── diagnostics (gate B + GUI) ──
callCount*: int
picks*: int
@@ -468,7 +474,7 @@ type
lastTileCol, lastTileRow: int
proc initStrafe*(): StrafeModule =
StrafeModule(debugGraphics: false)
StrafeModule(debugGraphics: false, fire: initFireTracker(StrafeFireFix))
proc removeBulletNear*(m: var StrafeModule, x, y: float) =
## Mark the tracked bullet closest to (x,y) within GridSize tolerance as dead.
@@ -482,17 +488,12 @@ proc removeBulletNear*(m: var StrafeModule, x, y: float) =
if bestIdx >= 0:
m.bullets.del(bestIdx)
proc prevEnergyGet(m: StrafeModule, id: int): float =
for e in m.prevEnergy:
if e.id == id: return e.energy
100.0
proc prevEnergyGet(m: StrafeModule, id: int): float = m.fire.prevEnergyGet(id)
proc prevEnergySet(m: var StrafeModule, id: int, energy: float) =
for i in 0..<m.prevEnergy.len:
if m.prevEnergy[i].id == id:
m.prevEnergy[i].energy = energy
return
m.prevEnergy.add((id: id, energy: energy))
## Kept for the guard test / callers that seed an energy reading directly;
## delegates to the shared tracker.
m.fire.prevEnergySet(id, energy)
proc clearGraphics*(m: var StrafeModule) =
## No-op: the SVG buffer is a module-level global cleared by the framework
@@ -501,7 +502,7 @@ proc clearGraphics*(m: var StrafeModule) =
proc resetRound*(m: var StrafeModule) =
m.bullets = @[]
m.prevEnergy = @[]
m.fire.reset()
m.targetValid = false
m.targetLava = 0.0
m.targetPathHeat = 0.0
@@ -521,10 +522,6 @@ proc resetRound*(m: var StrafeModule) =
m.wallEscapePicks = 0
m.escapeModeTicks = 0
m.lastMode = ""
m.hitBonusPending = 0.0
m.dealtPending = 0.0
m.fireFixSplitWaves = 0
m.fireFixCorrectedTicks = 0
m.callCount = 0
m.picks = 0
m.lastPickCall = 0
@@ -594,47 +591,30 @@ proc noteEnemyBulletHit*(m: var StrafeModule, power: float) =
## `power` the enemy spent firing in the same tick (net delta >= 0 reads as
## "no fire"). ModularBot forwards `onHitByBullet`'s `e.bullet.power` here so
## `detectFires` can add the bonus back before classifying the delta.
## No-op when `TR_STRAFE_FIRE_FIX` is off (shipped detector preserved).
if StrafeFireFix:
m.hitBonusPending += 3.0 * power
## No-op when the fix is off (shipped detector preserved).
m.fire.noteEnemyBulletHit(power)
proc noteDamageDealt*(m: var StrafeModule, damage: float) =
## The mirror contamination: OUR bullet damaging the enemy this tick adds
## `damage` to the enemy's energy drop, which can push it above the 3.0 power
## cap and get the enemy's OWN shot rejected by the shipped `<= 3.01` test.
## ModularBot forwards `onBulletHit`'s `e.damage` here.
## No-op when `TR_STRAFE_FIRE_FIX` is off.
if StrafeFireFix:
m.dealtPending += damage
## No-op when the fix is off.
m.fire.noteDamageDealt(damage)
proc detectFires(m: var StrafeModule, ws: WorldState) =
## The shared tracker does the delta correction + split; STRAFE supplies its
## shipped window (0.09 .. 3.01) and its own wave geometry.
for ei in ws.enemies:
let prev = m.prevEnergyGet(ei.id)
let raw = prev - ei.energy
var drop = raw
if StrafeFireFix:
# Undo the server's known energy contaminations. The bonus raises the
# enemy's energy (shrinking/negating the drop); our damage lowers it
# (inflating the drop). Both are known exactly from the events.
drop += m.hitBonusPending - m.dealtPending
if abs(drop - raw) > 1e-9: inc m.fireFixCorrectedTicks
m.prevEnergySet(ei.id, ei.energy)
if StrafeFireFix and drop > 3.01:
# NEVER silently drop a drop. A delta above the power cap is either
# several fires folded into one reading (unobserved radar latency) or
# un-modelled contamination; either way SOME heat beats none. Split into
# the fewest waves each <= 3.0, all from the same origin.
let n = int(ceil(drop / 3.0))
let p = drop / n.float
for _ in 0..<n:
m.spawnTrackedWave(ws, ei, p)
inc m.fireFixSplitWaves
elif drop >= 0.09 and drop <= 3.01:
m.spawnTrackedWave(ws, ei, drop)
if StrafeFireFix:
# Consumed: each event-tick correction applies to exactly one reading.
m.hitBonusPending = 0.0
m.dealtPending = 0.0
if StrafeFireDiag:
let raw = m.fire.prevEnergyGet(ei.id) - ei.energy
echo "[firediag] READ tick=", ws.tick, " id=", ei.id,
" raw=", raw,
" bonus=", m.fire.hitBonusPending,
" dealt=", m.fire.dealtPending
for p in m.fire.detect(ei.id, ei.energy, 0.09, 3.01):
m.spawnTrackedWave(ws, ei, p)
m.fire.endScan()
proc advanceBullets(m: var StrafeModule, selfX, selfY: float) =
var i = 0
@@ -1099,9 +1079,9 @@ proc computeMove*(m: var StrafeModule, ws: WorldState): MoveCommand =
m.dwell = 0
m.targetValid = false
m.bullets = @[]
m.prevEnergy = @[]
m.fire.prevEnergy = @[]
for ei in ws.enemies:
m.prevEnergySet(ei.id, ei.energy)
m.fire.prevEnergySet(ei.id, ei.energy)
m.advanceBullets(ws.selfX, ws.selfY)
m.detectFires(ws)