docs: complete environment variable reference (every knob, its default, and why)

Single place to answer 'what env vars exist, what do they default to, and what are
they for'. Grouped by area, with the measured reason each knob exists recorded
next to it, since several defaults are counterintuitive:

- The shipped rack is PATTERN ONLY, with the revert one-liner included.
- GUN_SELECTOR_TIEBREAK defaults off because it measured NEGATIVE on real hit
  rate, and the per-tick random draw inside the tie band is load-bearing
  (commitment cost 7.02% -> 5.10%, p=0.002).
- The power policy is ON because turning it off measured 10.6% -> 7.9% real hit
  rate; TR_POWER_FINISH_KILL exists because server 1.3.1 caps the damage SCORE at
  the energy actually removed, so overkill scores nothing.
- The ring mover is NOT the default and must not be shipped: best offline hit
  rate of anything measured, but it halves survival (16/49 -> 6/49, p=0.012).
- The proactive ram is off because it converts 0/6 times.
- TR_PATTERN_RAD_* are kept but are structurally incapable of changing the shot
  (the aim is bearing-only, measured byte-identical on the path metric).
- TR_TMHORIZON_NSTATES is the automata inertia ('mood'): lower = adapts faster.
- TR_VBULLET_ADMIT_ONLY=1 gave +68% tick rate (87 -> 146 ticks/s).

Documents the read-once-at-start convention and the operational trap that the bot
is spawned by the server/GUI, so a variable exported in an unrelated terminal does
NOT reach it. Also lists the compile-time -d: knobs and the test-harness jars.
Snapshot of the source at this commit; f9f8d84-era knobs added by the in-flight
jobs are included where already present.
This commit is contained in:
2026-09-22 23:45:53 +02:00
parent aed579b3af
commit 9ba932d1b1
+196
View File
@@ -0,0 +1,196 @@
# Environment variable reference
Every knob the bot reads. **All are read ONCE at process start.**
> **The bot is spawned by the server (or by the GUI if it starts its own server).**
> So the variable must be set on **that** process, not in an unrelated terminal.
> Export it in the shell that launches the server/GUI.
An unset (or empty, or unparseable) value means **the shipped default** — a typo
can never silently change behaviour, it warns on stderr and falls back.
---
## The ones you'll actually use live
| variable | default | what it does |
|---|---|---|
| `TR_MOVEMENT` | `tfil` | movement engine: `tfil` (shipped) or `tfil_ring` (range-weighted variant) |
| `TR_RACK_<GUN>` | see below | move a gun in/out of the rack per mode |
| `TR_POWER_POLICY` | `1` | `0` = uncapped control arm (today's behaviour without the energy policy) |
| `TR_POWER_LOG` | off | `1` = log each power decision with its reason |
| `TR_RAM_LOG` | off | `1` = log ram on/off with the reason |
| `TR_MOVEMENT_LOG` | off | `1` = log movement band/class changes |
| `TR_TMHORIZON_LOG` | off | `1` = let the horizon TM gun log its thinking per shot |
---
## 1. Gun rack — which gun(s) may be chosen
| variable | default | what it does |
|---|---|---|
| `TR_RACK_<GUN>` | `PATTERN=both`, all others `off` | membership: `both` \| `1v1` \| `melee` \| `off` |
| `GUN_RACK_DISABLE` | empty | comma-separated gun **ids** to remove entirely (older mechanism; the gun never even spawns virtual bullets) |
| `GUN_STATS_PATH` | `/tmp/gun_stats.jsonl` | where the per-round per-gun stats are written |
`<GUN>` names: `HEADON LINEAR TSETLIN CIRCULAR GUESSFACTOR PATTERN WALLBOUNCE ACCEL
STOPSHOT DISPLACE AVGLEAD DECAYGF KNN TMSELECT TMPATTERN TMHORIZON`.
**The shipped default is Pattern only.** To restore the full old rack:
```sh
TR_RACK_PATTERN=both TR_RACK_HEADON=both TR_RACK_LINEAR=both TR_RACK_TSETLIN=both \
TR_RACK_CIRCULAR=both TR_RACK_GUESSFACTOR=both TR_RACK_WALLBOUNCE=both \
TR_RACK_ACCEL=both TR_RACK_STOPSHOT=both TR_RACK_DISPLACE=both TR_RACK_AVGLEAD=both \
TR_RACK_DECAYGF=both TR_RACK_KNN=both TR_RACK_TMSELECT=both ./out/ModularBot
```
To force one gun alone: `TR_RACK_PATTERN=off TR_RACK_TMHORIZON=both`.
If **every** gun is `off`, the rack falls back to admitting all of them.
## 2. The selector (virtual-bullet fitness)
| variable | default | what it does |
|---|---|---|
| `GUN_VBULLET_METRIC` | `path` | how a virtual bullet is scored: `path` (swept ray — the better coarse signal, 7.4% vs 4.7% real) or `point` (arrival accuracy) |
| `GUN_SELECTOR_MODE` | `relative` | tie-band model: `relative` (scale-aware) or `absolute` (legacy fixed bars) |
| `GUN_SELECTOR_WINDOW` | `100` | rolling window (ticks) for the fitness rates |
| `GUN_SELECTOR_TIE` | `0.20` | tie band width: tied if `rate >= bestRate*(1-this)` |
| `GUN_SELECTOR_DWELL` | `10` | ticks the incumbent gun is held before it may be displaced |
| `GUN_SELECTOR_MARGIN` | `0.05` | a challenger must beat the incumbent by this fraction to displace it |
| `GUN_SELECTOR_TIEBREAK` | `off` | `off` (shipped) / `point` / `pointCommit`. Measured **negative** on real hit rate; opt-in only |
| `GUN_SELECTOR_POINT_TIE` | `0.5` | margin used by the `point` tie-break |
**Note:** the per-tick random draw inside the tie band is **load-bearing** —
replacing it with commitment to the virtual-best measurably *lowered* real hit
rate (7.02% → 5.10%, p=0.002). Three attempts to "smarten" the band all failed.
## 3. Power policy (energy economy)
| variable | default | what it does |
|---|---|---|
| `TR_POWER_POLICY` | `1` | `0` = uncapped control arm. **Measured: turning it off drops real hit rate 10.6%→7.9%** |
| `TR_POWER_FAR_DIST` | `200` px | beyond this = "bad chances zone" |
| `TR_POWER_FAR_CAP` | `1.0` | power cap beyond that distance |
| `TR_POWER_MID_CAP` | `2.0` | cap when close + healthy but chances are not above average |
| `TR_POWER_REF` | `0.0` | reference probability: `0` = the gun's own mean, `>0` = a fixed threshold |
| `TR_POWER_ENERGY_HI` | `80` | at/above this energy, no energy-slope cap |
| `TR_POWER_ENERGY_LO` | `20` | at/below this energy, cap = `ENERGY_MIN` |
| `TR_POWER_ENERGY_MIN` | `0.5` | the cap at/below `ENERGY_LO` |
| `TR_POWER_ENERGY_MAX` | `3.0` | the cap at/above `ENERGY_HI` (3.0 = effectively uncapped) |
| `TR_POWER_FINISH_KILL` | `1` | cap power at the **smallest bullet that can still kill** the enemy |
| `TR_POWER_LOG` | off | `1` = log each decision: `[power] p=… cap=… reason=far\|lowEnergy\|belowAvg\|full\|ram` |
WHY the slope: bullet speed is `20-3p`, so **lower power = faster bullet** (less
lead error, higher hit chance), fires more often (`10+2p` ticks) and drains energy
more slowly (`p`/shot). `E[dE] = p(3P-1)`, so break-even is **1/3 regardless of
power** — and our measured hit rates are 5–27%, far below it.
WHY the finishing cap: server 1.3.1 caps the damage **score** at the energy
actually removed, so **overkill scores nothing**. Damage is `4p` (p≤1) / `6p-2`
(p>1), so the smallest killing power is `E/4` for E≤4 and `(E+2)/6` for 4<E≤16.
## 4. Movement
| variable | default | what it does |
|---|---|---|
| `TR_MOVEMENT` | `tfil` | `tfil` (shipped) or `tfil_ring` (the range-weighted variant) |
| `TR_MOVEMENT_LOG` | off | `1` = log band/range-class changes for the ring mover |
| `TR_TFIL_RANGE_LO` / `_HI` | `100` / `200` px | the target-range band the ring mover prefers |
| `TR_TFIL_RANGE_TEMP` | `0.4` | peakiness of the range weighting. `0` = plain uniform draw = exact control |
| `TR_TFIL_RANGE_K` | `60` | softness of the falloff outside the band |
| `TR_TFIL_CORRIDOR_HEAT` | `10.0` | heat added along a bullet's corridor to the wall |
| `TR_TFIL_WALL_HOTNESS` | `15.0` | wall radiance |
**Ring mover is NOT the default and should not be shipped as-is:** it had the best
offline hit rate of anything measured, but it halves engagement range and **halves
survival** (round wins 16/49 → 6/49, p=0.012) — a glass cannon.
## 5. Ramming
| variable | default | what it does |
|---|---|---|
| `TR_RAM_OPPORTUNITY` | off | enable the proactive ram. **Measured: converts 0/6 times** — a straight-line pursuit cannot catch an equal-speed enemy |
| `TR_RAM_OPP_DIST` | `200` px | opportunity trigger distance |
| `TR_RAM_OPP_MARGIN` | `15` | how much MORE energy we must have than the enemy |
| `TR_RAM_ABORT_DMG` | `2.0` | abort an in-progress ram above this incoming **energy per turn** (the "bullet rain" abort) |
| `TR_RAM_PLAN` | off | the speculative "change of plan" trigger |
| `TR_RAM_PLAN_DIST` / `_MARGIN` / `_HITRATE` | `250` / `20` / `0.05` | its thresholds |
| `TR_RAM_LOG` | off | `1` = log `[ram] ON/OFF` with reason |
The **finisher** ram (enemy <20 energy, we are healthier) is always on and is the
only path that converts.
## 6. The horizon TM gun (`TMHORIZON`)
| variable | default | what it does |
|---|---|---|
| `TR_TMHORIZON_LOG` | off | `1` = per-shot thinking log + per-round summary |
| `TR_TMHORIZON_SHIFT` | `2.0` deg | how far it may move the aim off Pattern's answer. **`0` = predict but never move the aim** (isolates prediction from application) |
| `TR_TMHORIZON_BIG_MULT` | `1.5` | multiplier when the predicted correction is BIG |
| `TR_TMHORIZON_NSTATES` | `64` | **automata inertia** — the "mood". Lower = adapts faster, higher = more stubborn (2…4096) |
| `TR_TMHORIZON_RESET_ON_TARGET` | on | wipe the model when the target changes (no-op in 1v1) |
| `TR_TMHORIZON_WINDOW` | `0` | sliding window: retrain on only the most recent N samples. `0` = keep everything |
| `TR_TMHORIZON_RESET_DROP` | `0` | change detection: if rolling accuracy falls this many points below its own peak, treat it as "the enemy changed" and re-learn |
| `TR_TMHORIZON_ACCURVE` | off | `1` = log the rolling accuracy periodically |
| `TR_TMHORIZON_RETRAIN_EVERY` | `50` | samples between windowed retrains |
| `TR_TMHORIZON_EPOCHS` | `1` | epochs per windowed retrain |
Learning **persists across rounds** of the same battle (so it can overfit the
current enemy) and wipes only on a new battle or a target change. **No learning is
written to disk — nothing carries between battles.**
## 7. Other gun knobs
| variable | default | what it does |
|---|---|---|
| `TR_PATTERN_RAD_OFFSET` | `0.0` px | shift Pattern's aim along its own bearing (negative = aim short) |
| `TR_PATTERN_RAD_SCALE` | `1.0` | multiplier on Pattern's aim distance |
Both are **structurally incapable of changing the shot** — the live aim is
bearing-only, so a purely radial offset is invisible. Measured byte-identical on
`bmPath`. Kept for experiments; leave at defaults.
## 8. Measurement / instrumentation (all off by default, all zero-cost when off)
| variable | default | what it does |
|---|---|---|
| `TR_VBULLET_ADMIT_ONLY` | `1` | only admitted guns spawn virtual bullets. **Turning it on gave +68% tick rate** (87→146 ticks/s); `0` restores the old behaviour |
| `TR_RECORD_WORLDSTATE` | off | append every tick's WorldState to `/tmp/worldstate_record.jsonl` for offline replay |
| `TR_RADAR_FORCE_SPIN` | off | force the old stateless full-spin melee radar (for A/B on one binary) |
| `TR_RADAR_SCANLOG` | off | append per-round radar scan/coverage JSON |
| `TR_RADAR_SCAN_LOG_PATH` | `/tmp/radar_scan_log.jsonl` | where that goes |
| `TR_TRACKER_PROBE` | off | per-tick enemy tracker vs server enemy count |
| `TR_TRACKER_PROBE_PATH` | `/tmp/tracker_probe.jsonl` | where that goes |
## 9. Test harness only (not read by the bot itself)
| variable | default | what it does |
|---|---|---|
| `TR_SERVER_JAR` | the 1.3.1 jar in `~/Downloads/robocode-tankroyale/` | which server the tests launch. Set to the 0.35.5 jar to reproduce old numbers |
| `TR_BATTLE_RUNNER` | the 1.0.2 runner jar | which runner the tests launch |
| `TR_BATTLE_RUNNER_DIR` | derived | directory form of the runner |
## 10. Compile-time knobs (`-d:` flags, need a rebuild)
| flag | default | file |
|---|---|---|
| `-d:TMH_NSTATES=N` | 64 | `guns/tm_horizon.nim` — automata inertia (the runtime env overrides it) |
| `-d:TMH_NCLAUSES=N` | 40 | `guns/tm_horizon.nim` |
| `-d:TMH_MIN_OBS=N` | 24 | cold gate: below this many samples the TM emits no correction |
| `-d:TMH_STALE_MAX=N` | 8 | a sample is dropped if the enemy was not seen this recently |
| `-d:TM_S_DEF=x` | `"3.0"` | `guns/tm_pattern.nim` specificity (`s`). **Higher = LONGER clauses**, measured. `s=1.0` is degenerate |
| `-d:TM_NSTATES=N` | 64 | `guns/tm_pattern.nim` |
## Adding / finding knobs
```sh
# everything the code reads
grep -rn 'TR_[A-Z_]*"' --include=*.nim ModularBot_garage/src common_libs | sort -u
# what a specific knob defaults to
grep -rn 'TR_POWER_ENERGY_MIN' --include=*.nim .
```
Convention followed throughout: read once at module init, into a `let` with an
`EnvVar` name constant beside it, so one compiled binary can A/B every arm by
environment alone.