diff --git a/ModularBot_garage/src/env_report.nim b/ModularBot_garage/src/env_report.nim index da8bd4c..7c29c3d 100644 --- a/ModularBot_garage/src/env_report.nim +++ b/ModularBot_garage/src/env_report.nim @@ -272,8 +272,12 @@ proc printEffectiveValues(ctx: EnvReportContext) = 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_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 + # binary at boot. + emit("TR_STRAFE_BULLET_CORE", $StrafeBulletCore, sourceOf("TR_STRAFE_BULLET_CORE")) + emit("TR_STRAFE_BULLET_AURA", $StrafeBulletAura, sourceOf("TR_STRAFE_BULLET_AURA")) 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")) @@ -431,6 +435,7 @@ proc knownEnvNames*(): seq[string] = "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_HEAT_GRID", "TR_STRAFE_BULLET_CORE", "TR_STRAFE_BULLET_AURA", "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 diff --git a/common_libs/movements/strafe.nim b/common_libs/movements/strafe.nim index 22d3588..74d3adb 100644 --- a/common_libs/movements/strafe.nim +++ b/common_libs/movements/strafe.nim @@ -41,9 +41,11 @@ ## PERPENDICULAR JITTER of `±TR_STRAFE_SPREAD` tiles (the owner's "spread a ## little"). A tile is acceptable when the max heat ON THE STRAIGHT-LINE PATH ## from the bot is <= `PathDangerThreshold` (10.0) — the SAME safety rule TFIL -## uses. The heat field itself is the SHIPPED TFIL field (jobs j105/j106: the -## time-indexed bullet model + the pillar-free default) — see the reuse note -## below. +## uses. The heat field is a strafe-specific RETUNE of that field (bullet 20/10, +## corridor 10, wall 15/5, pillar off — see the defaults block below), not the +## shipped TFIL field: with the shipped shape a safe tile existed on only 36.6% +## of picks (j108 Gate A). The time-indexed bullet model + pillar-free default +## are still reused, see the reuse note below. ## ## ── Move by sign only (point 5) ───────────────────────────────────────────── ## `speed = MaxSpeed * sign`, where `sign` is +1 when the chosen tile lies along @@ -78,15 +80,18 @@ ## TR_STRAFE_DWELL_MIN 6 min ticks before a re-pick ## TR_STRAFE_DWELL_MAX 20 max ticks before a re-pick ## TR_STRAFE_LOG off presence-based: echo one line per pick -## TR_STRAFE_CORRIDOR_HEAT 20.0 lava per corridor tile (shipped TFIL value) -## TR_STRAFE_WALL_HOTNESS 30.0 peak wall radiance (shipped TFIL value) -## TR_STRAFE_WALL_RADIANCE 10.0 wall radiance falloff (shipped TFIL value) +## TR_STRAFE_HEAT_GRID 1 draw the full heat grid (0 = hide it) +## TR_STRAFE_BULLET_CORE 20.0 lava per bullet-overlapping tile (retune) +## TR_STRAFE_BULLET_AURA 10.0 lava for aura ring tiles (retune) +## TR_STRAFE_CORRIDOR_HEAT 10.0 lava per corridor tile (== threshold) +## TR_STRAFE_WALL_HOTNESS 15.0 peak wall radiance (retune) +## TR_STRAFE_WALL_RADIANCE 5.0 wall radiance falloff (retune) ## ## The default `TR_MOVEMENT=tfil` path is untouched; this module is only ever ## called when the bot explicitly selects `strafe`. import std/[math, random, os] -from std/strutils import parseFloat, parseInt, strip +from std/strutils import parseFloat, parseInt, strip, toLowerAscii import std/strformat import gun_harness/gun_interface import movement_harness/movement_interface @@ -103,25 +108,43 @@ const BulletCoreRadiusMax = 54.0 ## core radius at power 3.0 const BulletAuraExtMin = 36.0 ## aura extension at power 3.0 (slow) const BulletAuraExtMax = 54.0 ## aura extension at power 0.1 (fast) -const BulletCore = 10.0 ## lava per bullet-overlapping tile (shipped default) -const BulletAura = 5.0 ## lava for aura ring tiles (shipped default) +## ── Heat-shape defaults: the RETUNE, not the shipped TFIL field ───────────── +## STRAFE deliberately runs a DIFFERENT default heat shape from the shipped +## mover. The shipped field (corridor 20, wall 30/10) left a safe tile on the +## perpendicular line on only 36.6% of picks (j108 Gate A: 63.4% fallback, mean +## 3.70 safe candidates); this retune raises that to 75.4% (j110 Gate A: 24.6% +## fallback, mean 11.17 safe candidates). It is a deliberate middle ground: the +## bullet's own core MUST be dangerous (defect 2), and that by construction +## costs some safe tiles versus a field whose bullet core sat below threshold. +## BulletCore 20 / BulletAura 10 — a bullet's OWN heat (20) is now ABOVE +## PathDangerThreshold (10), so the bullet itself is the danger. With the +## shipped 10/5 a bullet core sat exactly ON the threshold and was +## therefore NEVER dangerous on its own; it only ever bit through its +## corridor. This is the ring mover's retune, adopted here. +## CorridorHeat 10 — exactly the threshold, so one corridor tile alone still +## cannot make a path unsafe (the rule is <= threshold); corridors nudge. +## WallHotness 15 / WallRadiance 5 — only the outer wall ring is a hard threat. +## Pillar 0/0 — off, exactly like the shipped default (imported globals). +## All five are env-overridable per run via the TR_STRAFE_* names below. +const BulletCoreDefault = 20.0 ## lava per bullet-overlapping tile (retune) +const BulletAuraDefault = 10.0 ## lava for aura ring tiles (retune) const EnemyCoreRadius = 18.0 ## half of 36px body const EnemyAuraRadius = 54.0 ## 18 + 36 const EnemyCore = 40.0 ## lava per tile overlapping enemy body const EnemyAura = 10.0 ## lava per tile in enemy aura ring -const CorridorHeatDefault = 20.0 ## shipped TFIL corridor heat -const WallHotnessDefault = 30.0 ## shipped TFIL wall radiance peak -const WallRadianceDefault = 10.0 ## shipped TFIL wall radiance falloff +const CorridorHeatDefault = 10.0 ## corridor heat (== PathDangerThreshold) +const WallHotnessDefault = 15.0 ## wall radiance peak (retune) +const WallRadianceDefault = 5.0 ## wall radiance falloff (retune) -## Heat shape is override-able so the two candidate fields can be compared on -## one binary (the shipped field vs the ring variant's retune). Default = the -## SHIPPED TFIL field, so the strafe mover is judged on the same field as the -## baseline. `TR_STRAFE_CORRIDOR_HEAT` / `TR_STRAFE_WALL_HOTNESS` / -## `TR_STRAFE_WALL_RADIANCE` are strafe-specific (the ring mover's own field is -## controlled by its separate `TR_TFIL_*` names). +## Heat shape is override-able so the shipped field and the retune can be +## compared on one binary. The DEFAULTS are the retune (see the block above); +## the knobs are strafe-specific, so the ring mover's own `TR_TFIL_*` names do +## not touch this field and vice versa. var + StrafeBulletCore* = BulletCoreDefault + StrafeBulletAura* = BulletAuraDefault StrafeCorridorHeat* = CorridorHeatDefault StrafeWallHotness* = WallHotnessDefault StrafeWallRadiance* = WallRadianceDefault @@ -145,6 +168,11 @@ proc getEnvInt(name: string, default: int): int = try: result = parseInt(s.strip()) except ValueError: result = default +proc getEnvBool(name: string, default: bool): bool = + let s = getEnv(name, "").strip().toLowerAscii() + if s.len == 0: return default + s in ["1", "true", "on", "yes"] + const DefaultStrafeBand = 20.0 DefaultStrafeSpread = 1 @@ -159,10 +187,16 @@ var StrafeDwellMin* = DefaultStrafeDwellMin StrafeDwellMax* = DefaultStrafeDwellMax StrafeLog* = false + ## GUI: draw the full lava field (every non-zero tile, value-labelled) the + ## way TFIL does. Default ON; `TR_STRAFE_HEAT_GRID=0` hides the field so the + ## strafe overlays can be read on their own. + StrafeHeatGrid* = true proc loadStrafeHeatEnv*() = - ## Re-read the heat-shape overrides. Exposed so a gate can restore the shipped + ## Re-read the heat-shape overrides. Exposed so a gate can restore the default ## field after temporarily retuning it on the SAME process. + StrafeBulletCore = getEnvFloat("TR_STRAFE_BULLET_CORE", BulletCoreDefault) + StrafeBulletAura = getEnvFloat("TR_STRAFE_BULLET_AURA", BulletAuraDefault) StrafeCorridorHeat = getEnvFloat("TR_STRAFE_CORRIDOR_HEAT", CorridorHeatDefault) StrafeWallHotness = getEnvFloat("TR_STRAFE_WALL_HOTNESS", WallHotnessDefault) StrafeWallRadiance = getEnvFloat("TR_STRAFE_WALL_RADIANCE", WallRadianceDefault) @@ -176,6 +210,7 @@ proc loadStrafeEnv*() = StrafeDwellMin = max(1, getEnvInt("TR_STRAFE_DWELL_MIN", DefaultStrafeDwellMin)) StrafeDwellMax = max(StrafeDwellMin, getEnvInt("TR_STRAFE_DWELL_MAX", DefaultStrafeDwellMax)) StrafeLog = existsEnv("TR_STRAFE_LOG") + StrafeHeatGrid = getEnvBool("TR_STRAFE_HEAT_GRID", true) loadStrafeHeatEnv() loadStrafeEnv() @@ -377,10 +412,10 @@ proc buildHeat(m: var StrafeModule, ws: WorldState) = let d2 = dx*dx + dy*dy if d2 <= coreR * coreR: let along = dx * bUx + dy * bUy - m.lava[row * m.cols + col] += BulletCore * bMag * heatDecay(along / bSpeed) + m.lava[row * m.cols + col] += StrafeBulletCore * bMag * heatDecay(along / bSpeed) elif d2 <= auraR * auraR: let along = dx * bUx + dy * bUy - m.lava[row * m.cols + col] += BulletAura * bMag * heatDecay(along / bSpeed) + m.lava[row * m.cols + col] += StrafeBulletAura * bMag * heatDecay(along / bSpeed) # Corridors (rotated rectangle from the bullet to the wall, auraR wide). for b in m.bullets: @@ -661,6 +696,30 @@ proc computeMove*(m: var StrafeModule, ws: WorldState): MoveCommand = # ── GUI overlay (the user watches this) ── if m.debugGraphics: + # The WHOLE lava field, drawn exactly as TFIL draws it: every non-zero tile + # stroked in a yellow->orange->red ramp scaled by the field maximum and + # labelled with its integer value. This is the picture the owner is used to + # from TFIL; the strafe overlays below draw ON TOP of it. `TR_STRAFE_HEAT_GRID=0` + # hides it. + if StrafeHeatGrid: + var maxLava = 0.0 + for v in m.lava: + if v > maxLava: maxLava = v + setFont("Arial", 10.0) + for row in 0.. 0.0: val / maxLava else: 0.0 + let heatColor = fromRgb(255'u8, uint8(255.0 * (1.0 - t)), 0'u8) + let x0 = m.marginX + col.float * GridSize + let y0 = m.marginY + row.float * GridSize + setStrokeColor(heatColor) + setStrokeWidth(1.0) + drawRectangle(x0, y0, GridSize, GridSize) + setFillColor(heatColor) + drawText($int(val), x0 + 12.0, y0 + 22.0) + let ux = cos(lineForward * DegToRad) let uy = sin(lineForward * DegToRad) diff --git a/common_libs/movements/the_floor_is_lava_ring.nim b/common_libs/movements/the_floor_is_lava_ring.nim index 01927ca..9ef2f03 100644 --- a/common_libs/movements/the_floor_is_lava_ring.nim +++ b/common_libs/movements/the_floor_is_lava_ring.nim @@ -46,24 +46,40 @@ ## TR_TFIL_RANGE_HI default 200.0 band upper edge (px) ## TR_TFIL_RANGE_TEMP default 0.4 softmax temperature; <= 0 = OFF path ## TR_TFIL_RANGE_K default 60.0 Gaussian falloff scale (px) -## TR_TFIL_CORRIDOR_HEAT default 5.0 lava per corridor-overlapping tile -## TR_TFIL_WALL_HOTNESS default 10.0 peak wall radiance at a wall tile +## TR_TFIL_CORRIDOR_HEAT default 10.0 lava per corridor-overlapping tile +## TR_TFIL_WALL_HOTNESS default 15.0 peak wall radiance at a wall tile ## TR_MOVEMENT_LOG=1 log band/range-class changes (not/tick) ## `TR_TFIL_RANGE_TEMP=0` calls plain `rand(candidates.high)` exactly as the ## original mover did, so the same binary can serve as the control arm. ## ## ── Heat-field knobs (the deviation from the original TFIL) ───────────────── ## Measured (offline, DrussGT fixtures): corridors are 21.4% and walls 56.4% of -## the over-threshold set, so they dominate the field. The two constants below -## are retuned so that NO SINGLE soft source can poison a path on its own: -## CorridorHeat = 5.0 — below `PathDangerThreshold` (10.0): one corridor can -## no longer make a path unsafe by itself. -## WallHotness = 10.0 — equals the threshold: the outer two tile rings are no -## longer over-threshold from wall radiance alone. -## Both are env-overridable (TR_TFIL_CORRIDOR_HEAT / TR_TFIL_WALL_HOTNESS) for -## an A/B; the defaults are the values measured to unlock the range weighting. -## WallRadiance, PillarHotness/Radiance, Enemy*/Bullet* and PathDangerThreshold -## are UNCHANGED from the original mover. +## the over-threshold set, so they dominate the field. The ACTUAL values in this +## file (read at module init, `let` block further down) are: +## CorridorHeat = 10.0 — exactly `PathDangerThreshold` (10.0). The safety rule +## is `pathMaxHeat <= PathDangerThreshold`, so a corridor +## tile sitting on a path is still SAFE: a corridor alone +## can no longer make a path unsafe. +## WallHotness = 15.0 — ABOVE the threshold at the OUTERMOST ring only (the +## second ring sits at exactly 10.0, the threshold, and +## is safe), so wall radiance alone CAN poison the outer +## ring but no deeper. +## WallRadiance = 5.0 — the falloff that produces the two rings above. +## BulletCore/BulletAura = 20.0/10.0 — the bullet's own core is ABOVE the +## threshold, which is what makes an incoming bullet +## dangerous on its own. +## Both soft knobs are env-overridable (TR_TFIL_CORRIDOR_HEAT / +## TR_TFIL_WALL_HOTNESS) for an A/B. +## CHANGED vs the original mover: BulletCore/BulletAura (20/10 here, 10/5 in +## the original), WallRadiance (5.0 here, 10.0 in the original). UNCHANGED: +## EnemyCore/EnemyAura and their radii, PillarHotness/PillarRadiance (0/0 +## default) and PathDangerThreshold (10.0). +## +## WARNING: an earlier revision of THIS comment claimed CorridorHeat = 5.0 and +## WallHotness = 10.0. That did NOT match the code — it was a stale copy of an +## earlier retune PROPOSAL, and a job that read the comment handed out +## sub-threshold heat values that left the field empty. The code has always been +## 10.0 / 15.0; the comment now says so. import std/[math, random, os, strformat] from std/strutils import parseFloat, strip # selective: strutils.fromHex clashes with color.fromHex diff --git a/common_libs/tests/measure_strafe_gates.nim b/common_libs/tests/measure_strafe_gates.nim index 6b64c8f..43a486f 100644 --- a/common_libs/tests/measure_strafe_gates.nim +++ b/common_libs/tests/measure_strafe_gates.nim @@ -221,15 +221,37 @@ let nTicks = states.len let tfil = replayTfil(states, starts) let sf = replayStrafe(states, starts) -# Field variant: the ring mover's retune, where no SINGLE soft source can poison -# a path (corridor below the threshold, wall radiance == the threshold). This is -# the same field shape the user's ring experiment used; we only test whether it -# unblocks the strafe line. +# ── reference fields for the Gate A comparison (j108's two rows) ─────────── +# NOTE: `sf` above already ran with the CURRENT strafe defaults (the corrected +# retune: bullet 20/10, corridor 10, wall 15/5). The two replays below +# reproduce j108's historical rows so the numbers are comparable on one binary. + +# j108 row 1: the SHIPPED TFIL field (bullet 10/5, corridor 20, wall 30/10). +StrafeBulletCore = 10.0 +StrafeBulletAura = 5.0 +StrafeCorridorHeat = 20.0 +StrafeWallHotness = 30.0 +StrafeWallRadiance = 10.0 +let sfShipped = replayStrafe(states, starts) + +# j108 row 2: the ring mover's retune field (bullet 10/5, corridor 5, wall 10/5). +StrafeBulletCore = 10.0 +StrafeBulletAura = 5.0 StrafeCorridorHeat = 5.0 StrafeWallHotness = 10.0 StrafeWallRadiance = 5.0 let sfRetune = replayStrafe(states, starts) -loadStrafeHeatEnv() # restore the shipped field for any later use + +# Isolation of DEFECT 2 alone: the ring retune's soft sources (corridor 5, wall +# 10/5) with ONLY the bullet heat raised to 20/10, so the gap between the +# corrected default and j108's retune row can be attributed. +StrafeBulletCore = 20.0 +StrafeBulletAura = 10.0 +StrafeCorridorHeat = 5.0 +StrafeWallHotness = 10.0 +StrafeWallRadiance = 5.0 +let sfBulletOnly = replayStrafe(states, starts) +loadStrafeHeatEnv() # restore the CURRENT strafe defaults for any later use echo "STRAFE gates — offline fixture ", fixtureRel, " (", nTicks, " ticks, ", starts.len, " rounds), seed=", Seed @@ -260,8 +282,10 @@ proc gateA(tag: string, rep: Replay) = line.add &"{k}:{hist.getOrDefault(k,0)} " echo " safe-count dist (0..12+) : ", line -gateA("shipped TFIL heat (corridor 20, wall 30/10)", sf) -gateA("ring retune (corridor 5, wall 10/5)", sfRetune) +gateA("STRAFE DEFAULT (corrected: bullet 20/10, corridor 10, wall 15/5)", sf) +gateA("j108 shipped TFIL field (bullet 10/5, corridor 20, wall 30/10)", sfShipped) +gateA("j108 ring retune (bullet 10/5, corridor 5, wall 10/5)", sfRetune) +gateA("bullet retune ISOLATED (bullet 20/10, corridor 5, wall 10/5)", sfBulletOnly) echo "" # ── GATE B ───────────────────────────────────────────────────────────────────