# Environment variable reference Every knob the bot reads. **All are read ONCE at process start.** > **The bot is spawned by the server (or by the GUI if it starts its own server).** > So the variable must be set on **that** process, not in an unrelated terminal. > Export it in the shell that launches the server/GUI. An unset (or empty, or unparseable) value means **the shipped default** — a typo can never silently change behaviour, it warns on stderr and falls back. Every process prints a one-shot, greppable boot report to stdout; see [Did my env vars actually reach the bot?](#did-my-env-vars-actually-reach-the-bot) below. `TR_ENV_REPORT=0` suppresses it. --- ## Did my env vars actually reach the bot? **The spawn trap:** the bot is spawned by the **server/GUI**, so it inherits the **server's** environment — exporting a variable in an unrelated terminal does nothing, because that terminal is not the bot's parent. ### 1. Ask the bot itself (boot-time report) Every process prints a one-shot, greppable report to **stdout** (`/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 three parts, all behind `[env] === ENVIRONMENT ... ===`: - **A. raw process environment** — every `TR_*`/`GUN_*` this process *actually* received, sorted, followed by `raw: N TR_*/GUN_* of M total environment variables`. It then prints the process identity: `pid`/`ppid`, the cwd, the bot's own command line, and the **parent's command line**. The parent command line is the conclusive proof of which process spawned the bot (the server/GUI — not your shell). If nothing matched it prints a loud `[env] WARNING: this process has NO TR_*/GUN_* variables - the env you exported did NOT reach the bot.` - **B. effective values** — for every knob, the value the bot will *actually* use and whether it came from `env` or the shipped `default`, including the effects of parsing, clamping and fallback (e.g. `TR_TMHORIZON_NSTATES` is clamped to `2..4096`; `TR_RACK_*` shows the empty-set fallback). Where a value is only resolvable later, it is labelled `(raw - not resolved here)` rather than guessed. - **build identity** — `NimVersion`, compile date/time, and the binary path + size + mtime, so a stale binary is obvious. Suppress the report with `TR_ENV_REPORT=0`. ### 2. The no-code check (authoritative) `/proc/PID/environ` is the environment **at exec time** — exactly what the bot started with, before it read anything: ```sh for p in $(pgrep -f ModularBot); do echo "== $p" tr '\0' '\n' < /proc/$p/environ | grep -E '^(TR_|GUN_)' done ``` ### 3. 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 ``` ### 4. 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. --- ## The ones you'll actually use live | variable | default | what it does | |---|---|---| | `TR_MOVEMENT` | `tfil` | movement engine: `tfil` (shipped) or `tfil_ring` (range-weighted variant) | | `TR_RACK_` | see below | move a gun in/out of the rack per mode | | `TR_POWER_POLICY` | `1` | `0` = uncapped control arm (today's behaviour without the energy policy) | | `TR_POWER_LOG` | off | `1` = log each power decision with its reason | | `TR_RAM_LOG` | off | `1` = log ram on/off with the reason | | `TR_MOVEMENT_LOG` | off | `1` = log movement band/class changes | | `TR_TMHORIZON_LOG` | off | `1` = let the horizon TM gun log its thinking per shot | | `TR_ENV_REPORT` | `1` | print the boot-time `[env]` process-environment report to stdout; `0` suppresses it | --- ## 1. Gun rack — which gun(s) may be chosen | variable | default | what it does | |---|---|---| | `TR_RACK_` | `PATTERN=both`, all others `off` | membership: `both` \| `1v1` \| `melee` \| `off` | | `GUN_RACK_DISABLE` | empty | comma-separated gun **ids** to remove entirely (older mechanism; the gun never even spawns virtual bullets) | | `GUN_STATS_PATH` | `/tmp/gun_stats.jsonl` | where the per-round per-gun stats are written | | `GUN_SHOTLOG_PATH` | `/tmp/shot_log.jsonl` | where the per-shot JSONL (Task A) is written | `` names: `HEADON LINEAR TSETLIN CIRCULAR GUESSFACTOR PATTERN WALLBOUNCE ACCEL STOPSHOT DISPLACE AVGLEAD DECAYGF KNN TMSELECT TMPATTERN TMHORIZON`. **The shipped default is Pattern only.** To restore the full old rack: ```sh TR_RACK_PATTERN=both TR_RACK_HEADON=both TR_RACK_LINEAR=both TR_RACK_TSETLIN=both \ TR_RACK_CIRCULAR=both TR_RACK_GUESSFACTOR=both TR_RACK_WALLBOUNCE=both \ TR_RACK_ACCEL=both TR_RACK_STOPSHOT=both TR_RACK_DISPLACE=both TR_RACK_AVGLEAD=both \ TR_RACK_DECAYGF=both TR_RACK_KNN=both TR_RACK_TMSELECT=both ./out/ModularBot ``` To force one gun alone: `TR_RACK_PATTERN=off TR_RACK_TMHORIZON=both`. If **every** gun is `off`, the rack falls back to admitting all of them. ## 2. The selector (virtual-bullet fitness) | variable | default | what it does | |---|---|---| | `GUN_VBULLET_METRIC` | `path` | how a virtual bullet is scored: `path` (swept ray — the better coarse signal, 7.4% vs 4.7% real) or `point` (arrival accuracy) | | `GUN_SELECTOR_MODE` | `relative` | tie-band model: `relative` (scale-aware) or `absolute` (legacy fixed bars) | | `GUN_SELECTOR_WINDOW` | `100` | rolling window (ticks) for the fitness rates | | `GUN_SELECTOR_TIE` | `0.20` | tie band width: tied if `rate >= bestRate*(1-this)` | | `GUN_SELECTOR_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) | | `GUN_SELECTOR_DWELL` | `10` | ticks the incumbent gun is held before it may be displaced | | `GUN_SELECTOR_MARGIN` | `0.05` | a challenger must beat the incumbent by this fraction to displace it | | `GUN_SELECTOR_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 | **Note:** the per-tick random draw inside the tie band is **load-bearing** — replacing it with commitment to the virtual-best measurably *lowered* real hit rate (7.02% → 5.10%, p=0.002). Three attempts to "smarten" the band all failed. ## 3. Power policy (energy economy) | variable | default | what it does | |---|---|---| | `TR_POWER_POLICY` | `1` | `0` = uncapped control arm. **Measured: turning it off drops real hit rate 10.6%→7.9%** | | `TR_POWER_FAR_DIST` | `200` px | beyond this = "bad chances zone" | | `TR_POWER_FAR_CAP` | `1.0` | power cap beyond that distance | | `TR_POWER_MID_CAP` | `2.0` | cap when close + healthy but chances are not above average | | `TR_POWER_REF` | `0.0` | reference probability: `0` = the gun's own mean, `>0` = a fixed threshold | | `TR_POWER_ENERGY_HI` | `80` | at/above this energy, no energy-slope cap | | `TR_POWER_ENERGY_LO` | `20` | at/below this energy, cap = `ENERGY_MIN` | | `TR_POWER_ENERGY_MIN` | `0.5` | the cap at/below `ENERGY_LO` | | `TR_POWER_ENERGY_MAX` | `3.0` | the cap at/above `ENERGY_HI` (3.0 = effectively uncapped) | | `TR_POWER_FINISH_KILL` | `1` | cap power at the **smallest bullet that can still kill** the enemy | | `TR_POWER_LOG` | off | `1` = log each decision: `[power] p=… cap=… reason=far\|energySlope\|finishKill\|belowAvg\|full\|ram` | WHY the slope: bullet speed is `20-3p`, so **lower power = faster bullet** (less lead error, higher hit chance), fires more often (`10+2p` ticks) and drains energy more slowly (`p`/shot). `E[dE] = p(3P-1)`, so break-even is **1/3 regardless of power** — and our measured hit rates are 5–27%, far below it. WHY the finishing cap: server 1.3.1 caps the damage **score** at the energy actually removed, so **overkill scores nothing**. Damage is `4p` (p≤1) / `6p-2` (p>1), so the smallest killing power is `E/4` for E≤4 and `(E+2)/6` for 4