## 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] 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 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 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 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) = # Find matching trace by prediction coords for i in 0.. 0.01 or abs(t.predY - e.prediction.y) > 0.01: continue # Shaped reward: residual = (actual enemy pos) - (bullet impact pos) # FeedbackEvent carries missDistance but not the direction. # We reconstruct: aimed at (predX, predY); miss is distance to current enemy. # Use miss distance as magnitude; direction unknown → scale back along aim vector. # ponytail: zero-direction residual when miss=0 is fine; TM learns from magnitude via pFeedback let missSign = if e.hit: 0.0 else: 1.0 let residualX = (e.prediction.x - t.predX) * missSign # trivially 0; real signal is missDistance # Better: treat miss distance as residual magnitude along (enemy - pred) direction # We don't have enemy pos here directly, but we can scale correction proportionally. # Simplest correct signal: pass missDistance as residual magnitude for both dims. let rMag = e.missDistance * missSign let lits = tmMakeLiterals(t.input) # Apply residual equally to both axes (we don't know direction split) # ponytail: split 50/50; upgrade to directional when FeedbackEvent carries enemy pos let r = rMag / sqrt(2.0) g.net.tmLearnOne(0, lits, t.cache, r) g.net.tmLearnOne(1, lits, t.cache, r) t.alive = false break