## 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