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.
40 KiB
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.
- In the bot folder, copy
.env.exampleto.env. - Open
.envand set the knobs you want. OneKEY=VALUEper line. Lines starting with#are comments. A trailing comment is allowed and stripped (KEY=0 # off) as long as the#is outside quotes; a value that still contains whitespace or a#after that gets one[dotenv] WARNINGline. - Start the bot from the bot folder (
./ModularBot). It picks up.envautomatically. - To use a different file, pass it on the command line:
./out/ModularBot --env-file /path/to/my.env. - If you ask for a file that does not exist, the bot stops with an error. A
missing default
.envis 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:
- 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; theenvInt/envFloat/envBoolhelpers fall back to the default with no warning at all. - Some flags are presence-based, not value-based. They are read with
existsEnv, soTR_POWER_LOG=0turns the log ON (any value does). Value-based (0/false/offreally 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_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.
# 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.
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_Xmapping holds only forNSTATES.TMH_NCLAUSES,TMH_S_DEF,TMH_MIN_OBS,TMH_STALE_MAXhave no env form — there is noTR_TMHORIZON_NCLAUSESetc. -d:TM_S_DEFis defined in bothtm_pattern.nim(default"3.0") andtsetlin.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_NSTATESis NOT an env var, the env var isTR_TMHORIZON_NSTATES. LikewiseTM_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 as9bf3005). 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 ...): everyTR_*/GUN_*this process actually received, sorted, followed by[env] raw: N TR_*/GUN_* of M total environment variables. IfN == 0it 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_NSTATESis clamped to2..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_RAM_FLOOR_ENERGY |
0.0 |
j160 firing floor: at/below this self energy we start no NEW shot, holding a ram reserve. 0 = off. ~5 = one p=1.0 return hit + two 0.1 shots. Bypassed while ramming |
TR_RAM_ENEMY_ENERGY |
0.0 |
j160 exhaustion trigger: last-scanned enemy energy <= this -> ram mode. 0 = off. Keeps the finisher's energy-surplus and 300px guards |
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_FIRE_LAG = <int> |
back-date every detected enemy fire by N ticks at spawn (0 = shipped; 1 = the measured live detection lag, j147) | n/a — it is a value knob |
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, the 1v1
radar lock, and the Tank Royale body API that actually drives and turns the
tank.
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_MINis a CAP, not a floor. It is the ceiling applied when our energy is at/belowTR_POWER_ENERGY_LO. Setting it to1.0raises 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
tfiltostrafeafter 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) gavestrafe+0.30 wins/run overtfil, 95% CI [+0.02, +0.58], sign-flip permutation p = 0.045.TR_MOVEMENT=tfilremains 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 |
TR_TFIL_RING_COMMIT_ARRIVAL |
off | presence/value: on = hold the committed dodge tile until we are actually ON it, instead of the fixed 5-tick dwell. Default path byte-identical. NEVER LIVE-TESTED |
TR_TFIL_RING_NOREV_SPEED |
0.0 |
px/tick. Below this self speed a mid-flight target switch may not turn the bot around; 0.0 = off (the pre-knob behaviour). NEVER LIVE-TESTED |
Defaults read in the_floor_is_lava_ring.nim:188-196 and :173-175.
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.