Files
SirRoboGarage/BNNBot_garage/src/WiSARDBot.nim
T
SirStone 254c7dc997 feat(ModularBot): pluggable bot with 4 guns, phantom meteor movement, radar harness
- Gun harness: virtual bullet tracker, rolling fitness, auto-selector
- Guns: head-on, linear (extrapolation), circular (integrated formula), tsetlin machine (learning)
- Movement: phantom meteor gravity engine (danger histograms, phantom bullets, fire detection)
- Radar: harness + radar_lock adapter
- Color-coded modules: turret/bullet color per gun, body per movement, scan per radar
- Beats Target, SpinBot, Crazy, TrackFire in 10-round battles
2026-09-20 00:37:10 +02:00

289 lines
10 KiB
Nim

# WiSARDBot — WiSARD regression predictor for aiming.
# Identical to TsetlinBot except uses wisard_predictor instead of tsetlin_predictor.
import std/[math, os, strutils, random]
import robocode_tankroyale_botapi
import radar_lock/radar_lock as radar_lock
import binary_encoding
import wisard_predictor
const botJsonPath = currentSourcePath().parentDir / "WiSARDBot.json"
const
BULLET_SLOTS = 50
EPSILON_START = 0.2
EPSILON_MIN = 0.05
EPSILON_DECAY = 0.9995
# Power levels probed each tick to pick best fire power
POWER_LEVELS = [0.1'f64, 0.4, 0.7, 1.0, 1.3, 1.6, 1.9, 2.2, 2.5, 3.0]
N_POWER = POWER_LEVELS.len
HIT_THRESHOLD = 0.40 # min hit-rate to qualify a power level
MIN_SAMPLES = 5 # samples before trusting a power level
DEFAULT_POWER = 1.0 # cold-start fallback
type
ProbeSlot = object # lightweight wave probe — no learning, just geometry
active: bool
powerIdx: int
fireX: float
fireY: float
aimAngleDeg: float
fireDist: float
bulletSpeed: float
age: int
type
WiSARDBot = ref object of Bot
hasContact: bool
enemyBearing: float
lastEnemyX: float
lastEnemyY: float
hasLastPos: bool
velocity: float
heading: float
distance: float
tick: int
prevVec: BinaryVector
hasPrev: bool
frameBuffer: array[WINDOW_SIZE, array[FRAME_BITS, uint8]]
bufferCount: int
net: WiSARDNet
bullets: array[BULLET_SLOTS, VirtualBullet]
bulletHead: int
virtualHits: int
virtualMiss: int
epsilon: float
aimAngle: float # absolute gun aim angle, updated each scan
firePower: float # dynamically selected fire power
powerStats: array[N_POWER, tuple[hits, total: int]]
probes: array[N_POWER, ProbeSlot] # one probe per power level per tick
method onScannedBot*(bot: WiSARDBot, e: ScannedBotEvent) =
let bx = getX(); let by = getY()
bot.enemyBearing = directionTo(bx, by, e.x, e.y)
bot.distance = distanceTo(bx, by, e.x, e.y)
bot.heading = e.direction
bot.velocity = e.speed
bot.lastEnemyX = e.x
bot.lastEnemyY = e.y
bot.hasLastPos = true
bot.hasContact = true
let arenaW = getArenaWidth().float
let arenaH = getArenaHeight().float
# lastEnemyX/Y already stored above — available for wave resolution in run()
let frame = EnemyScanFrame(
bearing: bot.enemyBearing,
distance: bot.distance,
velocity: bot.velocity,
heading: bot.heading,
enemyWallN: arenaH - e.y,
enemyWallS: e.y,
enemyWallE: arenaW - e.x,
enemyWallW: e.x,
enemyEnergy: e.energy,
)
let encoded = encodeFrame(frame)
for i in countdown(WINDOW_SIZE - 1, 1):
bot.frameBuffer[i] = bot.frameBuffer[i - 1]
bot.frameBuffer[0] = encoded
if bot.bufferCount < WINDOW_SIZE:
inc bot.bufferCount
if bot.bufferCount < WINDOW_SIZE:
return
let selfState = SelfState(
myWallN: arenaH - by,
myWallS: by,
myWallE: arenaW - bx,
myWallW: bx,
myEnergy: getEnergy(),
canFire: getGunHeat() <= 0.0,
)
let selfEncoded = encodeSelf(selfState)
let vec = encodeFullVector(bot.frameBuffer, selfEncoded)
# ── pick best fire power ──────────────────────────────────────────────
bot.firePower = DEFAULT_POWER
var bestPow = -1.0
for pi in 0..<N_POWER:
let s = bot.powerStats[pi]
if s.total >= MIN_SAMPLES:
let rate = s.hits.float / s.total.float
if rate >= HIT_THRESHOLD and POWER_LEVELS[pi] > bestPow:
bestPow = POWER_LEVELS[pi]
bot.firePower = POWER_LEVELS[pi]
let selectedSpeed = 20.0 - 3.0 * bot.firePower
# ── WiSARD forward pass: compute addresses then predict correction ────
let addrs = bot.net.computeAddresses(vec)
let (cx, cy) = bot.net.predictCorrection(addrs)
# Convert (cx, cy) correction to aim angle offset.
# Linear extrapolation first; cx/cy are residual corrections on top.
let bx2 = getX(); let by2 = getY()
var aimAngle = bot.enemyBearing
if bot.hasLastPos:
let ticksToArrive = bot.distance / selectedSpeed
let extrapolX = cos(degToRad(bot.heading)) * bot.velocity * ticksToArrive
let extrapolY = sin(degToRad(bot.heading)) * bot.velocity * ticksToArrive
let targetX = bot.lastEnemyX + extrapolX + cx
let targetY = bot.lastEnemyY + extrapolY + cy
echo "AIM: enemyXY=(" & formatFloat(bot.lastEnemyX, ffDecimal, 1) & "," & formatFloat(bot.lastEnemyY, ffDecimal, 1) &
") heading=" & formatFloat(bot.heading, ffDecimal, 1) &
" vel=" & formatFloat(bot.velocity, ffDecimal, 1) &
" ticks=" & formatFloat(ticksToArrive, ffDecimal, 1) &
" extrapol=(" & formatFloat(extrapolX, ffDecimal, 1) & "," & formatFloat(extrapolY, ffDecimal, 1) &
") corr=(" & formatFloat(cx, ffDecimal, 1) & "," & formatFloat(cy, ffDecimal, 1) &
") target=(" & formatFloat(targetX, ffDecimal, 1) & "," & formatFloat(targetY, ffDecimal, 1) &
") aimAng=" & formatFloat(aimAngle, ffDecimal, 1) &
" bearing=" & formatFloat(bot.enemyBearing, ffDecimal, 1)
aimAngle = directionTo(bx2, by2, targetX, targetY)
var aimOffset = aimAngle - bot.enemyBearing
# Normalise to [-180, 180]
while aimOffset > 180.0: aimOffset -= 360.0
while aimOffset < -180.0: aimOffset += 360.0
# epsilon-greedy exploration: perturb the correction
if rand(1.0) < bot.epsilon:
aimOffset += rand(10.0) - 5.0
bot.epsilon = max(EPSILON_MIN, bot.epsilon * EPSILON_DECAY)
bot.aimAngle = bot.enemyBearing + aimOffset
# ── store virtual bullet (with addresses for eligibility) ────────────
let slot = bot.bulletHead mod BULLET_SLOTS
bot.bulletHead = slot + 1
bot.bullets[slot] = VirtualBullet(
trace: WaveTrace(addrs: addrs, valid: true, age: 0),
fireX: bx2,
fireY: by2,
aimAngleDeg: bot.enemyBearing + aimOffset,
fireDist: bot.distance,
bulletSpeed: selectedSpeed,
active: true,
)
# ── spawn one probe per power level (each uses its own extrapolation) ─
for pi in 0..<N_POWER:
let probeSpeed = 20.0 - 3.0 * POWER_LEVELS[pi]
let probeTicks = bot.distance / probeSpeed
let probeExtrapolX = cos(degToRad(bot.heading)) * bot.velocity * probeTicks
let probeExtrapolY = sin(degToRad(bot.heading)) * bot.velocity * probeTicks
let probeTargetX = bot.lastEnemyX + probeExtrapolX + cx
let probeTargetY = bot.lastEnemyY + probeExtrapolY + cy
let probeAimAngle = directionTo(bx2, by2, probeTargetX, probeTargetY)
bot.probes[pi] = ProbeSlot(
active: true,
powerIdx: pi,
fireX: bx2,
fireY: by2,
aimAngleDeg: probeAimAngle,
fireDist: bot.distance,
bulletSpeed: probeSpeed,
age: 0,
)
# ── stats & echo ─────────────────────────────────────────────────────
let hamming = if bot.hasPrev: hammingDistance(bot.prevVec, vec) else: 0
let similarity = if bot.hasPrev: TOTAL_BITS - hamming else: 0
let overlap = if bot.hasPrev: popcount(bitwiseAnd(bot.prevVec, vec)) else: 0
let totalVirtual = bot.virtualHits + bot.virtualMiss
let hitPct = if totalVirtual > 0: bot.virtualHits.float / totalVirtual.float * 100.0 else: 0.0
echo align($bot.tick, 4), " ",
hamming, " ", similarity, " ", overlap, " ",
formatFloat(aimOffset, ffDecimal, 2), " ",
bot.virtualHits, " ", bot.virtualMiss, " ",
formatFloat(hitPct, ffDecimal, 1), "% ",
"pwr=", formatFloat(bot.firePower, ffDecimal, 1)
bot.prevVec = vec
bot.hasPrev = true
method onRoundStarted*(bot: WiSARDBot, e: RoundStartedEvent) =
setAdjustGunForBodyTurn(true)
setAdjustRadarForBodyTurn(true)
setAdjustRadarForGunTurn(true)
radar_lock.init()
bot.hasContact = false
bot.hasLastPos = false
bot.tick = 0
bot.hasPrev = false
bot.bufferCount = 0
for i in 0..<BULLET_SLOTS:
bot.bullets[i].active = false
for i in 0..<N_POWER:
bot.probes[i].active = false
bot.bulletHead = 0
# net and epsilon persist across rounds
method onGameStarted*(bot: WiSARDBot, e: GameStartedEventForBot) =
discard
method run*(bot: WiSARDBot) =
while isRunning():
inc bot.tick
setTargetSpeed(0.0)
setTurnRate(0.0)
# ── age & settle virtual bullets every game tick ──────────────────
if bot.hasLastPos:
for idx in 0..<BULLET_SLOTS:
var b = addr bot.bullets[idx]
if not b.active: continue
inc b.trace.age
let bulletDist = b.bulletSpeed * float(b.trace.age)
if bulletDist >= b.fireDist or b.trace.age >= TRACE_MAX_AGE:
let bulletX = b.fireX + cos(degToRad(b.aimAngleDeg)) * bulletDist
let bulletY = b.fireY + sin(degToRad(b.aimAngleDeg)) * bulletDist
let missDistance = hypot(bulletX - bot.lastEnemyX, bulletY - bot.lastEnemyY)
let resX = bot.lastEnemyX - bulletX
let resY = bot.lastEnemyY - bulletY
bot.net.learnCorrection(b.trace.addrs, resX, resY)
if missDistance < 36.0: inc bot.virtualHits
else: inc bot.virtualMiss
b.active = false
for pi in 0..<N_POWER:
var p = addr bot.probes[pi]
if not p.active: continue
inc p.age
let pd = p.bulletSpeed * float(p.age)
if pd >= p.fireDist or p.age >= TRACE_MAX_AGE:
let px = p.fireX + cos(degToRad(p.aimAngleDeg)) * pd
let py = p.fireY + sin(degToRad(p.aimAngleDeg)) * pd
let md = hypot(px - bot.lastEnemyX, py - bot.lastEnemyY)
inc bot.powerStats[pi].total
if md < 36.0: inc bot.powerStats[pi].hits
p.active = false
if not bot.hasContact:
setRadarTurnRate(45.0)
go()
continue
setRadarTurnRate(radar_lock.doRadar(getRadarDirection(), bot.enemyBearing))
# Aim gun and fire when ready
let gunDir = getGunDirection()
let gunDelta = normalizeRelativeAngle(bot.aimAngle - gunDir)
setGunTurnRate(gunDelta.clamp(-20.0, 20.0))
if getGunHeat() <= 0.0 and abs(gunDelta) < 2.0:
discard setFire(bot.firePower)
go()
when isMainModule:
randomize()
var bot = WiSARDBot(
net: initWiSARD(),
epsilon: EPSILON_START,
firePower: DEFAULT_POWER,
)
start(bot, botJsonPath)