254c7dc997
- 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
289 lines
10 KiB
Nim
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)
|