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.
This commit is contained in:
2026-09-27 14:52:16 +02:00
parent 940fa44631
commit 486e2a69c6
2 changed files with 646 additions and 64 deletions
+115 -5
View File
@@ -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*.
---