## 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") # LEADGAIN — 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)