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:
2026-09-25 23:51:12 +02:00
parent ed25ce29ad
commit ccff7e3e4a
3 changed files with 690 additions and 36 deletions
+9
View File
@@ -276,6 +276,13 @@ proc printEffectiveValues(ctx: EnvReportContext) =
emit("TR_STRAFE_RANGE_TOL", $StrafeRangeTol, sourceOf("TR_STRAFE_RANGE_TOL"))
emit("TR_STRAFE_TILT_MAX", $StrafeTiltMax, sourceOf("TR_STRAFE_TILT_MAX"))
emit("TR_STRAFE_TILT_GAIN", $StrafeTiltGain, sourceOf("TR_STRAFE_TILT_GAIN"))
# curved wings + wall escape (j112)
emit("TR_STRAFE_KAPPA", $StrafeKappa, sourceOf("TR_STRAFE_KAPPA"))
emit("TR_STRAFE_WALL_MARGIN", $StrafeWallMargin, sourceOf("TR_STRAFE_WALL_MARGIN"))
emit("TR_STRAFE_WING_MAX", $StrafeWingMax, sourceOf("TR_STRAFE_WING_MAX"))
emit("TR_STRAFE_WALL_BIAS", $StrafeWallBias, sourceOf("TR_STRAFE_WALL_BIAS"))
emit("TR_STRAFE_WALL_SAFE", $StrafeWallSafe, sourceOf("TR_STRAFE_WALL_SAFE"))
emit("TR_STRAFE_ESCAPE", onOff(StrafeEscape), sourceOf("TR_STRAFE_ESCAPE"))
emit("TR_STRAFE_HEAT_GRID", onOff(StrafeHeatGrid), sourceOf("TR_STRAFE_HEAT_GRID"))
# STRAFE's heat shape is its own RETUNE (bullet 20/10, corridor 10, wall
# 15/5), override-able per run so the shipped field can be A/B'd on one
@@ -441,6 +448,8 @@ proc knownEnvNames*(): seq[string] =
"TR_STRAFE_DWELL_MIN", "TR_STRAFE_DWELL_MAX", "TR_STRAFE_LOG",
"TR_STRAFE_RANGE", "TR_STRAFE_RANGE_TOL", "TR_STRAFE_TILT_MAX",
"TR_STRAFE_TILT_GAIN",
"TR_STRAFE_KAPPA", "TR_STRAFE_WALL_MARGIN", "TR_STRAFE_WING_MAX",
"TR_STRAFE_WALL_BIAS", "TR_STRAFE_WALL_SAFE", "TR_STRAFE_ESCAPE",
"TR_STRAFE_HEAT_GRID", "TR_STRAFE_BULLET_CORE", "TR_STRAFE_BULLET_AURA",
"TR_STRAFE_CORRIDOR_HEAT", "TR_STRAFE_WALL_HOTNESS",
"TR_STRAFE_WALL_RADIANCE",