## Tsetlin Machine gun — regression TM for aiming correction on top of linear extrapolation. ## Self-contained: includes binary encoding and TM predictor inline. ## Implements Gun interface: predict(state, bulletSpeed) → GunPrediction, onResult(FeedbackEvent). import std/[math, random, strformat] import gun_harness/gun_interface # ── Binary encoding (adapted from BNNBot_garage/src/binary_encoding.nim) ───── const TM_FRAME_BITS = 83 TM_SELF_BITS = 40 TM_WINDOW_SIZE = 10 TM_TOTAL_BITS* = TM_FRAME_BITS * TM_WINDOW_SIZE + TM_SELF_BITS # 870 TM_MAX_DISTANCE = 1414.0 # diagonal of 1000x1000 arena type TmBinaryVector = array[TM_TOTAL_BITS, uint8] TmFrameEncoded = array[TM_FRAME_BITS, uint8] TmSelfEncoded = array[TM_SELF_BITS, uint8] proc tmToGray(value: int): int = value xor (value shr 1) proc tmToBits(value: int, bits: int): seq[uint8] = result = newSeq[uint8](bits) let gray = tmToGray(value) for i in 0.. 0: hasIncluded = true if lits[lit] == 0: return 0'u8 return if hasIncluded: 1'u8 else: 0'u8 proc tmForwardWithCache(net: TmNet, input: TmBinaryVector, cache: var TmClauseCache): (float, float) = let lits = tmMakeLiterals(input) var vx = 0.0; var vy = 0.0 for c in 0..= pFeedback: continue let pol = tmPolarity(c) let cOut = cache[outIdx * TM_N_CLAUSES + c] if (error > 0.0 and pol > 0.0) or (error < 0.0 and pol < 0.0): # Type I / Ib feedback for lit in 0.. 0: net.states[si] = int16(max(st - 1, -TM_N_STATES)) # ── TsetlinGun public type ──────────────────────────────────────────────────── const TM_TRACE_SLOTS = 64 # ring buffer of pending traces # ponytail: 64 slots >> TRACE_MAX_AGE=40 ticks, safe margin; grow if many guns/bins DebugTM* = false # set true to print [tm-dbg] lines per onResult call type TmTrace = object predX, predY: float # key: matches FeedbackEvent.prediction input: TmBinaryVector cache: TmClauseCache alive: bool TsetlinGun* = object net: TmNet frameBuffer: array[TM_WINDOW_SIZE, TmFrameEncoded] bufferCount: int traces: array[TM_TRACE_SLOTS, TmTrace] traceHead: int shotCount: int ## total onResult calls received proc initTsetlinGun*(): TsetlinGun = # states init at 0 (boundary); one Type I step crosses into Include for s in result.net.states.mitems: s = 0'i16 randomize() proc isWarmedUp*(g: TsetlinGun): bool {.inline.} = g.bufferCount >= TM_WINDOW_SIZE proc predict*(g: var TsetlinGun, state: WorldState, bulletSpeed: float): GunPrediction = # Encode current frame and push into window let dist = hypot(state.enemyX - state.selfX, state.enemyY - state.selfY) let bearing = radToDeg(arctan2(state.enemyY - state.selfY, state.enemyX - state.selfX)) let frame = tmEncodeFrame( bearing, dist, state.enemySpeed, state.enemyHeading, state.arenaHeight - state.enemyY, state.enemyY, state.arenaWidth - state.enemyX, state.enemyX, state.selfEnergy, # use self energy as proxy (enemy energy not in WorldState) ) # Shift window: index 0 = newest for i in countdown(TM_WINDOW_SIZE - 1, 1): g.frameBuffer[i] = g.frameBuffer[i - 1] g.frameBuffer[0] = frame if g.bufferCount < TM_WINDOW_SIZE: inc g.bufferCount # Warm-up: until window is full, fall back to linear extrapolation let ticksToArrive = if bulletSpeed > 0.0: dist / bulletSpeed else: 1.0 let headingRad = degToRad(state.enemyHeading) let linearX = state.enemyX + cos(headingRad) * state.enemySpeed * ticksToArrive let linearY = state.enemyY + sin(headingRad) * state.enemySpeed * ticksToArrive if g.bufferCount < TM_WINDOW_SIZE: return GunPrediction(x: clamp(linearX, 0.0, state.arenaWidth), y: clamp(linearY, 0.0, state.arenaHeight)) let selfState = tmEncodeSelf( state.arenaHeight - state.selfY, state.selfY, state.arenaWidth - state.selfX, state.selfX, state.selfEnergy, true, # canFire not in WorldState; assume true ) let vec = tmEncodeFullVector(g.frameBuffer, selfState) var cache: TmClauseCache let (cx, cy) = tmForwardWithCache(g.net, vec, cache) let predX = clamp(linearX + cx, 0.0, state.arenaWidth) let predY = clamp(linearY + cy, 0.0, state.arenaHeight) # Store trace keyed by prediction coords let slot = g.traceHead mod TM_TRACE_SLOTS g.traces[slot] = TmTrace(predX: predX, predY: predY, input: vec, cache: cache, alive: true) g.traceHead = (slot + 1) mod TM_TRACE_SLOTS GunPrediction(x: predX, y: predY) proc onResult*(g: var TsetlinGun, e: FeedbackEvent) = inc g.shotCount # Find matching trace by prediction coords for i in 0.. 0.01 or abs(t.predY - e.prediction.y) > 0.01: continue # Directional residual: actual enemy pos minus our prediction # On hit residual is 0 (we were right); on miss we push toward actual position. let rx = if e.hit: 0.0 else: clamp(e.actualX - t.predX, -TM_RESID_MAX, TM_RESID_MAX) let ry = if e.hit: 0.0 else: clamp(e.actualY - t.predY, -TM_RESID_MAX, TM_RESID_MAX) let lits = tmMakeLiterals(t.input) g.net.tmLearnOne(0, lits, t.cache, rx) g.net.tmLearnOne(1, lits, t.cache, ry) when DebugTM: echo fmt"[tm-dbg] shot={g.shotCount} miss={e.missDistance:.1f}px predicted=({t.predX:.0f},{t.predY:.0f}) actual=({e.actualX:.0f},{e.actualY:.0f}) rx={rx:.1f} ry={ry:.1f} hit={e.hit}" t.alive = false break