Files
SirRoboGarage/docs/env_reference.md
T

32 KiB
Raw Blame History

Environment variable reference

Putting your settings in a .env file

Instead of typing export ... in a shell, put your settings in a file. The bot reads them for you. This is easier to keep tidy, and no stale shell variable can surprise you.

  1. In the bot folder, copy .env.example to .env.
  2. Open .env and set the knobs you want. One KEY=VALUE per line. Lines starting with # are comments.
  3. Start the bot from the bot folder (./ModularBot). It picks up .env automatically.
  4. To use a different file, pass it on the command line: ./out/ModularBot --env-file /path/to/my.env.
  5. If you ask for a file that does not exist, the bot stops with an error. A missing default .env is fine and its absence is silent.

The file wins over the shell. If the same name is set in both places with a different value, the file value is used, and the bot prints one line telling you which shell value was overridden. Start the report check with: grep '^\[env\]' /tmp/modularbot_stdout.log. Values that came from the file are labelled (source: .env).


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?.

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:

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:

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:

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:

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:

export TR_POWER_ENERGY_MIN=1.0

Terminal B — where you launch the GUI:

./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 strafe movement engine: strafe (DEFAULT — the measured champion), tfil (long-shipped; explicit override) or tfil_ring (range-weighted variant; any other value falls back to the tfil mover)
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

Module on/off switches — the [modules] boot list

Every module appears once in the boot report, so one command shows the state of them all right now:

grep '^\[modules\]' /tmp/modularbot_stdout.log

The new switches all share the prefix TR_MODULE_. A module that already had an off switch keeps its old name (no duplicate knobs). "OFF" always means the simpler fallback, never a broken bot.

switch ON means OFF falls back to
TR_MODULE_VBULLETS guns predict and score virtual bullets so the selector can rank them (default) no virtual bullets; the selector has no fitness and fires its floor gun (the first gun the rack admits)
TR_MODULE_RAM the ram decider may start a ram: finisher, desperation and the optional triggers (default) the bot never rams; the movement engine drives every tick
TR_MODULE_MOVE_TFIL the tfil / lava-field engine may be selected (default) it is skipped when picking the effective engine
TR_MODULE_MOVE_TFIL_RING the range-weighted ring engine may be selected (default) same — skipped in the fallback
TR_MODULE_MOVE_STRAFE the perpendicular-strafe engine may be selected (default) same — skipped in the fallback
TR_MODULE_MOVE_SURF the wave-surfer engine may be selected (default) same — skipped in the fallback
TR_MODULE_MOVE_LEARNED the learned danger engine may be selected (default) same — skipped in the fallback

There is no "no movement". If the engine named by TR_MOVEMENT is switched off, the effective engine is the first engine still on, in the order tfil, tfil_ring, strafe, surf, learned. If all of them are off the bot still runs tfil.

The modules that already had a switch — surfaced in [modules] under that one name, with no new knob:

switch ON means OFF falls back to
TR_RACK_<GUN> = both/1v1/melee that gun may be selected TR_RACK_<GUN>=off: the gun is removed from the rack
TR_POWER_POLICY energy-aware power caps (default) uncapped: the gun's own preferred power
TR_FIRE_FIX the corrected enemy-fire detector (default) the shipped prev - energy detector
TR_RADAR_FORCE_SPIN force the old stateless full-spin melee radar (off by default) the adaptive arc-narrowing radar (default)
TR_TFIL_HEAT_TIME time-indexed bullet heat (off by default) flat, time-independent heat (default)
TR_VBULLET_DEBUG draw the virtual-bullet overlay (off by default) nothing drawn
TR_GEO_DEBUG draw the geometry overlay (off by default) nothing drawn
TR_DEBUG_DRAW per-mover debug graphics (default) the movers draw nothing
TR_STRAFE_HEAT_GRID draw the full heat grid in STRAFE (default) the heat grid is hidden

Always on, listed in [modules] but with no switch, because the bot cannot work without them: the enemy tracker, target selection, the gun selector and the 1v1 radar lock.


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:

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

CORRECTION (gate v2, 2026-09-26). The default was flipped from tfil to strafe after a pre-registered fresh-data confirmation (docs/movement_campaign.md, "Fresh-data confirmation (gate v2)"): 300 fresh battles (2 arms × 15 opponents × 10 runs × 3 rounds) gave strafe +0.30 wins/run over tfil, 95% CI [+0.02, +0.58], sign-flip permutation p = 0.045. TR_MOVEMENT=tfil remains a fully working explicit override. The earlier gate-v1 confirmation had failed on the plain sign-test leg and did not flip the default; that history is unchanged in the ledger.

variable default what it does
TR_MOVEMENT strafe strafe (DEFAULT — measured champion), tfil (long-shipped; explicit override) or tfil_ring (the range-weighted variant; any other value falls back to the tfil mover, 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
TR_VBULLET_DEBUG off presence-based; overlay the virtual bullets in the GUI debug graphics (see below)
TR_VBULLET_DEBUG_GUN selected gun all/* for every gun, or a gun name (e.g. Pattern); unset = only the currently selected gun
TR_VBULLET_DEBUG_MAX 32 cap on bullets drawn per tick

The virtual-bullet overlay (TR_VBULLET_DEBUG, off by default) makes the selector's training signal visible. Turn it on with:

TR_VBULLET_DEBUG=1 TR_VBULLET_DEBUG_GUN=all ./out/ModularBot
  • travelled path — thin line from the fire point along the fire direction, as far as the bullet has actually flown.
  • aim ring — small hollow circle at the predicted target position.
  • miss vector — line from the aim ring to the closest-approach point; a filled dot there is a HIT, a hollow ring a MISS (the score the selector sees).
  • colour per gun is the SAME table as the turret (vbullet_draw.gunColors), with a one-line legend in the top-left corner.

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

# 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.