STRAFE movement: body pinned perpendicular to the threat, reversals by sign flip

New engine movements/strafe.nim, selected by TR_MOVEMENT=strafe (default stays
tfil, byte-identical — test_tfil_commit_env.nim's 30 checks still pass).

Design (the owner's):
- AXIS = incoming bullet's direction when a bullet is in flight, else the
  perpendicular of the enemy bearing. The body heading is kept inside a band
  (TR_STRAFE_BAND, default 20 deg) around the perpendicular LINE; it turns only
  when outside the band, and never turns to face a movement target.
- Candidate tiles on the perpendicular line through our position, both forward
  and backward, within TR_STRAFE_REACH px, with a perpendicular jitter of
  +/- TR_STRAFE_SPREAD tiles. A tile is acceptable when its path max heat is
  <= PathDangerThreshold, the SAME safety rule TFIL uses.
- Move by SIGN only: setForward(+/-MaxSpeed>). Dwell is re-picked after a random
  number of ticks in [TR_STRAFE_DWELL_MIN, TR_STRAFE_DWELL_MAX], on arrival, or
  on a serious threat spike.
- Heat machinery is REUSED from the shipped mover, not re-implemented: the
  exported heatDecay()/bulletMagScale() (j105 time-indexed model) and the
  PillarHotness/PillarRadiance globals (j106 pillar-free default). The heat
  shape is overridable via TR_STRAFE_CORRIDOR_HEAT/WALL_HOTNESS/WALL_RADIANCE
  (defaults = the shipped TFIL field).
- GUI overlay: strafe line, threat axis, candidate tiles (safe/unsafe), chosen
  target, sign-coloured movement ray, and the heading band.

Gates (offline, recorded DrussGT fixture, 20026 ticks):
- A TILE AVAILABILITY: shipped heat field -> a safe tile exists on only 36.6%
  of picks (63.4% fall back to the least-hot tile); the ring retune
  (corridor 5, wall 10/5) raises it to 91.9%.
- B PREDICTABILITY: reversal-interval entropy 5.84 bits vs TFIL 5.09; direction
  entropy 1.00 both; long-lag autocorrelation ~0 for both (no periodic
  component). Fewer reversals (710 vs 1453) and more full-speed ticks.
  measurements: common_libs/tests/measure_strafe_gates.nim

Also registers TR_STRAFE_* in the boot env report (ModularBot_garage/src/
env_report.nim) and wires the engine into ModularBot.nim (hold -> strafe,
ram trigger -> rammer).
This commit is contained in:
2026-09-25 22:38:52 +02:00
parent 99cf9e5c82
commit a50c0125d5
4 changed files with 1103 additions and 9 deletions
+24 -3
View File
@@ -25,6 +25,7 @@ import gun_harness/selector
import movements/ram_decision
import movements/the_floor_is_lava
import movements/the_floor_is_lava_ring
import movements/strafe
import guns/tm_horizon
import guns/bitbrain_gun
import guns/pattern_matcher
@@ -174,9 +175,12 @@ proc printEffectiveValues(ctx: EnvReportContext) =
echo "[env] --- B. effective values (resolved at boot) ---"
# ── movement / process-level ModularBot knobs ─────────────────────────────
# Only `tfil_ring` selects the ring engine; every other value runs `tfil`, so
# report the RESOLVED engine rather than the raw string.
let effectiveMovement = if ctx.movementName == "tfil_ring": "tfil_ring" else: "tfil"
# Only `tfil_ring` and `strafe` select a non-default engine; every other value
# runs `tfil`, so report the RESOLVED engine rather than the raw string.
let effectiveMovement =
if ctx.movementName == "tfil_ring": "tfil_ring"
elif ctx.movementName == "strafe": "strafe"
else: "tfil"
emit("TR_MOVEMENT", effectiveMovement, sourceOf("TR_MOVEMENT"))
emit("TR_MOVEMENT_LOG", onOff(MovementLog), sourceOfPresence("TR_MOVEMENT_LOG"))
emit("TR_VBULLET_ADMIT_ONLY", onOff(ctx.vBulletAdmitOnly),
@@ -259,6 +263,19 @@ proc printEffectiveValues(ctx: EnvReportContext) =
emit("TR_TFIL_PILLAR_ON", onOff(PillarHotness > 0.0),
sourceOf("TR_TFIL_PILLAR_ON"))
# ── movement (STRAFE) ─────────────────────────────────────────────────────
emit("TR_STRAFE_BAND", $StrafeBand, sourceOf("TR_STRAFE_BAND"))
emit("TR_STRAFE_SPREAD", $StrafeSpread, sourceOf("TR_STRAFE_SPREAD"))
emit("TR_STRAFE_REACH", $StrafeReach, sourceOf("TR_STRAFE_REACH"))
emit("TR_STRAFE_DWELL_MIN", $StrafeDwellMin, sourceOf("TR_STRAFE_DWELL_MIN"))
emit("TR_STRAFE_DWELL_MAX", $StrafeDwellMax, sourceOf("TR_STRAFE_DWELL_MAX"))
emit("TR_STRAFE_LOG", onOff(StrafeLog), sourceOfPresence("TR_STRAFE_LOG"))
# STRAFE's heat shape is override-able (default = the shipped TFIL field) so
# the shipped field and the ring retune can be A/B'd on one binary at boot.
emit("TR_STRAFE_CORRIDOR_HEAT", $StrafeCorridorHeat, sourceOf("TR_STRAFE_CORRIDOR_HEAT"))
emit("TR_STRAFE_WALL_HOTNESS", $StrafeWallHotness, sourceOf("TR_STRAFE_WALL_HOTNESS"))
emit("TR_STRAFE_WALL_RADIANCE", $StrafeWallRadiance, sourceOf("TR_STRAFE_WALL_RADIANCE"))
# ── ramming ───────────────────────────────────────────────────────────────
emit("TR_RAM_OPPORTUNITY", onOff(RamOppEnabled), sourceOf("TR_RAM_OPPORTUNITY"))
emit("TR_RAM_OPP_DIST", $RamOppDist, sourceOf("TR_RAM_OPP_DIST"))
@@ -409,6 +426,10 @@ proc knownEnvNames*(): seq[string] =
"TR_TFIL_COMMIT_LOG",
"TR_TFIL_HEAT_TIME", "TR_TFIL_HEAT_TAU", "TR_TFIL_HEAT_POWER_GAIN",
"TR_TFIL_PILLAR_ON",
"TR_STRAFE_BAND", "TR_STRAFE_SPREAD", "TR_STRAFE_REACH",
"TR_STRAFE_DWELL_MIN", "TR_STRAFE_DWELL_MAX", "TR_STRAFE_LOG",
"TR_STRAFE_CORRIDOR_HEAT", "TR_STRAFE_WALL_HOTNESS",
"TR_STRAFE_WALL_RADIANCE",
# harness vars (read by the test framework, inherited by the bot, so they
# must NOT be reported as typos)
"TR_SERVER_JAR", "TR_BATTLE_RUNNER", "TR_BATTLE_RUNNER_DIR",