fix(guns): speed-sensitive caches, dead stop-shot branch, exact TM trace pairing
Four guns cached a whole prediction per tick while predict() is called once per power bin, so every bin after the first (and the real fired shot, which shares lastState) reused the power-1.0 lead. Fixed by caching only the speed-INDEPENDENT derived state and recomputing the lead per requested speed: - stop_shot: also fixes prevSpeed being written before it was read, which made abs(speed) < abs(prev) permanently false and the entire stop-prediction branch unreachable (it was just Linear). - displacement: the cache key included bulletSpeed, so the guard missed on all four bins and the 15-tick window advanced ~4x/tick, making the inferred velocity ~4x too small. - averaged_lead: tick cache removed outright. pattern_matcher: split into speed-independent match+path and per-call lead. FeedbackEvent gains fireTick/powerBin (additive; only virtual_bullets constructs one) so guns can pair feedback to the exact shot instead of guessing by coordinates. tsetlin uses it: traces are now keyed exactly by (fireTick, powerBin) with a 1024-slot ring, and the 10-frame window shifts at most once per tick (it was shifting ~4-5x/tick, so isWarmedUp tripped after ~2 ticks). KNOWN INCOMPLETE: tsetlin still does not diverge from Linear in battle. The two named bugs are fixed (a 600-tick sim shows trainedShots=2141, traceMisses=0, and a fixed-input probe converges to a 9.6px correction), but the TM's clause feedback itself is broken: ~131 of 1740 literals end up included per clause, so its conjunction never fires. Sweeping TM_S, TM_N_CLAUSES and a two-branch Type-I update did not change the correction from 0. Needs a real TM fix or removal, not another bug fix. First-ever guard tests for the gun selector: common_libs/tests/ test_gun_harness.nim (14 checks, headless, no Java). There were none before, which is how six broken guns survived a full analysis cycle. Against the previous HEAD, 5 of these checks FAIL - that is the regression guard.
This commit is contained in:
@@ -15,9 +15,15 @@ type StopShotGun* = object
|
||||
prevHeading: float
|
||||
prevTick: int
|
||||
frames: int
|
||||
cachedTick: int
|
||||
cachedPredX: float
|
||||
cachedPredY: float
|
||||
# Per-tick derived state. It depends only on the ENEMY's motion (speed delta,
|
||||
# chosen deceleration, simulated stop point), never on the bullet speed, so it
|
||||
# is computed once per tick and shared by all four power bins. The speed-
|
||||
# dependent lead is recomputed from it on every call.
|
||||
derivedTick: int
|
||||
warmEnough: bool
|
||||
decelerating: bool
|
||||
decel: float
|
||||
stopX, stopY: float
|
||||
debugGraphics*: bool
|
||||
|
||||
proc initStopShotGun*(): StopShotGun = StopShotGun(debugGraphics: false)
|
||||
@@ -26,73 +32,68 @@ proc predict*(g: var StopShotGun, state: WorldState, bulletSpeed: float): GunPre
|
||||
if bulletSpeed <= 0.0:
|
||||
return GunPrediction(x: state.enemyX, y: state.enemyY)
|
||||
|
||||
# Per-tick cache: all power bins share one prediction
|
||||
if state.tick == g.cachedTick:
|
||||
return GunPrediction(x: g.cachedPredX, y: g.cachedPredY)
|
||||
|
||||
defer:
|
||||
g.cachedTick = state.tick
|
||||
g.cachedPredX = result.x
|
||||
g.cachedPredY = result.y
|
||||
|
||||
# Update history
|
||||
let isNew = state.tick > g.prevTick
|
||||
if isNew:
|
||||
g.prevSpeed = state.enemySpeed
|
||||
g.prevHeading = state.enemyHeading
|
||||
g.prevTick = state.tick
|
||||
inc g.frames
|
||||
# Roll the observation window forward at most once per tick. prevSpeed must
|
||||
# hold the PREVIOUS tick's speed when deceleration is tested, so it is read
|
||||
# before being overwritten with the current tick's speed. The old code
|
||||
# overwrote it first, making `prev == speed` and the stop branch unreachable.
|
||||
if state.tick != g.derivedTick:
|
||||
g.derivedTick = state.tick
|
||||
let prev = g.prevSpeed
|
||||
if state.tick > g.prevTick:
|
||||
g.prevSpeed = state.enemySpeed
|
||||
g.prevHeading = state.enemyHeading
|
||||
g.prevTick = state.tick
|
||||
inc g.frames
|
||||
g.warmEnough = g.frames >= 2
|
||||
# Detect deceleration: |speed| is shrinking toward zero.
|
||||
g.decelerating = g.warmEnough and abs(state.enemySpeed) < abs(prev) and
|
||||
abs(state.enemySpeed) > 0.01
|
||||
if g.decelerating:
|
||||
# Pick decel rate: braking (speed toward zero on same sign) = 2, else 1
|
||||
g.decel = if state.enemySpeed * prev > 0.0: BrakeDecel else: CoastDecel
|
||||
# Simulate the enemy coasting to a stop from its current position/heading.
|
||||
let headRad = degToRad(state.enemyHeading)
|
||||
let startSpeed = state.enemySpeed
|
||||
var v = startSpeed
|
||||
var sx = state.enemyX
|
||||
var sy = state.enemyY
|
||||
while abs(v) > 0.001:
|
||||
sx += v * cos(headRad)
|
||||
sy += v * sin(headRad)
|
||||
v += (if v > 0.0: -g.decel else: g.decel)
|
||||
if (v > 0.0) != (startSpeed > 0.0): v = 0.0 # crossed zero
|
||||
g.stopX = sx
|
||||
g.stopY = sy
|
||||
|
||||
# Need 2+ frames to detect deceleration
|
||||
if g.frames < 2:
|
||||
result = GunPrediction(x: state.enemyX, y: state.enemyY)
|
||||
return
|
||||
if not g.warmEnough:
|
||||
return GunPrediction(x: state.enemyX, y: state.enemyY)
|
||||
|
||||
let speed = state.enemySpeed
|
||||
let prev = g.prevSpeed
|
||||
let speed = state.enemySpeed
|
||||
let headRad = degToRad(state.enemyHeading)
|
||||
let dist = hypot(state.enemyX - state.selfX, state.enemyY - state.selfY)
|
||||
# Speed-dependent lead — always recomputed, never cached across power bins.
|
||||
let bulletTicks = dist / bulletSpeed
|
||||
|
||||
# Detect deceleration: |speed| is shrinking
|
||||
let decelerating = abs(speed) < abs(prev) and abs(speed) > 0.01
|
||||
|
||||
if not decelerating:
|
||||
if not g.decelerating:
|
||||
# Linear fallback
|
||||
let headRad = degToRad(state.enemyHeading)
|
||||
let dist = hypot(state.enemyX - state.selfX, state.enemyY - state.selfY)
|
||||
let t = dist / bulletSpeed
|
||||
var px = state.enemyX + cos(headRad) * speed * t
|
||||
var py = state.enemyY + sin(headRad) * speed * t
|
||||
result = GunPrediction(
|
||||
var px = state.enemyX + cos(headRad) * speed * bulletTicks
|
||||
var py = state.enemyY + sin(headRad) * speed * bulletTicks
|
||||
return GunPrediction(
|
||||
x: clamp(px, 0.0, state.arenaWidth),
|
||||
y: clamp(py, 0.0, state.arenaHeight)
|
||||
)
|
||||
return
|
||||
|
||||
# Pick decel rate: braking (speed toward zero on same sign) = 2, else 1
|
||||
# ponytail: simplified; TR has exact rules per direction but this is close enough
|
||||
let decel = if speed * prev > 0.0: BrakeDecel else: CoastDecel
|
||||
# If the bullet arrives well after the enemy stops, aim at the stop point;
|
||||
# otherwise blend a linear lead. stopTicks is speed-independent, so only this
|
||||
# comparison depends on the requested bulletSpeed.
|
||||
let stopTicks = abs(speed) / g.decel
|
||||
let px = if bulletTicks >= stopTicks: g.stopX
|
||||
else: state.enemyX + cos(headRad) * speed * bulletTicks
|
||||
let py = if bulletTicks >= stopTicks: g.stopY
|
||||
else: state.enemyY + sin(headRad) * speed * bulletTicks
|
||||
|
||||
# Simulate stop position
|
||||
let headRad = degToRad(state.enemyHeading)
|
||||
var v = speed
|
||||
var sx = state.enemyX
|
||||
var sy = state.enemyY
|
||||
while abs(v) > 0.001:
|
||||
sx += v * cos(headRad)
|
||||
sy += v * sin(headRad)
|
||||
let step = if v > 0.0: -decel else: decel
|
||||
v += step
|
||||
if (v > 0.0) != (speed > 0.0): v = 0.0 # crossed zero
|
||||
|
||||
# Bullet travel time to current pos, check against ticks to stop
|
||||
let dist = hypot(state.enemyX - state.selfX, state.enemyY - state.selfY)
|
||||
let bulletTicks = dist / bulletSpeed
|
||||
let stopTicks = abs(speed) / decel
|
||||
|
||||
# If bullet arrives well after stop, aim at stop; otherwise linear blend
|
||||
let px = if bulletTicks >= stopTicks: sx else: state.enemyX + cos(headRad) * speed * bulletTicks
|
||||
let py = if bulletTicks >= stopTicks: sy else: state.enemyY + sin(headRad) * speed * bulletTicks
|
||||
|
||||
result = GunPrediction(
|
||||
GunPrediction(
|
||||
x: clamp(px, BotRadius, state.arenaWidth - BotRadius),
|
||||
y: clamp(py, BotRadius, state.arenaHeight - BotRadius)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user