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:
@@ -23,11 +23,19 @@ type
|
||||
prevSpeed: float
|
||||
prevTick: int
|
||||
hasPrev: bool
|
||||
# per-tick cache — avoid re-searching for multiple power bins
|
||||
cacheTick: int
|
||||
cacheX: float
|
||||
cacheY: float
|
||||
# Per-tick, speed-INDEPENDENT pattern state. Both the history search and the
|
||||
# replayed enemy path depend only on observed movement, never on bulletSpeed,
|
||||
# so they are built at most once per tick. The bullet lead (number of replay
|
||||
# steps + coast) is derived from this path on every call.
|
||||
cacheValid: bool
|
||||
cacheTick: int
|
||||
bestMatch: int ## -1 = no usable match (linear fallback)
|
||||
playStart: int
|
||||
playAvail: int
|
||||
pathX: array[HistorySize + 1, float]
|
||||
pathY: array[HistorySize + 1, float]
|
||||
pathHeading: array[HistorySize + 1, float] ## radians after s steps
|
||||
pathSpeed: array[HistorySize + 1, float] ## speed after s steps
|
||||
debugGraphics*: bool
|
||||
|
||||
# --- circular buffer helpers ---
|
||||
@@ -55,11 +63,11 @@ proc linearPredict(state: WorldState, bulletSpeed: float): (float, float) =
|
||||
|
||||
# --- pattern search + play-forward ---
|
||||
|
||||
proc searchAndProject(g: PatternMatcherGun, state: WorldState,
|
||||
bulletSpeed: float): (float, float) =
|
||||
## Returns projected (x, y). Falls back to linear if history too short.
|
||||
proc findBestMatch(g: PatternMatcherGun): int =
|
||||
## Speed-independent history search. Returns the start index of the best
|
||||
## matching pattern, or -1 when there is not enough history.
|
||||
if g.count < PatternLen * 2:
|
||||
return linearPredict(state, bulletSpeed)
|
||||
return -1
|
||||
|
||||
# key = last PatternLen entries
|
||||
let keyStart = g.count - PatternLen
|
||||
@@ -79,14 +87,28 @@ proc searchAndProject(g: PatternMatcherGun, state: WorldState,
|
||||
if score < bestScore:
|
||||
bestScore = score
|
||||
bestMatch = i
|
||||
bestMatch
|
||||
|
||||
if bestMatch < 0:
|
||||
return linearPredict(state, bulletSpeed)
|
||||
|
||||
# play forward from bestMatch + PatternLen
|
||||
let playStart = bestMatch + PatternLen
|
||||
let playAvail = g.count - 1 - playStart # ticks we can replay
|
||||
proc buildPath(g: var PatternMatcherGun, state: WorldState, bestMatch: int) =
|
||||
## Precompute the matched pattern replayed forward from the current state.
|
||||
## Only depends on observed movement, so it is valid for every power bin.
|
||||
g.playStart = bestMatch + PatternLen
|
||||
g.playAvail = g.count - 1 - g.playStart # ticks we can replay
|
||||
g.pathX[0] = state.enemyX
|
||||
g.pathY[0] = state.enemyY
|
||||
g.pathHeading[0] = degToRad(state.enemyHeading)
|
||||
g.pathSpeed[0] = state.enemySpeed
|
||||
for s in 1 .. g.playAvail:
|
||||
let m = g.readAt(g.playStart + s - 1)
|
||||
g.pathHeading[s] = g.pathHeading[s - 1] + m.headingDelta
|
||||
g.pathSpeed[s] = m.velocity
|
||||
g.pathX[s] = g.pathX[s - 1] + cos(g.pathHeading[s]) * g.pathSpeed[s]
|
||||
g.pathY[s] = g.pathY[s - 1] + sin(g.pathHeading[s]) * g.pathSpeed[s]
|
||||
|
||||
proc projectFromPath(g: PatternMatcherGun, state: WorldState,
|
||||
bulletSpeed: float): (float, float) =
|
||||
## Speed-dependent lead: walk the cached path as far as this bulletSpeed's
|
||||
## estimated flight time reaches, then coast linearly for the remainder.
|
||||
# iterative time estimate
|
||||
let dist0 = hypot(state.enemyX - state.selfX, state.enemyY - state.selfY)
|
||||
var t = dist0 / bulletSpeed
|
||||
@@ -94,22 +116,14 @@ proc searchAndProject(g: PatternMatcherGun, state: WorldState,
|
||||
var ey = state.enemyY
|
||||
|
||||
for _ in 0..4:
|
||||
let steps = min(int(t + 0.5), playAvail)
|
||||
ex = state.enemyX
|
||||
ey = state.enemyY
|
||||
var heading = degToRad(state.enemyHeading)
|
||||
var speed = state.enemySpeed
|
||||
for s in 0 ..< steps:
|
||||
let m = g.readAt(playStart + s)
|
||||
heading += m.headingDelta
|
||||
speed = m.velocity
|
||||
ex += cos(heading) * speed
|
||||
ey += sin(heading) * speed
|
||||
let steps = min(int(t + 0.5), g.playAvail)
|
||||
ex = g.pathX[steps]
|
||||
ey = g.pathY[steps]
|
||||
# if we ran out of replay data, coast linearly from last simulated pos
|
||||
let remaining = t - steps.float
|
||||
if remaining > 0.0:
|
||||
ex += cos(heading) * speed * remaining
|
||||
ey += sin(heading) * speed * remaining
|
||||
ex += cos(g.pathHeading[steps]) * g.pathSpeed[steps] * remaining
|
||||
ey += sin(g.pathHeading[steps]) * g.pathSpeed[steps] * remaining
|
||||
let ndx = ex - state.selfX
|
||||
let ndy = ey - state.selfY
|
||||
t = sqrt(ndx * ndx + ndy * ndy) / bulletSpeed
|
||||
@@ -125,30 +139,33 @@ proc predict*(g: var PatternMatcherGun, state: WorldState,
|
||||
if bulletSpeed <= 0.0:
|
||||
return GunPrediction(x: state.enemyX, y: state.enemyY)
|
||||
|
||||
# Update history once per tick
|
||||
if g.hasPrev and state.tick > g.prevTick:
|
||||
var dh = degToRad(state.enemyHeading) - degToRad(g.prevHeading)
|
||||
# wrap to [-π, π]
|
||||
while dh > PI: dh -= 2.0 * PI
|
||||
while dh < -PI: dh += 2.0 * PI
|
||||
g.write(MoveTick(velocity: g.prevSpeed, headingDelta: dh))
|
||||
# Roll history forward and (re)build the speed-independent pattern path at
|
||||
# most once per tick. The old code returned a single cached (x, y) per tick,
|
||||
# so all four power bins shared bin 0's lead.
|
||||
if not g.cacheValid or state.tick != g.cacheTick:
|
||||
if g.hasPrev:
|
||||
var dh = degToRad(state.enemyHeading) - degToRad(g.prevHeading)
|
||||
# wrap to [-π, π]
|
||||
while dh > PI: dh -= 2.0 * PI
|
||||
while dh < -PI: dh += 2.0 * PI
|
||||
g.write(MoveTick(velocity: g.prevSpeed, headingDelta: dh))
|
||||
|
||||
if not g.hasPrev or state.tick > g.prevTick:
|
||||
g.prevHeading = state.enemyHeading
|
||||
g.prevSpeed = state.enemySpeed
|
||||
g.prevTick = state.tick
|
||||
g.hasPrev = true
|
||||
g.cacheValid = false # new tick invalidates cache
|
||||
g.cacheValid = true
|
||||
g.cacheTick = state.tick
|
||||
|
||||
# Return cached result for same-tick calls (multiple power bins)
|
||||
if g.cacheValid and state.tick == g.cacheTick:
|
||||
return GunPrediction(x: g.cacheX, y: g.cacheY)
|
||||
g.bestMatch = g.findBestMatch()
|
||||
if g.bestMatch >= 0:
|
||||
g.buildPath(state, g.bestMatch)
|
||||
|
||||
let (px, py) = g.searchAndProject(state, bulletSpeed)
|
||||
g.cacheX = px
|
||||
g.cacheY = py
|
||||
g.cacheTick = state.tick
|
||||
g.cacheValid = true
|
||||
if g.bestMatch < 0:
|
||||
let (px, py) = linearPredict(state, bulletSpeed)
|
||||
return GunPrediction(x: px, y: py)
|
||||
|
||||
let (px, py) = g.projectFromPath(state, bulletSpeed)
|
||||
GunPrediction(x: px, y: py)
|
||||
|
||||
proc onResult*(g: var PatternMatcherGun, e: FeedbackEvent) =
|
||||
|
||||
Reference in New Issue
Block a user