From 486e2a69c67e48659411e14f559284eb781bcf3f Mon Sep 17 00:00:00 2001 From: Davide Cappellini Date: Sun, 27 Sep 2026 14:52:16 +0200 Subject: [PATCH] docs(env): 6-block env reference + .env.example, every claim traceable Rewrites docs/env_reference.md and ModularBot_garage/.env.example so a reader can act on the file without re-deriving anything, and so every claim in it can be checked. WHAT 177 knobs documented across a 6-block format: WHAT / VALUES / STATUS / GOTCHA / TRY. Values are the built-in defaults, so `.env.example` is behaviourally identical to a clean run. No default value changed anywhere; the only added key is TR_FIRE_LAG=0, which is real (fire_tracker.nim:164). WHY (traceability) Every STATUS line now cites the job or commit behind the claim it makes. A documented default is only useful if you can tell whether it was verified or copied by hand; the citation makes that decidable without re-running the experiment. The presence-gated list was wrong: it claimed 7 knobs, the true number is 10. Three knobs were also wrongly labelled presence-gated; they are value-based and are now documented as such. All 31 `# TRY:` example values were checked against the code that parses them, so no example is rejected when copied. VERIFICATION Round-trip (j172 probe: printEnvReport clean vs .env.example applied through the repo's own env_dotenv loader, reports diffed): 0 mismatches, 0 warnings, 0 dropped keys (177 in file, 177 seen). Re-run after this commit's comment edit, unchanged. test_tfil_commit_env 159 PASS / 0 FAIL test_env_report 25 PASS / 0 FAIL test_tfil_ring_weights 24 PASS / 0 FAIL (earlier in the series) Also drops the stale "snapshot of commit 5e32ec1" pin from .env.example: a pinned hash goes stale the moment the next commit lands, which makes the "regenerate when a default changes" instruction worse than none. The line now just says the values mirror current defaults. --- ModularBot_garage/.env.example | 590 +++++++++++++++++++++++++++++---- docs/env_reference.md | 120 ++++++- 2 files changed, 646 insertions(+), 64 deletions(-) diff --git a/ModularBot_garage/.env.example b/ModularBot_garage/.env.example index fc88559..b08e337 100644 --- a/ModularBot_garage/.env.example +++ b/ModularBot_garage/.env.example @@ -10,19 +10,184 @@ # Every value below IS the built-in default, so running the bot with this file # is identical to a clean run with no file at all. Delete a line (or comment it # out with #) and that knob falls back to the built-in default. An inline -# `# comment` after a value is fine — the loader strips it. +# `# comment` after a value is fine — the loader strips it (a `#` that follows +# a space starts the comment; a `#` glued to the value, like `x#y`, is data). # -# A few switches are PRESENCE-only (`existsEnv`): for those, OFF means the line -# is absent, so they are shown commented out. Writing `=0` would still turn them -# ON. +# These values mirror the current defaults, not a frozen snapshot of one commit. +# Regenerate this file whenever a default changes, or it will start lying. + +# ═════════════════════════════════════════════════════════════════════════════ +# 1. ONE EXPERIMENT, END TO END +# ═════════════════════════════════════════════════════════════════════════════ # -# SNAPSHOT of the code at commit 5e32ec1. Regenerate this file whenever a default -# changes, or it will start lying. +# Pick ONE knob. Here the example is TR_TFIL_ARRIVE_TICKS, but the shape is the +# same for every knob in this file. +# +# # 1. write the arm. In ModularBot_garage/.env, change ONE line: +# # TR_TFIL_ARRIVE_TICKS=15.0 +# # A per-run file is better than editing .env, because it is how you +# # GUARANTEE the arm: whatever else is in .env or in your shell, this +# # file is the one that is applied. +# cat > /tmp/arm_arrive15.env <<'EOF' +# TR_MOVEMENT=tfil +# TR_TFIL_ARRIVE_TICKS=15.0 +# EOF +# +# # 2. RESTART THE BOT. Env is read ONCE, at boot (module init). Editing +# # .env while the bot runs changes nothing. There is no live reload. +# # (Two exceptions read lazily on first use: TR_PATTERN_RAD_* and a few +# # TR_TMHORIZON_* — do not rely on either.) +# cd ModularBot_garage && ./ModularBot.sh # or restart the GUI +# +# # 3. CONFIRM IT TOOK EFFECT, before you read a single result line. +# # `source: .env` = your file was applied. `source: default` = it was not. +# grep '^\[env\]' /tmp/modularbot_stdout.log | grep -E 'env file|ARRIVE_TICKS' +# # [env] env file: /tmp/arm_arrive15.env (source: TR_ENV_FILE) +# # [env] TR_TFIL_ARRIVE_TICKS = 15.0 (source: .env) +# # A value showing `(source: default)` means YOUR FILE NEVER REACHED THE BOT. +# +# # 4. point at the file instead of copying it into .env: +# TR_ENV_FILE=/tmp/arm_arrive15.env ./out/ModularBot +# ./out/ModularBot --env-file /tmp/arm_arrive15.env +# # This is what an A/B run does: one frozen binary, one env file per arm. +# # A file you ASKED for and that does not exist stops the bot with an error +# # (it never silently falls back); a missing default .env is silent. +# +# # 5. the module inventory, when you want to know what is on at all: +# grep '^\[modules\]' /tmp/modularbot_stdout.log +# +# ═════════════════════════════════════════════════════════════════════════════ +# 2. SAFE TO EXPERIMENT WITH RIGHT NOW +# ═════════════════════════════════════════════════════════════════════════════ +# +# The honest list is SHORT. After the recent campaign most experimental knobs +# are either never live-tested or already measured null/harmful, and this file +# says so on every one of them. These four are safe in the sense that they +# either cannot change a decision, or are the ones a measurement actually +# supports. +# +# TR_GEO_DEBUG=on Draw-only: the candidate-tile geometry overlay. +# Watch: the circle on the two tanks and each +# heading line. Good: you can SEE the tile the +# picker chose. Cannot change any decision. +# TR_VBULLET_DEBUG=1 Draw-only: each admitted gun's virtual bullets. +# TR_VBULLET_DEBUG_GUN=all +# Watch: travelled path, aim ring, miss vector. +# Good: you can see the signal the selector ranks +# on. Also draw-only. Needs a gun in the rack. +# TR_TFIL_DIAG=on Observability only, on the tfil mover. Fills the +# per-pick LOSS HISTOGRAM. Watch: the tfil pick log +# line. Good: the sReach/sCool/sSafe/sCand counts +# tell you where tiles are lost. Provably does not +# move a single command (guard-tested). +# TR_MOVEMENT=tfil The long-shipped mover, as an explicit override. +# Watch: nothing to compare against — it is the +# same engine you had before j119. Good: you are +# reproducing an older, documented behaviour. Only +# do this together with the tfil knobs below. +# +# Anything else on this list is a MEASUREMENT, not a free change: read its +# STATUS line before you type it. +# +# ═════════════════════════════════════════════════════════════════════════════ +# 3. ALREADY REJECTED OR MEASURED NULL — WITH THE NUMBER +# ═════════════════════════════════════════════════════════════════════════════ +# +# TR_TFIL_GEO_MODE=both-rej REJECTED live, 420 battles, 15-opponent panel. +# TR_TFIL_GEO_TAU=60 damage/run -8.83, p(sign-flip) = 0.0061, +# Wilcoxon p = 0.011. Round wins null. +# Docs: docs/tfil_geo_ab.md. DO NOT re-run it. +# TR_RAM_FLOOR_ENERGY=5 CLEAN NULL, 900 battles, 450 runs/arm. +# -0.018 wins/run, p(sign-flip) = 0.7676, under +# a 0.1420 wins/run MDE. Docs: +# docs/ram_floor_exhaustion_ab.md. DO NOT re-run +# it — the mechanism fired on 0.04% of ticks, so +# more runs buy resolution on an effect that is +# not there. +# TR_RAM_FLOOR_ENERGY=10/20 MEASURED COSTLY OFFLINE (24.7% of ticks blocked +# at 20) and the live zone they guard is almost +# empty: only 4.8% of shots are ever taken below +# 10 energy. Do not go above 5. +# TR_TMHORIZON_WINDOW=150 MEASURED HARMFUL live: 26.5% round wins vs 49.0% +# for the shipped rack, p = 0.036. Keep 0. +# TR_POWER_POLICY=0 MEASURED HARMFUL live: real hit rate 10.61% -> +# 7.88%, p = 0.0012. Keep it on. +# TR_TFIL_HEAT_TIME=1 MEASURED HARMFUL live at every tau tried +# (3/5/9/15): tau15 alone is -22 damage/run, +# p = 0.046. Keep it off. +# TR_MOVEMENT=tfil_ring MEASURED: round wins 16/49 -> 6/49, p = 0.012. A +# glass cannon — best live hit rate of anything +# measured, half the survival. Do not ship. +# TR_RACK_* (the full rack) MEASURED NEGATIVE VALUE: Pattern ALONE beats +# the full 13-gun rack, p = 0.0012. Adding guns +# costs rounds. +# TR_TFIL_TURN_BIAS=9 LIVE NULL: +0.15 wins/run, p(sign) = 0.244, under +# TR_TFIL_TURN_REF_DEG=0 a 0.30 MDE; 300 battles. Docs: +# docs/movement_campaign.md (j145). +# TR_RAM_OPPORTUNITY=on MEASURED not to convert: 0/59 +# opportunity -> contact. The finisher ram is the +# only path that converts, and it is always on. +# +# READ THIS BEFORE YOU TRUST ANY "null" ABOVE. A null only excludes an +# effect at or above the MDE that run resolved. The 15-opponent panel at +# 14 runs/arm resolves ~0.17 wins/run and ~7.65 damage/run; the j163 run +# resolved 0.1420 wins/run. So "clean null" here means "no effect >= that +# size", NOT "no effect". +# +# ═════════════════════════════════════════════════════════════════════════════ +# 4. PRESENCE-GATED KNOBS — FOR THESE, `NAME=0` TURNS THE FEATURE ON +# ═════════════════════════════════════════════════════════════════════════════ +# +# grep -rn 'existsEnv' ModularBot_garage/src common_libs | grep -v /tests/ +# +# The knobs below are read with `existsEnv`, not by value. OFF means THE LINE +# IS ABSENT. Writing `TR_POWER_LOG=0` does not disable the power log — it +# ENABLES it, because 0 is a perfectly good value for a knob nobody reads. +# That is why they are all shown COMMENTED OUT in this file: there is no +# "off" spelling for them, only absence. To disable one, DELETE its line. +# +# TR_POWER_LOG one line per power-decision CHANGE +# TR_RAM_LOG one line per ram start/stop, with the reason +# TR_MOVEMENT_LOG movement band / range-class changes +# TR_STRAFE_LOG one line per strafe tile pick +# TR_SURF_LOG one line per wave-surfing decision +# TR_FIRE_DIAG per-reading fire-detection tick/raw/correction +# TR_RECORD_WORLDSTATE dump every observed world state to JSONL +# TR_RADAR_SCANLOG log every radar scan tick +# TR_RADAR_FORCE_SPIN force the old full-360 spin radar +# TR_TRACKER_PROBE dump the enemy-tracker internals +# +# The VALUE-based switches are the opposite: 0 / false / no / off really +# disable them, and anything else enables them. Those are TR_RESULT_LOG, +# TR_TMHORIZON_LOG, TR_TMHORIZON_ACCURVE, TR_TMHORIZON_RESET_ON_TARGET, +# TR_LEADGAIN_LOG, TR_LEARNED_LOG, TR_LEARNED_GLOBAL, TR_LEARNED_REAL_EVENTS, +# TR_POWER_POLICY, TR_POWER_FINISH_KILL, TR_RAM_OPPORTUNITY, TR_RAM_PLAN, +# TR_FIRE_FIX, TR_STRAFE_FIRE_FIX, TR_STRAFE_ESCAPE, TR_STRAFE_HEAT_GRID, +# TR_TFIL_HEAT_TIME, TR_TFIL_PILLAR_ON, TR_TFIL_DIAG, TR_TFIL_NO_REV, +# TR_TFIL_HOLD_WHEN_TRAPPED, TR_TFIL_COMMIT_ARRIVAL, +# TR_TFIL_RING_COMMIT_ARRIVAL, GUN_SELECTOR_POOL, GUN_VBULLET_ADMIT_ONLY, +# TR_VBULLET_DEBUG, TR_GEO_DEBUG, TR_DEBUG_DRAW, TR_ENV_REPORT. +# (TR_TFIL_DIAG / _NO_REV / _HOLD_WHEN_TRAPPED / _COMMIT_ARRIVAL / +# _RING_COMMIT_ARRIVAL are read by value in the source; the boot report +# labels them by presence, which only affects the "(source: ...)" line, never +# the value.) # ── movement ───────────────────────────────────────────────────────────────── -# Which dodging engine runs: tfil, tfil_ring, strafe, surf or learned. +# Which dodging engine runs. VALUE: strafe (default) | tfil | tfil_ring | +# surf | learned. Any unrecognised value silently runs `tfil`, with no warning. +# WHAT: the engine that picks the dodge tile every tick. +# STATUS: DEFAULT = strafe, the measured champion — 300 fresh battles, +# +0.30 wins/run over tfil, 95% CI [+0.02, +0.58], sign-flip p = 0.045 +# (docs/movement_campaign.md, "Fresh-data confirmation (gate v2)"). +# tfil_ring is measured harmful (round wins 16/49 -> 6/49, p = 0.012). +# surf and learned are wired and measured, and neither beats strafe. +# GOTCHA: a typo does not warn. `TR_MOVEMENT=straf` runs tfil. +# TRY: TR_MOVEMENT=tfil -> the long-shipped engine; the `[env]` block then +# reads `move.effective = tfil`. Pairs with the TR_TFIL_* knobs below. TR_MOVEMENT=strafe -# Whole-engine on/off switches. 0 removes an engine from the TR_MOVEMENT choices. +# Whole-engine on/off switches. VALUE: on | 0/false/no/off. 0 removes an engine +# from the TR_MOVEMENT choices; the effective engine then falls back to the +# first still-enabled one, and tfil if all are off (there is no "no movement"). TR_MODULE_MOVE_TFIL=on # the long-shipped "floor is lava" engine TR_MODULE_MOVE_TFIL_RING=on # the same, re-weighted toward a target range TR_MODULE_MOVE_STRAFE=on # perpendicular strafe with sign-flip reversals @@ -30,7 +195,17 @@ TR_MODULE_MOVE_SURF=on # wave surfing, steered by the GuessFactor TR_MODULE_MOVE_LEARNED=on # learned per-state danger field # ── gun rack: which guns the bot may choose (off | 1v1 | melee | both) ─────── -# The shipped rack is Pattern only. Turn a gun on with `both`. +# WHAT: which guns the selector is allowed to fire. The shipped rack is +# PATTERN only; every other gun is `off`. +# VALUES: off | 1v1 | melee | both. Aliases: any/empty->both, single/lock->1v1, +# only1v1->1v1, multi/onlymelee->melee, none/disabled/disable->off. +# STATUS: MEASURED — the full 13-gun rack is WORSE than Pattern alone, +# p = 0.0012. Do not re-enable guns to "improve" the bot. +# GOTCHA: an UNRECOGNISED value warns on stderr and falls back to `both`, i.e. +# a typo ADDS the gun back into the rack. It never turns one off. +# TRY: TR_RACK_PATTERN=off -> nothing admitted in 1v1; the selector falls +# back to the full rack, so do not ship this. Real use is a PAIR: +# TR_RACK_PATTERN=off + TR_RACK_HEADON=both -> exactly one gun fires. TR_RACK_PATTERN=both # the only admitted gun; both = usable in 1v1 and melee TR_RACK_HEADON=off # aim straight at the target, no lead TR_RACK_LINEAR=off # constant-angle linear aim @@ -48,16 +223,24 @@ TR_RACK_TMSELECT=off # Tsetlin machine used as the shot selector TR_RACK_TMPATTERN=off # Tsetlin machine used as a pattern matcher TR_RACK_TMHORIZON=off # horizon Tsetlin automata gun TR_RACK_LEADGAIN=off # per-range-band learned lead-gain corrector -# Give every admitted gun a fixed share of the turns instead of ranking them, -# e.g. TR_RACK_SHARE=PATTERN:60%,HEADON:40%. Empty = the ranking selector. +# Give every named gun a fixed share of the turns instead of ranking them. +# e.g. TR_RACK_SHARE=PATTERN:60%,HEADON:40% (GUN:weight, comma separated, +# the % sign is optional). Empty = the ranking selector. GOTCHA: every gun you +# name must ALSO be admitted by its own TR_RACK_ line, or the share is +# refused with a loud `[gun_harness] ERROR` and the ranking selector is used +# instead. In the shipped rack that means PATTERN and nothing else. TR_RACK_SHARE= # Drop whole guns by rack id (comma separated, e.g. 16). Empty = keep them all. +# Ids: 0 HEADON 1 LINEAR 2 TSETLIN 3 CIRCULAR 4 GUESSFACTOR 5 PATTERN +# 6 WALLBOUNCE 7 ACCEL 8 STOPSHOT 9 DISPLACE 10 AVGLEAD 11 DECAYGF 12 KNN +# 13 TMSELECT 14 TMPATTERN 15 TMHORIZON 16 LEADGAIN. A disabled gun never even +# spawns a virtual bullet, so its fitness stays empty and it cannot be picked. GUN_RACK_DISABLE= # ── gun selector (which admitted gun fires this tick) ─────────────────────── GUN_VBULLET_METRIC=path # fitness measure: path (time-to-collision) or point GUN_SELECTOR_MODE=relative # rank guns against the incumbent (absolute = vs a fixed bar) -GUN_SELECTOR_WINDOW=100 # ticks of virtual-bullet history behind the fitness +GUN_SELECTOR_WINDOW=100 # ticks of virtual-bullet history behind the fitness (clamped 1..100) GUN_SELECTOR_MINOBS=50 # observations a gun needs before it may compete GUN_SELECTOR_TIE=0.2 # relative margin two guns must differ by to count as separated GUN_SELECTOR_FLOOR=0.25 # fitness fraction of the peak below which a band is unsafe @@ -73,6 +256,8 @@ GUN_SELECTOR_SEED= # ── power / energy policy ──────────────────────────────────────────────────── # Every rule below only ever CAPS power; the gun's own preference is the ceiling. +# The measured case for keeping it on: TR_POWER_POLICY=0 drops the real hit rate +# from 10.61% to 7.88%, p = 0.0012. TR_POWER_POLICY=on # 0 = no cap at all (the control arm) TR_POWER_FAR_DIST=200.0 # px; past this the enemy is in the bad-chances zone TR_POWER_FAR_CAP=1.0 # cap applied past TR_POWER_FAR_DIST @@ -85,7 +270,7 @@ TR_POWER_ENERGY_MAX=3.0 # the cap at/above ENERGY_HI; 3.0 means effectively unc TR_POWER_FINISH_KILL=on # cap to the smallest bullet that still kills a low-energy enemy # ── radar ──────────────────────────────────────────────────────────────────── -#TR_RADAR_FORCE_SPIN=1 # presence-only: force the old full 360 spin instead of 1v1 lock +#TR_RADAR_FORCE_SPIN=1 # PRESENCE-only: force the old full 360 spin instead of 1v1 lock TR_RADAR_SCAN_LOG_PATH=/tmp/radar_scan_log.jsonl # where the per-tick scan log is written # ── ram ────────────────────────────────────────────────────────────────────── @@ -98,42 +283,274 @@ TR_RAM_PLAN=off # the change-of-plan trigger (enemy outguns us while TR_RAM_PLAN_DIST=250.0 # px; max range at which the plan trigger may fire TR_RAM_PLAN_MARGIN=20.0 # energy advantage the plan trigger needs TR_RAM_PLAN_HITRATE=0.05 # pooled virtual hit rate below which the gun duel counts as failing -TR_RAM_FLOOR_ENERGY=0.0 # j160 firing floor: at/below this self energy stop firing (0 = off) -TR_RAM_ENEMY_ENERGY=0.0 # j160 exhaustion: last-scanned enemy energy <= this -> ram (0 = off) +# WHAT: at or below this much SELF energy we start no new shot, keeping a +# reserve for the ram. Units: energy points. +# VALUES: energy, 0.0 = off (today's behaviour). Any float parses. +# STATUS: =5 is a CLEAN NULL and must not be re-run: -0.018 wins/run, +# p(sign-flip) = 0.7676, under a 0.1420 wins/run MDE, 900 battles. The +# mechanism fired on 0.04% of ticks, ~200x less than the offline ruler +# predicted. Docs: docs/ram_floor_exhaustion_ab.md. +# GOTCHA: 0.0 genuinely disables it, but any POSITIVE value arms it, and the +# higher it goes the more of the low-energy zone it blocks (20 blocked 24.7% +# of all ticks offline). +# TRY: TR_RAM_FLOOR_ENERGY=5 -> already measured, do not re-run. If you +# want to see it at all, that is the only defensible value; anything +# higher is worse by the offline ruler and by the live null. +TR_RAM_FLOOR_ENERGY=0.0 +# WHAT: the last-scanned enemy energy at or below this switches the ram decider +# into exhaustion mode (they are out of ammo, we are not). Units: energy. +# VALUES: energy, 0.0 = off. Any float parses. +# STATUS: DEFAULT-OFF, never live-tested. It is the second half of the j160 pair; +# the other half (FLOOR_ENERGY) measured null, so the pair is not a win. +# GOTCHA: 0.0 is the only safe "off". The always-on finisher (enemy < 20 energy +# and we are healthier, within 300 px) already covers most of this; setting +# this to 20 makes the two overlap. +# TRY: TR_RAM_ENEMY_ENERGY=10 -> ram once a scan shows them at <= 10. +# Watch: the `[ram] ON rrExhausted` line. Good: the reason field says +# `exhausted` rather than `finisher`. Needs TR_RAM_LOG=1 to see it. +TR_RAM_ENEMY_ENERGY=0.0 # ── movement internals: tfil (the floor-is-lava field) ────────────────────── TR_TFIL_RANGE_LO=100.0 # px; lower edge of the range band the ring mover prefers TR_TFIL_RANGE_HI=200.0 # px; upper edge of that band TR_TFIL_RANGE_TEMP=0.4 # sharpness of the ring mover's weighted random draw TR_TFIL_RANGE_K=60.0 # px; how fast the weight falls off outside the band -TR_TFIL_RING_COMMIT_ARRIVAL=off # tfil_ring only: on = hold the dodge tile until we are ON it (not a fixed dwell) -TR_TFIL_RING_NOREV_SPEED=0.0 # tfil_ring only; px/tick; below this, a mid-flight switch may not turn the bot around +# WHAT (tfil_ring only): hold the committed dodge tile until we are actually ON +# it, instead of the fixed dwell. VALUE: on | 0/off. +# STATUS: DEFAULT-OFF, never live-tested. Ported to the ring fork in j165; the +# tfil original (TR_TFIL_COMMIT_ARRIVAL) has a real but under-powered live +# result — see that line. Neither is a proven win. +# GOTCHA: the ring mover is NOT the default and is not shippable (round wins +# 16/49 -> 6/49, p = 0.012), so this only matters while you are measuring it. +# TRY: TR_MOVEMENT=tfil_ring + TR_TFIL_RING_COMMIT_ARRIVAL=1 +# -> the `[env]` block reads TR_TFIL_RING_COMMIT_ARRIVAL = on. +TR_TFIL_RING_COMMIT_ARRIVAL=off +# WHAT (tfil_ring only): below this self speed (px/tick), a mid-flight switch +# may not turn the bot around. UNITS: px/tick (top speed is 8). +# STATUS: DEFAULT-OFF (0.0), never live-tested. The tfil original is in the +# j144 recommendation below. +# TRY: TR_TFIL_RING_NOREV_SPEED=4 -> 4 px/tick is half of top speed, the +# value j144 used on tfil. Accepts any float >= 0. +TR_TFIL_RING_NOREV_SPEED=0.0 TR_TFIL_CORRIDOR_HEAT=10.0 # lava painted per corridor-overlapping tile -TR_TFIL_CORRIDOR_TICKS=0.0 # corridor length in ticks: 0 = to the wall (shipped); N>0 = min(to wall, bullet speed * N) +# WHAT: cap the bullet-danger corridor at `bullet speed x this many ticks`, +# instead of running it all the way to the arena wall. UNITS: ticks. +# VALUES: ticks, 0.0 = off (corridor reaches the wall, today's behaviour). +# Any float parses; negatives are treated as 0. +# STATUS: DEFAULT-OFF, never live-tested, and NOT recommended. Commit 5e213df +# (j148) shipped it with no measurement at all: no battle, no offline ruler. +# The neighbouring j146 field-shape sweep, which is the closest evidence, +# says halving the corridor restores a safe tile set offline (filter-broken +# 63.5% -> 30.4%) and is a LIVE NULL on outcome. +# GOTCHA: value knob - 0.0 genuinely disables it. It is NOT a presence knob. +# TRY: TR_TFIL_CORRIDOR_TICKS=20 -> a 20-tick look-ahead. Bullet speed is +# 20 - 3*power, so over the shipped power bins 1.0..3.0 that is +# 17.0..11.0 px/tick = 340..220 px of corridor, instead of the whole +# wall. Watch: in the debug overlay the corridor stops short of the +# wall. Only bind it to tfil; strafe has its own knob below. +TR_TFIL_CORRIDOR_TICKS=0.0 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_ARRIVE_TICKS=0.0 # hard bound: never pick a tile farther than this (ticks at 8px/tick); 0 = off = today's draw -TR_TFIL_HOLD_WHEN_TRAPPED=off # on = when NO safe tile exists, hold position one tick instead of taking the 2 least-hot blocked tiles; off = today's fallback -TR_TFIL_HOLD_MAX_TICKS=0 # BOUNDED version of the above: hold at most N ticks per empty-safe-set streak (default 0 = off = today's fallback). The budget is DERIVED from the enemy's rate of fire (2 x 3.0-power shots = 16 ticks); the hold is released the tick a safe tile exists and overridden outright if a tracked bullet reaches us within min(N, 16) ticks -TR_TFIL_DANGER_THRESHOLD=10.0 # hard heat filter on the path; lava is quantised to 5, so the effective steps are 10/15/20 and 10-14 admits exactly what 10 does -TR_TFIL_DIAG=off # on = fill the per-pick picker loss histogram (TfilLoss*); observability only -TR_TFIL_GEO_MODE=off # off | turn | dist | both, each with a `-soft` (default) / `-topk` / `-rej` suffix; shapes the DRAW over the heat-filtered pool -TR_TFIL_GEO_TAU=0.0 # deg; the geometric cost scale. 0 = off = today's uniform draw -TR_TFIL_NO_REV=off # on = never reverse direction inside a corridor +# WHAT: how hot a bullet paints the tile it is sitting on. UNITS: lava points +# on the picker's heat scale. +# VALUES: any float >= 0. 10.0 is exactly the danger threshold, so a bullet is +# never dangerous on its own; 20.0 puts one bullet's own tile over it. +# STATUS: live-tested ONLY as part of the j146 five-shape batch, 375 battles: +# core 10 -> 20 was a live NULL on both primaries, and the arm that combined +# it with no corridor/wall field LOST 13.45 damage/run, p(sign-flip) = +# 0.0095. The middle shape (corridor 10 / wall 15/5 / core 20 / aura 10) is +# also a null. Do not retune the shape on one axis. +# GOTCHA: changing CORE alone with the shipped corridor (10) and wall (15) is +# the one combination j146 did NOT isolate. +# TRY: TR_TFIL_BULLET_CORE=20 + TR_TFIL_BULLET_AURA=10 -> the j146 +# "middle" bullet heat. Only meaningful as part of the whole middle +# shape; on its own it is a null at best. +TR_TFIL_BULLET_CORE=10.0 +# WHAT: the heat painted on the bullet's AURA ring (the tiles around it), as +# opposed to the core tile. UNITS: lava points. +# VALUES: any float >= 0; 5.0 is half the core's 10.0. +# STATUS: live-tested only inside the j146 batch — null, see BULLET_CORE. +# TRY: TR_TFIL_BULLET_AURA=10 -> doubles the aura heat; pairs with +# TR_TFIL_BULLET_CORE=20 in the j146 "middle" shape. +TR_TFIL_BULLET_AURA=5.0 +# WHAT: when a dodge commitment is cancelled, replan from whose state? +# self = today's behaviour. VALUE: self | enemy | off. +# STATUS: `self` is the shipped default; `off` is the pre-j144 behaviour and is +# exactly what an earlier A/B (cc11ede arm D) tried and could not measure. +# TRY: TR_TFIL_TILE_REPLAN=off -> the tile-crossing cancel stops firing. +# Accepts self, enemy, off, none, never, 0, false (case-insensitive); +# anything else falls back to self with no warning. +TR_TFIL_TILE_REPLAN=self +TR_TFIL_COMMIT_TICKS=15 # ticks to commit to a dodge point before replanning (min 1) +# WHAT: hold the committed dodge tile until we are actually ON it, instead of +# letting our own tile-boundary crossing cancel it. VALUE: on | 0/off. +# STATUS: LIVE, 600 battles in two blocks (j144). Mechanism is real and +# confirmed: incoming hit rate 18.07% -> 14.92%, sign-flip p = 0.0013, damage +# taken -24.57/run. Outcome is NOT distinguishable: +0.28 wins/run, +# p(sign) = 0.0574 against a 0.31 MDE. The ledger's answer for your own .env +# is YES to this knob, and NO to COMMIT_MARGIN. +# GOTCHA: only meaningful with TR_MOVEMENT=tfil; the default strafe mover has +# no tfil commitment. With TR_TFIL_COMMIT_ARRIVAL on, COMMIT_TICKS becomes a +# MINIMUM dwell, not a maximum. +# TRY: TR_MOVEMENT=tfil + TR_TFIL_COMMIT_ARRIVAL=1 +# -> `[env] TR_TFIL_COMMIT_ARRIVAL = on (source: .env)`. +TR_TFIL_COMMIT_ARRIVAL=off +# WHAT: leave the committed tile only if the best alternative is at least this +# much COOLER on the same pathMaxHeat scale the picker uses. UNITS: lava +# points; 10.0 is exactly one PathDangerThreshold level. +# VALUES: any float >= 0; 0.0 = off = today's behaviour (any improvement ends +# the commitment). +# STATUS: LIVE, j144, 600 battles, as the `arrive_hyst` arm. It is the WEAKEST +# of the three j144 arms on both primaries (+0.19 wins/run, p = 0.092) and +# the ledger's explicit answer is: YES to COMMIT_ARRIVAL, NO to this. +# GOTCHA: it is a hysteresis, so it makes the bot commit harder; a large value +# with a busy field means it holds a tile that is no longer the best one. +# TRY: TR_TFIL_COMMIT_MARGIN=10 -> the j144 value. Already measured, and +# the answer was no. Use it only to isolate COMMIT_ARRIVAL, not as an +# improvement. +TR_TFIL_COMMIT_MARGIN=0.0 +# WHAT: refuse a candidate tile we cannot REACH inside this many ticks. The +# bot's top speed is 8 px/tick, so 1 tick = 8 px. UNITS: ticks. +# VALUES: ticks, 0.0 = off = today's uniform draw over every safe tile. +# STATUS: DEFAULT-OFF, never live-tested. Found by the j151 offline ruler: 65% +# of tfil picks outran the 15-tick commitment and the chosen tile was reached +# only 6.5% of the time. +# GOTCHA: it is a HARD bound, not a preference. If every safe tile is out of +# range the pool empties and the code falls back to today's full pool, so it +# can never starve the draw — it can also silently do nothing. +# TRY: TR_TFIL_ARRIVE_TICKS=15 -> refuse anything farther than 15*8 = 120 +# px. That is deliberately the same length as TR_TFIL_COMMIT_TICKS, +# i.e. "only pick a tile you can still reach while you hold it". +TR_TFIL_ARRIVE_TICKS=0.0 +# WHAT: when the safe-tile set is EMPTY (no tile under the heat threshold), +# hold position for one tick instead of promoting the 2 least-hot blocked +# tiles. VALUE: on | 0/off. +# STATUS: DEFAULT-OFF, never live-tested. j153 wrote the proposal and the A/B +# design; it was NOT run (docs/tfil_hold_when_trapped_ab.md is titled +# "NOT RUN"). Four mechanism-positive / outcome-null results preceded it, so +# a null was always the likely answer. +# GOTCHA: it can never latch — the pick site only runs on a replan tick, so the +# next tick re-reads the field from scratch. The gun is untouched: a held +# tick still fires exactly like every other tick. +# TRY: TR_TFIL_HOLD_WHEN_TRAPPED=on -> the tfil pick log shows `hold` +# instead of `promote` on a trapped tick. Needs TR_TFIL_COMMIT_LOG set. +TR_TFIL_HOLD_WHEN_TRAPPED=off +# WHAT: the BOUNDED version of the knob above: while the safe set stays empty, +# hold for at most this many ticks per empty streak. UNITS: ticks (integer). +# VALUES: integer >= 0; 0 = off = today's promote-the-2 fallback. +# STATUS: DEFAULT-OFF, never live-tested (j154, no battle). +# GOTCHA: the budget is DERIVED, not guessed: the enemy fires two 3.0-power +# shots 16 ticks apart, so 16 is the first window that admits its second +# shot. The hold is released the tick a safe tile exists, the counter resets +# when one is taken, and a tracked bullet reaching us within min(N,16) ticks +# overrides the hold outright (panic release). +# TRY: TR_TFIL_HOLD_MAX_TICKS=16 -> the derived budget. Any integer parses; +# a junk value degrades to 0 (off). +TR_TFIL_HOLD_MAX_TICKS=0 +# WHAT: the hard heat filter on a candidate's path. UNITS: lava points. +# VALUES: any float >= 0. Lava is QUANTISED to 5, so the only values that +# change anything are 10, 15 and 20: 10-14 admits exactly what 10 admits. +# STATUS: 10.0 is the shipped const; j150 made it sweepable so the offline +# ruler could move it. Never live-tested as a knob. +# GOTCHA: raising it does not make the bot braver, it makes the safe set +# smaller and more of the picks forced. j146's `nofield` arm is the warning: +# -13.45 damage/run, p = 0.0095. +# TRY: TR_TFIL_DANGER_THRESHOLD=15 -> one quantisation step stricter. The +# picker log then reports fewer safe candidates per pick. +TR_TFIL_DANGER_THRESHOLD=10.0 +# WHAT: fill the per-pick LOSS HISTOGRAM (TfilLoss*): how many tiles die at +# each picker stage. VALUE: on | 0/off (read by value, not by presence). +# STATUS: DIAGNOSTIC ONLY, j150. Provably does not move a single move command +# (byte-for-byte, guard-tested). Never measured on outcome, by design. +# GOTCHA: nothing. It is the safest tfil knob in this file. +# TRY: TR_TFIL_DIAG=on -> the tfil pick log gains the per-stage counts. +TR_TFIL_DIAG=off +# WHAT: shape the tile DRAW over the heat-filtered pool by geometry (how far +# the tile sits from where we are already going) as well as by heat. +# VALUES: off | turn | dist | both, each optionally suffixed -soft (default), +# -topk or -rej. `distance`=dist, `rejection`=rej are also accepted. +# Anything unrecognised, and `off`, means OFF — today's uniform draw. +# The form is NOT printed by the boot report, only the dim. +# STATUS: DEFAULT-OFF. THE ONE LIVE-TESTED ARM IS `both-rej` + TAU=60 AND IT +# WAS REJECTED: -8.83 damage/run, p(sign-flip) = 0.0061, Wilcoxon p = 0.011, +# 420 battles, 15 opponents. Round wins null. Offline it did exactly what was +# predicted (arrivals 4.5% -> 29.4%) and that is WHY it is bad: the bot ends +# up 26 px further out on 15/15 opponents and deals less. See +# docs/tfil_geo_ab.md. DO NOT re-run both-rej. +# GOTCHA: this knob ALONE is inert — TR_TFIL_GEO_TAU=0.0 means the weighting is +# off whatever the mode says. And it needs TR_MOVEMENT=tfil. +# TRY: TR_TFIL_GEO_MODE=both-soft + TR_TFIL_GEO_TAU=45 +# -> the j152 offline headline (arrivals 4.5% -> 16.9%, top-tile share +# only 7.0% -> 8.6%). Still never live-tested, and the family has one +# measured loss, so treat it as a hypothesis. +TR_TFIL_GEO_MODE=off +# WHAT: the geometric cost scale, in degrees. 0.0 = off, i.e. exactly today's +# uniform draw. UNITS: degrees. VALUES: any float >= 0. +# STATUS: never live-tested with a mode other than off. The one live arm used +# TAU=60 and lost (see GEO_MODE). +# GOTCHA: TAU is IGNORED unless GEO_MODE is not off. A big TAU with mode=off +# looks like it is doing something and is not. +# TRY: TR_TFIL_GEO_TAU=45 -> 45 degrees of turn cost. Pairs with +# TR_TFIL_GEO_MODE=both-soft; on its own it changes nothing. +TR_TFIL_GEO_TAU=0.0 +# WHAT: inside a corridor, never pick a tile in the reverse direction. VALUE: +# on | 0/false/no/off (read by value, so `=off` really disables it). +# STATUS: never live-tested as a standalone arm. The j144/j145 arms relied on +# the NOREV_SPEED knob instead, which is stricter. +# GOTCHA: soft only — every weight is floored, so the pool can never empty; and +# it overlaps TR_TFIL_NOREV_SPEED, which is the knob that was actually run. +# TRY: TR_TFIL_NO_REV=on -> a 3:1 forward:rearward draw weight inside a +# corridor. Accepts on/1/true/yes to enable, 0/false/no/off to disable. +TR_TFIL_NO_REV=off TR_TFIL_COMMIT_LOG= # path for the per-commit log; empty = no log -TR_TFIL_COMMIT_ARRIVAL=off # on = hold the dodge tile until we are ON it (not a fixed dwell) -TR_TFIL_COMMIT_MARGIN=0.0 # lava an alternative tile must be cooler by before it wins the tile -TR_TFIL_NOREV_SPEED=0.0 # px/tick; below this, a mid-flight switch may not turn the bot around -TR_TFIL_TURN_BIAS=0.0 # turn TIEBREAK odds ratio among SAFE tiles; 0 = uniform draw as today -TR_TFIL_TURN_REF_DEG=45.0 # deg; turn below which the tiebreak applies no penalty +# WHAT: while our own speed is below this, a mid-flight target switch may not +# take a tile more than 90 degrees off the travel direction. UNITS: px/tick +# (top speed 8). VALUES: any float >= 0; 0.0 = off = shipped. +# STATUS: LIVE, j144/j145, 600 battles. Mechanism confirmed: slow opposite-way +# mid-flight switches fell 394 -> 64 offline. Outcome not distinguishable on +# its own (+0.12 wins/run, p = 0.39); the ledger recommends 4 in your .env +# together with COMMIT_ARRIVAL, not alone. +# GOTCHA: the pool can never be emptied — with every candidate behind us it +# takes the least-bad turn. 0.0 genuinely disables it. +# TRY: TR_TFIL_NOREV_SPEED=4 -> 4 px/tick = half of top speed, the j144 +# value. Watch: fewer >90 deg switches in the tfil pick log. +TR_TFIL_NOREV_SPEED=0.0 +# WHAT: turn TIEBREAK odds ratio among the SAFE tiles: a straight-ahead safe +# tile is drawn `1 + bias` times as often as a 180-degree one. +# w = max(1, round(1 + BIAS * (1 - max(0,|turn| - REF)/180))) +# UNITS: dimensionless. VALUES: any float >= 0; 0.0 = uniform draw as today. +# STATUS: LIVE NULL, j145, 300 battles: +0.15 wins/run, p(sign) = 0.244, under +# a 0.30 MDE. Docs: docs/movement_campaign.md. A real but small mechanism +# with an under-powered outcome. +# GOTCHA: the turn cost is NEVER folded into the heat score — the filter stays +# hard, and every weight is floored at 1, so the pool can never empty. +# TRY: TR_TFIL_TURN_BIAS=9 + TR_TFIL_TURN_REF_DEG=0 +# -> the j145 arm value (the knee of the offline bias curve: mean +# |turn| -11%, opposite picks -22%). Already measured null. +TR_TFIL_TURN_BIAS=0.0 +# WHAT: the turn below which the tiebreak above applies NO penalty. UNITS: +# degrees. VALUES: any float >= 0; 45.0 = today's default. +# STATUS: inert unless TR_TFIL_TURN_BIAS > 0. j145 used 0 with bias 9. +# GOTCHA: on its own this knob does nothing at all. +# TRY: TR_TFIL_TURN_REF_DEG=0 -> penalise every turn, not just the sharp +# ones. Pairs with TR_TFIL_TURN_BIAS=9; alone it is a no-op. +TR_TFIL_TURN_REF_DEG=45.0 +# WHAT: paint heat on the virtual centre pillar. VALUE: on | 0/off. The shipped +# field has NO pillar: PillarHotness/PillarRadiance are 0.0. +# STATUS: live-tested, and the OWNER OVERRULED the recommendation to turn it +# back on: `old` (pillar on) was best on damage/run (287) and round wins +# (35/70) but the contrast is INSIDE the MDE (33 damage/run, 1.22 wins/run +# at n=10) and damage taken was 30.8/run higher with the pillar off +# (p = 0.040, not corrected for multiple arms). Decision: pillar stays +# removed. Docs: docs/tfil_heat_pillar_ab.md. +# GOTCHA: this restores an INVENTED hazard with no physical object behind it. +# Reversing that decision needs its own pre-registered A/B. +# TRY: TR_TFIL_PILLAR_ON=1 -> the pre-change field (30/10), for a fair +# A/B against the shipped one. Needs TR_MOVEMENT=tfil. +TR_TFIL_PILLAR_ON=off TR_TFIL_HEAT_TIME=off # on = index bullet heat by time (flat field when off) TR_TFIL_HEAT_TAU=9.0 # ticks a tracked bullet's heat lives for TR_TFIL_HEAT_POWER_GAIN=1.0 # scale of the heat a bullet paints, per firepower -TR_TFIL_PILLAR_ON=off # on = paint heat on the arena centre, which has no pillar # ── movement internals: strafe ─────────────────────────────────────────────── TR_STRAFE_BAND=20.0 # degrees the heading may sit off the perpendicular @@ -152,14 +569,42 @@ TR_STRAFE_WALL_BIAS=0.35 # how strongly a tile farther from the wall is preferr TR_STRAFE_WALL_SAFE=24.0 # px; a wing point never lands nearer than this to a wall TR_STRAFE_ESCAPE=on # the guaranteed wall escape when every candidate is hot TR_STRAFE_FIRE_FIX=on # strafe's share of the shared TR_FIRE_FIX switch -TR_FIRE_FIX=on # 0 = the shipped previous-energy bullet detector -#TR_FIRE_DIAG=1 # presence-only: per-reading tick/raw/correction trace -#TR_FIRE_LAG=0 # ticks to back-date each detected fire at spawn (0=shipped; 1=the measured live detection lag) +# WHAT: the shared enemy-fire detector. VALUE: on | 0/off. One switch, read by +# every mover; each mover may AND it with its own (TR_STRAFE_FIRE_FIX). +# STATUS: j134 propagated it to all five movers; the per-mover catch table went +# 98.888% -> 100% of enemy fires on a 70-battle corpus. +# GOTCHA: TR_STRAFE_FIRE_FIX is an AND, so turning TR_FIRE_FIX off is enough; +# turning only TR_STRAFE_FIRE_FIX off does not disable strafe's detector. +# TRY: TR_FIRE_FIX=0 -> the shipped previous-energy detector everywhere +# (the control arm for any fire-detector A/B). +TR_FIRE_FIX=on +#TR_FIRE_DIAG=1 # PRESENCE-only: per-reading tick/raw/correction trace +# WHAT: back-date every detected enemy fire by this many ticks when the ghost +# is spawned. UNITS: ticks (integer, clamped at 0). 0 = shipped. +# STATUS: LIVE, j147, 180 battles. The 1-tick detection lag is OURS and was +# measured on 1777 matched ghost spawns (displacement 19.1 px mean on tfil, +# 16.1 on strafe; the arrival deadline the mover reads was 0.99 / 0.77 ticks +# late). With =1 it falls to 5.4 / 9.0 px and 0.06 ticks. The live OUTCOME is +# null on both movers (+2.08 / +3.77 damage/run, every p > 0.6), so it stays 0. +# GOTCHA: a junk or negative value degrades to 0, never a negative back-date. +# TRY: TR_FIRE_LAG=1 -> one bullet step at power 1.0 is 17 px; the ghost is +# born that far downrange. Watch the tfil/strafe pick log's arrival +# deadline, which is what the knob actually corrects. +TR_FIRE_LAG=0 TR_STRAFE_HEAT_GRID=on # draw the whole heat grid; 0 leaves only the chosen tile TR_STRAFE_BULLET_CORE=20.0 # strafe's own retune: lava per bullet-overlapping tile TR_STRAFE_BULLET_AURA=10.0 # strafe's own retune: lava for the bullet aura ring TR_STRAFE_CORRIDOR_HEAT=10.0 # strafe's own retune: lava per corridor tile -TR_STRAFE_CORRIDOR_TICKS=0.0 # same length bound for strafe: 0 = to the wall (shipped) +# WHAT: the same corridor LENGTH bound as TR_TFIL_CORRIDOR_TICKS, for the +# strafe mover. UNITS: ticks. VALUES: ticks, 0.0 = to the wall (shipped). +# STATUS: DEFAULT-OFF, never live-tested (commit 5e213df, j148). Same standing +# as the tfil one: no battle, no offline ruler, not recommended. +# GOTCHA: this is the DEFAULT engine's knob. Setting only the tfil one does +# nothing at all, because the shipped movement is strafe. +# TRY: TR_STRAFE_CORRIDOR_TICKS=20 -> 20-tick look-ahead, 220..340 px over +# the shipped power bins, instead of the whole wall. Watch: the strafe +# heat grid's corridor stops before the wall. +TR_STRAFE_CORRIDOR_TICKS=0.0 TR_STRAFE_WALL_HOTNESS=15.0 # strafe's own retune: peak wall heat TR_STRAFE_WALL_RADIANCE=5.0 # strafe's own retune: wall heat falloff @@ -168,7 +613,7 @@ TR_SURF_PREF_DIST=400.0 # px; the wave distance the mover tries to sit at TR_SURF_DIST_BAND=50.0 # px dead band around it TR_SURF_WALL_MARGIN=48.0 # px kept from the wall when picking a wave point TR_SURF_RADIAL_FRAC=0.35 # how much of the remaining weight goes to the radial blend -#TR_SURF_LOG=1 # presence-only: one line per wave-surfing decision +#TR_SURF_LOG=1 # PRESENCE-only: one line per wave-surfing decision # ── movement internals: learned (per-state learned danger) ────────────────── TR_LEARNED_DECAY_EVERY=128 # learns between forgetting passes; 0 never forgets @@ -183,18 +628,20 @@ TR_LEARNED_WALL_MARGIN=48.0 # px kept from the wall TR_LEARNED_GLOBAL=off # on = ignore the learned state (ablation arm) TR_LEARNED_LABEL=histogram # histogram (default) or outcome: what a wave is labelled with TR_LEARNED_REAL_EVENTS=off # on = resolve a wave on the real bullet event, not on energy -#TR_LEARNED_LOG=1 # presence-only: one line per learned decision +#TR_LEARNED_LOG=1 # VALUE-based (1/on/yes to enable, 0/off to disable) # ── guns ───────────────────────────────────────────────────────────────────── # Virtual bullets: the prediction the whole gun selector is built on. TR_MODULE_VBULLETS=on # 0 = no gun predicts or spawns; the selector falls back to its floor gun -# TMH — the horizon Tsetlin automata gun. +# TMH — the horizon Tsetlin automata gun. Inert unless TR_RACK_TMHORIZON=both. TR_TMHORIZON_SHIFT=2.0 # degrees added to the aim; 0 disables the correction arm TR_TMHORIZON_BIG_MULT=1.5 # extra scale applied when the error magnitude is big TR_TMHORIZON_RESET_ON_TARGET=on # wipe the automata when the target changes TR_TMHORIZON_NSTATES=64 # automata state count (the inertia it can hold) TR_TMHORIZON_WINDOW=0 # samples kept in the sliding window; 0 keeps everything +# e.g. TR_TMHORIZON_WINDOW=30 -> retrain on the 30 most recent samples only. +# MEASURED HARMFUL LIVE at 150: 26.5% round wins vs 49.0%, p = 0.036. Keep 0. TR_TMHORIZON_RESET_DROP=0.0 # rolling accuracy drop, in points, that forces a retrain TR_TMHORIZON_ACCURVE=off # log the accuracy curve even without the thinking log TR_TMHORIZON_RETRAIN_EVERY=50 # samples between full retrains in sliding mode @@ -207,7 +654,7 @@ TR_LEADGAIN_MIN_OBS=8 # samples a band needs before its gain is trusted TR_LEADGAIN_DECAY=250 # samples between count-decay passes TR_LEADGAIN_DECAY_FRAC=0.02 # fraction each decay pass takes off every count TR_LEADGAIN_RESET_ON_TARGET=on # wipe the learned gains when the target changes -TR_LEADGAIN_LOG=off # on = one line per gain change +TR_LEADGAIN_LOG=off # on = one line per gain change (value-based: 0 disables) # Kept only so a pre-rename .env does not warn. The gun does not read them. TR_LEADGAIN_N=32 # NO-OP: the old SBC geometry, no longer used TR_LEADGAIN_NADE=256 # NO-OP: the old ADE count, no longer used @@ -221,6 +668,10 @@ TR_LEADGAIN_SEED=20240921 # NO-OP: the old seed, no longer used TR_PATTERN_LEN=10 # ticks of movement history used as the search key TR_PATTERN_DEPTH=500 # how far back the history scan may reach TR_PATTERN_RAD_OFFSET=0.0 # px added to the aim distance; negative aims short +# Both RAD_* knobs are read LAZILY, inside predict() — the one place a +# mid-run change can matter, and it still does not. The live aim is bearing +# only, so a purely radial offset is structurally invisible: MEASURED +# byte-identical on bmPath. Docs: docs/env_reference.md §7. TR_PATTERN_RAD_SCALE=1.0 # multiplier on the whole aim distance # The SBC library (common_libs/bitbrain), not a gun knob. Registered so a config @@ -250,32 +701,53 @@ TR_BITBRAIN_DECAY_SHIFT=1 # forgetting strength, counted mode only; 0 disa #TR_BITBRAIN_RESET_ON_TARGET=on # LEGACY: old name of TR_LEADGAIN_RESET_ON_TARGET # ── debug overlays (all on top of the gun; they never change a decision) ───── -TR_DEBUG_DRAW=on # master switch for every mover's debugGraphics -TR_GEO_DEBUG=off # draw the shared candidate-tile geometry overlay -TR_VBULLET_DEBUG=off # draw each admitted gun's virtual-bullet paths -TR_VBULLET_DEBUG_GUN= # which gun the overlay draws: empty = the selected one -TR_VBULLET_DEBUG_MAX=32 # max virtual bullets drawn per gun +# WHAT: master switch for every mover's debugGraphics. VALUE: on | 0/off. +TR_DEBUG_DRAW=on +# WHAT: draw the shared candidate-tile geometry overlay. VALUE: on | 0/off. +# DRAW ONLY — it cannot change a decision, so it is the safest knob here. +# GOTCHA: independent of TR_DEBUG_DRAW: the overlay is drawn either way, and +# TR_DEBUG_DRAW=0 is what suppresses the movers' own graphics. +# TRY: TR_GEO_DEBUG=on -> the geometry circles appear over the arena. +TR_GEO_DEBUG=off +# WHAT: draw each admitted gun's virtual-bullet paths. VALUE: 1/on/yes to +# enable, 0/off/no/false to disable, unset = off. DRAW ONLY. +# GOTCHA: needs a gun in the rack to be legible; the shipped rack admits +# Pattern only, so set TR_VBULLET_DEBUG_GUN too. +# TRY: TR_VBULLET_DEBUG=1 + TR_VBULLET_DEBUG_GUN=all +# -> travelled path, aim ring and miss vector for every admitted gun. +TR_VBULLET_DEBUG=off +# WHICH gun the overlay draws. VALUES: empty = the currently selected gun; +# `all` or `*` = every gun; otherwise a gun name, e.g. `Pattern`. +# TRY: TR_VBULLET_DEBUG_GUN=all -> every admitted gun at once. +TR_VBULLET_DEBUG_GUN= +TR_VBULLET_DEBUG_MAX=32 # max virtual bullets drawn per gun (clamped to >= 1) TR_VBULLET_ADMIT_ONLY=on # on = a gun the rack does not admit is not even predicted -# ── logs (set the value to 1; presence alone turns some of them on) ────────── -TR_RESULT_LOG=on # one line per round result -#TR_POWER_LOG=1 # presence-only: one line per power decision -#TR_RAM_LOG=1 # presence-only: one line per ram start/stop and why -#TR_MOVEMENT_LOG=1 # presence-only: movement band/class changes -#TR_STRAFE_LOG=1 # presence-only: one line per strafe tile pick -#TR_TMHORIZON_LOG=1 # presence-only: the per-shot thinking of the TM horizon gun +# ── logs (set the value to 1; PRESENCE alone turns these on) ───────────────── +# WHAT: one line per round result. VALUE-based (unlike the block below): 0/off +# really disables it, which is why this one is written out uncommented. +# TRY: TR_RESULT_LOG=off -> no [result] lines at all. +TR_RESULT_LOG=on +#TR_POWER_LOG=1 # PRESENCE-only: one line per power decision (0 would ENABLE it) +#TR_RAM_LOG=1 # PRESENCE-only: one line per ram start/stop and why +#TR_MOVEMENT_LOG=1 # PRESENCE-only: movement band/class changes +#TR_STRAFE_LOG=1 # PRESENCE-only: one line per strafe tile pick +#TR_TMHORIZON_LOG=1 # VALUE-based: the per-shot thinking of the TM horizon gun GUN_STATS_PATH=/tmp/gun_stats.jsonl # where the per-round gun stats are written GUN_SHOTLOG_PATH=/tmp/shot_log.jsonl # where the per-shot log is written # ── measurement helpers (leave off unless you are measuring) ───────────────── -#TR_RECORD_WORLDSTATE=1 # presence-only: dump every observed world state -#TR_RADAR_SCANLOG=1 # presence-only: log every radar scan tick -#TR_TRACKER_PROBE=1 # presence-only: dump the enemy-tracker's internal state +#TR_RECORD_WORLDSTATE=1 # PRESENCE-only: dump every observed world state +#TR_RADAR_SCANLOG=1 # PRESENCE-only: log every radar scan tick +#TR_TRACKER_PROBE=1 # PRESENCE-only: dump the enemy-tracker's internal state TR_TRACKER_PROBE_PATH=/tmp/tracker_probe.jsonl # where that dump is written # ── dotenv / boot report ───────────────────────────────────────────────────── # Name of the env file to load. Must be set in the REAL environment, not in the # file it points at. Empty = use ./.env, else .env next to the binary. +# GOTCHA: the line below sets it to the EMPTY string, which is the correct +# "use the default" spelling; putting a real path here would make this file +# load itself, recursively, at every start. TR_ENV_FILE= # 1 = print the [env] report on startup (default). 0 = do not print it. TR_ENV_REPORT=1 diff --git a/docs/env_reference.md b/docs/env_reference.md index 4072044..b3c3a9f 100644 --- a/docs/env_reference.md +++ b/docs/env_reference.md @@ -47,13 +47,123 @@ misled people:** warning at all**. 2. **Some flags are presence-based, not value-based.** They are read with `existsEnv`, so **`TR_POWER_LOG=0` turns the log ON** (any value does). - Presence-based: `TR_POWER_LOG`, `TR_RAM_LOG`, `TR_MOVEMENT_LOG`, - `TR_RECORD_WORLDSTATE`, `TR_RADAR_FORCE_SPIN`, `TR_RADAR_SCANLOG`, - `TR_TRACKER_PROBE`. Value-based (`0`/`false`/`off` really disable): - `TR_ENV_REPORT`, `TR_TMHORIZON_LOG`, `TR_TMHORIZON_ACCURVE`, + Value-based (`0`/`false`/`off` really disable): `TR_ENV_REPORT`, + `TR_TMHORIZON_LOG`, `TR_TMHORIZON_ACCURVE`, `TR_TMHORIZON_RESET_ON_TARGET`, `TR_POWER_POLICY`, `TR_POWER_FINISH_KILL`, `TR_RAM_OPPORTUNITY`, `TR_RAM_PLAN`, `GUN_SELECTOR_POOL`, - `TR_VBULLET_ADMIT_ONLY`. + `TR_VBULLET_ADMIT_ONLY`, `TR_LEADGAIN_LOG`, `TR_LEARNED_LOG`, + `TR_LEARNED_GLOBAL`, `TR_LEARNED_REAL_EVENTS`, `TR_FIRE_FIX`, + `TR_STRAFE_FIRE_FIX`, `TR_STRAFE_ESCAPE`, `TR_STRAFE_HEAT_GRID`, + `TR_TFIL_HEAT_TIME`, `TR_TFIL_PILLAR_ON`, `TR_TFIL_DIAG`, `TR_TFIL_NO_REV`, + `TR_TFIL_HOLD_WHEN_TRAPPED`, `TR_TFIL_COMMIT_ARRIVAL`, + `TR_TFIL_RING_COMMIT_ARRIVAL`, `TR_VBULLET_DEBUG`, `TR_GEO_DEBUG`, + `TR_DEBUG_DRAW`, `TR_RESULT_LOG`. + +### THE FULL PRESENCE-GATED LIST (j172, from `grep -rn existsEnv`) + +The list above was **incomplete**: it was missing four knobs. The complete set, +from `grep -rn 'existsEnv' ModularBot_garage/src common_libs | grep -v /tests/`, +is: + +| knob | read at | what it logs / does | +|---|---|---| +| `TR_POWER_LOG` | `ModularBot.nim:145` | one line per power-decision CHANGE | +| `TR_RAM_LOG` | `ram_decision.nim:119` | one line per ram start/stop + reason | +| `TR_MOVEMENT_LOG` | `the_floor_is_lava_ring.nim:192` | movement band / range-class changes | +| `TR_STRAFE_LOG` | `strafe.nim:402` | one line per strafe tile pick | +| `TR_SURF_LOG` | `wave_surfer.nim:102` | one line per wave-surfing decision | +| `TR_FIRE_DIAG` | `ModularBot.nim:137`, `the_floor_is_lava.nim:299`, `strafe.nim:415` | per-reading fire-detection tick/raw/correction | +| `TR_RECORD_WORLDSTATE` | `ModularBot.nim:70` | dump every observed world state | +| `TR_RADAR_SCANLOG` | `ModularBot.nim:80` | log every radar scan tick | +| `TR_RADAR_FORCE_SPIN` | `ModularBot.nim:79` | force the old full-360 spin radar | +| `TR_TRACKER_PROBE` | `ModularBot.nim:86` | dump the enemy-tracker internals | + +**For these ten, `NAME=0` turns the feature ON.** OFF means the line is ABSENT. +That is why `.env.example` shows every one of them commented out: there is no +"off" spelling, only absence. To disable one, delete its line. + +Two more are read with `existsEnv` but are *not* features — `TR_ENV_FILE` (an +empty value is the correct "use the default" spelling) and the loader's own +`existsEnv(e.key)` conflict check. + +**And one label is misleading:** `env_report.nim` prints `TR_TFIL_DIAG`, +`TR_TFIL_HOLD_WHEN_TRAPPED`, `TR_TFIL_COMMIT_ARRIVAL` and +`TR_TFIL_RING_COMMIT_ARRIVAL` through `sourceOfPresence`, but all four are read +**by value** (`getEnvBool`) in the source. The value is always right; only the +`(source: ...)` label is affected. Do not read that label as "presence-gated". + +--- + +## ONE EXPERIMENT, END TO END (mirrored from `.env.example`) + +Pick ONE knob. Here it is `TR_TFIL_ARRIVE_TICKS`; the shape is the same for +every knob. + +```sh +# 1. write the arm as its own file — that is how you GUARANTEE the arm, because +# nothing else can be applied on top of it +cat > /tmp/arm_arrive15.env <<'EOF' +TR_MOVEMENT=tfil +TR_TFIL_ARRIVE_TICKS=15.0 +EOF + +# 2. RESTART THE BOT. Env is read ONCE, at boot (module init). Editing .env +# while the bot runs changes nothing; there is no live reload. +cd ModularBot_garage && ./ModularBot.sh # or restart the GUI + +# 3. CONFIRM IT TOOK EFFECT, before reading a single result line. +# `source: .env` = your file was applied. `source: default` = it was not. +grep '^\[env\]' /tmp/modularbot_stdout.log | grep -E 'env file|ARRIVE_TICKS' +# [env] env file: /tmp/arm_arrive15.env (source: TR_ENV_FILE) +# [env] TR_TFIL_ARRIVE_TICKS = 15.0 (source: .env) + +# 4. point at the file instead of copying it into .env: +TR_ENV_FILE=/tmp/arm_arrive15.env ./out/ModularBot +./out/ModularBot --env-file /tmp/arm_arrive15.env +# A file you ASKED for and that does not exist stops the bot with an error; a +# missing default .env is silent. This is what an A/B run does: one frozen +# binary, one env file per arm. + +# 5. what is switched on at all: +grep '^\[modules\]' /tmp/modularbot_stdout.log +``` + +## SAFE TO EXPERIMENT WITH RIGHT NOW + +The honest list is SHORT: after the recent campaign most experimental knobs are +either never live-tested or already measured null/harmful, and `.env.example` +says so on every one of them. These four are safe in the sense that they either +cannot change a decision, or are the ones a measurement actually supports. + +| knob | what changes | what to watch | a good result | +|---|---|---|---| +| `TR_GEO_DEBUG=on` | draw-only geometry overlay | the circles on the two tanks, each heading line | you can SEE the tile the picker chose; it cannot change a decision | +| `TR_VBULLET_DEBUG=1` + `TR_VBULLET_DEBUG_GUN=all` | draw-only: each admitted gun's virtual bullets | travelled path, aim ring, miss vector | you can see the signal the selector ranks on; also draw-only | +| `TR_TFIL_DIAG=on` | fills the per-pick loss histogram (tfil only) | the tfil pick log line | `sReach/sCool/sSafe/sCand` tell you where tiles are lost; provably moves no command | +| `TR_MOVEMENT=tfil` | runs the long-shipped mover | nothing to compare against | you are reproducing an older, documented behaviour; only do it together with the `TR_TFIL_*` knobs | + +## ALREADY REJECTED OR MEASURED NULL — WITH THE NUMBER + +Do not re-run these by accident. + +| knob / arm | result | where | +|---|---|---| +| `TR_TFIL_GEO_MODE=both-rej` + `TR_TFIL_GEO_TAU=60` | **REJECTED** live, 420 battles, 15 opponents: damage/run **-8.83**, p(sign-flip) **0.0061**, Wilcoxon p 0.011. Round wins null. Offline it did what was predicted (arrivals 4.5% -> 29.4%) and that is why it is bad: +26 px distance on 15/15, less damage. | `docs/tfil_geo_ab.md` | +| `TR_RAM_FLOOR_ENERGY=5` | **CLEAN NULL**: **-0.018 wins/run**, p(sign-flip) **0.7676**, under a **0.1420 wins/run** MDE, 900 battles. Mechanism fired on 0.04% of ticks (~200x less than the offline ruler said). Do not re-test: more runs buy resolution on an effect that is not there. | `docs/ram_floor_exhaustion_ab.md` | +| `TR_RAM_FLOOR_ENERGY=10/20` | measured COSTLY offline (20 blocked 24.7% of all ticks) and the zone it guards is nearly empty: only 4.8% of shots are taken below 10 energy. | same | +| `TR_TMHORIZON_WINDOW=150` | **MEASURED HARMFUL** live: 26.5% round wins vs 49.0% for the shipped rack, p = 0.036. Keep 0. | env_reference "Measured verdicts" | +| `TR_POWER_POLICY=0` | **MEASURED HARMFUL** live: real hit rate 10.61% -> 7.88%, p = 0.0012. | same | +| `TR_TFIL_HEAT_TIME=1` | **MEASURED HARMFUL** live at every tau tried (3/5/9/15); tau15 alone is -22 damage/run, p = 0.046. | `docs/tfil_heat_pillar_ab.md` | +| `TR_MOVEMENT=tfil_ring` | **MEASURED**: round wins 16/49 -> 6/49, p = 0.012. Best live hit rate of anything measured, half the survival. | same / env_reference | +| the full `TR_RACK_*` rack | **MEASURED NEGATIVE VALUE**: Pattern ALONE beats the full 13-gun rack, p = 0.0012. Adding guns costs rounds. | `docs/gun_rack_analysis.md` | +| `TR_TFIL_TURN_BIAS=9` + `_TURN_REF_DEG=0` | **LIVE NULL**: +0.15 wins/run, p(sign) 0.244, under a 0.30 MDE, 300 battles. | `docs/movement_campaign.md` (j145) | +| `TR_RAM_OPPORTUNITY=on` | **MEASURED not to convert**: 0/59 opportunity -> contact. The finisher ram is the only path that converts, and it is always on. | env_reference | +| `TR_TFIL_PILLAR_ON=1` | live-tested, and the recommendation to revert to pillar-on was **OVERRULED by the owner**: the contrast is inside the MDE (33 damage/run, 1.22 wins/run at n=10). Pillar stays removed. | `docs/tfil_heat_pillar_ab.md` | + +> **A null is only a null at the resolution that run reached.** The frozen +> 15-opponent panel at 14 runs/arm resolves ~0.17 wins/run and ~7.65 damage/run; +> the j163 run resolved 0.1420 wins/run. "Clean null" here means *no effect at or +> above that size* — not *no effect*. ---