## 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, ## the hand-computed "other" points, and the owner-requested triangle whose ## corners are the enemy-heading OTHER point, the enemy and our bot. Also pins ## the two env switches and the SVG emission. ## ## 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 # ── the owner-requested triangle ───────────────────────────────────────────── proc triEdges(prims: seq[GeoPrim]): seq[GeoPrim] = for p in prims: if p.kind == gpTriangleEdge: result.add p proc triRings(prims: seq[GeoPrim]): seq[GeoPrim] = for p in prims: if p.kind == gpTriangleCorner: result.add p proc edgeLen(p: GeoPrim): float = hypot(p.x2 - p.x1, p.y2 - p.y1) proc hasVertex(prims: seq[GeoPrim], x, y: float): bool = ## The closed edge loop covers every corner, so scan the edge endpoints. for p in triEdges(prims): if (approx(p.x1, x) and approx(p.y1, y)) or (approx(p.x2, x) and approx(p.y2, y)): return true false proc testTriangleCaseB() = ## The EXISTING case: self (0,0) heading 45, enemy (100,0) heading 0. ## The enemy heading (east) lies ALONG the diameter, so the enemy heading's ## OTHER intersection is hand-computed to be our own bot (0,0). The triangle ## is therefore FLAT: corner 1 == corner 3. let selfX = 0.0; let selfY = 0.0 let enX = 100.0; let enY = 0.0; let enH = 0.0 let c = makeDiameterCircle(selfX, selfY, enX, enY) let soEnemy = pickSelfOther(lineCircleIntersections(enX, enY, enH, c), enX, enY) check "T/B: enemy-heading other point is our bot (0,0)", soEnemy.hasOther and approx(soEnemy.other.x, 0.0) and approx(soEnemy.other.y, 0.0) let prims = trianglePrims(soEnemy.other, GeomPoint(x: enX, y: enY), GeomPoint(x: selfX, y: selfY)) check "T/B: triangle corners are (0,0), enemy (100,0), us (0,0)", hasVertex(prims, 0.0, 0.0) and hasVertex(prims, 100.0, 0.0) check "T/B: the collapsed triangle has 2 edges (v3->v1 skipped)", triEdges(prims).len == 2 check "T/B: coincident corners share one ring (2 not 3)", triRings(prims).len == 2 check "T/B: no zero-length edge is emitted", (block: var ok = true for p in triEdges(prims): if edgeLen(p) < GeoZeroLen: ok = false ok) check "T/B: all three corners lie ON the circle", onCircle(soEnemy.other, c) and onCircle(GeomPoint(x: enX, y: enY), c) and onCircle(GeomPoint(x: selfX, y: selfY), c) proc testTriangleCaseD() = ## Non-degenerate: self (0,0), enemy (100,0) heading 135. The enemy heading ## line meets the circle again at the hand-computed (50,50). let selfX = 0.0; let selfY = 0.0 let enX = 100.0; let enY = 0.0; let enH = 135.0 let c = makeDiameterCircle(selfX, selfY, enX, enY) let soEnemy = pickSelfOther(lineCircleIntersections(enX, enY, enH, c), enX, enY) check "T/D: enemy-heading other point is hand-computed (50,50)", soEnemy.hasOther and approx(soEnemy.other.x, 50.0, 1e-5) and approx(soEnemy.other.y, 50.0, 1e-5) let prims = trianglePrims(soEnemy.other, GeomPoint(x: enX, y: enY), GeomPoint(x: selfX, y: selfY)) check "T/D: triangle corners are (50,50), enemy (100,0), us (0,0)", hasVertex(prims, 50.0, 50.0) and hasVertex(prims, 100.0, 0.0) and hasVertex(prims, 0.0, 0.0) check "T/D: 3 edges and 3 distinct corner rings", triEdges(prims).len == 3 and triRings(prims).len == 3 check "T/D: every edge has non-zero length", (block: var ok = true for p in triEdges(prims): if edgeLen(p) < GeoZeroLen: ok = false ok) check "T/D: all three corners lie ON the circle", onCircle(soEnemy.other, c) and onCircle(GeomPoint(x: enX, y: enY), c) and onCircle(GeomPoint(x: selfX, y: selfY), c) check "T/D: triangle colour differs from circle / heading lines / marks", GeoTriangleColor != GeoCircleColor and GeoTriangleColor != GeoSelfLineColor and GeoTriangleColor != GeoEnemyLineColor and GeoTriangleColor != GeoOtherMarkColor proc testTriangleTangent() = ## TANGENT: enemy (100,0) heading 90 (north) — the vertical line x=100 is ## tangent to the circle at the enemy, so corner 1 coincides with corner 2. ## The zero-length edge must be skipped, not emitted. let selfX = 0.0; let selfY = 0.0 let enX = 100.0; let enY = 0.0; let enH = 90.0 let c = makeDiameterCircle(selfX, selfY, enX, enY) let soEnemy = pickSelfOther(lineCircleIntersections(enX, enY, enH, c), enX, enY) check "T/tangent: other point coincides with the enemy (100,0)", soEnemy.hasOther and approx(soEnemy.other.x, 100.0) and approx(soEnemy.other.y, 0.0) let prims = trianglePrims(soEnemy.other, GeomPoint(x: enX, y: enY), GeomPoint(x: selfX, y: selfY)) check "T/tangent: zero-length edge SKIPPED (2 edges, not 3)", triEdges(prims).len == 2 check "T/tangent: no emitted edge has zero length", (block: var ok = true for p in triEdges(prims): if edgeLen(p) < GeoZeroLen: ok = false ok) check "T/tangent: coincident corners share one ring (2 not 3)", triRings(prims).len == 2 # ── 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 + 4 triangle = 11", prims.len == 11 var circles, lines, selfMarks, otherMarks, triEdge, triCorner = 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 of gpTriangleEdge: inc triEdge of gpTriangleCorner: inc triCorner check "primitive mix is (1 circle, 2 lines, 2 self, 2 other, 2 tri edge, 2 tri ring)", circles == 1 and lines == 2 and selfMarks == 2 and otherMarks == 2 and triEdge == 2 and triCorner == 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 (11)", n == 11 check "SVG carries 4 line segments (2 heading + 2 triangle edges)", svg.count("") testEnvSwitches() testCaseA() testCaseB() testCaseC() testDegenerate() testTriangleCaseB() testTriangleCaseD() testTriangleTangent() testPrimsAndEmit() if failures > 0: echo "\n", failures, " check(s) FAILED" quit(1) echo "\nAll geo-overlay checks passed."