j129: virtual-bullet debug overlay (TR_VBULLET_DEBUG, default off) - travelled path, predicted aim ring, hit/miss vector; shared turret colour table
This commit is contained in:
@@ -43,6 +43,7 @@ import movement_harness/bullet_shadows
|
|||||||
import targeting/enemy_tracker
|
import targeting/enemy_tracker
|
||||||
import targeting/target_selector
|
import targeting/target_selector
|
||||||
import env_report
|
import env_report
|
||||||
|
import vbullet_draw
|
||||||
|
|
||||||
const botJsonPath = currentSourcePath().parentDir / "ModularBot.json"
|
const botJsonPath = currentSourcePath().parentDir / "ModularBot.json"
|
||||||
const DebugVBullets = false
|
const DebugVBullets = false
|
||||||
@@ -160,6 +161,17 @@ const TmHorizonId = 15
|
|||||||
## head — hence every other gun's learning order — is byte-for-byte unchanged.
|
## head — hence every other gun's learning order — is byte-for-byte unchanged.
|
||||||
const BitBrainId = 16
|
const BitBrainId = 16
|
||||||
|
|
||||||
|
## ── virtual-bullet debug overlay (TR_VBULLET_DEBUG*) ───────────────────────
|
||||||
|
## OFF by default: the shipped bot draws nothing. When on, the tracker's
|
||||||
|
## virtual bullets are overlaid so the training signal the selector learns
|
||||||
|
## from is visible (travelled path, predicted aim point, miss vector). The
|
||||||
|
## colour table is shared with the turret (see `vbullet_draw.gunColors`).
|
||||||
|
## Default filter: ONLY the currently selected gun, to stay legible; set
|
||||||
|
## `TR_VBULLET_DEBUG_GUN=all` or name a gun (e.g. `Pattern`).
|
||||||
|
let VBulletDebugOn = vBulletDebugEnabled()
|
||||||
|
let VBulletDebugMax = vBulletDebugMaxBullets()
|
||||||
|
let VBulletDebugGun = resolveGunFilter(getEnv(VBulletDebugGunEnv, ""), GunNames)
|
||||||
|
|
||||||
const
|
const
|
||||||
CLR_GUN = "\e[33m" # yellow
|
CLR_GUN = "\e[33m" # yellow
|
||||||
CLR_MOVE = "\e[36m" # cyan
|
CLR_MOVE = "\e[36m" # cyan
|
||||||
@@ -1267,6 +1279,11 @@ method run*(bot: ModularBot) =
|
|||||||
for id, es in bot.enemyTracker.enemies:
|
for id, es in bot.enemyTracker.enemies:
|
||||||
enemyPositions[id] = (x: es.x, y: es.y, lastSeenTick: es.lastSeenTick, alive: es.alive)
|
enemyPositions[id] = (x: es.x, y: es.y, lastSeenTick: es.lastSeenTick, alive: es.alive)
|
||||||
|
|
||||||
|
# Debug-only: bullets that resolve during `tickBullets` below are cleared
|
||||||
|
# from the tracker in the same tick, so capture their outcome here for the
|
||||||
|
# overlay's miss vector. Empty (and unused) when the overlay is off.
|
||||||
|
var resolvedMarks: seq[VbResolved]
|
||||||
|
|
||||||
let st = bot.lastState
|
let st = bot.lastState
|
||||||
bot.tracker.tickBullets(st, enemyPositions, proc(gunId: GunId, binIdx: int, fe: FeedbackEvent) =
|
bot.tracker.tickBullets(st, enemyPositions, proc(gunId: GunId, binIdx: int, fe: FeedbackEvent) =
|
||||||
case gunId
|
case gunId
|
||||||
@@ -1294,6 +1311,10 @@ method run*(bot: ModularBot) =
|
|||||||
of BitBrainId: bot.bitbrain.onResult(fe)
|
of BitBrainId: bot.bitbrain.onResult(fe)
|
||||||
else: discard
|
else: discard
|
||||||
if fe.hit: inc bot.virtualHits else: inc bot.virtualMiss
|
if fe.hit: inc bot.virtualHits else: inc bot.virtualMiss
|
||||||
|
if VBulletDebugOn:
|
||||||
|
resolvedMarks.add VbResolved(
|
||||||
|
gunId: gunId, aimX: fe.prediction.x, aimY: fe.prediction.y,
|
||||||
|
actualX: fe.actualX, actualY: fe.actualY, hit: fe.hit)
|
||||||
when DebugVBullets:
|
when DebugVBullets:
|
||||||
let total = bot.virtualHits + bot.virtualMiss
|
let total = bot.virtualHits + bot.virtualMiss
|
||||||
let pct = if total > 0: bot.virtualHits.float / total.float * 100.0 else: 0.0
|
let pct = if total > 0: bot.virtualHits.float / total.float * 100.0 else: 0.0
|
||||||
@@ -1320,25 +1341,11 @@ method run*(bot: ModularBot) =
|
|||||||
if selectedGun != bot.currentGun:
|
if selectedGun != bot.currentGun:
|
||||||
bot.currentGun = selectedGun
|
bot.currentGun = selectedGun
|
||||||
bot.cfgDirty = true # gun switch: emit the config line at tick end
|
bot.cfgDirty = true # gun switch: emit the config line at tick end
|
||||||
case selectedGun
|
# Per-gun colours live in `vbullet_draw.gunColors`, shared with the
|
||||||
of 0: setTurretColor("#FFFFFF"); setBulletColor("#FFFFFF") # HEADON: pure white (was #FF3333 red)
|
# virtual-bullet overlay so turret and overlay can never drift apart.
|
||||||
of 1: setTurretColor("#3366FF"); setBulletColor("#6699FF")
|
let (turretColor, bulletColor) = gunColors(selectedGun)
|
||||||
of 2: setTurretColor("#9933FF"); setBulletColor("#CC66FF")
|
setTurretColor(turretColor)
|
||||||
of 3: setTurretColor("#33CC33"); setBulletColor("#66FF66")
|
setBulletColor(bulletColor)
|
||||||
of 4: setTurretColor("#FFCC00"); setBulletColor("#FFE066")
|
|
||||||
of 5: setTurretColor("#00CC44"); setBulletColor("#66FF99") # PATTERN: green (was #FF6600 orange, too close to BitBrain's pink)
|
|
||||||
of 6: setTurretColor("#CCCCCC"); setBulletColor("#EEEEEE")
|
|
||||||
of 7: setTurretColor("#FF00FF"); setBulletColor("#FF66FF")
|
|
||||||
of 8: setTurretColor("#990000"); setBulletColor("#CC3333")
|
|
||||||
of 9: setTurretColor("#006600"); setBulletColor("#009900")
|
|
||||||
of 10: setTurretColor("#996633"); setBulletColor("#CC9966")
|
|
||||||
of 11: setTurretColor("#008888"); setBulletColor("#00AAAA")
|
|
||||||
of 12: setTurretColor("#CC00CC"); setBulletColor("#FF44FF")
|
|
||||||
of 13: setTurretColor("#00FFCC"); setBulletColor("#66FFDD")
|
|
||||||
of TmPatternId: setTurretColor("#AAFF00"); setBulletColor("#CCFF66")
|
|
||||||
of TmHorizonId: setTurretColor("#00AAFF"); setBulletColor("#66CCFF")
|
|
||||||
of BitBrainId: setTurretColor("#FF1493"); setBulletColor("#FF69B4")
|
|
||||||
else: discard
|
|
||||||
|
|
||||||
let pred = case selectedGun
|
let pred = case selectedGun
|
||||||
of 1: bot.linear.predict(bot.lastState, bulletSpeed(power))
|
of 1: bot.linear.predict(bot.lastState, bulletSpeed(power))
|
||||||
@@ -1387,6 +1394,12 @@ method run*(bot: ModularBot) =
|
|||||||
|
|
||||||
setGunTurnRate(normDelta)
|
setGunTurnRate(normDelta)
|
||||||
|
|
||||||
|
# Virtual-bullet overlay (default off). Drawn AFTER selection so the
|
||||||
|
# default "selected gun only" filter uses this tick's choice.
|
||||||
|
if VBulletDebugOn:
|
||||||
|
discard drawVirtualBullets(bot.tracker, resolvedMarks, selectedGun,
|
||||||
|
VBulletDebugGun, VBulletDebugMax, GunNames)
|
||||||
|
|
||||||
# Emit exactly one config line per tick, at the END of the tick, so the gun
|
# Emit exactly one config line per tick, at the END of the tick, so the gun
|
||||||
# field reflects the selection made above rather than the previous tick's.
|
# field reflects the selection made above rather than the previous tick's.
|
||||||
if bot.cfgDirty:
|
if bot.cfgDirty:
|
||||||
@@ -1449,6 +1462,9 @@ when isMainModule:
|
|||||||
printEnvReport(EnvReportContext(
|
printEnvReport(EnvReportContext(
|
||||||
movementName: MovementName,
|
movementName: MovementName,
|
||||||
vBulletAdmitOnly: VBulletAdmitOnly,
|
vBulletAdmitOnly: VBulletAdmitOnly,
|
||||||
|
vBulletDebug: VBulletDebugOn,
|
||||||
|
vBulletDebugGun: getEnv(VBulletDebugGunEnv, ""),
|
||||||
|
vBulletDebugMax: VBulletDebugMax,
|
||||||
recordWorldState: RecordWorldState,
|
recordWorldState: RecordWorldState,
|
||||||
radarForceSpin: RadarForceSpin,
|
radarForceSpin: RadarForceSpin,
|
||||||
radarScanLog: RadarScanLog,
|
radarScanLog: RadarScanLog,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import movements/learned_surfer
|
|||||||
import guns/tm_horizon
|
import guns/tm_horizon
|
||||||
import guns/bitbrain_gun
|
import guns/bitbrain_gun
|
||||||
import guns/pattern_matcher
|
import guns/pattern_matcher
|
||||||
|
import vbullet_draw
|
||||||
|
|
||||||
const EnvReportEnableEnvVar* = "TR_ENV_REPORT"
|
const EnvReportEnableEnvVar* = "TR_ENV_REPORT"
|
||||||
|
|
||||||
@@ -44,6 +45,9 @@ type
|
|||||||
## diverge from what the bot will actually use.
|
## diverge from what the bot will actually use.
|
||||||
movementName*: string
|
movementName*: string
|
||||||
vBulletAdmitOnly*: bool
|
vBulletAdmitOnly*: bool
|
||||||
|
vBulletDebug*: bool
|
||||||
|
vBulletDebugGun*: string
|
||||||
|
vBulletDebugMax*: int
|
||||||
recordWorldState*: bool
|
recordWorldState*: bool
|
||||||
radarForceSpin*: bool
|
radarForceSpin*: bool
|
||||||
radarScanLog*: bool
|
radarScanLog*: bool
|
||||||
@@ -197,6 +201,14 @@ proc printEffectiveValues(ctx: EnvReportContext) =
|
|||||||
emit("TR_MOVEMENT_LOG", onOff(MovementLog), sourceOfPresence("TR_MOVEMENT_LOG"))
|
emit("TR_MOVEMENT_LOG", onOff(MovementLog), sourceOfPresence("TR_MOVEMENT_LOG"))
|
||||||
emit("TR_VBULLET_ADMIT_ONLY", onOff(ctx.vBulletAdmitOnly),
|
emit("TR_VBULLET_ADMIT_ONLY", onOff(ctx.vBulletAdmitOnly),
|
||||||
sourceOf("TR_VBULLET_ADMIT_ONLY"))
|
sourceOf("TR_VBULLET_ADMIT_ONLY"))
|
||||||
|
emit("TR_VBULLET_DEBUG", onOff(ctx.vBulletDebug),
|
||||||
|
sourceOfPresence(VBulletDebugEnv))
|
||||||
|
emit("TR_VBULLET_DEBUG_GUN",
|
||||||
|
(if ctx.vBulletDebugGun.len == 0: "(unset -> selected gun)"
|
||||||
|
else: ctx.vBulletDebugGun),
|
||||||
|
sourceOf(VBulletDebugGunEnv))
|
||||||
|
emit("TR_VBULLET_DEBUG_MAX", $ctx.vBulletDebugMax,
|
||||||
|
sourceOf(VBulletDebugMaxEnv))
|
||||||
emit("TR_POWER_LOG", onOff(ctx.powerLog), sourceOfPresence("TR_POWER_LOG"))
|
emit("TR_POWER_LOG", onOff(ctx.powerLog), sourceOfPresence("TR_POWER_LOG"))
|
||||||
emit("TR_RESULT_LOG", onOff(ctx.resultLog), sourceOf("TR_RESULT_LOG"))
|
emit("TR_RESULT_LOG", onOff(ctx.resultLog), sourceOf("TR_RESULT_LOG"))
|
||||||
emit("TR_RECORD_WORLDSTATE", onOff(ctx.recordWorldState),
|
emit("TR_RECORD_WORLDSTATE", onOff(ctx.recordWorldState),
|
||||||
@@ -469,6 +481,7 @@ proc knownEnvNames*(): seq[string] =
|
|||||||
"TR_MOVEMENT", "TR_MOVEMENT_LOG", "TR_RECORD_WORLDSTATE",
|
"TR_MOVEMENT", "TR_MOVEMENT_LOG", "TR_RECORD_WORLDSTATE",
|
||||||
"TR_RADAR_FORCE_SPIN", "TR_RADAR_SCANLOG", "TR_RADAR_SCAN_LOG_PATH",
|
"TR_RADAR_FORCE_SPIN", "TR_RADAR_SCANLOG", "TR_RADAR_SCAN_LOG_PATH",
|
||||||
"TR_TRACKER_PROBE", "TR_TRACKER_PROBE_PATH", "TR_VBULLET_ADMIT_ONLY",
|
"TR_TRACKER_PROBE", "TR_TRACKER_PROBE_PATH", "TR_VBULLET_ADMIT_ONLY",
|
||||||
|
VBulletDebugEnv, VBulletDebugGunEnv, VBulletDebugMaxEnv,
|
||||||
"TR_POWER_LOG",
|
"TR_POWER_LOG",
|
||||||
"TR_RESULT_LOG",
|
"TR_RESULT_LOG",
|
||||||
# the .env loader's own environment hook (TR_ENV_FILE names the file)
|
# the .env loader's own environment hook (TR_ENV_FILE names the file)
|
||||||
|
|||||||
@@ -0,0 +1,244 @@
|
|||||||
|
## Debug overlay for the virtual-bullet tracker (the training signal the gun
|
||||||
|
## selector learns from). DEFAULT OFF: when the env knobs are unset the shipped
|
||||||
|
## bot draws exactly what it drew before.
|
||||||
|
##
|
||||||
|
## Why this lives in `ModularBot_garage/src` and not next to the tracker: the
|
||||||
|
## tracker is shared with the offline range, while the overlay is a GUI-only
|
||||||
|
## concern of the live bot. It imports the bot API's debug-graphics module to
|
||||||
|
## emit SVG, and the tracker to read the bullets.
|
||||||
|
##
|
||||||
|
## Visual vocabulary (one colour per gun, the SAME table as the turret):
|
||||||
|
##
|
||||||
|
## path thin line from the fire point along the fire direction, as far as
|
||||||
|
## the bullet has actually travelled (`travelDist`).
|
||||||
|
## aim small hollow circle at the PREDICTED target position (`aimX/Y`).
|
||||||
|
## miss short line from the aim point to the closest-approach point, plus
|
||||||
|
## a HIT dot (filled) / MISS ring (hollow) there. This is the score.
|
||||||
|
##
|
||||||
|
## The colour table is `gunColors`, shared with `ModularBot.run`'s turret
|
||||||
|
## colouring, so the overlay and the turret can never drift apart.
|
||||||
|
##
|
||||||
|
## Env knobs (all read once at boot by ModularBot and registered in
|
||||||
|
## `env_report.nim`):
|
||||||
|
## TR_VBULLET_DEBUG presence/1 = on. default off.
|
||||||
|
## TR_VBULLET_DEBUG_GUN "" = selected gun only (default); "all"/"*" = every
|
||||||
|
## gun; otherwise a gun name (e.g. `Pattern`).
|
||||||
|
## TR_VBULLET_DEBUG_MAX cap on bullets drawn per tick. default 32.
|
||||||
|
|
||||||
|
import std/[math, os, strutils, algorithm]
|
||||||
|
import robocode_tankroyale_botapi
|
||||||
|
# The bot API also exports a case-insensitively identical `BOT_RADIUS`, so bring
|
||||||
|
# the harness module in qualified only and reference `gun_interface.BotRadius`.
|
||||||
|
from gun_harness/gun_interface import nil
|
||||||
|
import gun_harness/virtual_bullets as vb
|
||||||
|
|
||||||
|
const
|
||||||
|
VBulletDebugEnv* = "TR_VBULLET_DEBUG"
|
||||||
|
VBulletDebugGunEnv* = "TR_VBULLET_DEBUG_GUN"
|
||||||
|
VBulletDebugMaxEnv* = "TR_VBULLET_DEBUG_MAX"
|
||||||
|
VBulletDebugDefaultMax* = 32
|
||||||
|
## Bullets drawn per tick when `TR_VBULLET_DEBUG_MAX` is unset. With the
|
||||||
|
## default SELECTED-gun filter a gun spawns at most one flight per power bin
|
||||||
|
## per tick (~3-4 in flight), so 32 is generous while still bounding the
|
||||||
|
## worst case: at most 32 paths + 32 aim rings + miss vectors. A wall of
|
||||||
|
## 200 overlapping lines is a failed feature, so the cap is enforced.
|
||||||
|
|
||||||
|
VbFilterSelected* = -1
|
||||||
|
VbFilterAll* = -2
|
||||||
|
|
||||||
|
VbPathWidth* = 1.0
|
||||||
|
VbAimRadius* = 5.0
|
||||||
|
VbAimWidth* = 1.5
|
||||||
|
VbMarkRadius* = 3.0
|
||||||
|
|
||||||
|
# ── env resolution (pure, so a test can pin it) ──────────────────────────────
|
||||||
|
|
||||||
|
proc vBulletDebugEnabled*(): bool =
|
||||||
|
## Presence/1 = on; explicit 0/false/no/off = off; unset = off (shipped).
|
||||||
|
let v = getEnv(VBulletDebugEnv, "").strip().toLowerAscii()
|
||||||
|
v.len > 0 and v notin ["0", "false", "no", "off"]
|
||||||
|
|
||||||
|
proc vBulletDebugMaxBullets*(): int =
|
||||||
|
## `TR_VBULLET_DEBUG_MAX`, clamped to at least 1. Bad values fall back.
|
||||||
|
try:
|
||||||
|
result = max(1, parseInt(getEnv(VBulletDebugMaxEnv, "").strip()))
|
||||||
|
except ValueError:
|
||||||
|
result = VBulletDebugDefaultMax
|
||||||
|
|
||||||
|
proc resolveGunFilter*(spec: string, names: openArray[string]): int =
|
||||||
|
## "" -> selected gun only; "all"/"*" -> every gun; a gun name
|
||||||
|
## (case-insensitive) -> that gun; unknown -> selected (safe fallback).
|
||||||
|
let s = spec.strip()
|
||||||
|
if s.len == 0: return VbFilterSelected
|
||||||
|
let low = s.toLowerAscii()
|
||||||
|
if low == "all" or low == "*": return VbFilterAll
|
||||||
|
for i, n in names:
|
||||||
|
if cmpIgnoreCase(n, s) == 0: return i
|
||||||
|
VbFilterSelected
|
||||||
|
|
||||||
|
# ── the shared per-gun colour table ──────────────────────────────────────────
|
||||||
|
|
||||||
|
proc gunColors*(gunId: int): tuple[turret, bullet: string] =
|
||||||
|
## SINGLE source of truth for the per-gun colours, used by BOTH the live
|
||||||
|
## turret colouring in `ModularBot.run` and this overlay. Keep gun ids in
|
||||||
|
## step with `GunNames`.
|
||||||
|
case gunId
|
||||||
|
of 0: ("#FFFFFF", "#FFFFFF") # HeadOn — white
|
||||||
|
of 1: ("#3366FF", "#6699FF") # Linear — blue
|
||||||
|
of 2: ("#9933FF", "#CC66FF") # Tsetlin — violet
|
||||||
|
of 3: ("#33CC33", "#66FF66") # Circular — green
|
||||||
|
of 4: ("#FFCC00", "#FFE066") # GuessFactor — amber
|
||||||
|
of 5: ("#00CC44", "#66FF99") # Pattern — green
|
||||||
|
of 6: ("#CCCCCC", "#EEEEEE") # WallBounce — grey
|
||||||
|
of 7: ("#FF00FF", "#FF66FF") # Accel — magenta
|
||||||
|
of 8: ("#990000", "#CC3333") # StopShot — dark red
|
||||||
|
of 9: ("#006600", "#009900") # Displace — dark green
|
||||||
|
of 10: ("#996633", "#CC9966") # AvgLead — brown
|
||||||
|
of 11: ("#008888", "#00AAAA") # DecayGF — teal
|
||||||
|
of 12: ("#CC00CC", "#FF44FF") # KNN — purple
|
||||||
|
of 13: ("#00FFCC", "#66FFDD") # TMSelect — cyan
|
||||||
|
of 14: ("#AAFF00", "#CCFF66") # TMPattern — lime
|
||||||
|
of 15: ("#00AAFF", "#66CCFF") # TMHorizon — sky
|
||||||
|
of 16: ("#FF1493", "#FF69B4") # BitBrain — deep pink
|
||||||
|
else: ("#FFFFFF", "#FFFFFF")
|
||||||
|
|
||||||
|
proc gunTurretColor*(gunId: int): string = gunColors(gunId).turret
|
||||||
|
proc gunBulletColor*(gunId: int): string = gunColors(gunId).bullet
|
||||||
|
|
||||||
|
# ── primitives ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type
|
||||||
|
VbPrimKind* = enum
|
||||||
|
vpPath ## travelled path (line)
|
||||||
|
vpAim ## predicted aim point (hollow circle)
|
||||||
|
vpMiss ## aim -> closest-approach vector (line)
|
||||||
|
vpHit ## hit at closest approach (filled circle)
|
||||||
|
vpMissMark ## miss at closest approach (hollow circle)
|
||||||
|
|
||||||
|
VbPrim* = object
|
||||||
|
kind*: VbPrimKind
|
||||||
|
x1*, y1*: float ## line start / circle centre
|
||||||
|
x2*, y2*: float ## line end (circles ignore)
|
||||||
|
r*: float ## circle radius
|
||||||
|
gunId*: int
|
||||||
|
color*: string
|
||||||
|
|
||||||
|
VbResolved* = object
|
||||||
|
## One bullet resolution captured from `onResolved`, so the miss vector of
|
||||||
|
## a bullet that DISAPPEARED this tick is still visible (the tracker clears
|
||||||
|
## `active` the moment it resolves).
|
||||||
|
gunId*: int
|
||||||
|
aimX*, aimY*: float
|
||||||
|
actualX*, actualY*: float
|
||||||
|
hit*: bool
|
||||||
|
|
||||||
|
proc matchesFilter(gunId, selectedGun, filterGun: int): bool {.inline.} =
|
||||||
|
if filterGun == VbFilterAll: return true
|
||||||
|
if filterGun == VbFilterSelected: return gunId == selectedGun
|
||||||
|
gunId == filterGun
|
||||||
|
|
||||||
|
proc collectVirtualBulletPrims*(t: VirtualTracker, selectedGun, filterGun,
|
||||||
|
maxBullets: int): seq[VbPrim] =
|
||||||
|
## Pure: the draw primitives for the tracker's ACTIVE bullets, newest first,
|
||||||
|
## capped at `maxBullets` bullets (a bullet may add up to 4 primitives).
|
||||||
|
var drawn = 0
|
||||||
|
for k in 1 .. MaxBullets:
|
||||||
|
if drawn >= maxBullets: break
|
||||||
|
let i = (t.head - k + MaxBullets) mod MaxBullets
|
||||||
|
let b = t.bullets[i]
|
||||||
|
if not b.active: continue
|
||||||
|
if not matchesFilter(b.gunId, selectedGun, filterGun): continue
|
||||||
|
inc drawn
|
||||||
|
|
||||||
|
let dx = b.aimX - b.fireX
|
||||||
|
let dy = b.aimY - b.fireY
|
||||||
|
let dist = hypot(dx, dy)
|
||||||
|
var ux, uy: float
|
||||||
|
if dist > 1e-6:
|
||||||
|
ux = dx / dist
|
||||||
|
uy = dy / dist
|
||||||
|
let curX = b.fireX + ux * b.travelDist
|
||||||
|
let curY = b.fireY + uy * b.travelDist
|
||||||
|
let col = gunTurretColor(b.gunId)
|
||||||
|
|
||||||
|
result.add VbPrim(kind: vpPath, x1: b.fireX, y1: b.fireY, x2: curX, y2: curY,
|
||||||
|
gunId: b.gunId, color: col)
|
||||||
|
result.add VbPrim(kind: vpAim, x1: b.aimX, y1: b.aimY, r: VbAimRadius,
|
||||||
|
gunId: b.gunId, color: col)
|
||||||
|
if b.bestMissDist < Inf:
|
||||||
|
result.add VbPrim(kind: vpMiss, x1: b.aimX, y1: b.aimY,
|
||||||
|
x2: b.bestMissX, y2: b.bestMissY,
|
||||||
|
gunId: b.gunId, color: col)
|
||||||
|
result.add VbPrim(
|
||||||
|
kind: (if b.bestMissDist < gun_interface.BotRadius: vpHit else: vpMissMark),
|
||||||
|
x1: b.bestMissX, y1: b.bestMissY, r: VbMarkRadius,
|
||||||
|
gunId: b.gunId, color: col)
|
||||||
|
|
||||||
|
proc collectResolvedPrims*(resolved: seq[VbResolved], selectedGun, filterGun,
|
||||||
|
maxBullets: int): seq[VbPrim] =
|
||||||
|
## Pure: miss-vector + hit/miss marker for bullets that resolved this tick.
|
||||||
|
## Works for BOTH metrics (the point metric records no running best-miss).
|
||||||
|
var drawn = 0
|
||||||
|
for r in resolved:
|
||||||
|
if drawn >= maxBullets: break
|
||||||
|
if not matchesFilter(r.gunId, selectedGun, filterGun): continue
|
||||||
|
inc drawn
|
||||||
|
let col = gunTurretColor(r.gunId)
|
||||||
|
result.add VbPrim(kind: vpMiss, x1: r.aimX, y1: r.aimY,
|
||||||
|
x2: r.actualX, y2: r.actualY, gunId: r.gunId, color: col)
|
||||||
|
result.add VbPrim(kind: (if r.hit: vpHit else: vpMissMark),
|
||||||
|
x1: r.actualX, y1: r.actualY, r: VbMarkRadius,
|
||||||
|
gunId: r.gunId, color: col)
|
||||||
|
|
||||||
|
# ── emit to the GUI debug-graphics buffer ────────────────────────────────────
|
||||||
|
|
||||||
|
proc drawVbLegend(prims: seq[VbPrim], names: openArray[string]) =
|
||||||
|
## One colour swatch + gun name per gun that contributed a primitive, top-left
|
||||||
|
## corner. Cheap and it makes the colours self-explanatory.
|
||||||
|
var seen: seq[int]
|
||||||
|
for p in prims:
|
||||||
|
if p.gunId notin seen: seen.add p.gunId
|
||||||
|
seen.sort()
|
||||||
|
var y = 24.0
|
||||||
|
for g in seen:
|
||||||
|
if g < 0 or g >= names.len: continue
|
||||||
|
setFillColor(gunTurretColor(g))
|
||||||
|
fillRectangle(8.0, y - 10.0, 10.0, 10.0)
|
||||||
|
setFillColor("#FFFFFF")
|
||||||
|
setFont("Arial", 12.0)
|
||||||
|
drawText(names[g], 22.0, y)
|
||||||
|
y += 16.0
|
||||||
|
|
||||||
|
proc emitVbPrims*(prims: seq[VbPrim], names: openArray[string] = []): int =
|
||||||
|
## Emit primitives into the current tick's SVG buffer and return the count.
|
||||||
|
## Thin lines so a busy sky stays readable.
|
||||||
|
for p in prims:
|
||||||
|
case p.kind
|
||||||
|
of vpPath, vpMiss:
|
||||||
|
setStrokeColor(p.color)
|
||||||
|
setStrokeWidth(VbPathWidth)
|
||||||
|
drawLine(p.x1, p.y1, p.x2, p.y2)
|
||||||
|
of vpAim:
|
||||||
|
setStrokeColor(p.color)
|
||||||
|
setStrokeWidth(VbAimWidth)
|
||||||
|
drawCircle(p.x1, p.y1, p.r)
|
||||||
|
of vpHit:
|
||||||
|
setFillColor(p.color)
|
||||||
|
fillCircle(p.x1, p.y1, p.r)
|
||||||
|
of vpMissMark:
|
||||||
|
setStrokeColor(p.color)
|
||||||
|
setStrokeWidth(VbAimWidth)
|
||||||
|
drawCircle(p.x1, p.y1, p.r)
|
||||||
|
if names.len > 0:
|
||||||
|
drawVbLegend(prims, names)
|
||||||
|
result = prims.len
|
||||||
|
|
||||||
|
proc drawVirtualBullets*(t: VirtualTracker, resolved: seq[VbResolved],
|
||||||
|
selectedGun, filterGun, maxBullets: int,
|
||||||
|
names: openArray[string]): int =
|
||||||
|
## Entry point called once per tick by ModularBot. Returns primitives drawn.
|
||||||
|
let active = collectVirtualBulletPrims(t, selectedGun, filterGun, maxBullets)
|
||||||
|
let rest = collectResolvedPrims(resolved, selectedGun, filterGun, maxBullets)
|
||||||
|
var prims = active
|
||||||
|
prims.add rest
|
||||||
|
emitVbPrims(prims, names)
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
## Deterministic, offline check of the virtual-bullet overlay (`vbullet_draw`).
|
||||||
|
##
|
||||||
|
## NO battle, NO Java, NO server: it fabricates a tracker state, calls the same
|
||||||
|
## `collectVirtualBulletPrims` / `emitVbPrims` the live bot calls, and asserts
|
||||||
|
## the primitive counts, the geometry (path and aim point are collinear), the
|
||||||
|
## filter and cap behaviour, and the SVG emission. It also pins the shared
|
||||||
|
## turret/overlay colour table.
|
||||||
|
##
|
||||||
|
## Run: nim c -r ModularBot_garage/tests/test_vbullet_draw.nim
|
||||||
|
|
||||||
|
import std/[math, strutils, tables, sequtils]
|
||||||
|
import robocode_tankroyale_botapi
|
||||||
|
import gun_harness/gun_interface
|
||||||
|
import gun_harness/virtual_bullets as vb
|
||||||
|
import vbullet_draw
|
||||||
|
|
||||||
|
var failures = 0
|
||||||
|
proc check(name: string, ok: bool) =
|
||||||
|
if ok: echo "PASS: ", name
|
||||||
|
else: echo "FAIL: ", name; inc failures
|
||||||
|
|
||||||
|
# ── a fixed tracker: gun 1 (Linear) and gun 5 (Pattern), 4 power bins each ────
|
||||||
|
const SelfX = 100.0
|
||||||
|
const SelfY = 100.0
|
||||||
|
const AimX = 300.0
|
||||||
|
const AimY = 300.0
|
||||||
|
const TargetId = 7
|
||||||
|
|
||||||
|
proc makeTracker(): VirtualTracker =
|
||||||
|
result = vb.initTracker(17)
|
||||||
|
let s0 = WorldState(selfX: SelfX, selfY: SelfY,
|
||||||
|
enemyX: AimX, enemyY: AimY, enemySpeed: 0.0,
|
||||||
|
enemyHeading: 0.0, arenaWidth: 1000.0, arenaHeight: 1000.0,
|
||||||
|
tick: 0)
|
||||||
|
var preds: array[len(PowerBins), GunPrediction]
|
||||||
|
for i in 0..<len(PowerBins):
|
||||||
|
preds[i] = GunPrediction(x: AimX, y: AimY)
|
||||||
|
result.spawnBullets(1, preds, s0, TargetId)
|
||||||
|
result.spawnBullets(5, preds, s0, TargetId)
|
||||||
|
|
||||||
|
proc tickOnce(t: var VirtualTracker, tick: int) =
|
||||||
|
## Advance every bullet one tick against the (stationary) target. Produces a
|
||||||
|
## non-degenerate `travelDist` and a `bestMissDist` for the path metric.
|
||||||
|
var enemies: Table[int, tuple[x, y: float, lastSeenTick: int, alive: bool]]
|
||||||
|
enemies[TargetId] = (x: AimX, y: AimY, lastSeenTick: tick, alive: true)
|
||||||
|
let st = WorldState(selfX: SelfX, selfY: SelfY,
|
||||||
|
enemyX: AimX, enemyY: AimY, enemySpeed: 0.0,
|
||||||
|
enemyHeading: 0.0, arenaWidth: 1000.0, arenaHeight: 1000.0,
|
||||||
|
tick: tick)
|
||||||
|
t.tickBullets(st, enemies, proc(gunId: GunId, binIdx: int, fe: FeedbackEvent) = discard)
|
||||||
|
|
||||||
|
proc testCountsAndGeometry() =
|
||||||
|
var t = makeTracker()
|
||||||
|
t.tickOnce(1)
|
||||||
|
t.tickOnce(2)
|
||||||
|
|
||||||
|
let prims = collectVirtualBulletPrims(t, selectedGun = 1,
|
||||||
|
filterGun = VbFilterSelected,
|
||||||
|
maxBullets = 100)
|
||||||
|
# Reconstruction: 4 gun-1 bullets, each path+aim (+miss+marker since the path
|
||||||
|
# metric recorded a closest approach). Independent of the production loop.
|
||||||
|
var expected = 0
|
||||||
|
var activeGun1 = 0
|
||||||
|
for b in t.bullets:
|
||||||
|
if b.active and b.gunId == 1:
|
||||||
|
inc activeGun1
|
||||||
|
expected += 2
|
||||||
|
if b.bestMissDist < Inf: expected += 2
|
||||||
|
check "selected-gun filter draws exactly the active gun-1 bullets: " &
|
||||||
|
$activeGun1, activeGun1 == len(PowerBins)
|
||||||
|
check "primitive count == 2 (path+aim) [+2 miss] per active bullet: " &
|
||||||
|
$prims.len & " == " & $expected, prims.len == expected
|
||||||
|
|
||||||
|
# Walk the per-bullet groups: path then aim, then optional miss+marker.
|
||||||
|
var i = 0
|
||||||
|
var collinear = 0
|
||||||
|
var badCross = 0
|
||||||
|
while i + 1 < prims.len and prims[i].kind == vpPath:
|
||||||
|
let p = prims[i]
|
||||||
|
let a = prims[i + 1]
|
||||||
|
if a.kind != vpAim:
|
||||||
|
inc badCross
|
||||||
|
else:
|
||||||
|
# |(aim-fire) x (pathEnd-fire)| / (|aim-fire|*|path|) == sin(angle) ~ 0
|
||||||
|
let cross = (a.x1 - p.x1) * (p.y2 - p.y1) - (a.y1 - p.y1) * (p.x2 - p.x1)
|
||||||
|
let scale = max(1.0, hypot(a.x1 - p.x1, a.y1 - p.y1) *
|
||||||
|
hypot(p.x2 - p.x1, p.y2 - p.y1))
|
||||||
|
if abs(cross) / scale < 1e-6: inc collinear else: inc badCross
|
||||||
|
i += 2
|
||||||
|
if i < prims.len and prims[i].kind == vpMiss:
|
||||||
|
check "miss marker follows its miss vector",
|
||||||
|
prims[i + 1].kind in {vpHit, vpMissMark}
|
||||||
|
i += 2
|
||||||
|
check "every drawn bullet has a path+aim pair", badCross == 0
|
||||||
|
check "all drawn aims are collinear with their path: " & $collinear,
|
||||||
|
collinear == activeGun1
|
||||||
|
|
||||||
|
proc testFilter() =
|
||||||
|
var t = makeTracker()
|
||||||
|
let sel = collectVirtualBulletPrims(t, 1, VbFilterSelected, 100)
|
||||||
|
let p5 = collectVirtualBulletPrims(t, 1, 5, 100)
|
||||||
|
let all = collectVirtualBulletPrims(t, 1, VbFilterAll, 100)
|
||||||
|
check "selected filter yields only gun 1",
|
||||||
|
sel.len > 0 and sel.allIt(it.gunId == 1)
|
||||||
|
check "named filter (Pattern id 5) yields only gun 5",
|
||||||
|
p5.len > 0 and p5.allIt(it.gunId == 5)
|
||||||
|
check "all filter yields both guns",
|
||||||
|
all.len == sel.len + p5.len
|
||||||
|
|
||||||
|
proc testCap() =
|
||||||
|
var t = makeTracker()
|
||||||
|
let one = collectVirtualBulletPrims(t, 1, VbFilterAll, 1)
|
||||||
|
var bullets = 0
|
||||||
|
for p in one:
|
||||||
|
if p.kind == vpPath: inc bullets
|
||||||
|
check "cap=1 draws exactly one bullet", bullets == 1
|
||||||
|
|
||||||
|
proc testResolved() =
|
||||||
|
let resolved = @[
|
||||||
|
VbResolved(gunId: 1, aimX: AimX, aimY: AimY,
|
||||||
|
actualX: AimX + 25.0, actualY: AimY, hit: false),
|
||||||
|
VbResolved(gunId: 5, aimX: AimX, aimY: AimY,
|
||||||
|
actualX: AimX + 3.0, actualY: AimY, hit: true),
|
||||||
|
]
|
||||||
|
let miss = collectResolvedPrims(resolved, 1, VbFilterAll, 100)
|
||||||
|
check "one resolved bullet -> miss vector + marker (2 prims): " & $miss.len,
|
||||||
|
miss.len == 4
|
||||||
|
check "first resolved is a MISS marker",
|
||||||
|
miss[0].kind == vpMiss and miss[1].kind == vpMissMark
|
||||||
|
check "second resolved is a HIT marker",
|
||||||
|
miss[2].kind == vpMiss and miss[3].kind == vpHit
|
||||||
|
|
||||||
|
proc testEmit() =
|
||||||
|
clearGraphics()
|
||||||
|
var t = makeTracker()
|
||||||
|
let prims = collectVirtualBulletPrims(t, 1, VbFilterSelected, 100)
|
||||||
|
let n = emitVbPrims(prims, ["HeadOn", "Linear", "Tsetlin", "Circular",
|
||||||
|
"GuessFactor", "Pattern"])
|
||||||
|
let svg = svgOutput()
|
||||||
|
check "emit returns the primitive count: " & $n, n == prims.len
|
||||||
|
check "SVG carries one <line> per path primitive",
|
||||||
|
svg.count("<line") == 4 # 4 bullets x 1 travelled path
|
||||||
|
check "SVG carries the aim circles and the legend",
|
||||||
|
svg.count("<circle") == 4 and "<text" in svg
|
||||||
|
|
||||||
|
proc testColours() =
|
||||||
|
check "gun 5 (Pattern) turret is green", gunTurretColor(5) == "#00CC44"
|
||||||
|
check "gun 0 (HeadOn) turret is white", gunTurretColor(0) == "#FFFFFF"
|
||||||
|
check "gun 16 (BitBrain) turret is deep pink", gunTurretColor(16) == "#FF1493"
|
||||||
|
check "unknown gun falls back to white", gunTurretColor(99) == "#FFFFFF"
|
||||||
|
check "turret and bullet colours both defined",
|
||||||
|
gunBulletColor(5) == "#66FF99"
|
||||||
|
|
||||||
|
check "resolveGunFilter default = selected", resolveGunFilter("", ["A", "Pattern"]) == VbFilterSelected
|
||||||
|
check "resolveGunFilter all", resolveGunFilter("all", ["A"]) == VbFilterAll
|
||||||
|
check "resolveGunFilter '*'", resolveGunFilter("*", ["A"]) == VbFilterAll
|
||||||
|
check "resolveGunFilter by name (case-insensitive)",
|
||||||
|
resolveGunFilter("pAtTeRn", ["A", "Pattern"]) == 1
|
||||||
|
check "resolveGunFilter unknown falls back to selected",
|
||||||
|
resolveGunFilter("Nonesuch", ["A"]) == VbFilterSelected
|
||||||
|
|
||||||
|
testCountsAndGeometry()
|
||||||
|
testFilter()
|
||||||
|
testCap()
|
||||||
|
testResolved()
|
||||||
|
testEmit()
|
||||||
|
testColours()
|
||||||
|
|
||||||
|
if failures > 0:
|
||||||
|
echo "\n", failures, " check(s) FAILED"
|
||||||
|
quit(1)
|
||||||
|
echo "\nAll vbullet-draw checks passed."
|
||||||
@@ -449,6 +449,22 @@ Measured byte-identical on `bmPath`. Kept for experiments; leave at defaults.
|
|||||||
| `TR_RADAR_SCAN_LOG_PATH` | `/tmp/radar_scan_log.jsonl` | where that goes |
|
| `TR_RADAR_SCAN_LOG_PATH` | `/tmp/radar_scan_log.jsonl` | where that goes |
|
||||||
| `TR_TRACKER_PROBE` | off | presence-based; per-tick enemy tracker vs server enemy count |
|
| `TR_TRACKER_PROBE` | off | presence-based; per-tick enemy tracker vs server enemy count |
|
||||||
| `TR_TRACKER_PROBE_PATH` | `/tmp/tracker_probe.jsonl` | where that goes |
|
| `TR_TRACKER_PROBE_PATH` | `/tmp/tracker_probe.jsonl` | where that goes |
|
||||||
|
| `TR_VBULLET_DEBUG` | off | presence-based; overlay the virtual bullets in the GUI debug graphics (see below) |
|
||||||
|
| `TR_VBULLET_DEBUG_GUN` | selected gun | `all`/`*` for every gun, or a gun name (e.g. `Pattern`); unset = only the currently selected gun |
|
||||||
|
| `TR_VBULLET_DEBUG_MAX` | `32` | cap on bullets drawn per tick |
|
||||||
|
|
||||||
|
The **virtual-bullet overlay** (`TR_VBULLET_DEBUG`, off by default) makes the
|
||||||
|
selector's training signal visible. Turn it on with:
|
||||||
|
|
||||||
|
TR_VBULLET_DEBUG=1 TR_VBULLET_DEBUG_GUN=all ./out/ModularBot
|
||||||
|
|
||||||
|
- **travelled path** — thin line from the fire point along the fire direction,
|
||||||
|
as far as the bullet has actually flown.
|
||||||
|
- **aim ring** — small hollow circle at the predicted target position.
|
||||||
|
- **miss vector** — line from the aim ring to the closest-approach point; a
|
||||||
|
filled dot there is a HIT, a hollow ring a MISS (the score the selector sees).
|
||||||
|
- colour per gun is the SAME table as the turret (`vbullet_draw.gunColors`), with
|
||||||
|
a one-line legend in the top-left corner.
|
||||||
|
|
||||||
The **adaptive-melee radar** has no env knobs. Its tuning lives in compile-time
|
The **adaptive-melee radar** has no env knobs. Its tuning lives in compile-time
|
||||||
constants in `radars/adaptive_melee_radar.nim:36-50`: `MaxRadarTurnRate=45`,
|
constants in `radars/adaptive_melee_radar.nim:36-50`: `MaxRadarTurnRate=45`,
|
||||||
|
|||||||
Reference in New Issue
Block a user