diff --git a/ModularBot_garage/.env.example b/ModularBot_garage/.env.example index 64ef797..d6e8cdf 100644 --- a/ModularBot_garage/.env.example +++ b/ModularBot_garage/.env.example @@ -107,6 +107,8 @@ TR_TFIL_RANGE_K=60.0 # px; how fast the weight falls off outside the band TR_TFIL_CORRIDOR_HEAT=10.0 # lava painted per corridor-overlapping tile TR_TFIL_WALL_HOTNESS=15.0 # peak heat painted on tiles next to a wall TR_TFIL_WALL_RADIANCE=10.0 # how fast wall heat falls off with distance +TR_TFIL_BULLET_CORE=10.0 # tfil only: lava per bullet-overlapping tile (== PathDangerThreshold, so a bullet is never hot on its own) +TR_TFIL_BULLET_AURA=5.0 # tfil only: lava for the bullet's aura ring tiles TR_TFIL_TILE_REPLAN=self # self | enemy | off: when a dodge commitment is cancelled TR_TFIL_COMMIT_TICKS=15 # ticks to commit to a dodge point before replanning TR_TFIL_NO_REV=off # on = never reverse direction inside a corridor diff --git a/ModularBot_garage/src/env_report.nim b/ModularBot_garage/src/env_report.nim index 4bfa2ca..4c3145d 100644 --- a/ModularBot_garage/src/env_report.nim +++ b/ModularBot_garage/src/env_report.nim @@ -314,6 +314,10 @@ proc printEffectiveValues(ctx: EnvReportContext) = sourceOf("TR_TFIL_WALL_HOTNESS")) emit("TR_TFIL_WALL_RADIANCE", $the_floor_is_lava.WallRadiance, sourceOf("TR_TFIL_WALL_RADIANCE")) + emit("TR_TFIL_BULLET_CORE", $the_floor_is_lava.BulletCore, + sourceOf("TR_TFIL_BULLET_CORE")) + emit("TR_TFIL_BULLET_AURA", $the_floor_is_lava.BulletAura, + sourceOf("TR_TFIL_BULLET_AURA")) emit("TR_TFIL_TILE_REPLAN", tileReplanName(TfilTileReplanMode), sourceOf("TR_TFIL_TILE_REPLAN")) emit("TR_TFIL_COMMIT_TICKS", $TfilCommitTicks, sourceOf("TR_TFIL_COMMIT_TICKS")) @@ -638,7 +642,7 @@ proc knownEnvNames*(): seq[string] = "TR_RAM_PLAN_MARGIN", "TR_RAM_PLAN_HITRATE", "TR_RAM_LOG", "TR_TFIL_RANGE_LO", "TR_TFIL_RANGE_HI", "TR_TFIL_RANGE_TEMP", "TR_TFIL_RANGE_K", "TR_TFIL_CORRIDOR_HEAT", "TR_TFIL_WALL_HOTNESS", - "TR_TFIL_WALL_RADIANCE", + "TR_TFIL_WALL_RADIANCE", "TR_TFIL_BULLET_CORE", "TR_TFIL_BULLET_AURA", "TR_TFIL_TILE_REPLAN", "TR_TFIL_COMMIT_TICKS", "TR_TFIL_NO_REV", "TR_TFIL_COMMIT_LOG", "TR_TFIL_COMMIT_ARRIVAL", "TR_TFIL_COMMIT_MARGIN", "TR_TFIL_NOREV_SPEED", "TR_TFIL_TURN_BIAS", "TR_TFIL_TURN_REF_DEG", diff --git a/common_libs/movements/the_floor_is_lava.nim b/common_libs/movements/the_floor_is_lava.nim index 9174707..afc46a1 100644 --- a/common_libs/movements/the_floor_is_lava.nim +++ b/common_libs/movements/the_floor_is_lava.nim @@ -18,8 +18,19 @@ 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 accumulation per bullet-overlapping tile -const BulletAura = 5.0 ## lava accumulation for aura ring tiles +## j146: the bullet's OWN heat was a Nim `const`, so no env arm could move it +## and 10.0 is exactly `PathDangerThreshold` — a bullet is never dangerous on +## its own and the corridor carries the whole field. Same env-overridable-var +## pattern as `CorridorHeat` below; the DEFAULTS are today's `const`s, so the +## default field is bit-identical. +## TR_TFIL_BULLET_CORE default 10.0 lava per bullet-overlapping tile +## TR_TFIL_BULLET_AURA default 5.0 lava for aura ring tiles +const + DefaultBulletCore = 10.0 + DefaultBulletAura = 5.0 +var + BulletCore* = DefaultBulletCore + BulletAura* = DefaultBulletAura const EnemyCoreRadius = 18.0 ## half of 36px body const EnemyAuraRadius = 54.0 ## 18 + 36 @@ -201,6 +212,8 @@ proc loadTfilHeatShapeEnv*() = CorridorHeat = max(0.0, getEnvFloat("TR_TFIL_CORRIDOR_HEAT", DefaultCorridorHeat)) WallHotness = max(0.0, getEnvFloat("TR_TFIL_WALL_HOTNESS", DefaultWallHotness)) WallRadiance = max(0.0, getEnvFloat("TR_TFIL_WALL_RADIANCE", DefaultWallRadiance)) + BulletCore = max(0.0, getEnvFloat("TR_TFIL_BULLET_CORE", DefaultBulletCore)) + BulletAura = max(0.0, getEnvFloat("TR_TFIL_BULLET_AURA", DefaultBulletAura)) loadTfilHeatShapeEnv() @@ -307,6 +320,10 @@ type ## candidate set of the last pick (j145: ## lets a caller measure the REGRET of the ## draw instead of only the drawn value) + lastPickSafe: int ## how many SAFE tiles (pathMaxHeat + ## <= PathDangerThreshold) the last pick + ## drew from (j146: the size of the set + ## the heat SHAPE leaves available) proc initTFIL*(): TFILModule = TFILModule(debugGraphics: false, fire: initFireTracker()) @@ -352,6 +369,7 @@ proc resetRound*(m: var TFILModule) = m.picks = 0 m.lastPickPromoted = false m.lastPickMinTurn = 0.0 + m.lastPickSafe = 0 # ── Commit diagnostics (TR_TFIL_COMMIT_LOG, off by default) ────────────────── # One JSONL line per computeMove call, used by the A/B to prove the treatment @@ -1119,6 +1137,7 @@ proc computeMove*(m: var TFILModule, ws: WorldState): MoveCommand = var minTurn = Inf for t in candidates: minTurn = min(minTurn, t.turnDeg) m.lastPickMinTurn = if minTurn == Inf: ct.turnDeg else: minTurn + m.lastPickSafe = candidates.len m.commitTarget = (x: m.marginX + (ct.col.float + 0.5) * GridSize, y: m.marginY + (ct.row.float + 0.5) * GridSize) m.commitTicks = TfilCommitTicks diff --git a/docs/movement_campaign.md b/docs/movement_campaign.md index 81e6474..abe0451 100644 --- a/docs/movement_campaign.md +++ b/docs/movement_campaign.md @@ -3218,3 +3218,96 @@ nothing. **The shipped default is untouched.** `TR_MOVEMENT=strafe` remains the default (`ModularBot_garage/src/ModularBot.nim:129`); both new knobs default to off-effect, so `TR_MOVEMENT=tfil` still means today's tfil, byte-for-byte. + +--- + +# Batch 7 — TFIL field shape (j146) + +*Pre-registered BEFORE any battle of this batch was launched. No battle of this +batch existed when this section was written; the frozen binary for it is the +commit that adds the two bullet-heat knobs.* + +## The cause this batch fixes + +Two consecutive tfil fixes (j144 `d2005ab` arrival + no-rev, j145 `39c90fd` +turn-bias) improved the MECHANISM and the live outcome stayed an under-powered +null. j145's offline gate then found the UPSTREAM cause, on the same fixture: + +1. **58.8% of tfil's picks have no safe set to tie-break in** — fewer than two + reachable tiles under `PathDangerThreshold = 10`. +2. `TR_TFIL_CORRIDOR_HEAT = 20` is **twice** that threshold, so ONE far bullet's + corridor marks a wide swath unsafe by itself; j145 measured `filter broken` on + **59%** of picks. +3. In tfil **the bullet's own heat is inert**: `BulletCore = 10.0` / + `BulletAura = 5.0` were Nim `const`s, and 10 is exactly the threshold, so a + bullet is never dangerous on its own and the corridor carries all the weight. + +The SHAPE was already measured — but on **strafe**, in j119 batch 4, where the +engine and the field differ. `field_strong` (corridor 20, wall 30/10, i.e. exactly +what tfil ships) was **-0.29 wins/run, p=0.039**; `field_off` was **-0.47 +wins/run, p=0.0063**; the reference strafe shape — bullet core 20 / aura 10, +corridor 10, wall 15 / radiance 5 — was the champion. **Both extremes lose and +the middle wins, and tfil has never been run on the middle shape.** + +## The treatment (Task A) + +`BulletCore` / `BulletAura` in `the_floor_is_lava.nim` go from `const` to +env-overridable `var`s, the exact pattern j119/j134 already used for +`CorridorHeat` / `WallHotness` / `WallRadiance`: + +| knob | default | meaning | +|---|---|---| +| `TR_TFIL_BULLET_CORE` | `10.0` (= today's `const`) | lava per bullet-overlapping tile | +| `TR_TFIL_BULLET_AURA` | `5.0` (= today's `const`) | lava for the bullet's aura ring | + +**Default-off-effect**: the defaults are today's `const`s, so the default path is +byte-for-byte unchanged and the golden in `test_tfil_commit_env.nim` is not +regenerated. + +## Arms (frozen, all `TR_MOVEMENT=tfil`, j144 ON in every arm) + +j144's recommended `.env` (`TR_TFIL_COMMIT_ARRIVAL=1 TR_TFIL_NOREV_SPEED=4`) is +ON in **every** arm, so the shape is isolated **on top of the current best tfil**. +The virtual pillar is OFF (shipped 0/0) in every arm. Columns are +corridor / wall hotness / wall radiance / bullet core / bullet aura. + +| # | arm | shape | what it isolates | +|---|---|---|---| +| 1 | `shape_shipped` | 20/30/10/10/5 | **the reference** — today's field, on j144 | +| 2 | `shape_middle` | 10/15/5/20/10 | the shape strafe won on | +| 3 | `shape_corr10` | 10/30/10/10/5 | "corridor alone was the problem" | +| 4 | `shape_bullets` | 20/30/10/20/10 | "bullets made dangerous themselves" | +| 5 | `shape_nofield` | 0/0/0/20/10 | the "field off" control strafe measured as WORSE | + +`shape_nofield` is here so a `shape_middle` win cannot be a FALSE WINNER: it +separates "the middle shape is right" from "any less lava is right". Note that +`TR_TFIL_WALL_HOTNESS=0` is what zeroes the wall (radiance 0 would paint a FLAT +field over the whole arena — the opposite of "no walls"). The enemy body/aura +heat (`EnemyCore 40` / `EnemyAura 10`) is a `const` with no knob and is present in +every arm; "field off" therefore means no **corridor/wall** field. + +Panel: the FROZEN 15-opponent `tools/ab/panel_movement.txt`. Harness: +`tools/ab/tournament_run.sh` + `tournament_analyze.py`. Arms file +`tools/ab/arms_tfil_shape.txt`. + +## Pre-registered prediction and decision rule + +* **Prediction.** `shape_middle` > `shape_shipped` on damage/run and round wins + (it is the shape strafe measured as best, and it is the only arm that both + halves the corridor AND makes the bullet itself dangerous, which is what + should restore a real safe set). `shape_corr10` and `shape_bullets` should + each move part of the way. `shape_nofield` should be the WEAKEST arm, matching + strafe's `field_off` — if `shape_nofield` ties `shape_middle` on the primary + metrics, the win is "less lava", not "this shape", and that is recorded as a + wrong prediction. +* **Mechanism, not verdict.** The **offline `filter broken` rate and the mean + safe-candidate count** are the mechanism. The **incoming hit rate** is the + live mechanism. The **verdict is damage/run and round wins** under the + campaign's pre-registered rule 2: the cross-opponent sign test p < 0.05 on one + primary metric with the other not down, with SD/SE/95% CI/MDE reported. +* **If nothing separates**, the verdict is *not distinguishable* and the shape is + **not changed**. The pre-registered bar is not re-interpreted afterwards. +* **A null does NOT retract j144's mechanism result** and does not change the + shipped `TR_MOVEMENT=strafe` default. + +*(results appended below after the battles)* diff --git a/tools/ab/arms_tfil_shape.txt b/tools/ab/arms_tfil_shape.txt new file mode 100644 index 0000000..d83d878 --- /dev/null +++ b/tools/ab/arms_tfil_shape.txt @@ -0,0 +1,43 @@ +# ───────────────────────────────────────────────────────────────────────────── +# arms_tfil_shape.txt — j146: the TFIL FIELD SHAPE, five arms on the frozen +# movement panel (tools/ab/panel_movement.txt), all with TR_MOVEMENT=tfil pinned +# EXPLICITLY and the j144 fixes ON, so only the SHAPE varies. +# +# WHY. The strafe sweep (j119 batch 4) already measured the field SHAPE, not the +# engine: the shape tfil SHIPS (corridor 20, wall 30/10) was -0.29 wins/run +# (p=0.039) and field_off was -0.47 wins/run (p=0.0063); the champion was the +# MIDDLE shape (bullet core 20 / aura 10, corridor 10, wall 15 / radiance 5). +# tfil has never been run on the middle shape. On top of that, j145 found 59% of +# tfil's picks have no safe set to tie-break in, because corridor 20 is TWICE +# PathDangerThreshold 10 and tfil's own bullet heat (core 10) is exactly the +# threshold, so a bullet is never dangerous by itself. +# +# Every arm carries `TR_TFIL_COMMIT_ARRIVAL=1 TR_TFIL_NOREV_SPEED=4` (j144's +# recommended .env) so the shape is isolated ON TOP OF THE CURRENT BEST TFIL. +# The pillar stays OFF (shipped default 0/0) in every arm. +# +# Columns: CORRIDOR / WALL_HOTNESS / WALL_RADIANCE / BULLET_CORE / BULLET_AURA +# +# Pre-registered in docs/movement_campaign.md ("Batch 7 — TFIL field shape") +# BEFORE any of these battles ran. Reference is `shape_shipped`. +# +# Format: name | ENV=value ENV=value | label +# ───────────────────────────────────────────────────────────────────────────── + +# 1. THE ARM TO BEAT — the field tfil ships today (20/30/10/10/5), on j144. +shape_shipped | TR_MOVEMENT=tfil TR_TFIL_COMMIT_ARRIVAL=1 TR_TFIL_NOREV_SPEED=4 TR_TFIL_CORRIDOR_HEAT=20 TR_TFIL_WALL_HOTNESS=30 TR_TFIL_WALL_RADIANCE=10 TR_TFIL_BULLET_CORE=10 TR_TFIL_BULLET_AURA=5 | today's shape, on j144 (control / reference) + +# 2. THE CANDIDATE — the shape strafe measured as the champion. +shape_middle | TR_MOVEMENT=tfil TR_TFIL_COMMIT_ARRIVAL=1 TR_TFIL_NOREV_SPEED=4 TR_TFIL_CORRIDOR_HEAT=10 TR_TFIL_WALL_HOTNESS=15 TR_TFIL_WALL_RADIANCE=5 TR_TFIL_BULLET_CORE=20 TR_TFIL_BULLET_AURA=10 | the middle shape strafe won on (10/15/5/20/10) + +# 3. "corridor alone was the problem" — only the corridor is halved. +shape_corr10 | TR_MOVEMENT=tfil TR_TFIL_COMMIT_ARRIVAL=1 TR_TFIL_NOREV_SPEED=4 TR_TFIL_CORRIDOR_HEAT=10 TR_TFIL_WALL_HOTNESS=30 TR_TFIL_WALL_RADIANCE=10 TR_TFIL_BULLET_CORE=10 TR_TFIL_BULLET_AURA=5 | shipped, corridor 20->10 only + +# 4. "bullets made dangerous themselves" — only the bullet's own heat is raised. +shape_bullets | TR_MOVEMENT=tfil TR_TFIL_COMMIT_ARRIVAL=1 TR_TFIL_NOREV_SPEED=4 TR_TFIL_CORRIDOR_HEAT=20 TR_TFIL_WALL_HOTNESS=30 TR_TFIL_WALL_RADIANCE=10 TR_TFIL_BULLET_CORE=20 TR_TFIL_BULLET_AURA=10 | shipped, bullet core/aura 10/5 -> 20/10 only + +# 5. THE "field off" CONTROL — corridors, walls and pillar all zero, so the +# bullets' own heat is the only bullet threat. strafe measured this shape as +# the WORST of the three; it is here so a middle-shape win cannot be a false +# winner (it proves the win is the middle, not simply "less lava"). +shape_nofield | TR_MOVEMENT=tfil TR_TFIL_COMMIT_ARRIVAL=1 TR_TFIL_NOREV_SPEED=4 TR_TFIL_CORRIDOR_HEAT=0 TR_TFIL_WALL_HOTNESS=0 TR_TFIL_WALL_RADIANCE=0 TR_TFIL_BULLET_CORE=20 TR_TFIL_BULLET_AURA=10 | no corridor/wall field, bullets only (0/0/0/20/10)