188 lines
7.6 KiB
Nim
188 lines
7.6 KiB
Nim
## 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 testEntryPoint() =
|
|
## The exact proc `ModularBot.run` calls when TR_VBULLET_DEBUG is on.
|
|
clearGraphics()
|
|
var t = makeTracker()
|
|
let resolved = @[VbResolved(gunId: 1, aimX: AimX, aimY: AimY,
|
|
actualX: AimX + 25.0, actualY: AimY, hit: false)]
|
|
let n = drawVirtualBullets(t, resolved, 1, VbFilterSelected, 100,
|
|
["HeadOn", "Linear", "Pattern"])
|
|
let svg = svgOutput()
|
|
check "entry point: 4 active bullets (8 prims) + 1 resolved (2 prims): " & $n,
|
|
n == 10
|
|
check "entry point emits 4 path lines + 1 miss line and a legend",
|
|
svg.count("<line") == 5 and svg.count("<circle") == 5 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()
|
|
testEntryPoint()
|
|
testColours()
|
|
|
|
if failures > 0:
|
|
echo "\n", failures, " check(s) FAILED"
|
|
quit(1)
|
|
echo "\nAll vbullet-draw checks passed."
|