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
+52 -46
View File
@@ -84,6 +84,7 @@ import std/[math, os]
from std/strutils import parseFloat, parseInt, strip, toLowerAscii
import gun_harness/gun_interface
import movement_harness/movement_interface
import movement_harness/fire_tracker
import bitbrain/sbc
const
@@ -163,6 +164,9 @@ var
LearnedLabel* = llHistogram
LearnedRealEvents* = false
LearnedLog* = false
## j134: the shared fire-detection correction (`TR_FIRE_FIX`, default on).
## Off = the shipped `prev - energy` detector byte-for-byte.
LearnedFireFix* = true
proc getEnvFloat(name: string, default: float): float =
let s = getEnv(name, "")
@@ -195,6 +199,7 @@ proc loadLearnedEnv*() =
LearnedWallMargin = max(0.0, getEnvFloat(LearnedWallMarginEnv, 48.0))
LearnedGlobal = envOn(LearnedGlobalEnv)
LearnedRealEvents = envOn(LearnedRealEventsEnv)
LearnedFireFix = envOn("TR_FIRE_FIX", true)
LearnedLog = envOn(LearnedLogEnv)
LearnedLabel =
case getEnv(LearnedLabelEnv, "").strip().toLowerAscii()
@@ -272,7 +277,7 @@ type
missGlobal: int ## llOutcome: non-hits seen (for the prior)
scores: seq[float]
waves: seq[LSWave]
prevEnergy: seq[tuple[id: int, energy: float]]
fire: FireTracker ## shared energy-drop detector (j134)
strafeDir: float64
dir: float64 ## direction commanded last tick (+-1)
prevX, prevY: float64 ## our position one tick ago
@@ -290,7 +295,7 @@ proc resetRound*(m: var LearnedSurferModule) =
## it survives a round boundary without becoming a battle-long static average
## (the j115 defect). Use `resetBattle` for a hard wipe.
m.waves = @[]
m.prevEnergy = @[]
m.fire.reset()
m.strafeDir = 1.0
m.dir = 1.0
m.prevX = 0.0
@@ -307,6 +312,7 @@ proc resetRound*(m: var LearnedSurferModule) =
proc initLearnedSurfer*(): LearnedSurferModule =
result.debugGraphics = false
result.fire = initFireTracker(LearnedFireFix)
result.sbc = initCountedSbc(LS_NADE, LS_BINS,
max(0, LearnedDecayEvery),
max(0, LearnedDecayShift))
@@ -485,58 +491,57 @@ proc predictState*(m: var LearnedSurferModule, row, col: int,
# ── fire detection + state ──────────────────────────────────────────────────
proc prevEnergyGet(m: LearnedSurferModule, id: int): float =
for e in m.prevEnergy:
if e.id == id: return e.energy
100.0
proc prevEnergyGet(m: LearnedSurferModule, id: int): float = m.fire.prevEnergyGet(id)
proc prevEnergySet(m: var LearnedSurferModule, 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))
m.fire.prevEnergySet(id, energy)
proc noteEnemyBulletHit*(m: var LearnedSurferModule, power: float) =
## `onHitByBullet` -> the shooter's `3*power` bonus (no-op when off).
m.fire.noteEnemyBulletHit(power)
proc noteDamageDealt*(m: var LearnedSurferModule, damage: float) =
## `onBulletHit` -> our same-tick damage to the enemy (no-op when off).
m.fire.noteDamageDealt(damage)
proc detectFire(m: var LearnedSurferModule, id: int, ex, ey, eenergy: float,
ws: WorldState) =
## One enemy's energy sample. A plausible one-tick firepower drop IS a wave;
## the state is the wave-relative state at THIS tick (the fire tick).
let prev = m.prevEnergyGet(id)
let drop = prev - eenergy
m.prevEnergySet(id, eenergy)
if drop < 0.09 or drop > 3.01: return
## One enemy's energy sample. A plausible (corrected) one-tick firepower
## drop IS a wave; the state is the wave-relative state at THIS tick (the
## fire tick). Window 0.09..3.01, LEARNED's shipped window; the shared
## tracker corrects the delta and splits an over-cap drop across waves.
for drop in m.fire.detect(id, eenergy, 0.09, 3.01):
let botX = ws.selfX
let botY = ws.selfY
let bspeed = 20.0 - 3.0 * drop
let d = hypot(botX - ex, botY - ey)
let bearing = arctan2(botY - ey, botX - ex) # centre line
let ux = cos(bearing)
let uy = sin(bearing)
let botX = ws.selfX
let botY = ws.selfY
let bspeed = 20.0 - 3.0 * drop
let d = hypot(botX - ex, botY - ey)
let bearing = arctan2(botY - ey, botX - ex) # centre line
let ux = cos(bearing)
let uy = sin(bearing)
# vlat: lateral velocity in the wave frame. The centre line passes through us
# at this tick, so lat(now) == 0 and vlat == -lat(prev).
let dxp = m.prevX - ex
let dyp = m.prevY - ey
var vlat = -(dxp * (-uy) + dyp * ux)
if m.prevX == 0.0 and m.prevY == 0.0 and m.prevHeading == 0.0: vlat = 0.0
let roomDx = if vlat >= 0.0: -uy else: uy
let roomDy = if vlat >= 0.0: ux else: -ux
let room = roomToWall(botX, botY, roomDx, roomDy, ws.arenaWidth, ws.arenaHeight)
let turn = wrap180(float(ws.selfHeading) - float(m.prevHeading))
# vlat: lateral velocity in the wave frame. The centre line passes through us
# at this tick, so lat(now) == 0 and vlat == -lat(prev).
let dxp = m.prevX - ex
let dyp = m.prevY - ey
var vlat = -(dxp * (-uy) + dyp * ux)
if m.prevX == 0.0 and m.prevY == 0.0 and m.prevHeading == 0.0: vlat = 0.0
let roomDx = if vlat >= 0.0: -uy else: uy
let roomDy = if vlat >= 0.0: ux else: -ux
let room = roomToWall(botX, botY, roomDx, roomDy, ws.arenaWidth, ws.arenaHeight)
let turn = wrap180(float(ws.selfHeading) - float(m.prevHeading))
let row = code(vlat, VlatEdges) * LS_Q + code(d, DistEdges)
let col = code(room, RoomEdges) * LS_Q + code(turn, TurnEdges)
let row = code(vlat, VlatEdges) * LS_Q + code(d, DistEdges)
let col = code(room, RoomEdges) * LS_Q + code(turn, TurnEdges)
m.waves.add LSWave(
ownerId: id, fireTick: ws.tick,
originX: ex, originY: ey, bearing: bearing, speed: bspeed,
startDist: d, power: drop,
ticksLeft: max(1, int(ceil(d / max(bspeed, 1e-9)))),
fresh: true,
selfEnergyAtFire: ws.selfEnergy,
stateRow: row, stateCol: col,
)
m.waves.add LSWave(
ownerId: id, fireTick: ws.tick,
originX: ex, originY: ey, bearing: bearing, speed: bspeed,
startDist: d, power: drop,
ticksLeft: max(1, int(ceil(d / max(bspeed, 1e-9)))),
fresh: true,
selfEnergyAtFire: ws.selfEnergy,
stateRow: row, stateCol: col,
)
# ── the mover ───────────────────────────────────────────────────────────────
@@ -551,6 +556,7 @@ proc computeMove*(m: var LearnedSurferModule, ws: WorldState): MoveCommand =
m.detectFire(ei.id, ei.x, ei.y, ei.energy, ws)
if seen == 0 and (ws.enemyX != 0.0 or ws.enemyY != 0.0):
m.detectFire(-1, ws.enemyX, ws.enemyY, ws.enemyEnergy, ws)
m.fire.endScan()
# ── 2. advance + resolve waves; every resolution is a training sample ────
var i = 0
+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)
+47 -36
View File
@@ -6,6 +6,7 @@ import std/os
import std/strutils except fromHex # `fromHex` would clash with color.fromHex
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
@@ -104,6 +105,9 @@ var
TfilCommitTicks*: int = DefaultTfilCommitTicks
TfilNoRev*: bool = false
TfilCommitLogPath*: string = ""
## j134: the shared fire-detection correction (`TR_FIRE_FIX`, default on).
## Off = the shipped `prev - energy` detector byte-for-byte.
TfilFireFix*: bool = true
proc getEnvInt(name: string, default: int): int =
let s = getEnv(name, "")
@@ -132,6 +136,7 @@ proc loadTfilCommitEnv*() =
TfilCommitTicks = max(1, getEnvInt("TR_TFIL_COMMIT_TICKS", DefaultTfilCommitTicks))
TfilNoRev = getEnvBool("TR_TFIL_NO_REV", false)
TfilCommitLogPath = getEnv("TR_TFIL_COMMIT_LOG", "")
TfilFireFix = getEnvBool("TR_FIRE_FIX", true)
loadTfilCommitEnv()
@@ -243,7 +248,7 @@ type
arenaWidth, arenaHeight: float
lava: seq[float] # flat row-major, index = row*cols + col
bullets: seq[TrackedBullet]
prevEnergy: seq[tuple[id: int, energy: float]] # enemy id -> last known energy
fire: FireTracker ## shared energy-drop detector (j134)
commitTarget: tuple[x, y: float] ## world coords of committed dodge point
commitTicks: int ## ticks remaining on commitment
commitLava: float ## lava at commit time (for spike detection)
@@ -258,7 +263,7 @@ type
lastPickCall: int ## callCount at the last pick (log only)
picks: int ## number of picks this round (log only)
proc initTFIL*(): TFILModule = TFILModule(debugGraphics: false)
proc initTFIL*(): TFILModule = TFILModule(debugGraphics: false, fire: initFireTracker(TfilFireFix))
proc removeBulletNear*(m: var TFILModule, x, y: float) =
## Mark the tracked bullet closest to (x,y) within GridSize tolerance as dead.
@@ -272,17 +277,10 @@ proc removeBulletNear*(m: var TFILModule, x, y: float) =
if bestIdx >= 0:
m.bullets.del(bestIdx)
proc prevEnergyGet(m: TFILModule, id: int): float =
for e in m.prevEnergy:
if e.id == id: return e.energy
100.0
proc prevEnergyGet(m: TFILModule, id: int): float = m.fire.prevEnergyGet(id)
proc prevEnergySet(m: var TFILModule, 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))
m.fire.prevEnergySet(id, energy)
proc clearGraphics*(m: var TFILModule) =
## No-op: the SVG buffer is a module-level global cleared by the framework
@@ -291,7 +289,7 @@ proc clearGraphics*(m: var TFILModule) =
proc resetRound*(m: var TFILModule) =
m.bullets = @[]
m.prevEnergy = @[]
m.fire.reset()
m.commitTicks = 0
m.cachedHull = @[]
m.cachedInsideTiles = @[]
@@ -349,30 +347,43 @@ proc initGrid(m: var TFILModule, arenaWidth, arenaHeight: float) =
m.arenaHeight = arenaHeight
m.lava = newSeq[float](m.cols * m.rows) # all 0.0
proc spawnTrackedWave(m: var TFILModule, ws: WorldState, ei: EnemyInfo,
power: float) =
## One tracked bullet/wave for a confirmed fire of `power`: linear prediction
## of our position at arrival becomes the aim heading. (Was inlined in
## `detectFires`; extracted so the shared split can spawn several.)
let speed = 20.0 - 3.0 * power
let dist = sqrt((ws.selfX - ei.x)^2 + (ws.selfY - ei.y)^2)
let travelTime = dist / speed
let predX = ws.selfX + ws.selfSpeed * cos(ws.selfHeading * PI / 180.0) * travelTime
let predY = ws.selfY + ws.selfSpeed * sin(ws.selfHeading * PI / 180.0) * travelTime
let heading = arctan2(predY - ei.y, predX - ei.x)
if m.bullets.len >= MaxTrackedBullets:
m.bullets.del(0) # ponytail: drop oldest; fine for 20-bullet cap
m.bullets.add TrackedBullet(
originX: ei.x, originY: ei.y,
x: ei.x, y: ei.y,
velX: speed * cos(heading),
velY: speed * sin(heading),
power: power,
alive: true,
age: 0)
proc noteEnemyBulletHit*(m: var TFILModule, power: float) =
## `onHitByBullet` -> the shooter's `3*power` bonus (no-op when off).
m.fire.noteEnemyBulletHit(power)
proc noteDamageDealt*(m: var TFILModule, damage: float) =
## `onBulletHit` -> our same-tick damage to the enemy (no-op when off).
m.fire.noteDamageDealt(damage)
proc detectFires(m: var TFILModule, ws: WorldState) =
## Check all enemies for energy drops; spawn a tracked bullet per confirmed fire.
## Check all enemies for energy drops; spawn a tracked bullet per confirmed
## fire. The shared tracker corrects the delta and splits over-cap drops.
for ei in ws.enemies:
let prev = m.prevEnergyGet(ei.id)
let drop = prev - ei.energy
m.prevEnergySet(ei.id, ei.energy)
if drop >= 0.09 and drop <= 3.01:
let speed = 20.0 - 3.0 * drop
# Linear prediction: aim at where we will be when the bullet arrives
let dist = sqrt((ws.selfX - ei.x)^2 + (ws.selfY - ei.y)^2)
let travelTime = dist / speed
let predX = ws.selfX + ws.selfSpeed * cos(ws.selfHeading * PI / 180.0) * travelTime
let predY = ws.selfY + ws.selfSpeed * sin(ws.selfHeading * PI / 180.0) * travelTime
let heading = arctan2(predY - ei.y, predX - ei.x)
if m.bullets.len >= MaxTrackedBullets:
m.bullets.del(0) # ponytail: drop oldest; fine for 20-bullet cap
m.bullets.add TrackedBullet(
originX: ei.x, originY: ei.y,
x: ei.x, y: ei.y,
velX: speed * cos(heading),
velY: speed * sin(heading),
power: drop,
alive: true,
age: 0)
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 TFILModule, selfX, selfY: float) =
## Advance positions and reap bullets that are: passed us, out of bounds, or too old.
@@ -519,9 +530,9 @@ proc computeMove*(m: var TFILModule, ws: WorldState): MoveCommand =
m.bullets = @[] # bullet positions are hopelessly stale
m.blockedTile = (col: 0, row: 0, active: false)
# Re-snapshot prevEnergy so energy changes during ramming aren't misread as fires
m.prevEnergy = @[]
m.fire.prevEnergy = @[]
for ei in ws.enemies:
m.prevEnergySet(ei.id, ei.energy)
m.fire.prevEnergySet(ei.id, ei.energy)
# Tile-change replan — see the knob rationale at the top of the file.
if (not jumped) and (m.callCount > 0) and (m.commitTicks > 0):
@@ -82,9 +82,10 @@
## 10.0 / 15.0; the comment now says so.
import std/[math, random, os, strformat]
from std/strutils import parseFloat, strip # selective: strutils.fromHex clashes with color.fromHex
from std/strutils import parseFloat, strip, toLowerAscii # selective: strutils.fromHex clashes with color.fromHex
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
@@ -128,6 +129,18 @@ proc getEnvFloat(name: string, default: float): float =
except ValueError:
result = default
proc getEnvBool(name: string, default: bool): bool =
let s = getEnv(name, "").strip().toLowerAscii()
if s.len == 0: return default
s in ["1", "true", "on", "yes"]
## j134: the shared fire-detection correction (`TR_FIRE_FIX`, default on).
## Off = the shipped `prev - energy` detector byte-for-byte.
var TfilRingFireFix* = true
proc loadTfilRingFireEnv*() =
TfilRingFireFix = getEnvBool("TR_FIRE_FIX", true)
loadTfilRingFireEnv()
const
DefaultRangeLo = 100.0
DefaultRangeHi = 200.0
@@ -223,7 +236,7 @@ type
arenaWidth, arenaHeight: float
lava: seq[float] # flat row-major, index = row*cols + col
bullets: seq[TrackedBullet]
prevEnergy: seq[tuple[id: int, energy: float]] # enemy id -> last known energy
fire: FireTracker ## shared energy-drop detector (j134)
commitTarget: tuple[x, y: float] ## world coords of committed dodge point
commitTicks: int ## ticks remaining on commitment
commitLava: float ## lava at commit time (for spike detection)
@@ -241,7 +254,8 @@ type
loggedOnce: bool
proc initTFILRing*(): TFILRingModule =
TFILRingModule(debugGraphics: false, band: (lo: RangeLo, hi: RangeHi))
TFILRingModule(debugGraphics: false, band: (lo: RangeLo, hi: RangeHi),
fire: initFireTracker(TfilRingFireFix))
proc removeBulletNear*(m: var TFILRingModule, x, y: float) =
## Mark the tracked bullet closest to (x,y) within GridSize tolerance as dead.
@@ -255,17 +269,10 @@ proc removeBulletNear*(m: var TFILRingModule, x, y: float) =
if bestIdx >= 0:
m.bullets.del(bestIdx)
proc prevEnergyGet(m: TFILRingModule, id: int): float =
for e in m.prevEnergy:
if e.id == id: return e.energy
100.0
proc prevEnergyGet(m: TFILRingModule, id: int): float = m.fire.prevEnergyGet(id)
proc prevEnergySet(m: var TFILRingModule, 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))
m.fire.prevEnergySet(id, energy)
proc clearGraphics*(m: var TFILRingModule) =
## No-op: the SVG buffer is a module-level global cleared by the framework
@@ -274,7 +281,7 @@ proc clearGraphics*(m: var TFILRingModule) =
proc resetRound*(m: var TFILRingModule) =
m.bullets = @[]
m.prevEnergy = @[]
m.fire.reset()
m.commitTicks = 0
m.cachedHull = @[]
m.cachedInsideTiles = @[]
@@ -299,30 +306,42 @@ proc initGrid(m: var TFILRingModule, arenaWidth, arenaHeight: float) =
m.arenaHeight = arenaHeight
m.lava = newSeq[float](m.cols * m.rows) # all 0.0
proc spawnTrackedWave(m: var TFILRingModule, ws: WorldState, ei: EnemyInfo,
power: float) =
## One tracked bullet/wave for a confirmed fire of `power` (extracted from
## `detectFires` so the shared split can spawn several).
let speed = 20.0 - 3.0 * power
let dist = sqrt((ws.selfX - ei.x)^2 + (ws.selfY - ei.y)^2)
let travelTime = dist / speed
let predX = ws.selfX + ws.selfSpeed * cos(ws.selfHeading * PI / 180.0) * travelTime
let predY = ws.selfY + ws.selfSpeed * sin(ws.selfHeading * PI / 180.0) * travelTime
let heading = arctan2(predY - ei.y, predX - ei.x)
if m.bullets.len >= MaxTrackedBullets:
m.bullets.del(0) # ponytail: drop oldest; fine for 20-bullet cap
m.bullets.add TrackedBullet(
originX: ei.x, originY: ei.y,
x: ei.x, y: ei.y,
velX: speed * cos(heading),
velY: speed * sin(heading),
power: power,
alive: true,
age: 0)
proc noteEnemyBulletHit*(m: var TFILRingModule, power: float) =
## `onHitByBullet` -> the shooter's `3*power` bonus (no-op when off).
m.fire.noteEnemyBulletHit(power)
proc noteDamageDealt*(m: var TFILRingModule, damage: float) =
## `onBulletHit` -> our same-tick damage to the enemy (no-op when off).
m.fire.noteDamageDealt(damage)
proc detectFires(m: var TFILRingModule, ws: WorldState) =
## Check all enemies for energy drops; spawn a tracked bullet per confirmed fire.
## Check all enemies for energy drops; spawn a tracked bullet per confirmed
## fire. The shared tracker corrects the delta and splits over-cap drops.
for ei in ws.enemies:
let prev = m.prevEnergyGet(ei.id)
let drop = prev - ei.energy
m.prevEnergySet(ei.id, ei.energy)
if drop >= 0.09 and drop <= 3.01:
let speed = 20.0 - 3.0 * drop
# Linear prediction: aim at where we will be when the bullet arrives
let dist = sqrt((ws.selfX - ei.x)^2 + (ws.selfY - ei.y)^2)
let travelTime = dist / speed
let predX = ws.selfX + ws.selfSpeed * cos(ws.selfHeading * PI / 180.0) * travelTime
let predY = ws.selfY + ws.selfSpeed * sin(ws.selfHeading * PI / 180.0) * travelTime
let heading = arctan2(predY - ei.y, predX - ei.x)
if m.bullets.len >= MaxTrackedBullets:
m.bullets.del(0) # ponytail: drop oldest; fine for 20-bullet cap
m.bullets.add TrackedBullet(
originX: ei.x, originY: ei.y,
x: ei.x, y: ei.y,
velX: speed * cos(heading),
velY: speed * sin(heading),
power: drop,
alive: true,
age: 0)
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 TFILRingModule, selfX, selfY: float) =
## Advance positions and reap bullets that are: passed us, out of bounds, or too old.
@@ -456,9 +475,9 @@ proc computeMove*(m: var TFILRingModule, ws: WorldState): MoveCommand =
m.bullets = @[] # bullet positions are hopelessly stale
m.blockedTile = (col: 0, row: 0, active: false)
# Re-snapshot prevEnergy so energy changes during ramming aren't misread as fires
m.prevEnergy = @[]
m.fire.prevEnergy = @[]
for ei in ws.enemies:
m.prevEnergySet(ei.id, ei.energy)
m.fire.prevEnergySet(ei.id, ei.energy)
# Tile-change replan: catches gradual displacement that position threshold misses
if (not jumped) and (m.callCount > 0) and (m.commitTicks > 0):
+31 -20
View File
@@ -50,6 +50,7 @@ import std/[math, os]
from std/strutils import parseFloat, strip, toLowerAscii
import gun_harness/gun_interface
import movement_harness/movement_interface
import movement_harness/fire_tracker
const
WS_BINS = 31
@@ -76,6 +77,9 @@ var
SurfWallMargin* = DefaultWallMargin
SurfRadialFrac* = DefaultRadialFrac
SurfLog* = false
## j134: the shared fire-detection correction (`TR_FIRE_FIX`, default on).
## Off = the shipped `prev - energy` detector byte-for-byte.
SurfFireFix* = true
proc getEnvFloat(name: string, default: float): float =
let s = getEnv(name, "")
@@ -83,6 +87,11 @@ proc getEnvFloat(name: string, default: float): float =
try: result = parseFloat(s.strip())
except ValueError: result = default
proc envOn(name: string, default = false): bool =
let s = getEnv(name, "").strip().toLowerAscii()
if s.len == 0: return default
s notin ["0", "false", "no", "off"]
proc loadSurfEnv*() =
## Read the surfer knobs; callable again after `putEnv` so a gate can
## exercise arms in one process.
@@ -91,6 +100,7 @@ proc loadSurfEnv*() =
SurfWallMargin = max(0.0, getEnvFloat(SurfWallMarginEnv, DefaultWallMargin))
SurfRadialFrac = clamp(getEnvFloat(SurfRadialFracEnv, DefaultRadialFrac), 0.0, 1.0)
SurfLog = existsEnv(SurfLogEnv)
SurfFireFix = envOn("TR_FIRE_FIX", true)
loadSurfEnv()
@@ -106,7 +116,7 @@ type
WaveSurferModule* = object
bins: array[WS_BINS, float64]
waves: seq[WSWave]
prevEnergy: seq[tuple[id: int, energy: float]]
fire: FireTracker ## shared energy-drop detector (j134)
strafeDir: float64 ## +1.0 or -1.0
debugGraphics*: bool
@@ -114,12 +124,12 @@ proc resetRound*(m: var WaveSurferModule) =
## Wipe per-round state. The danger histogram IS reset here (defect 2): it is
## a per-round learner, not a battle-long static average.
m.waves = @[]
m.prevEnergy = @[]
m.fire.reset()
m.strafeDir = 1.0
for i in 0..<WS_BINS: m.bins[i] = 1.0
proc initWaveSurfer*(): WaveSurferModule =
var m = WaveSurferModule(debugGraphics: false)
var m = WaveSurferModule(debugGraphics: false, fire: initFireTracker(SurfFireFix))
m.resetRound()
m
@@ -132,17 +142,18 @@ proc removeBulletNear*(m: var WaveSurferModule, x, y: float) {.inline.} =
## No-op: the surfer tracks WAVES (energy drops), not bullet bodies.
discard
proc prevEnergyGet(m: WaveSurferModule, id: int): float =
for e in m.prevEnergy:
if e.id == id: return e.energy
100.0
proc prevEnergyGet(m: WaveSurferModule, id: int): float = m.fire.prevEnergyGet(id)
proc prevEnergySet(m: var WaveSurferModule, 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))
m.fire.prevEnergySet(id, energy)
proc noteEnemyBulletHit*(m: var WaveSurferModule, power: float) =
## `onHitByBullet` -> the shooter's `3*power` bonus (no-op when off).
m.fire.noteEnemyBulletHit(power)
proc noteDamageDealt*(m: var WaveSurferModule, damage: float) =
## `onBulletHit` -> our same-tick damage to the enemy (no-op when off).
m.fire.noteDamageDealt(damage)
proc gfToBin(gf: float64): int {.inline.} =
clamp(int(round((gf.clamp(-1.0, 1.0) + 1.0) * 0.5 * float64(WS_BINS - 1))),
@@ -176,13 +187,12 @@ proc nearestWave(m: WaveSurferModule, botX, botY: float64): int =
proc detectFire(m: var WaveSurferModule, id: int, ex, ey, eenergy,
botX, botY: float64) =
## One enemy's energy sample: emit a wave iff its energy dropped by a
## plausible firepower in one tick. `drop` is the firepower exactly.
let prev = m.prevEnergyGet(id)
let drop = prev - eenergy
m.prevEnergySet(id, eenergy)
if drop >= 0.1 and drop <= 3.0:
let bspeed = 20.0 - 3.0 * drop
## One enemy's energy sample: emit a wave per plausible firepower in the
## (corrected) one-tick drop. `drop` IS the firepower. Window 0.1..3.0,
## SURF's shipped window; the shared tracker corrects the delta and splits an
## over-cap drop across several waves.
for power in m.fire.detect(id, eenergy, 0.1, 3.0):
let bspeed = 20.0 - 3.0 * power
let bearing = arctan2(botY - ey, botX - ex)
let d = hypot(botX - ex, botY - ey)
m.waves.add WSWave(
@@ -191,7 +201,7 @@ proc detectFire(m: var WaveSurferModule, id: int, ex, ey, eenergy,
speed: bspeed,
radius: 0.0,
startDist: d,
power: drop,
power: power,
)
proc computeMove*(m: var WaveSurferModule, ws: WorldState): MoveCommand =
@@ -206,6 +216,7 @@ proc computeMove*(m: var WaveSurferModule, ws: WorldState): MoveCommand =
if seen == 0 and (ws.enemyX != 0.0 or ws.enemyY != 0.0):
# Fallback to the tracked target when `enemies` is empty.
m.detectFire(-1, ws.enemyX, ws.enemyY, ws.enemyEnergy, botX, botY)
m.fire.endScan()
# ── Advance waves; record the GF of each wave that reaches us ─────────────
var i = 0