Files
SirRoboGarage/docs/env_reference.md
T
SirStone e670788eee env_reference: the ring mover was NEVER measured offline - correct a false label
The offline-harness audit (`e40c849`, `docs/offline_harness_trust.md`) found that the
claim "best offline hit rate of anything measured" for the ring mover was false.

The 20.28% figure is a LIVE number: `docs/feature_ab_results.md` and commit `bfdcdf8`
record 35 real-DrussGT bridge battles with a server-side event sidecar as the ground
truth, and the 6/49 round wins is likewise live. There is no offline measurement of
the ring mover anywhere - the offline harness scores GUNS, not movements, and has no
movement driver at all.

So this was NOT an offline-vs-live calibration failure, which is how it has been
described repeatedly (including by the orchestrator). It was a METRIC MISMATCH: a
movement arm judged on hit rate instead of damage/run and round wins - and hit rate is
precisely the metric that concealed its collapse.

The lesson previously attached to this result was therefore the wrong one. The
paragraph now says what actually happened and points at the audit.

Docs-only; no code touched.
2026-09-24 21:44:17 +02:00

479 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Environment variable reference
Every knob the bot reads. **Most are read once per process, at module init; a few
are read lazily on first use** (`TR_TMHORIZON_SHIFT`/`_BIG_MULT`/`_LOG`/
`_RESET_ON_TARGET` in `ensureConfig`, `TR_PATTERN_RAD_*` inside `predict`). Either
way, changing one mid-run has no effect.
> **The bot is spawned by the server (or by the GUI if it starts its own server).**
> It inherits the **server's** environment. Export in the shell that launches the
> server/GUI — not in an unrelated terminal. See
> [Did my env vars actually reach the bot?](#did-my-env-vars-actually-reach-the-bot).
**Read these two traps before the tables — they are the two ways this document has
misled people:**
1. **Defaults are silent for numeric/bool knobs.** Unset / empty / unparseable
means the shipped default. Only the enumerated *string* knobs
(`TR_RACK_*`, `GUN_VBULLET_METRIC`, `GUN_SELECTOR_MODE`, `GUN_SELECTOR_RANK`,
`GUN_SELECTOR_TIEBREAK`) **warn on stderr** on a bad value; the
`envInt`/`envFloat`/`envBool` helpers fall back to the default with **no
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`,
`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`.
---
## COMPILE-TIME vs RUNTIME: the two namespaces
**A `-d:` compile-time define and a runtime environment variable are different
namespaces. The naming is only *partly* parallel, so guessing an env name from a
define name is unreliable.**
| compile-time define | runtime env | notes |
|---|---|---|
| `-d:TMH_NSTATES=N` | `TR_TMHORIZON_NSTATES=N` | **The ONLY pair with both forms.** Env wins at runtime; clamped `2..4096`. `tm_horizon.nim:102,139,381` |
| `-d:TMH_NCLAUSES=N` | *(none)* | compile-time ONLY — no env form. `tm_horizon.nim:101` |
| `-d:TMH_S_DEF="x"` | *(none)* | compile-time ONLY. `tm_horizon.nim:103` |
| `-d:TMH_MIN_OBS=N`, `-d:TMH_STALE_MAX=N` | *(none)* | compile-time ONLY. `tm_horizon.nim:106,108` |
| `-d:TM_NCLAUSES=N`, `-d:TM_NSTATES=N`, `-d:TM_S_DEF="x"`, `-d:TM_MIN_OBS=N`, `-d:TM_CLASSES=N` | *(none)* | `tm_pattern.nim` — compile-time ONLY. Note **`TM_NCLAUSES`** (no underscore) here. `tm_pattern.nim:50,53,55,57,59` |
| `-d:TM_N_CLAUSES=N`, `-d:TM_N_STATES=N`, `-d:TM_S_DEF="x"`, `-d:TM_T_DEF`, `-d:TM_WINDOW_SIZE=N` | *(none)* | `tsetlin.nim` — compile-time ONLY. Note **`TM_N_CLAUSES`** (underscore) here, and `TM_N_STATES` not `TM_NSTATES`. `tsetlin.nim:29,116,118,119,120` |
Rules and warnings:
- The `-d:TMH_X` ↔ `TR_TMHORIZON_X` mapping holds **only for `NSTATES`**.
`TMH_NCLAUSES`, `TMH_S_DEF`, `TMH_MIN_OBS`, `TMH_STALE_MAX` have **no env
form** — there is no `TR_TMHORIZON_NCLAUSES` etc.
- `-d:TM_S_DEF` is defined in **both** `tm_pattern.nim` (default `"3.0"`) and
`tsetlin.nim` (default `"1.5"`); the same flag sets both.
- **If you set an env var and the bot seems to ignore it, the name is probably
wrong — `TMH_NSTATES` is NOT an env var, the env var is
`TR_TMHORIZON_NSTATES`.** Likewise `TM_N_CLAUSES`, `TMH_NCLAUSES`, etc. are
compile-time only.
---
## Did my env vars actually reach the bot?
### 1. The spawn trap in one sentence
The bot is spawned by the **server/GUI**, so it inherits the **server's**
environment — exporting a variable in a separate terminal does **nothing**,
because that terminal is not the bot's parent.
### 2. The no-code check (works today, no boot report needed)
`/proc/PID/environ` is the environment **at exec time** — authoritative, exactly
what the bot started with, before it read anything:
```sh
for p in $(pgrep -f ModularBot); do
echo "== pid $p"
tr '\0' '\n' < /proc/$p/environ | grep -E '^(TR_|GUN_)' | sort \
|| echo ' (none - wrong process!)'
done
```
**Warning:** filtering to `TR_|GUN_` **hides a wrongly-named variable** — e.g. a
variable called `TMH_NSTATES` will not show up under this filter (and neither will
a `TR_TMHORIZON_*` typo). When you are debugging a *missing* var, grep for the
distinctive part of the name with **no prefix filter**:
```sh
tr '\0' '\n' < /proc/$p/environ | grep -i tmh
```
### 3. The boot report (`TR_ENV_REPORT`, default `1`)
> **Available in the build that adds the boot report** (`ModularBot_garage/src/
> env_report.nim`, committed as `9bf3005`). If your binary predates it, use §2.
The bot prints a one-shot `[env]` block to **stdout** at boot
(`/tmp/modularbot_stdout.log` when launched by the GUI, or the GUI/server
console). Recover just the report with:
```sh
grep '^\[env\]' /tmp/modularbot_stdout.log
```
It has two sections plus build identity:
- **A. raw process environment** (`[env] --- A. raw process environment ...`):
every `TR_*`/`GUN_*` this process *actually* received, sorted, followed by
`[env] raw: N TR_*/GUN_* of M total environment variables`. If `N == 0` it
prints a loud
`[env] WARNING: this process has NO TR_*/GUN_* variables - the env you exported did NOT reach the bot.`
It then prints the process identity: `pid`/`ppid`, `cwd`, the bot's own command
line (`self_cmd`), and the **parent's command line** (`parent_cmd`). The parent
command line is the conclusive proof of who spawned the bot — under the GUI it
is the server/GUI, never your interactive shell.
- **B. effective values** (`[env] --- B. effective values (resolved at boot) ---`):
one line per knob, `[env] NAME = value (source: env|default)`, i.e. the value
the bot will *actually* use after parsing, clamping and fallback (e.g.
`TR_TMHORIZON_NSTATES` is clamped to `2..4096`; the rack's empty-set fallback is
shown). Where a value is only resolvable later it says
`(raw - not resolved here)` rather than guessing. It also prints
`[env] rack active 1v1 = ...` and `[env] rack active melee = ...`.
- **build identity**: `NimVersion`, compile date/time, and the binary path + size
+ mtime, so a stale binary is obvious.
`TR_ENV_REPORT=0` suppresses the report (it is value-based: `0`/`false`/`no`/`off`
all suppress).
### 4. Launching the GUI with knobs
Export in the **same shell** that launches the GUI/server, then start it from that
shell. Because the launcher **appends** to the log, clear it **first** so you read
only the new run:
```sh
rm -f /tmp/modularbot_stdout.log
export TR_POWER_ENERGY_MIN=1.0 TR_RACK_PATTERN=off
./start-gui.sh # whatever launches the server/GUI
grep '^\[env\]' /tmp/modularbot_stdout.log
```
### 5. Prove the trap deliberately
Terminal A — where you *exported*, but never launched anything:
```sh
export TR_POWER_ENERGY_MIN=1.0
```
Terminal B — where you launch the GUI:
```sh
./start-gui.sh
grep '^\[env\]' /tmp/modularbot_stdout.log | grep WARNING
# [env] raw: 0 TR_*/GUN_* of ... total environment variables
# [env] WARNING: this process has NO TR_*/GUN_* variables - the env you exported did NOT reach the bot.
```
Terminal A's variable never reached the bot because the bot's parent is the
server, not terminal A. Fix it by exporting in terminal B before launching.
---
## Measured verdicts — read before tuning (MEASURED on the live arena vs real DrussGT)
These are live battles, not offline replay. Do not re-derive an offline-only win
and ship it. (Offline claims are labelled as such.)
| experiment | result | verdict |
|---|---|---|
| shipped rack (Pattern only) | **24/49 rounds = 49.0%** (up from 12.5% earlier in the session) | the baseline |
| `TR_TMHORIZON_WINDOW=150` (with TMHorizon admitted) | **13/49 = 26.5%** vs 49.0%, **p=0.036** | **MEASURED HARMFUL live.** Looked like an offline +9.3pp late-accuracy win; the arena disagreed. Keep `0`. |
| TMHorizon `TR_TMHORIZON_NSTATES=2` | 42.9% | no better than shipped (p>=0.8) |
| TMHorizon `TR_TMHORIZON_NSTATES=8` | 42.9% | no better than shipped (p>=0.8) |
| TMHorizon `TR_TMHORIZON_NSTATES=64` (default) | 53.1% | not significant vs Pattern (p>=0.8) |
| refusing to fire below power 1.0 | 22/49 vs 21/49, **p=1.0** | no help |
| sub-1.0 vs >=1.0 bullet accuracy | 10.80% vs 10.35%, **p=0.37** | sub-1.0 bullets are **not** less accurate |
| `TR_POWER_POLICY=0` (uncapped) | real hit rate 10.61% → 7.88%, p=0.0012 | policy helps; keep it on |
| `TR_VBULLET_ADMIT_ONLY=1` | 87 → 146 ticks/s (+68%) | keep it on |
| `TR_MOVEMENT=tfil_ring` | round wins 16/49 → 6/49, p=0.012 | glass cannon; do not ship |
| `TR_RAM_OPPORTUNITY=1` | 0/59 opportunity→contact conversions | finisher-only is the default |
On the TM rows: **no TM configuration beats the shipped rack**, and **low inertia
does not help** (contrary to an earlier offline trend, where it was a substitute
for forgetting, not a fix). On the power rows: the floor experiment exposed the
real tradeoff — power `p` costs a `10+2p` tick reload, so a floor costs ~13% of
your shots/round, while the bullets it suppresses were *not* less accurate (they
are simply worth less damage per hit, `4p`, which is what makes low-power
shooting *look* like missing).
---
## 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; any other value falls back to `tfil`) |
| `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 | **presence-based**: if the var exists at all (even `=0`) log each power decision |
| `TR_RAM_LOG` | off | **presence-based**: log ram on/off with the reason |
| `TR_MOVEMENT_LOG` | off | **presence-based**: log movement band/class changes |
| `TR_TMHORIZON_LOG` | off | value-based: `1` = let the horizon TM gun log its thinking per shot |
| `TR_ENV_REPORT` | `1` | print the boot-time `[env]` report to stdout; `0` suppresses it |
---
## 1. Gun rack — which gun(s) may be chosen
| variable | default | what it does |
|---|---|---|
| `TR_RACK_<GUN>` | `PATTERN=both`, all other 15 guns `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_SHOTLOG_PATH` | `/tmp/shot_log.jsonl` | where the per-shot JSONL is written |
`<GUN>` names (ids 0-15, `selector.nim:52`):
`HEADON LINEAR TSETLIN CIRCULAR GUESSFACTOR PATTERN WALLBOUNCE ACCEL STOPSHOT
DISPLACE AVGLEAD DECAYGF KNN TMSELECT TMPATTERN TMHORIZON`.
Shipped default membership (`selector.nim:DefaultRackMembership`):
| gun | default | gun | default |
|---|---|---|---|
| HEADON | off | STOPSHOT | off |
| LINEAR | off | DISPLACE | off |
| TSETLIN | off | AVGLEAD | off |
| CIRCULAR | off | DECAYGF | off |
| GUESSFACTOR | off | KNN | off |
| **PATTERN** | **both** | TMSELECT | off |
| WALLBOUNCE | off | TMPATTERN | off |
| ACCEL | off | TMHORIZON | off |
`TR_RACK_<GUN>` accepts aliases: `both`/`any`/empty → both;
`1v1`/`single`/`lock` → 1v1; `melee`/`multi` → melee;
`off`/`none`/`disabled` → off. An **unknown value warns on stderr and falls back
to `both`** (`selector.nim:parseRackMembership`).
The `1v1`/`melee` split is driven by **server truth**, `getEnemyCount()`:
`== 1` → 1v1 rack, `> 1` → melee rack (`virtual_bullets.rackMode`).
**Empty-set fallback:** if every gun is filtered out for a mode, the *selector*
falls back to the **full rack** so it can never end up with no gun
(`admittedGuns`, `virtual_bullets.nim`). Note this fallback is on *selection*;
the spawn gate (`TR_VBULLET_ADMIT_ONLY`) uses `rackAdmitted` and would spawn
nothing, so an all-`off` rack is not a useful configuration.
**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`.
Enabling a gun is `TR_RACK_<GUN>=both`; TMHorizon additionally needs
`TR_RACK_TMHORIZON=both` before any `TR_TMHORIZON_*` knob can matter.
## 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; **clamped `1..100`** |
| `GUN_SELECTOR_TIE` | `0.20` | tie band width: tied if `rate >= bestRate*(1-this)` |
| `GUN_SELECTOR_FLOOR` | `0.25` | relative floor: fire HeadOn only if best rate < this × its peak |
| `GUN_SELECTOR_MINOBS` | `50` | min observations before a gun×bin may compete (clamped to ≥1) |
| `GUN_SELECTOR_POOL` | `on` | pool the gun's power bins when computing its rate |
| `GUN_SELECTOR_RANK` | `mean` | window statistic: `mean` \| `wilson` \| `ucb` \| `thompson` \| `shrunk` |
| `GUN_SELECTOR_SHRINK` | `20.0` | empirical-Bayes shrink strength (`shrunk` rank only; clamped ≥0) |
| `GUN_SELECTOR_DWELL` | `10` | ticks the incumbent gun is held before it may be displaced (clamped ≥0) |
| `GUN_SELECTOR_MARGIN` | `0.05` | a challenger must beat the incumbent by this fraction to displace it (clamped ≥0) |
| `GUN_SELECTOR_SEED` | unset | integer seed for the tie-break RNG; unset = time+pid per process |
| `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 (clamped `0..1`) |
Constants live in `virtual_bullets.nim:16,28,31,33,35`.
**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.61%→7.88%, p=0.0012** |
| `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` — **NOT a minimum-power floor** (see below) |
| `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 | presence-based; logs each decision: `[power] p=… cap=… reason=… dist=… selfE=… enemyE=… gun=…` |
Constants live in `virtual_bullets.nim:842-851`; the rule core is
`applyPowerPolicy`/`energySlopeCap`/`powerToKill`.
> **`TR_POWER_ENERGY_MIN` is a CAP, not a floor.** It is the ceiling applied when
> *our* energy is at/below `TR_POWER_ENERGY_LO`. Setting it to `1.0` raises the
> low-energy ceiling to 1.0; it does **not** forbid shots below 1.0. Sub-1.0
> bullets come from the caps (`ENERGY_MIN`, `FAR_CAP`, `FINISH_KILL`). MEASURED:
> refusing to fire below power 1.0 does not help (22/49 vs 21/49, p=1.0), and
> sub-1.0 bullets are *not* less accurate (10.80% vs 10.35%, p=0.37).
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. A power floor at
1.0 costs ~13% of your shots/round (the longer reload) and buys no accuracy.
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; any other value falls back to `tfil`, no warning) |
| `TR_MOVEMENT_LOG` | off | presence-based; `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` | peak wall radiance |
Defaults read in `the_floor_is_lava_ring.nim:116-133`.
**Discrepancy to be aware of:** that file's header comment still says
`CORRIDOR_HEAT default 5.0` / `WALL_HOTNESS default 10.0` (the pre-retune values);
the **code defaults are `10.0` / `15.0`** (commit `7f6ccfb`). The code is the
truth.
**Ring mover is NOT the default and should not be shipped as-is.** It had the best
**LIVE** hit rate of anything measured (20.28%, server-side event sidecar ground truth), but it
halves engagement range and **halves survival** (round wins 16/49 → 6/49, p=0.012) — a glass
cannon.
**CORRECTION (see `docs/offline_harness_trust.md`):** this line previously claimed the ring mover
had "the best offline hit rate". That was **wrong — the ring mover was never measured offline at
all**; the offline harness scores *guns*, and has no movement driver. So this was **not** an
offline-vs-live calibration failure (which is how it was repeatedly described). It was a **metric
mismatch**: a movement arm judged on hit rate instead of damage/run and round wins — and hit rate
is exactly the metric that hid its collapse.
## 5. Ramming
| variable | default | what it does |
|---|---|---|
| `TR_RAM_OPPORTUNITY` | off | enable the proactive ram. **MEASURED: 0/59 opportunity→contact conversions** — 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 | presence-based; `1` = log `[ram] ON/OFF` with reason |
Defaults in `ram_decision.nim:79-97`. 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`)
**Only relevant when the rack admits it** (`TR_RACK_TMHORIZON=both`); the shipped
rack is Pattern-only, so these knobs are inert by default.
| variable | default | what it does |
|---|---|---|
| `TR_TMHORIZON_LOG` | off | value-based; `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. Clamped `2..4096`; overrides the `-d:TMH_NSTATES` compile-time value |
| `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. **ON is MEASURED HARMFUL live: 26.5% vs 49.0% round wins, p=0.036 — keep it at `0`.** (Offline it was +9.3pp late accuracy.) |
| `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 | value-based; `1` = log the rolling accuracy periodically |
| `TR_TMHORIZON_RETRAIN_EVERY` | `50` | samples between windowed retrains (clamped ≥1) |
| `TR_TMHORIZON_EPOCHS` | `1` | epochs per windowed retrain (clamped ≥1) |
Defaults in `tm_horizon.nim:126-152,381-412`.
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.**
**Measured:** no TM configuration beats the shipped rack — N=2 42.9%, N=8 42.9%,
N=64 53.1% (all p>=0.8 vs Pattern), N+window 26.5%. Low-inertia `NSTATES=2` does
**not** help (contrary to an earlier offline trend).
## 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 |
(`pattern_matcher.nim:26-27`.) 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. **MEASURED: +68% tick rate** (87→146 ticks/s); `0` restores the old behaviour |
| `TR_ENV_REPORT` | `1` | print the boot-time `[env]` report to stdout; `0` suppresses it (value-based) |
| `TR_RECORD_WORLDSTATE` | off | presence-based; append every tick's WorldState to `/tmp/worldstate_record.jsonl` for offline replay |
| `TR_RADAR_FORCE_SPIN` | off | presence-based; force the old stateless full-spin melee radar (for A/B on one binary) |
| `TR_RADAR_SCANLOG` | off | presence-based; append per-round radar scan/coverage JSON |
| `TR_RADAR_SCAN_LOG_PATH` | `/tmp/radar_scan_log.jsonl` | where that goes |
| `TR_TRACKER_PROBE` | off | presence-based; per-tick enemy tracker vs server enemy count |
| `TR_TRACKER_PROBE_PATH` | `/tmp/tracker_probe.jsonl` | where that goes |
The **adaptive-melee radar** has no env knobs. Its tuning lives in compile-time
constants in `radars/adaptive_melee_radar.nim:36-50`: `MaxRadarTurnRate=45`,
`FreshnessTicks=16`, `FreshStreakTicks=3`, `MarginDeg=20`,
`EnterTrackWidthDeg=270`, `ExitTrackWidthDeg=300`. Only
`TR_RADAR_FORCE_SPIN`/`TR_RADAR_SCANLOG` are runtime.
## 9. Test harness only (not read by the bot itself)
| variable | default | what it does |
|---|---|---|
| `TR_SERVER_JAR` | `/home/davide/Downloads/robocode-tankroyale/robocode-tankroyale-server.jar` (1.3.1) | which server the tests launch. Set to the 0.35.5 jar (`server_manager.LegacyServerJar`) to reproduce old numbers |
| `TR_BATTLE_RUNNER` | `/home/davide/Projects/tank-royale/runner/examples/lib/robocode-tankroyale-runner.jar` | which runner the tests launch (`runner_process.nim`) |
| `TR_BATTLE_RUNNER_DIR` | `tools/battle_runner/` | class directory form of the runner |
(`TR_SAMPLE_BOTS` is read by `SNNBot_garage/tests/test_bullet_economy.nim` only — not
by the ModularBot test harness.)
## 10. Compile-time knobs (`-d:` flags, need a rebuild)
| flag | default | file / effect |
|---|---|---|
| `-d:TMH_NSTATES=N` | 64 | `guns/tm_horizon.nim:102` — automata inertia (the runtime `TR_TMHORIZON_NSTATES` overrides it) |
| `-d:TMH_NCLAUSES=N` | 40 | `guns/tm_horizon.nim:101` |
| `-d:TMH_S_DEF="x"` | `"3.0"` | `guns/tm_horizon.nim:103` specificity (`s`) |
| `-d:TMH_MIN_OBS=N` | 24 | `guns/tm_horizon.nim:106` — cold gate: below this many samples the TM emits no correction |
| `-d:TMH_STALE_MAX=N` | 8 | `guns/tm_horizon.nim:108` — a sample is dropped if the enemy was not seen this recently |
| `-d:TM_CLASSES=N` | 5 | `guns/tm_pattern.nim:50` — GF buckets |
| `-d:TM_NCLAUSES=N` | 40 | `guns/tm_pattern.nim:53` — clauses per class |
| `-d:TM_NSTATES=N` | 64 | `guns/tm_pattern.nim:55` |
| `-d:TM_S_DEF="x"` | `"3.0"` | `guns/tm_pattern.nim:57` specificity (`s`). **Higher = LONGER clauses**, measured. `s=1.0` is degenerate |
| `-d:TM_MIN_OBS=N` | 24 | `guns/tm_pattern.nim:59` |
| `-d:TM_CONF_MARGIN_DEF` / `TM_SHRINK_DEF` / `TM_GF_MODE` / `TM_SOFT_BETA_DEF` / `TM_RADIAL_RANGE_DEF` / `TM_RAD_MARGIN_DEF` / `TM_REV_TURN_DEG_DEF` / `TM_REV_MARGIN_DEF` / `TM_REV_GAIN_DEF` | see `tm_pattern.nim:62-96` | strdefines, compile-time only |
| `-d:TM_WINDOW_SIZE=N` | 10 | `guns/tsetlin.nim:29` |
| `-d:TM_N_CLAUSES=N` | 50 | `guns/tsetlin.nim:116` — clauses per output |
| `-d:TM_N_STATES=N` | 32 | `guns/tsetlin.nim:118` — automaton range `[-N..N]` |
| `-d:TM_S_DEF="x"` | `"1.5"` | `guns/tsetlin.nim:119` — **same flag name as `tm_pattern`; one `-d` sets both** |
| `-d:TM_T_DEF` | `""` (→ `float(TM_HALF)`) | `guns/tsetlin.nim:120` |
## Names that look real but do nothing
| name | reality |
|---|---|
| `TMH_NSTATES` (as an env var) | not an env var. The runtime var is `TR_TMHORIZON_NSTATES` |
| `TR_VBULLET_METRIC` | does not exist. The metric env var is `GUN_VBULLET_METRIC` |
| `TR_POWER_LOW_ENERGY` | removed (replaced by the energy slope). Only referenced in a comment; `getEnv` never reads it |
| `TR_TRACKER_RECONCILE` | printed/set by `common_libs/tests/measure_corpse_melee.nim` only; **no bot module reads it** |
## 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.