9cd6e9b8ce
shipped metric The radial TM beats Linear on bmPoint, but its head never beat the majority baseline after the label bias was fixed - suggesting the win is a constant lean rather than learning. So: sweep a stateless constant short-range offset (new `common_libs/guns/radial_offset.nim`, no learning at all) against the learned TM. VERDICT (measured, offline range, seeds=3, 18 paired runs, 231 TM rounds): 1. **bmPoint - REPLACE the TM with a constant.** `RO_s0.95` (aim distance x0.95) TIES it early (9/9, p=1.0) and BEATS it overall (15/3, p=0.0075; 7.47% vs 6.89% per-run mean). A fixed -20px does the same. The head never beats its majority baseline (56.2% vs 57.2%). 2. **bmPath (the SHIPPED metric) - the radial avenue is a DEAD END.** TMRadial is a systematic LOSS there (2/16, p=0.0013); every constant is within +-0.2pp; the only real bmPath effect is the BotRadius clamp. So the radial shift cannot help the shipped configuration. 3. The per-adversary optimum DOES vary (fixed -10 for crazy, -30 for tr_crazy, scale 0.95 for three others) - but ONE GLOBAL CONSTANT still beats the adaptively-trained head, so the "fragility justifies learning" argument FAILS. THE REAL FINDING UNDERNEATH, and it generalises beyond this gun: the base linear prediction systematically OVERSHOOTS. Measured raw per-tick base radial error has mean -71 to -100 px; the enemy is NEARER than the prediction in 63-81% of shots and farther in only 4-14%, CONSISTENT ACROSS ALL SIX CAPTURES. Radial label histogram [415166,126461,120325,44694,19351] = 57.2% majority class, mean label -82.3 px, mean applied shift -37.6 px. So the net-short bias is a GENUINE property of these range-holders against a constant-velocity extrapolation (they decelerate and turn, so the true position is closer than the straight-line guess) - NOT a fixture artefact. That is worth chasing for the guns that actually ship. Caveat: bmPoint is not the shipped metric (bmPath won the real-hit-rate A/B for SELECTION), so a bmPoint win is not yet evidence of a real win. That needs a live test - and the natural target is Pattern, which is now the default and best gun. Adds radial_offset.nim + sweep_radial_offset.nim; tm_pattern.nim gains additive instrumentation only (radial label mean and applied-shift mean; no behaviour change, and test_tm_pattern_registration still passes all 20 checks).
56 lines
2.5 KiB
Nim
56 lines
2.5 KiB
Nim
## Constant-offset radial gun — the ABLATION CONTROL for `guns/tm_pattern.nim`'s
|
|
## radial head.
|
|
##
|
|
## Why this exists: the radial Tsetlin head was shown to win on `bmPoint` not by
|
|
## out-classifying a majority baseline (its online accuracy sits AT/BELOW the
|
|
## majority-class rate) but — per its own committed write-up — because the net
|
|
## applied radial correction is a positive average shift of the aim distance. If
|
|
## that is true, a FIXED radial shift should reproduce most or all of the win
|
|
## with no learning, no 0.36 ms/tick cost and no risk.
|
|
##
|
|
## This gun is that fixed shift, and nothing else:
|
|
##
|
|
## base = `forecastLinear` — the exact self-consistent forecast `LinearGun`
|
|
## and the TM base use.
|
|
## aim = the base BEARING unchanged; the aim DISTANCE scaled by `scale` and
|
|
## shifted by `offsetPx`:
|
|
## aimDist = f.dist * scale + offsetPx
|
|
## clamp = the SAME `[BotRadius, arena-BotRadius]` clamp the TM's corrective
|
|
## (non-base) path uses, so this is byte-comparable with TMRadial.
|
|
##
|
|
## `scale == 1.0 and offsetPx == 0.0` is the pure base through the corrective
|
|
## clamp — useful as a clamp-only diagnostic against `LinearGun`'s `[0, arena]`.
|
|
## There is NO learning, no history and no per-tick state: two runs on the same
|
|
## state stream are identical by construction.
|
|
|
|
import std/math
|
|
import gun_harness/gun_interface
|
|
import guns/lead_forecast
|
|
|
|
type
|
|
RadialOffsetGun* = object
|
|
scale*: float ## multiplicative factor on the base fire distance
|
|
offsetPx*: float ## fixed px added to the base fire distance (negative = short)
|
|
debugGraphics*: bool
|
|
|
|
proc initRadialOffsetGun*(scale = 1.0, offsetPx = 0.0): RadialOffsetGun =
|
|
RadialOffsetGun(scale: scale, offsetPx: offsetPx)
|
|
|
|
proc predict*(g: var RadialOffsetGun, state: WorldState,
|
|
bulletSpeed: float): GunPrediction =
|
|
if bulletSpeed <= 0.0:
|
|
return GunPrediction(x: state.enemyX, y: state.enemyY)
|
|
let f = forecastLinear(state, bulletSpeed)
|
|
let aimDist = f.dist * g.scale + g.offsetPx
|
|
let px = state.selfX + cos(f.bearing) * aimDist
|
|
let py = state.selfY + sin(f.bearing) * aimDist
|
|
# Mirror the TM's corrective path exactly (TMRadial when radOffset != 0):
|
|
# same bearing, adjusted distance, BotRadius-inset clamp.
|
|
GunPrediction(
|
|
x: clamp(px, BotRadius, state.arenaWidth - BotRadius),
|
|
y: clamp(py, BotRadius, state.arenaHeight - BotRadius),
|
|
)
|
|
|
|
proc onResult*(g: var RadialOffsetGun, e: FeedbackEvent) =
|
|
discard # analytical, stateless — the ablation has no learning by design
|