e53690036b
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.
103 lines
4.1 KiB
Nim
103 lines
4.1 KiB
Nim
## Stop-shot gun: predicts the point where a decelerating enemy will stop.
|
|
## Many bots pause briefly when reversing direction — catches them at the stop.
|
|
## Falls back to linear prediction when enemy is not decelerating.
|
|
## Coordinate system: 0° = East, CCW positive (Tank Royale standard).
|
|
|
|
import std/math
|
|
import gun_harness/gun_interface
|
|
|
|
# TR deceleration constants (px/tick²)
|
|
const BrakeDecel = 2.0 ## same-direction stop
|
|
const CoastDecel = 1.0 ## cross-zero coasting stop
|
|
|
|
type StopShotGun* = object
|
|
prevSpeed: float
|
|
prevHeading: float
|
|
prevTick: int
|
|
frames: int
|
|
# 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)
|
|
|
|
proc predict*(g: var StopShotGun, state: WorldState, bulletSpeed: float): GunPrediction =
|
|
if bulletSpeed <= 0.0:
|
|
return GunPrediction(x: state.enemyX, y: state.enemyY)
|
|
|
|
# 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 not g.warmEnough:
|
|
return GunPrediction(x: state.enemyX, y: state.enemyY)
|
|
|
|
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
|
|
|
|
if not g.decelerating:
|
|
# Linear fallback
|
|
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)
|
|
)
|
|
|
|
# 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
|
|
|
|
GunPrediction(
|
|
x: clamp(px, BotRadius, state.arenaWidth - BotRadius),
|
|
y: clamp(py, BotRadius, state.arenaHeight - BotRadius)
|
|
)
|
|
|
|
proc onResult*(g: var StopShotGun, e: FeedbackEvent) =
|
|
discard # analytical gun — no learning
|