j135 draw-only geometry overlay (TR_GEO_DEBUG) + global debug-draw switch (TR_DEBUG_DRAW)
This commit is contained in:
@@ -44,6 +44,7 @@ import targeting/enemy_tracker
|
|||||||
import targeting/target_selector
|
import targeting/target_selector
|
||||||
import env_report
|
import env_report
|
||||||
import vbullet_draw
|
import vbullet_draw
|
||||||
|
import geo_overlay
|
||||||
|
|
||||||
const botJsonPath = currentSourcePath().parentDir / "ModularBot.json"
|
const botJsonPath = currentSourcePath().parentDir / "ModularBot.json"
|
||||||
const DebugVBullets = false
|
const DebugVBullets = false
|
||||||
@@ -176,6 +177,19 @@ let VBulletDebugOn = vBulletDebugEnabled()
|
|||||||
let VBulletDebugMax = vBulletDebugMaxBullets()
|
let VBulletDebugMax = vBulletDebugMaxBullets()
|
||||||
let VBulletDebugGun = resolveGunFilter(getEnv(VBulletDebugGunEnv, ""), GunNames)
|
let VBulletDebugGun = resolveGunFilter(getEnv(VBulletDebugGunEnv, ""), GunNames)
|
||||||
|
|
||||||
|
## ── global debug-draw switch (TR_DEBUG_DRAW, default ON) ───────────────────
|
||||||
|
## When 0/false/no/off, every mover is built with `debugGraphics = false` and
|
||||||
|
## the virtual-bullet overlay is skipped, so nothing but the (independent,
|
||||||
|
## default-off) TR_GEO_DEBUG geometry overlay is left on screen. When on, every
|
||||||
|
## per-element knob behaves exactly as before.
|
||||||
|
let DebugDrawOn = debugDrawEnabled()
|
||||||
|
|
||||||
|
## ── geometry overlay (TR_GEO_DEBUG, default OFF) ───────────────────────────
|
||||||
|
## Draw-only visual aid (see `geo_overlay.nim`): the two bots as the endpoints
|
||||||
|
## of a circle diameter, each heading line, and each line's intersection points
|
||||||
|
## with the circle. No decision, movement or gun change.
|
||||||
|
let GeoDebugOn = geoDebugEnabled()
|
||||||
|
|
||||||
const
|
const
|
||||||
CLR_GUN = "\e[33m" # yellow
|
CLR_GUN = "\e[33m" # yellow
|
||||||
CLR_MOVE = "\e[36m" # cyan
|
CLR_MOVE = "\e[36m" # cyan
|
||||||
@@ -1357,7 +1371,7 @@ 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:
|
if DebugDrawOn and VBulletDebugOn:
|
||||||
resolvedMarks.add VbResolved(
|
resolvedMarks.add VbResolved(
|
||||||
gunId: gunId, aimX: fe.prediction.x, aimY: fe.prediction.y,
|
gunId: gunId, aimX: fe.prediction.x, aimY: fe.prediction.y,
|
||||||
actualX: fe.actualX, actualY: fe.actualY, hit: fe.hit)
|
actualX: fe.actualX, actualY: fe.actualY, hit: fe.hit)
|
||||||
@@ -1440,12 +1454,19 @@ method run*(bot: ModularBot) =
|
|||||||
|
|
||||||
setGunTurnRate(normDelta)
|
setGunTurnRate(normDelta)
|
||||||
|
|
||||||
# Virtual-bullet overlay (default off). Drawn AFTER selection so the
|
# Virtual-bullet overlay (default off; suppressed by TR_DEBUG_DRAW=0).
|
||||||
# default "selected gun only" filter uses this tick's choice.
|
# Drawn AFTER selection so the default "selected gun only" filter uses
|
||||||
if VBulletDebugOn:
|
# this tick's choice.
|
||||||
|
if DebugDrawOn and VBulletDebugOn:
|
||||||
discard drawVirtualBullets(bot.tracker, resolvedMarks, selectedGun,
|
discard drawVirtualBullets(bot.tracker, resolvedMarks, selectedGun,
|
||||||
VBulletDebugGun, VBulletDebugMax, GunNames)
|
VBulletDebugGun, VBulletDebugMax, GunNames)
|
||||||
|
|
||||||
|
# Geometry overlay (TR_GEO_DEBUG, default off). Independent of
|
||||||
|
# TR_DEBUG_DRAW: this is the one thing left when debug drawing is off.
|
||||||
|
if GeoDebugOn:
|
||||||
|
discard drawGeoOverlay(ws.selfX, ws.selfY, ws.selfHeading,
|
||||||
|
ws.enemyX, ws.enemyY, ws.enemyHeading)
|
||||||
|
|
||||||
# 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:
|
||||||
@@ -1490,9 +1511,9 @@ when isMainModule:
|
|||||||
bitbrain: initBitBrainGun(),
|
bitbrain: initBitBrainGun(),
|
||||||
radar: RadarLockModule(),
|
radar: RadarLockModule(),
|
||||||
meleeRadar: initAdaptiveMeleeRadar(),
|
meleeRadar: initAdaptiveMeleeRadar(),
|
||||||
mover: TFILModule(debugGraphics: true),
|
mover: TFILModule(debugGraphics: DebugDrawOn),
|
||||||
ringMover: TFILRingModule(debugGraphics: true),
|
ringMover: TFILRingModule(debugGraphics: DebugDrawOn),
|
||||||
strafeMover: StrafeModule(debugGraphics: true),
|
strafeMover: StrafeModule(debugGraphics: DebugDrawOn),
|
||||||
surfMover: initWaveSurfer(),
|
surfMover: initWaveSurfer(),
|
||||||
learnedMover: initLearnedSurfer(),
|
learnedMover: initLearnedSurfer(),
|
||||||
rammer: initRammer(),
|
rammer: initRammer(),
|
||||||
@@ -1512,6 +1533,8 @@ when isMainModule:
|
|||||||
vBulletDebugGun: getEnv(VBulletDebugGunEnv, ""),
|
vBulletDebugGun: getEnv(VBulletDebugGunEnv, ""),
|
||||||
vBulletDebugMax: VBulletDebugMax,
|
vBulletDebugMax: VBulletDebugMax,
|
||||||
recordWorldState: RecordWorldState,
|
recordWorldState: RecordWorldState,
|
||||||
|
geoDebug: GeoDebugOn,
|
||||||
|
debugDraw: DebugDrawOn,
|
||||||
radarForceSpin: RadarForceSpin,
|
radarForceSpin: RadarForceSpin,
|
||||||
radarScanLog: RadarScanLog,
|
radarScanLog: RadarScanLog,
|
||||||
radarScanLogPath: RadarScanLogPath,
|
radarScanLogPath: RadarScanLogPath,
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import guns/tm_horizon
|
|||||||
import guns/bitbrain_gun
|
import guns/bitbrain_gun
|
||||||
import guns/pattern_matcher
|
import guns/pattern_matcher
|
||||||
import vbullet_draw
|
import vbullet_draw
|
||||||
|
import geo_overlay
|
||||||
|
|
||||||
const EnvReportEnableEnvVar* = "TR_ENV_REPORT"
|
const EnvReportEnableEnvVar* = "TR_ENV_REPORT"
|
||||||
|
|
||||||
@@ -49,6 +50,8 @@ type
|
|||||||
vBulletDebugGun*: string
|
vBulletDebugGun*: string
|
||||||
vBulletDebugMax*: int
|
vBulletDebugMax*: int
|
||||||
recordWorldState*: bool
|
recordWorldState*: bool
|
||||||
|
geoDebug*: bool
|
||||||
|
debugDraw*: bool
|
||||||
radarForceSpin*: bool
|
radarForceSpin*: bool
|
||||||
radarScanLog*: bool
|
radarScanLog*: bool
|
||||||
radarScanLogPath*: string
|
radarScanLogPath*: string
|
||||||
@@ -209,6 +212,9 @@ proc printEffectiveValues(ctx: EnvReportContext) =
|
|||||||
sourceOf(VBulletDebugGunEnv))
|
sourceOf(VBulletDebugGunEnv))
|
||||||
emit("TR_VBULLET_DEBUG_MAX", $ctx.vBulletDebugMax,
|
emit("TR_VBULLET_DEBUG_MAX", $ctx.vBulletDebugMax,
|
||||||
sourceOf(VBulletDebugMaxEnv))
|
sourceOf(VBulletDebugMaxEnv))
|
||||||
|
# global debug-draw switch (default on) + the draw-only geometry overlay
|
||||||
|
emit(DebugDrawEnv, onOff(ctx.debugDraw), sourceOfPresence(DebugDrawEnv))
|
||||||
|
emit(GeoDebugEnv, onOff(ctx.geoDebug), sourceOfPresence(GeoDebugEnv))
|
||||||
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),
|
||||||
@@ -488,6 +494,7 @@ proc knownEnvNames*(): seq[string] =
|
|||||||
"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,
|
VBulletDebugEnv, VBulletDebugGunEnv, VBulletDebugMaxEnv,
|
||||||
|
DebugDrawEnv, GeoDebugEnv,
|
||||||
"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,225 @@
|
|||||||
|
## Geometry overlay (TR_GEO_DEBUG) — a DRAW-ONLY visual aid.
|
||||||
|
##
|
||||||
|
## No decision, no movement, no gun change: this module only emits SVG for the
|
||||||
|
## GUI. DEFAULT OFF, so the shipped bot is byte-for-byte unchanged. It exists so
|
||||||
|
## the owner can reason about a specific piece of geometry:
|
||||||
|
##
|
||||||
|
## * the two bots are the endpoints of a DIAMETER, so both lie exactly on the
|
||||||
|
## circle (centre = midpoint, radius = distance / 2);
|
||||||
|
## * a line along OUR heading through our position;
|
||||||
|
## * a line along the ENEMY heading through the enemy position;
|
||||||
|
## * the intersection points of each line with the circle, marked. A line
|
||||||
|
## through a point that is ON the circle meets it twice — the bot itself and
|
||||||
|
## ONE OTHER point — and the OTHER point is drawn visually distinct (that is
|
||||||
|
## the one worth looking at). A tangent line's two points coincide; we draw
|
||||||
|
## what the maths gives.
|
||||||
|
##
|
||||||
|
## Env knobs (read once at boot by ModularBot, registered in env_report.nim):
|
||||||
|
## TR_GEO_DEBUG presence/1 = on. default OFF.
|
||||||
|
## TR_DEBUG_DRAW presence/1 = on (default); 0/false/no/off disables every
|
||||||
|
## mover's `debugGraphics` and the virtual-bullet overlay, so
|
||||||
|
## the geometry overlay is the ONLY thing left on screen.
|
||||||
|
##
|
||||||
|
## The geometry is pure and unit-tested by `tests/test_geo_overlay.nim`; only
|
||||||
|
## `emitGeoPrims` touches the GUI buffer.
|
||||||
|
|
||||||
|
import std/[math, os, strutils]
|
||||||
|
import robocode_tankroyale_botapi
|
||||||
|
|
||||||
|
const
|
||||||
|
GeoDebugEnv* = "TR_GEO_DEBUG"
|
||||||
|
DebugDrawEnv* = "TR_DEBUG_DRAW"
|
||||||
|
|
||||||
|
const DegToRad = PI / 180.0
|
||||||
|
|
||||||
|
# ── env resolution (pure, so a test can pin it) ──────────────────────────────
|
||||||
|
|
||||||
|
proc envFlagOn(value: string, default: bool): bool =
|
||||||
|
## "" -> `default`; explicit 0/false/no/off -> false; anything else -> true.
|
||||||
|
let s = value.strip().toLowerAscii()
|
||||||
|
if s.len == 0: return default
|
||||||
|
s notin ["0", "false", "no", "off"]
|
||||||
|
|
||||||
|
proc geoDebugEnabled*(): bool =
|
||||||
|
## `TR_GEO_DEBUG`: default OFF (the shipped bot draws nothing new).
|
||||||
|
envFlagOn(getEnv(GeoDebugEnv, ""), false)
|
||||||
|
|
||||||
|
proc debugDrawEnabled*(): bool =
|
||||||
|
## `TR_DEBUG_DRAW`: default ON. When off, every mover is built with
|
||||||
|
## `debugGraphics = false` and the virtual-bullet overlay is skipped.
|
||||||
|
envFlagOn(getEnv(DebugDrawEnv, ""), true)
|
||||||
|
|
||||||
|
# ── the geometry (pure) ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type
|
||||||
|
GeomCircle* = object
|
||||||
|
cx*, cy*, r*: float
|
||||||
|
|
||||||
|
GeomPoint* = object
|
||||||
|
x*, y*: float
|
||||||
|
|
||||||
|
proc makeDiameterCircle*(ax, ay, bx, by: float): GeomCircle =
|
||||||
|
## The circle whose DIAMETER is the segment a-b: centre at the midpoint,
|
||||||
|
## radius half the distance. Both a and b therefore lie on the circumference.
|
||||||
|
## A degenerate (coincident) pair yields r = 0; callers skip the draw.
|
||||||
|
result.cx = (ax + bx) * 0.5
|
||||||
|
result.cy = (ay + by) * 0.5
|
||||||
|
result.r = 0.5 * hypot(bx - ax, by - ay)
|
||||||
|
|
||||||
|
proc distTo*(p: GeomPoint, c: GeomCircle): float {.inline.} =
|
||||||
|
hypot(p.x - c.cx, p.y - c.cy)
|
||||||
|
|
||||||
|
proc onCircle*(p: GeomPoint, c: GeomCircle): bool {.inline.} =
|
||||||
|
abs(p.distTo(c) - c.r) < 1e-6
|
||||||
|
|
||||||
|
proc lineCircleIntersections*(px, py, headingDeg: float,
|
||||||
|
c: GeomCircle): seq[GeomPoint] =
|
||||||
|
## The two intersection points of the INFINITE line through (px, py) along
|
||||||
|
## `headingDeg` with circle `c`. The line is parametrised P(t) = p + t*d with
|
||||||
|
## d a unit vector, so the quadratic has a = 1 and the roots are signed
|
||||||
|
## distances along d. Returns the two roots when the line meets the circle
|
||||||
|
## (coincident when tangent), and nothing when it misses.
|
||||||
|
##
|
||||||
|
## Tank Royale bearings: 0° = east, positive counter-clockwise.
|
||||||
|
let h = headingDeg * DegToRad
|
||||||
|
let dx = cos(h)
|
||||||
|
let dy = sin(h)
|
||||||
|
let ox = px - c.cx
|
||||||
|
let oy = py - c.cy
|
||||||
|
let b = 2.0 * (ox * dx + oy * dy)
|
||||||
|
let cc = ox * ox + oy * oy - c.r * c.r
|
||||||
|
let disc = b * b - 4.0 * cc
|
||||||
|
if disc < 0.0: return @[]
|
||||||
|
let s = sqrt(disc)
|
||||||
|
let t1 = (-b - s) * 0.5
|
||||||
|
let t2 = (-b + s) * 0.5
|
||||||
|
result.add GeomPoint(x: px + t1 * dx, y: py + t1 * dy)
|
||||||
|
result.add GeomPoint(x: px + t2 * dx, y: py + t2 * dy)
|
||||||
|
|
||||||
|
type
|
||||||
|
GeoPrimKind* = enum
|
||||||
|
gpCircle ## the diameter circle outline
|
||||||
|
gpLineSelf ## line along OUR heading
|
||||||
|
gpLineEnemy ## line along the ENEMY heading
|
||||||
|
gpMarkSelf ## intersection that coincides with the bot (hollow ring)
|
||||||
|
gpMarkOther ## the OTHER intersection (filled, distinct)
|
||||||
|
|
||||||
|
GeoPrim* = object
|
||||||
|
kind*: GeoPrimKind
|
||||||
|
x1*, y1*: float ## lines: first endpoint
|
||||||
|
x2*, y2*: float ## lines: second endpoint
|
||||||
|
cx*, cy*, r*: float ## circles: centre + radius
|
||||||
|
color*: string
|
||||||
|
|
||||||
|
const
|
||||||
|
GeoCircleColor* = "#888888" # grey — the diameter circle
|
||||||
|
GeoSelfLineColor* = "#00FFFF" # cyan — OUR heading line
|
||||||
|
GeoEnemyLineColor* = "#FF8000" # orange— ENEMY heading line
|
||||||
|
GeoSelfMarkColor* = "#FFFFFF" # white — the bot itself (hollow ring)
|
||||||
|
GeoOtherMarkColor* = "#FF00FF" # magenta — the OTHER intersection (filled)
|
||||||
|
GeoCircleWidth* = 1.0
|
||||||
|
GeoLineWidth* = 1.0
|
||||||
|
GeoMarkWidth* = 1.5
|
||||||
|
GeoSelfMarkRadius* = 4.0
|
||||||
|
GeoOtherMarkRadius* = 4.0
|
||||||
|
GeoLineMargin* = 10.0
|
||||||
|
## Extra length (px) the heading segment extends beyond the far
|
||||||
|
## intersection; a segment of half-length 2r + margin always contains both
|
||||||
|
## intersections because a chord of a circle is at most 2r long.
|
||||||
|
|
||||||
|
proc pickSelfOther*(pts: seq[GeomPoint], ax, ay: float):
|
||||||
|
tuple[self, other: GeomPoint, hasOther: bool] =
|
||||||
|
## The intersection nearest the anchor `(ax, ay)` is the bot itself (the
|
||||||
|
## anchor lies ON the circle, so the distance is ~0); the farther one is the
|
||||||
|
## OTHER point. `hasOther` distinguishes a real second point from a tangent
|
||||||
|
## coincidence.
|
||||||
|
if pts.len == 0: return
|
||||||
|
var iSelf = 0
|
||||||
|
var dSelf = hypot(pts[0].x - ax, pts[0].y - ay)
|
||||||
|
for i in 1 ..< pts.len:
|
||||||
|
let d = hypot(pts[i].x - ax, pts[i].y - ay)
|
||||||
|
if d < dSelf: dSelf = d; iSelf = i
|
||||||
|
result.self = pts[iSelf]
|
||||||
|
var iOther = -1
|
||||||
|
var dOther = 0.0
|
||||||
|
for i in 0 ..< pts.len:
|
||||||
|
if i == iSelf: continue
|
||||||
|
let d = hypot(pts[i].x - ax, pts[i].y - ay)
|
||||||
|
if iOther < 0 or d > dOther: iOther = i; dOther = d
|
||||||
|
if iOther >= 0:
|
||||||
|
result.other = pts[iOther]
|
||||||
|
result.hasOther = true
|
||||||
|
|
||||||
|
proc headingSegment(px, py, headingDeg, halfLen: float): tuple[x1, y1, x2, y2: float] =
|
||||||
|
## Segment centred on (px, py), `halfLen` long each way along `headingDeg`.
|
||||||
|
let h = headingDeg * DegToRad
|
||||||
|
let dx = cos(h) * halfLen
|
||||||
|
let dy = sin(h) * halfLen
|
||||||
|
(px - dx, py - dy, px + dx, py + dy)
|
||||||
|
|
||||||
|
proc collectGeoPrims*(selfX, selfY, selfHeading,
|
||||||
|
enemyX, enemyY, enemyHeading: float): seq[GeoPrim] =
|
||||||
|
## Pure: the primitives the overlay draws. Empty in the degenerate case where
|
||||||
|
## the two bots coincide (distance ~0) — skip rather than divide by zero.
|
||||||
|
let c = makeDiameterCircle(selfX, selfY, enemyX, enemyY)
|
||||||
|
if c.r < 1e-6: return
|
||||||
|
|
||||||
|
result.add GeoPrim(kind: gpCircle, cx: c.cx, cy: c.cy, r: c.r,
|
||||||
|
color: GeoCircleColor)
|
||||||
|
|
||||||
|
let halfLen = 2.0 * c.r + GeoLineMargin
|
||||||
|
|
||||||
|
# OUR heading line + its two intersection points.
|
||||||
|
let segSelf = headingSegment(selfX, selfY, selfHeading, halfLen)
|
||||||
|
result.add GeoPrim(kind: gpLineSelf, x1: segSelf.x1, y1: segSelf.y1,
|
||||||
|
x2: segSelf.x2, y2: segSelf.y2, color: GeoSelfLineColor)
|
||||||
|
let ipSelf = lineCircleIntersections(selfX, selfY, selfHeading, c)
|
||||||
|
let soSelf = pickSelfOther(ipSelf, selfX, selfY)
|
||||||
|
if soSelf.hasOther:
|
||||||
|
result.add GeoPrim(kind: gpMarkSelf, cx: soSelf.self.x, cy: soSelf.self.y,
|
||||||
|
r: GeoSelfMarkRadius, color: GeoSelfMarkColor)
|
||||||
|
result.add GeoPrim(kind: gpMarkOther, cx: soSelf.other.x, cy: soSelf.other.y,
|
||||||
|
r: GeoOtherMarkRadius, color: GeoOtherMarkColor)
|
||||||
|
|
||||||
|
# ENEMY heading line + its two intersection points.
|
||||||
|
let segEnemy = headingSegment(enemyX, enemyY, enemyHeading, halfLen)
|
||||||
|
result.add GeoPrim(kind: gpLineEnemy, x1: segEnemy.x1, y1: segEnemy.y1,
|
||||||
|
x2: segEnemy.x2, y2: segEnemy.y2, color: GeoEnemyLineColor)
|
||||||
|
let ipEnemy = lineCircleIntersections(enemyX, enemyY, enemyHeading, c)
|
||||||
|
let soEnemy = pickSelfOther(ipEnemy, enemyX, enemyY)
|
||||||
|
if soEnemy.hasOther:
|
||||||
|
result.add GeoPrim(kind: gpMarkSelf, cx: soEnemy.self.x, cy: soEnemy.self.y,
|
||||||
|
r: GeoSelfMarkRadius, color: GeoSelfMarkColor)
|
||||||
|
result.add GeoPrim(kind: gpMarkOther, cx: soEnemy.other.x,
|
||||||
|
cy: soEnemy.other.y, r: GeoOtherMarkRadius,
|
||||||
|
color: GeoOtherMarkColor)
|
||||||
|
|
||||||
|
# ── emit to the GUI debug-graphics buffer ────────────────────────────────────
|
||||||
|
|
||||||
|
proc emitGeoPrims*(prims: seq[GeoPrim]): int =
|
||||||
|
## Emit the primitives into the current tick's SVG buffer. Thin lines.
|
||||||
|
for p in prims:
|
||||||
|
case p.kind
|
||||||
|
of gpCircle:
|
||||||
|
setStrokeColor(p.color)
|
||||||
|
setStrokeWidth(GeoCircleWidth)
|
||||||
|
drawCircle(p.cx, p.cy, p.r)
|
||||||
|
of gpLineSelf, gpLineEnemy:
|
||||||
|
setStrokeColor(p.color)
|
||||||
|
setStrokeWidth(GeoLineWidth)
|
||||||
|
drawLine(p.x1, p.y1, p.x2, p.y2)
|
||||||
|
of gpMarkSelf:
|
||||||
|
setStrokeColor(p.color)
|
||||||
|
setStrokeWidth(GeoMarkWidth)
|
||||||
|
drawCircle(p.cx, p.cy, p.r)
|
||||||
|
of gpMarkOther:
|
||||||
|
setFillColor(p.color)
|
||||||
|
fillCircle(p.cx, p.cy, p.r)
|
||||||
|
result = prims.len
|
||||||
|
|
||||||
|
proc drawGeoOverlay*(selfX, selfY, selfHeading,
|
||||||
|
enemyX, enemyY, enemyHeading: float): int =
|
||||||
|
## Entry point called once per tick by ModularBot when `TR_GEO_DEBUG` is on.
|
||||||
|
## Returns the number of primitives drawn.
|
||||||
|
emitGeoPrims(collectGeoPrims(selfX, selfY, selfHeading,
|
||||||
|
enemyX, enemyY, enemyHeading))
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
## Deterministic, offline check of the geometry overlay (`geo_overlay`).
|
||||||
|
##
|
||||||
|
## NO battle, NO Java, NO server: fixed positions/headings, then assert the
|
||||||
|
## circle/diameter property, the two line-circle intersections per heading line,
|
||||||
|
## and the hand-computed "other" points. Also pins the two env switches and the
|
||||||
|
## SVG emission (2 line segments + 5 circles: 1 outline + 2 hollow self marks +
|
||||||
|
## 2 filled other marks).
|
||||||
|
##
|
||||||
|
## Run: nim c -r ModularBot_garage/tests/test_geo_overlay.nim
|
||||||
|
|
||||||
|
import std/[math, os, strutils]
|
||||||
|
import robocode_tankroyale_botapi
|
||||||
|
import geo_overlay
|
||||||
|
|
||||||
|
var failures = 0
|
||||||
|
proc check(name: string, ok: bool) =
|
||||||
|
if ok: echo "PASS: ", name
|
||||||
|
else: echo "FAIL: ", name; inc failures
|
||||||
|
|
||||||
|
proc approx(a, b: float, eps = 1e-6): bool = abs(a - b) < eps
|
||||||
|
|
||||||
|
proc crossOnLine(px, py, headingDeg, qx, qy: float): float =
|
||||||
|
## |(q - p) x direction| / |q - p| == perpendicular distance of q from the
|
||||||
|
## line through p along `headingDeg`; 0 when q is on the line.
|
||||||
|
let h = headingDeg * PI / 180.0
|
||||||
|
let dx = cos(h)
|
||||||
|
let dy = sin(h)
|
||||||
|
let vx = qx - px
|
||||||
|
let vy = qy - py
|
||||||
|
let len = hypot(vx, vy)
|
||||||
|
if len < 1e-9: return 0.0
|
||||||
|
abs(vx * dy - vy * dx)
|
||||||
|
|
||||||
|
# ── the env switches ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
proc testEnvSwitches() =
|
||||||
|
delEnv(GeoDebugEnv)
|
||||||
|
check "TR_GEO_DEBUG defaults OFF", not geoDebugEnabled()
|
||||||
|
putEnv(GeoDebugEnv, "1")
|
||||||
|
check "TR_GEO_DEBUG=1 is ON", geoDebugEnabled()
|
||||||
|
putEnv(GeoDebugEnv, "0")
|
||||||
|
check "TR_GEO_DEBUG=0 is OFF", not geoDebugEnabled()
|
||||||
|
delEnv(GeoDebugEnv)
|
||||||
|
|
||||||
|
delEnv(DebugDrawEnv)
|
||||||
|
check "TR_DEBUG_DRAW defaults ON", debugDrawEnabled()
|
||||||
|
putEnv(DebugDrawEnv, "0")
|
||||||
|
check "TR_DEBUG_DRAW=0 is OFF", not debugDrawEnabled()
|
||||||
|
putEnv(DebugDrawEnv, "false")
|
||||||
|
check "TR_DEBUG_DRAW=false is OFF", not debugDrawEnabled()
|
||||||
|
putEnv(DebugDrawEnv, "1")
|
||||||
|
check "TR_DEBUG_DRAW=1 is ON", debugDrawEnabled()
|
||||||
|
delEnv(DebugDrawEnv)
|
||||||
|
|
||||||
|
# ── hand-computed case A: heading along the diameter ─────────────────────────
|
||||||
|
|
||||||
|
proc testCaseA() =
|
||||||
|
let selfX = 100.0; let selfY = 100.0; let selfH = 0.0
|
||||||
|
let enX = 300.0; let enY = 100.0; let enH = 180.0
|
||||||
|
let c = makeDiameterCircle(selfX, selfY, enX, enY)
|
||||||
|
check "A: centre is the midpoint (200,100)",
|
||||||
|
approx(c.cx, 200.0) and approx(c.cy, 100.0)
|
||||||
|
check "A: radius is distance/2 (=100)", approx(c.r, 100.0)
|
||||||
|
check "A: our bot lies on the circle",
|
||||||
|
onCircle(GeomPoint(x: selfX, y: selfY), c)
|
||||||
|
check "A: enemy lies on the circle",
|
||||||
|
onCircle(GeomPoint(x: enX, y: enY), c)
|
||||||
|
|
||||||
|
let ipSelf = lineCircleIntersections(selfX, selfY, selfH, c)
|
||||||
|
check "A: our line yields exactly two intersections", ipSelf.len == 2
|
||||||
|
let soSelf = pickSelfOther(ipSelf, selfX, selfY)
|
||||||
|
check "A: our own point is an intersection (100,100)",
|
||||||
|
approx(soSelf.self.x, 100.0) and approx(soSelf.self.y, 100.0)
|
||||||
|
check "A: our OTHER point is hand-computed (300,100)",
|
||||||
|
soSelf.hasOther and approx(soSelf.other.x, 300.0) and
|
||||||
|
approx(soSelf.other.y, 100.0)
|
||||||
|
|
||||||
|
let ipEnemy = lineCircleIntersections(enX, enY, enH, c)
|
||||||
|
check "A: enemy line yields exactly two intersections", ipEnemy.len == 2
|
||||||
|
let soEnemy = pickSelfOther(ipEnemy, enX, enY)
|
||||||
|
check "A: enemy's own point is an intersection (300,100)",
|
||||||
|
approx(soEnemy.self.x, 300.0) and approx(soEnemy.self.y, 100.0)
|
||||||
|
check "A: enemy's OTHER point is hand-computed (100,100)",
|
||||||
|
soEnemy.hasOther and approx(soEnemy.other.x, 100.0) and
|
||||||
|
approx(soEnemy.other.y, 100.0)
|
||||||
|
|
||||||
|
# ── hand-computed case B: headings not along the diameter ────────────────────
|
||||||
|
|
||||||
|
proc testCaseB() =
|
||||||
|
let selfX = 0.0; let selfY = 0.0; let selfH = 45.0
|
||||||
|
let enX = 100.0; let enY = 0.0; let enH = 0.0
|
||||||
|
let c = makeDiameterCircle(selfX, selfY, enX, enY)
|
||||||
|
check "B: centre (50,0) r=50", approx(c.cx, 50.0) and approx(c.r, 50.0)
|
||||||
|
check "B: both bots on the circle",
|
||||||
|
onCircle(GeomPoint(x: selfX, y: selfY), c) and
|
||||||
|
onCircle(GeomPoint(x: enX, y: enY), c)
|
||||||
|
|
||||||
|
let ipSelf = lineCircleIntersections(selfX, selfY, selfH, c)
|
||||||
|
check "B: our line yields exactly two intersections", ipSelf.len == 2
|
||||||
|
let soSelf = pickSelfOther(ipSelf, selfX, selfY)
|
||||||
|
check "B: our own point is (0,0)",
|
||||||
|
approx(soSelf.self.x, 0.0) and approx(soSelf.self.y, 0.0)
|
||||||
|
check "B: our OTHER point is hand-computed (50,50)",
|
||||||
|
soSelf.hasOther and approx(soSelf.other.x, 50.0) and
|
||||||
|
approx(soSelf.other.y, 50.0)
|
||||||
|
check "B: the other point lies on BOTH the line and the circle",
|
||||||
|
crossOnLine(selfX, selfY, selfH, soSelf.other.x, soSelf.other.y) < 1e-6 and
|
||||||
|
onCircle(soSelf.other, c)
|
||||||
|
|
||||||
|
let ipEnemy = lineCircleIntersections(enX, enY, enH, c)
|
||||||
|
check "B: enemy line yields exactly two intersections", ipEnemy.len == 2
|
||||||
|
let soEnemy = pickSelfOther(ipEnemy, enX, enY)
|
||||||
|
check "B: enemy's other point is (0,0)",
|
||||||
|
soEnemy.hasOther and approx(soEnemy.other.x, 0.0) and
|
||||||
|
approx(soEnemy.other.y, 0.0)
|
||||||
|
check "B: enemy's other point lies on BOTH line and circle",
|
||||||
|
crossOnLine(enX, enY, enH, soEnemy.other.x, soEnemy.other.y) < 1e-6 and
|
||||||
|
onCircle(soEnemy.other, c)
|
||||||
|
|
||||||
|
# ── case C: tangent line (two points coincide) ───────────────────────────────
|
||||||
|
|
||||||
|
proc testCaseC() =
|
||||||
|
let selfX = 0.0; let selfY = 0.0; let selfH = 90.0
|
||||||
|
let enX = 100.0; let enY = 0.0; let enH = 0.0
|
||||||
|
let c = makeDiameterCircle(selfX, selfY, enX, enY)
|
||||||
|
let ipSelf = lineCircleIntersections(selfX, selfY, selfH, c)
|
||||||
|
check "C: tangent line still gives two (coincident) points", ipSelf.len == 2
|
||||||
|
check "C: both tangent points are the bot itself",
|
||||||
|
approx(ipSelf[0].x, 0.0) and approx(ipSelf[0].y, 0.0) and
|
||||||
|
approx(ipSelf[1].x, 0.0) and approx(ipSelf[1].y, 0.0)
|
||||||
|
|
||||||
|
# ── degenerate case: bots coincide ───────────────────────────────────────────
|
||||||
|
|
||||||
|
proc testDegenerate() =
|
||||||
|
let prims = collectGeoPrims(50.0, 50.0, 0.0, 50.0, 50.0, 180.0)
|
||||||
|
check "coincident bots: nothing drawn (no divide by zero)", prims.len == 0
|
||||||
|
|
||||||
|
# ── primitive collection + emission ──────────────────────────────────────────
|
||||||
|
|
||||||
|
proc testPrimsAndEmit() =
|
||||||
|
let prims = collectGeoPrims(100.0, 100.0, 0.0, 300.0, 100.0, 180.0)
|
||||||
|
check "non-degenerate case draws 1 circle + 2 lines + 4 marks = 7",
|
||||||
|
prims.len == 7
|
||||||
|
var circles, lines, selfMarks, otherMarks = 0
|
||||||
|
for p in prims:
|
||||||
|
case p.kind
|
||||||
|
of gpCircle: inc circles
|
||||||
|
of gpLineSelf, gpLineEnemy: inc lines
|
||||||
|
of gpMarkSelf: inc selfMarks
|
||||||
|
of gpMarkOther: inc otherMarks
|
||||||
|
check "primitive mix is (1 circle, 2 lines, 2 self marks, 2 other marks)",
|
||||||
|
circles == 1 and lines == 2 and selfMarks == 2 and otherMarks == 2
|
||||||
|
check "self and other marks use distinct colours",
|
||||||
|
GeoSelfMarkColor != GeoOtherMarkColor
|
||||||
|
|
||||||
|
clearGraphics()
|
||||||
|
let n = drawGeoOverlay(100.0, 100.0, 0.0, 300.0, 100.0, 180.0)
|
||||||
|
let svg = svgOutput()
|
||||||
|
check "drawGeoOverlay returns the primitive count (7)", n == 7
|
||||||
|
check "SVG carries 2 line segments", svg.count("<line") == 2
|
||||||
|
check "SVG carries 5 circles (1 outline + 2 hollow + 2 filled)",
|
||||||
|
svg.count("<circle") == 5
|
||||||
|
check "SVG is non-empty and wrapped", svg.startsWith("<svg") and
|
||||||
|
svg.endsWith("</svg>")
|
||||||
|
|
||||||
|
testEnvSwitches()
|
||||||
|
testCaseA()
|
||||||
|
testCaseB()
|
||||||
|
testCaseC()
|
||||||
|
testDegenerate()
|
||||||
|
testPrimsAndEmit()
|
||||||
|
|
||||||
|
if failures > 0:
|
||||||
|
echo "\n", failures, " check(s) FAILED"
|
||||||
|
quit(1)
|
||||||
|
echo "\nAll geo-overlay checks passed."
|
||||||
Reference in New Issue
Block a user