j146 TFIL: the bullet's own heat becomes tunable (TR_TFIL_BULLET_CORE/AURA, default 10/5 = today) and the field-shape A/B is pre-registered

This commit is contained in:
2026-09-26 22:16:06 +02:00
parent 5dd921c26b
commit 298ea6d586
5 changed files with 164 additions and 3 deletions
+2
View File
@@ -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_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_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_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_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_COMMIT_TICKS=15 # ticks to commit to a dodge point before replanning
TR_TFIL_NO_REV=off # on = never reverse direction inside a corridor TR_TFIL_NO_REV=off # on = never reverse direction inside a corridor
+5 -1
View File
@@ -314,6 +314,10 @@ proc printEffectiveValues(ctx: EnvReportContext) =
sourceOf("TR_TFIL_WALL_HOTNESS")) sourceOf("TR_TFIL_WALL_HOTNESS"))
emit("TR_TFIL_WALL_RADIANCE", $the_floor_is_lava.WallRadiance, emit("TR_TFIL_WALL_RADIANCE", $the_floor_is_lava.WallRadiance,
sourceOf("TR_TFIL_WALL_RADIANCE")) 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), emit("TR_TFIL_TILE_REPLAN", tileReplanName(TfilTileReplanMode),
sourceOf("TR_TFIL_TILE_REPLAN")) sourceOf("TR_TFIL_TILE_REPLAN"))
emit("TR_TFIL_COMMIT_TICKS", $TfilCommitTicks, sourceOf("TR_TFIL_COMMIT_TICKS")) 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_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_LO", "TR_TFIL_RANGE_HI", "TR_TFIL_RANGE_TEMP",
"TR_TFIL_RANGE_K", "TR_TFIL_CORRIDOR_HEAT", "TR_TFIL_WALL_HOTNESS", "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_TILE_REPLAN", "TR_TFIL_COMMIT_TICKS", "TR_TFIL_NO_REV",
"TR_TFIL_COMMIT_LOG", "TR_TFIL_COMMIT_ARRIVAL", "TR_TFIL_COMMIT_MARGIN", "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", "TR_TFIL_NOREV_SPEED", "TR_TFIL_TURN_BIAS", "TR_TFIL_TURN_REF_DEG",
+21 -2
View File
@@ -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 BulletAuraExtMin = 36.0 ## aura extension at power 3.0 (slow)
const BulletAuraExtMax = 54.0 ## aura extension at power 0.1 (fast) const BulletAuraExtMax = 54.0 ## aura extension at power 0.1 (fast)
const BulletCore = 10.0 ## lava accumulation per bullet-overlapping tile ## j146: the bullet's OWN heat was a Nim `const`, so no env arm could move it
const BulletAura = 5.0 ## lava accumulation for aura ring tiles ## 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 EnemyCoreRadius = 18.0 ## half of 36px body
const EnemyAuraRadius = 54.0 ## 18 + 36 const EnemyAuraRadius = 54.0 ## 18 + 36
@@ -201,6 +212,8 @@ proc loadTfilHeatShapeEnv*() =
CorridorHeat = max(0.0, getEnvFloat("TR_TFIL_CORRIDOR_HEAT", DefaultCorridorHeat)) CorridorHeat = max(0.0, getEnvFloat("TR_TFIL_CORRIDOR_HEAT", DefaultCorridorHeat))
WallHotness = max(0.0, getEnvFloat("TR_TFIL_WALL_HOTNESS", DefaultWallHotness)) WallHotness = max(0.0, getEnvFloat("TR_TFIL_WALL_HOTNESS", DefaultWallHotness))
WallRadiance = max(0.0, getEnvFloat("TR_TFIL_WALL_RADIANCE", DefaultWallRadiance)) 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() loadTfilHeatShapeEnv()
@@ -307,6 +320,10 @@ type
## candidate set of the last pick (j145: ## candidate set of the last pick (j145:
## lets a caller measure the REGRET of the ## lets a caller measure the REGRET of the
## draw instead of only the drawn value) ## 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()) proc initTFIL*(): TFILModule = TFILModule(debugGraphics: false, fire: initFireTracker())
@@ -352,6 +369,7 @@ proc resetRound*(m: var TFILModule) =
m.picks = 0 m.picks = 0
m.lastPickPromoted = false m.lastPickPromoted = false
m.lastPickMinTurn = 0.0 m.lastPickMinTurn = 0.0
m.lastPickSafe = 0
# ── Commit diagnostics (TR_TFIL_COMMIT_LOG, off by default) ────────────────── # ── Commit diagnostics (TR_TFIL_COMMIT_LOG, off by default) ──────────────────
# One JSONL line per computeMove call, used by the A/B to prove the treatment # 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 var minTurn = Inf
for t in candidates: minTurn = min(minTurn, t.turnDeg) for t in candidates: minTurn = min(minTurn, t.turnDeg)
m.lastPickMinTurn = if minTurn == Inf: ct.turnDeg else: minTurn m.lastPickMinTurn = if minTurn == Inf: ct.turnDeg else: minTurn
m.lastPickSafe = candidates.len
m.commitTarget = (x: m.marginX + (ct.col.float + 0.5) * GridSize, m.commitTarget = (x: m.marginX + (ct.col.float + 0.5) * GridSize,
y: m.marginY + (ct.row.float + 0.5) * GridSize) y: m.marginY + (ct.row.float + 0.5) * GridSize)
m.commitTicks = TfilCommitTicks m.commitTicks = TfilCommitTicks
+93
View File
@@ -3218,3 +3218,96 @@ nothing.
**The shipped default is untouched.** `TR_MOVEMENT=strafe` remains the default **The shipped default is untouched.** `TR_MOVEMENT=strafe` remains the default
(`ModularBot_garage/src/ModularBot.nim:129`); both new knobs default to (`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. 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)*
+43
View File
@@ -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)