j138 draw-only: yellow triangle (enemy-heading other point, enemy, us) in the geometry overlay

This commit is contained in:
2026-09-26 15:14:48 +02:00
parent 189d990812
commit 10473fc211
2 changed files with 196 additions and 18 deletions
+128 -12
View File
@@ -2,9 +2,9 @@
##
## 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).
## 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
@@ -135,31 +135,144 @@ 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 = 7",
prims.len == 7
var circles, lines, selfMarks, otherMarks = 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
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
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 (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 "drawGeoOverlay returns the primitive count (11)", n == 11
check "SVG carries 4 line segments (2 heading + 2 triangle edges)",
svg.count("<line") == 4
check "SVG carries 7 circles (1 outline + 2 hollow + 2 filled + 2 tri rings)",
svg.count("<circle") == 7
check "SVG is non-empty and wrapped", svg.startsWith("<svg") and
svg.endsWith("</svg>")
@@ -168,6 +281,9 @@ testCaseA()
testCaseB()
testCaseC()
testDegenerate()
testTriangleCaseB()
testTriangleCaseD()
testTriangleTangent()
testPrimsAndEmit()
if failures > 0: