STRAFE: curved wings + guaranteed wall/corner escape; shipped TFIL default untouched
Task j112. Two changes to the TR_MOVEMENT=strafe engine, both OFF the shipped tfil path; the binary default is still tfil. CURVED WINGS: the candidate set was the straight 1-D line through the bot, which a bounded segment always terminates at a wall. It is now an adaptive parabola with the vertex on the bot: point(y) = bot + yhat*y + xhat*kappa(y)*y^2 xhat is the unit vector away from the nearest wall(s) (summed inward normals, so a corner yields the diagonal). kappa grows as the wall approaches and saturates at TR_STRAFE_KAPPA; every wing point is clamped inside TR_STRAFE_WALL_SAFE, so the wing FLATTENS and runs parallel to the wall instead of touching it. In open space kappa == 0 and the wing is exactly the old straight line. The wing chord at the reach tilts the heading band toward the interior by atan(kappa*reach) (capped by TR_STRAFE_WING_MAX); the body still only turns slowly to follow that tangent, never to face the target, and reversals are still setForward sign flips. GUARANTEED ESCAPE: with every candidate over threshold the old fallback minimised pathMaxHeat, whose gradient points AT the wall (the shortest path has the least wall exposure), so the least-hot tile was the adjacent one and led further along the wall. Near a wall the picker now ranks by the DESTINATION (farthest from the wall, then coolest tile) and commands the sign whose velocity has a positive component along the wall-away normal. That sign is re-asserted EVERY tick, so speed*heading . away >= 0 while escape is active: the clearance cannot fall. mode=escape reaches the [strafe] log. A mild wall-margin bias (TR_STRAFE_WALL_BIAS) prefers higher-clearance tiles when near a wall. GUI/log: the curved wings are drawn as an orange polyline (candidates follow the curve), a white ray + ESCAPE label marks the escape, and the [strafe] line now carries wall=<dist> kappa=<..> mode=<pick|fallback|escape|radial>. Gates (offline, kinematic replay of the DrussGT fixtures; see common_libs/tests/measure_strafe_wings.nim, plus the reused j108/j111 gates): wall occupancy (within 54 px) falls 25.6->4.3 / 18.3->4.1 / 23.8->4.3 / 27.8->4.7 percent and the longest continuous wall run 191->31 / 49->25 / 208->47 / 246->27 ticks; corner-region occupancy 4.7->0.0 percent with the longest corner run 71->7. The escape sweep (3520 start x heading x enemy runs, 110k escape ticks) shows ZERO per-tick guarantee violations and a worst corner run of 21 ticks. Open-space parity is bit-identical (kappa == 0), reversals are still sign flips (0 non-sign commands), and mean turn/speed are unchanged (OFF 4.42 deg/tick, 31.3 percent no-turn vs ON 4.45 / 30.7; reversal-interval entropy 5.309 -> 5.311 bits). The fixtures are OPEN-LOOP, so these are veto-capable checks, not a live win claim.
This commit is contained in:
@@ -0,0 +1,412 @@
|
||||
## STRAFE curved-wings gates (job j112). OFFLINE / cheap: no Java, no server, no
|
||||
## battle. Run with:
|
||||
##
|
||||
## nim c -r --nimcache:/tmp/nc_j112 --path:common_libs \
|
||||
## common_libs/tests/measure_strafe_wings.nim
|
||||
##
|
||||
## Reuses the fixture loader + kinematic replay shape of j111's
|
||||
## `measure_strafe_range_stall.nim`. The "before" arm is the module with the
|
||||
## wings DISABLED (`TR_STRAFE_KAPPA=0`, `TR_STRAFE_ESCAPE=0`,
|
||||
## `TR_STRAFE_WALL_BIAS=0`), which is exactly the pre-j112 straight-line mover;
|
||||
## the "after" arm is the module defaults.
|
||||
##
|
||||
## GATE A WALL/CORNER OCCUPANCY (the load-bearing metric). On the DrussGT
|
||||
## fixtures, the fraction of commanded ticks within WallProxPx of a
|
||||
## wall and inside the corner region, and the LONGEST CONTINUOUS RUN
|
||||
## inside each. Before vs after.
|
||||
##
|
||||
## GATE B ESCAPE PROOF. Two things:
|
||||
## (1) a per-tick guarantee check over a dense grid of start positions
|
||||
## x headings x enemy placements: whenever the mover runs in
|
||||
## escape mode, is (velocity . wall-away-normal) >= 0? A single
|
||||
## violation falsifies the guarantee.
|
||||
## (2) the worst-case longest corner run over the same sweep.
|
||||
##
|
||||
## GATE C REGRESSION. Open-space parity (with no wall within the margin the
|
||||
## ON and OFF command streams must be bit-identical and kappa == 0),
|
||||
## the sign-flip property (every command is exactly +/-MaxSpeed), and
|
||||
## speed / turn-rate ON vs OFF.
|
||||
##
|
||||
## HONESTY: the fixtures are OPEN-LOOP (recorded while the enemy was reacting to
|
||||
## a PREVIOUS mover). A positive here is NOT a live win; it is a veto-capable
|
||||
## check only.
|
||||
|
||||
import std/[os, json, math, random, strutils, algorithm]
|
||||
import gun_harness/gun_interface
|
||||
import movements/strafe
|
||||
|
||||
const repoRoot = currentSourcePath().parentDir.parentDir.parentDir
|
||||
const ArenaW = 800.0
|
||||
const ArenaH = 600.0
|
||||
const GridSize = 36.0
|
||||
const Cols = int(ArenaW / GridSize) # 22
|
||||
const Rows = int(ArenaH / GridSize) # 16
|
||||
const MarginX = (ArenaW - Cols.float * GridSize) / 2.0
|
||||
const MarginY = (ArenaH - Rows.float * GridSize) / 2.0
|
||||
const Seed = 20250923
|
||||
|
||||
## "Near a wall" and "in a corner region" in px. 54 == 1.5 tiles, 72 == 2 tiles.
|
||||
const WallProxPx = 54.0
|
||||
const CornerPx = 72.0
|
||||
|
||||
# ── fixture loading (same shape as j111's gate) ──────────────────────────────
|
||||
|
||||
type Sample = object
|
||||
enemyX, enemyY, enemyEnergy: float
|
||||
selfX, selfY, selfHeading, selfEnergy: float
|
||||
|
||||
proc loadSamples(fixture: string): seq[Sample] =
|
||||
let path = repoRoot / "tools" / "fixtures" / fixture
|
||||
for rawLine in lines(path):
|
||||
let line = rawLine.strip()
|
||||
if line.len == 0: continue
|
||||
let n = parseJson(line)
|
||||
if n.hasKey("meta") or n.hasKey("end"): continue
|
||||
result.add Sample(
|
||||
enemyX: n["ex"].getFloat(), enemyY: n["ey"].getFloat(),
|
||||
enemyEnergy: n["ee"].getFloat(),
|
||||
selfX: n["sx"].getFloat(), selfY: n["sy"].getFloat(),
|
||||
selfHeading: n["sh"].getFloat(), selfEnergy: n["se"].getFloat())
|
||||
|
||||
proc loadRoundStarts(fixture: string): seq[int] =
|
||||
let side = repoRoot / "tools" / "fixtures" / "drussgt_meta" /
|
||||
(fixture & ".rounds.json")
|
||||
if not fileExists(side): return
|
||||
for r in parseFile(side)["rounds"]:
|
||||
result.add r["startTick"].getInt()
|
||||
|
||||
proc makeWs(s: Sample, x, y, h, sp: float): WorldState =
|
||||
WorldState(
|
||||
enemyX: s.enemyX, enemyY: s.enemyY, enemyHeading: 0.0, enemySpeed: 0.0,
|
||||
enemyEnergy: s.enemyEnergy,
|
||||
selfX: x, selfY: y, selfHeading: h, selfSpeed: sp,
|
||||
selfEnergy: s.selfEnergy, arenaWidth: ArenaW, arenaHeight: ArenaH,
|
||||
enemies: @[EnemyInfo(id: 1, x: s.enemyX, y: s.enemyY, energy: s.enemyEnergy)])
|
||||
|
||||
proc clearance(x, y: float): float =
|
||||
min(min(x, ArenaW - x), min(y, ArenaH - y))
|
||||
|
||||
proc wrap180(d: float): float {.inline.} =
|
||||
result = d
|
||||
while result > 180.0: result -= 360.0
|
||||
while result < -180.0: result += 360.0
|
||||
|
||||
proc inCorner(x, y: float): bool =
|
||||
(x < CornerPx or x > ArenaW - CornerPx) and
|
||||
(y < CornerPx or y > ArenaH - CornerPx)
|
||||
|
||||
# ── mode switch ──────────────────────────────────────────────────────────────
|
||||
|
||||
proc setWings(on: bool) =
|
||||
## ON == module defaults. OFF == wings disabled, which is byte-for-byte the
|
||||
## pre-j112 mover (the candidate geometry and the fallback ranking revert).
|
||||
for n in ["TR_STRAFE_KAPPA", "TR_STRAFE_ESCAPE", "TR_STRAFE_WALL_BIAS",
|
||||
"TR_STRAFE_WALL_MARGIN", "TR_STRAFE_WING_MAX", "TR_STRAFE_WALL_SAFE"]:
|
||||
delEnv(n)
|
||||
if not on:
|
||||
putEnv("TR_STRAFE_KAPPA", "0")
|
||||
putEnv("TR_STRAFE_ESCAPE", "0")
|
||||
putEnv("TR_STRAFE_WALL_BIAS", "0")
|
||||
loadStrafeEnv()
|
||||
|
||||
proc maxF(v: seq[float]): float =
|
||||
result = 0.0
|
||||
for x in v: result = max(result, x)
|
||||
|
||||
proc mean(v: seq[float]): float =
|
||||
if v.len == 0: return 0.0
|
||||
var t = 0.0
|
||||
for x in v: t += x
|
||||
t / v.len.float
|
||||
|
||||
proc fmtF(x: float, d = 2): string = formatFloat(x, ffDecimal, d)
|
||||
|
||||
# ── GATE A: occupancy on the fixtures ────────────────────────────────────────
|
||||
|
||||
type OccResult = object
|
||||
ticks: int
|
||||
nearWall: int
|
||||
inCorner: int
|
||||
longestWallRun: int
|
||||
longestCornerRun: int
|
||||
minClearance: float
|
||||
meanClearance: float
|
||||
kappaSum: float
|
||||
escapeTicks: int
|
||||
|
||||
proc simOccupancy(samples: seq[Sample], starts: seq[int]): OccResult =
|
||||
randomize(Seed)
|
||||
var m = initStrafe()
|
||||
var x, y, h, sp = 0.0
|
||||
var runWall = 0
|
||||
var runCorner = 0
|
||||
var clrSum = 0.0
|
||||
var clrMin = 1e9
|
||||
for i in 0..<samples.len:
|
||||
let s = samples[i]
|
||||
if i == 0 or i in starts:
|
||||
m.resetRound()
|
||||
x = s.selfX; y = s.selfY; h = s.selfHeading; sp = 0.0
|
||||
runWall = 0; runCorner = 0
|
||||
let ws = makeWs(s, x, y, h, sp)
|
||||
let cmd = m.computeMove(ws)
|
||||
h += cmd.turnRate
|
||||
sp = cmd.speed
|
||||
x = clamp(x + sp * cos(h * PI / 180.0), 18.0, ArenaW - 18.0)
|
||||
y = clamp(y + sp * sin(h * PI / 180.0), 18.0, ArenaH - 18.0)
|
||||
let c = clearance(x, y)
|
||||
clrSum += c
|
||||
clrMin = min(clrMin, c)
|
||||
inc result.ticks
|
||||
if c < WallProxPx:
|
||||
inc result.nearWall
|
||||
inc runWall
|
||||
else:
|
||||
result.longestWallRun = max(result.longestWallRun, runWall); runWall = 0
|
||||
if inCorner(x, y):
|
||||
inc result.inCorner
|
||||
inc runCorner
|
||||
else:
|
||||
result.longestCornerRun = max(result.longestCornerRun, runCorner); runCorner = 0
|
||||
if m.kappa > 0.0: result.kappaSum += m.kappa
|
||||
if m.escapeActive: inc result.escapeTicks
|
||||
result.longestWallRun = max(result.longestWallRun, runWall)
|
||||
result.longestCornerRun = max(result.longestCornerRun, runCorner)
|
||||
result.minClearance = clrMin
|
||||
result.meanClearance = if result.ticks == 0: 0.0 else: clrSum / result.ticks.float
|
||||
|
||||
# ── GATE B: escape proof over a start x heading x enemy sweep ────────────────
|
||||
|
||||
type Sweep = object
|
||||
runs: int
|
||||
guaranteeViol: int ## ticks in escape mode with (v . away) < 0
|
||||
escapeTicks: int
|
||||
worstCorner: int ## longest continuous corner run, worst run
|
||||
worstName: string
|
||||
worstEndClear: float
|
||||
stallRuns: int ## runs whose longest corner run exceeded 2 s
|
||||
stallThresh: int
|
||||
|
||||
proc simSweep(bx, by, bh, ex, ey: float, nTicks: int): tuple[longestCorner, viol, escTicks: int, endClear: float] =
|
||||
randomize(Seed)
|
||||
var m = initStrafe()
|
||||
var x = bx; var y = by; var h = bh; var sp = 0.0
|
||||
var runCorner = 0
|
||||
for i in 0..<nTicks:
|
||||
let s = Sample(enemyX: ex, enemyY: ey, enemyEnergy: 100.0,
|
||||
selfX: x, selfY: y, selfHeading: h, selfEnergy: 100.0)
|
||||
let ws = makeWs(s, x, y, h, sp)
|
||||
let cmd = m.computeMove(ws)
|
||||
# The guarantee check: in escape mode the commanded velocity must have a
|
||||
# non-negative component along the wall-away normal.
|
||||
if m.escapeActive:
|
||||
inc result.escTicks
|
||||
let vDotAway = cmd.speed * cos(wrap180(h - m.escapeBearing) * PI / 180.0)
|
||||
if vDotAway < -1e-6: inc result.viol
|
||||
h += cmd.turnRate
|
||||
sp = cmd.speed
|
||||
x = clamp(x + sp * cos(h * PI / 180.0), 18.0, ArenaW - 18.0)
|
||||
y = clamp(y + sp * sin(h * PI / 180.0), 18.0, ArenaH - 18.0)
|
||||
if inCorner(x, y):
|
||||
inc runCorner
|
||||
else:
|
||||
result.longestCorner = max(result.longestCorner, runCorner); runCorner = 0
|
||||
result.longestCorner = max(result.longestCorner, runCorner)
|
||||
result.endClear = clearance(x, y)
|
||||
|
||||
proc sweep(): Sweep =
|
||||
const Ticks = 300
|
||||
const StallTicks = 60 ## 2 s at 30 ticks/s: "sitting" in a corner
|
||||
# Enemy placements: centre + four cardinal offsets (clamped into the arena)
|
||||
# give a spread of threat bearings, hence of strafe-line orientations.
|
||||
var xs: seq[float]
|
||||
var x = 36.0
|
||||
while x < ArenaW - 36.0: xs.add x; x += 72.0
|
||||
var ys: seq[float]
|
||||
var y = 36.0
|
||||
while y < ArenaH - 36.0: ys.add y; y += 72.0
|
||||
var headings: seq[float]
|
||||
var hh = 0.0
|
||||
while hh < 360.0: headings.add hh; hh += 45.0
|
||||
for sx in xs:
|
||||
for sy in ys:
|
||||
for en in [0, 1, 2, 3, 4]:
|
||||
let ex = case en
|
||||
of 1: sx
|
||||
of 2: sx
|
||||
of 3: clamp(sx - 250.0, 18.0, ArenaW - 18.0)
|
||||
of 4: clamp(sx + 250.0, 18.0, ArenaW - 18.0)
|
||||
else: ArenaW / 2.0
|
||||
let ey = case en
|
||||
of 1: clamp(sy - 250.0, 18.0, ArenaH - 18.0)
|
||||
of 2: clamp(sy + 250.0, 18.0, ArenaH - 18.0)
|
||||
of 3: sy
|
||||
of 4: sy
|
||||
else: ArenaH / 2.0
|
||||
for bh in headings:
|
||||
let r = simSweep(sx, sy, bh, ex, ey, Ticks)
|
||||
inc result.runs
|
||||
result.guaranteeViol += r.viol
|
||||
result.escapeTicks += r.escTicks
|
||||
if r.longestCorner > result.worstCorner:
|
||||
result.worstCorner = r.longestCorner
|
||||
result.worstName = "start=(" & $int(sx) & "," & $int(sy) & ") h=" &
|
||||
$int(bh) & " enemy=" & $en
|
||||
if r.longestCorner > StallTicks: inc result.stallRuns
|
||||
result.stallThresh = StallTicks
|
||||
|
||||
# ── GATE C: regression ───────────────────────────────────────────────────────
|
||||
|
||||
type ParityResult = object
|
||||
identical: bool
|
||||
maxDiff: float
|
||||
maxKappa: float
|
||||
minClear: float
|
||||
nonSignComm: int
|
||||
reversals: int
|
||||
completion: int
|
||||
meanTurn: float
|
||||
noTurnPct: float
|
||||
meanSpeed: float
|
||||
|
||||
proc openSpace(off: bool): tuple[cmds: seq[(float, float)], kappaMax, clrMin: float] =
|
||||
randomize(Seed)
|
||||
var m = initStrafe()
|
||||
var x = ArenaW / 2.0
|
||||
var y = ArenaH / 2.0
|
||||
var h = 0.0
|
||||
var sp = 0.0
|
||||
result.clrMin = 1e9
|
||||
for i in 0..<30:
|
||||
# Enemy due north => the strafe line is horizontal; the bot never reaches a
|
||||
# wall inside 30 ticks (max travel 240 px, clearance stays > 160 px).
|
||||
let s = Sample(enemyX: x, enemyY: y - 200.0, enemyEnergy: 100.0,
|
||||
selfX: x, selfY: y, selfHeading: h, selfEnergy: 100.0)
|
||||
let ws = makeWs(s, x, y, h, sp)
|
||||
let cmd = m.computeMove(ws)
|
||||
result.cmds.add (cmd.speed, cmd.turnRate)
|
||||
result.kappaMax = max(result.kappaMax, m.kappa)
|
||||
result.clrMin = min(result.clrMin, m.wallDist)
|
||||
h += cmd.turnRate
|
||||
sp = cmd.speed
|
||||
x = x + sp * cos(h * PI / 180.0)
|
||||
y = y + sp * sin(h * PI / 180.0)
|
||||
|
||||
proc regression(off, on: seq[Sample], offStarts, onStarts: seq[int]): ParityResult =
|
||||
# 1. open-space parity
|
||||
let a = openSpace(true)
|
||||
let b = openSpace(false)
|
||||
result.identical = a.cmds.len == b.cmds.len
|
||||
for i in 0..<min(a.cmds.len, b.cmds.len):
|
||||
let d = max(abs(a.cmds[i][0] - b.cmds[i][0]), abs(a.cmds[i][1] - b.cmds[i][1]))
|
||||
result.maxDiff = max(result.maxDiff, d)
|
||||
if d > 1e-9: result.identical = false
|
||||
result.maxKappa = max(a.kappaMax, b.kappaMax)
|
||||
result.minClear = min(a.clrMin, b.clrMin)
|
||||
# 2. sign-flip property + turn/speed stats on the main fixture (ON)
|
||||
randomize(Seed)
|
||||
var m = initStrafe()
|
||||
var x, y, h, sp = 0.0
|
||||
var prevSign = 0.0
|
||||
var turnSum = 0.0
|
||||
var noTurn = 0
|
||||
var n = 0
|
||||
for i in 0..<on.len:
|
||||
let s = on[i]
|
||||
if i == 0 or i in onStarts:
|
||||
m.resetRound()
|
||||
x = s.selfX; y = s.selfY; h = s.selfHeading; sp = 0.0
|
||||
prevSign = 0.0
|
||||
let ws = makeWs(s, x, y, h, sp)
|
||||
let cmd = m.computeMove(ws)
|
||||
if abs(abs(cmd.speed) - 8.0) > 1e-9: inc result.nonSignComm
|
||||
let sign = if cmd.speed > 0.0: 1.0 else: -1.0
|
||||
if sign != prevSign and prevSign != 0.0: inc result.reversals
|
||||
prevSign = sign
|
||||
turnSum += abs(cmd.turnRate)
|
||||
if abs(cmd.turnRate) < 0.5: inc noTurn
|
||||
inc n
|
||||
h += cmd.turnRate
|
||||
sp = cmd.speed
|
||||
x = clamp(x + sp * cos(h * PI / 180.0), 18.0, ArenaW - 18.0)
|
||||
y = clamp(y + sp * sin(h * PI / 180.0), 18.0, ArenaH - 18.0)
|
||||
result.completion = n
|
||||
result.meanTurn = if n == 0: 0.0 else: turnSum / n.float
|
||||
result.noTurnPct = if n == 0: 0.0 else: 100.0 * noTurn.float / n.float
|
||||
result.meanSpeed = 8.0
|
||||
|
||||
# ── driver ────────────────────────────────────────────────────────────────────
|
||||
|
||||
const Fixtures = [
|
||||
"tr_drussgt_vs_modularbot.jsonl",
|
||||
"tr_drussgt_vs_corners.jsonl",
|
||||
"tr_drussgt_vs_spinbot.jsonl",
|
||||
"tr_drussgt_vs_crazy.jsonl",
|
||||
]
|
||||
|
||||
type Loaded = object
|
||||
name: string
|
||||
samples: seq[Sample]
|
||||
starts: seq[int]
|
||||
|
||||
var loaded: seq[Loaded]
|
||||
for f in Fixtures:
|
||||
loaded.add Loaded(name: f, samples: loadSamples(f), starts: loadRoundStarts(f))
|
||||
|
||||
echo "STRAFE curved-wings gates (j112) — offline, seed=", Seed
|
||||
echo "WallProx=", fmtF(WallProxPx, 0), "px CornerPx=", fmtF(CornerPx, 0),
|
||||
"px (before = wings OFF, after = module defaults)"
|
||||
echo ""
|
||||
|
||||
# ── GATE A ───────────────────────────────────────────────────────────────────
|
||||
echo "=== GATE A — WALL/CORNER OCCUPANCY (kinematic replay) ==="
|
||||
echo " fixture wings nearWall% longestWall corner% longestCorner minClr meanClr escapeTicks"
|
||||
for L in loaded:
|
||||
for on in [false, true]:
|
||||
setWings(on)
|
||||
let r = simOccupancy(L.samples, L.starts)
|
||||
echo " ", alignLeft(L.name, 32), " ",
|
||||
(if on: "ON " else: "OFF"), " ",
|
||||
align(fmtF(100.0 * r.nearWall.float / max(1, r.ticks).float, 1), 8), "% ",
|
||||
align($r.longestWallRun, 10), " ",
|
||||
align(fmtF(100.0 * r.inCorner.float / max(1, r.ticks).float, 1), 6), "% ",
|
||||
align($r.longestCornerRun, 12), " ",
|
||||
align(fmtF(r.minClearance, 0), 5), " ",
|
||||
align(fmtF(r.meanClearance, 0), 6), " ",
|
||||
align($r.escapeTicks, 10)
|
||||
echo ""
|
||||
|
||||
# ── GATE B ───────────────────────────────────────────────────────────────────
|
||||
echo "=== GATE B — ESCAPE PROOF (grid x 8 headings x 5 enemy placements, 300 ticks) ==="
|
||||
setWings(true)
|
||||
let sw = sweep()
|
||||
echo " runs : ", sw.runs
|
||||
echo " escape-mode ticks : ", sw.escapeTicks
|
||||
echo " GUARANTEE VIOLATIONS : ", sw.guaranteeViol,
|
||||
" (velocity . wall-away must never be < 0 in escape mode)"
|
||||
echo " worst longest corner run : ", sw.worstCorner, " ticks at ", sw.worstName
|
||||
echo " runs over ", sw.stallThresh, " corner ticks : ", sw.stallRuns, " / ", sw.runs
|
||||
echo ""
|
||||
|
||||
# ── GATE C ───────────────────────────────────────────────────────────────────
|
||||
echo "=== GATE C — REGRESSION ==="
|
||||
setWings(false); let offRes = loaded[0]
|
||||
setWings(true); let onRes = loaded[0]
|
||||
let reg = regression(offRes.samples, onRes.samples, offRes.starts, onRes.starts)
|
||||
echo " open space parity (ON vs OFF command stream, 30 ticks):"
|
||||
echo " bit-identical : ", reg.identical, " (max |diff| = ", fmtF(reg.maxDiff, 6), ")"
|
||||
echo " max kappa in open space : ", fmtF(reg.maxKappa, 6), " (must be 0)"
|
||||
echo " min clearance seen : ", fmtF(reg.minClear, 1), " px (> wall margin 108)"
|
||||
echo " sign-flip property (every command exactly +/-8 px/tick):"
|
||||
echo " non-sign commands : ", reg.nonSignComm, " / ", reg.completion
|
||||
echo " reversals (sign flips) : ", reg.reversals
|
||||
echo " speed / turn on ", onRes.name, ":"
|
||||
echo " mean |turnRate| : ", fmtF(reg.meanTurn, 2), " deg/tick"
|
||||
echo " no-turn ticks (<0.5) : ", fmtF(reg.noTurnPct, 1), "%"
|
||||
echo " mean |speed| : ", fmtF(reg.meanSpeed, 2), " px/tick"
|
||||
echo ""
|
||||
echo "HONEST READ: the fixtures are OPEN-LOOP (the enemy reacted to a previous"
|
||||
echo "mover at capture time). A positive here is NOT a live win; it is a"
|
||||
echo "veto-capable check only. The escape GUARANTEE is the per-tick proof above"
|
||||
echo "(0 violations), not the fixture numbers."
|
||||
setWings(true)
|
||||
Reference in New Issue
Block a user