Boot-time env report + fix the env reference
The bot is spawned by the server/GUI, so it inherits the SERVER's environment. The user could not tell whether their exports reached the bot, so print a one-shot greppable report at boot: grep '^\[env\]' /tmp/modularbot_stdout.log Section A prints every TR_*/GUN_* this process actually received, the count vs the total env size, a loud warning when nothing matched, and the process identity (pid/ppid, cwd, self command line, and the PARENT command line) so the spawn trap is obvious. Section B prints the resolved effective value of every documented knob with its source (env|default), including clamps and the rack's empty-set fallback. Build identity (NimVersion, compile date/time, binary path/size/mtime) pins the exact artifact. Suppress with TR_ENV_REPORT=0. docs/env_reference.md: add the missing GUN_SHOTLOG_PATH, GUN_SELECTOR_MINOBS/FLOOR/POOL/RANK/SHRINK/SEED, TR_ENV_REPORT and -d:TM_NCLAUSES; record the measured TR_TMHORIZON_WINDOW verdict; and add a prominent 'Did my env vars actually reach the bot?' section with the boot report, the /proc/PID/environ no-code check, the correct GUI launch recipe, and how to prove the trap deliberately.
This commit is contained in:
@@ -36,6 +36,7 @@ import movement_harness/virtual_bodies as mvb
|
||||
import movement_harness/bullet_shadows
|
||||
import targeting/enemy_tracker
|
||||
import targeting/target_selector
|
||||
import env_report
|
||||
|
||||
const botJsonPath = currentSourcePath().parentDir / "ModularBot.json"
|
||||
const DebugVBullets = false
|
||||
@@ -1261,4 +1262,23 @@ when isMainModule:
|
||||
# Must run AFTER the gun constructors (Tsetlin/TMSelector call randomize()
|
||||
# unconditionally) so the explicit seed override is the value that survives.
|
||||
seedSelectorRng()
|
||||
# Boot-time environment report (once per process, stdout). Prints what env
|
||||
# this process actually inherited, the resolved effective value of every
|
||||
# documented knob, and the build identity behind `grep '^\[env\]'`.
|
||||
printEnvReport(EnvReportContext(
|
||||
movementName: MovementName,
|
||||
vBulletAdmitOnly: VBulletAdmitOnly,
|
||||
recordWorldState: RecordWorldState,
|
||||
radarForceSpin: RadarForceSpin,
|
||||
radarScanLog: RadarScanLog,
|
||||
radarScanLogPath: RadarScanLogPath,
|
||||
trackerProbe: TrackerProbe,
|
||||
trackerProbePath: TrackerProbePath,
|
||||
powerLog: PowerLog,
|
||||
gunStatsPath: GunStatsPath,
|
||||
shotLogPath: ShotLogPath,
|
||||
disabledGuns: DisabledGuns,
|
||||
tmHorizon: bot.tmHorizon,
|
||||
patternMatcher: bot.patternMatcher,
|
||||
))
|
||||
start(bot, botJsonPath)
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
## Boot-time environment report for ModularBot.
|
||||
##
|
||||
## The user runs the bot behind the SERVER/GUI, so the bot inherits the
|
||||
## SERVER's environment — not the environment of whatever terminal they
|
||||
## exported a variable in. This report answers, once per process at boot and
|
||||
## in stdout, two questions:
|
||||
##
|
||||
## A. What TR_*/GUN_* variables did THIS process actually receive? (plus the
|
||||
## process identity — pid, parent pid, parent command line — so "I exported
|
||||
## it in the wrong terminal" is obvious at a glance).
|
||||
## B. For every documented knob, what value will the bot actually use and did
|
||||
## it come from the env or from the shipped default?
|
||||
##
|
||||
## Every line starts with `[env]` so the whole interface is:
|
||||
##
|
||||
## grep '^\[env\]' /tmp/modularbot_stdout.log
|
||||
##
|
||||
## It runs once (idempotent guard) and can be suppressed with TR_ENV_REPORT=0.
|
||||
## It deliberately does NOT dump the whole environment: only TR_*/GUN_* are
|
||||
## printed, because unrelated variables may hold secrets or machine paths.
|
||||
|
||||
import std/[os, strutils, algorithm, times, sets]
|
||||
import gun_harness/virtual_bullets
|
||||
import gun_harness/selector
|
||||
import movements/ram_decision
|
||||
import movements/the_floor_is_lava_ring
|
||||
import guns/tm_horizon
|
||||
import guns/pattern_matcher
|
||||
|
||||
const EnvReportEnableEnvVar* = "TR_ENV_REPORT"
|
||||
|
||||
# ── resolved values that live in ModularBot.nim (not importable here) ─────────
|
||||
|
||||
type
|
||||
EnvReportContext* = object
|
||||
## Everything the report needs that ModularBot owns. Values are the
|
||||
## RESOLVED process-start values, not re-reads, so the report cannot
|
||||
## diverge from what the bot will actually use.
|
||||
movementName*: string
|
||||
vBulletAdmitOnly*: bool
|
||||
recordWorldState*: bool
|
||||
radarForceSpin*: bool
|
||||
radarScanLog*: bool
|
||||
radarScanLogPath*: string
|
||||
trackerProbe*: bool
|
||||
trackerProbePath*: string
|
||||
powerLog*: bool
|
||||
gunStatsPath*: string
|
||||
shotLogPath*: string
|
||||
disabledGuns*: HashSet[int]
|
||||
tmHorizon*: TmHorizonGun
|
||||
patternMatcher*: PatternMatcherGun
|
||||
|
||||
# ── helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
proc onOff(b: bool): string {.inline.} = (if b: "on" else: "off")
|
||||
|
||||
proc metricName(m: BulletMetric): string =
|
||||
case m
|
||||
of bmPoint: "point"
|
||||
of bmPath: "path"
|
||||
|
||||
proc selectorModeName(m: SelectorMode): string =
|
||||
case m
|
||||
of smAbsolute: "absolute"
|
||||
of smRelative: "relative"
|
||||
|
||||
proc rankName(r: RankStat): string =
|
||||
## The word `GUN_SELECTOR_RANK` accepts on the command line, not the enum.
|
||||
case r
|
||||
of rsMean: "mean"
|
||||
of rsWilson: "wilson"
|
||||
of rsUCB: "ucb"
|
||||
of rsThompson: "thompson"
|
||||
of rsShrunk: "shrunk"
|
||||
|
||||
proc tieBreakName(t: TieBreakMode): string =
|
||||
case t
|
||||
of tbOff: "off"
|
||||
of tbPoint: "point"
|
||||
of tbPointCommit: "pointCommit"
|
||||
|
||||
proc sourceOf(name: string): string =
|
||||
## Parsed-value knobs go through `getEnv(name, "")`; an empty value means the
|
||||
## shipped default, so a non-empty value is the only thing that counts as
|
||||
## "from the env".
|
||||
if getEnv(name, "").len > 0: "env" else: "default"
|
||||
|
||||
proc sourceOfPresence(name: string): string =
|
||||
## Flags read with `existsEnv` are "from the env" whenever they are present,
|
||||
## even if their value is "0" (presence is what the code tests).
|
||||
if existsEnv(name): "env" else: "default"
|
||||
|
||||
proc disabledGunsString(guns: HashSet[int]): string =
|
||||
if guns.len == 0: return "none"
|
||||
var ids: seq[int]
|
||||
for id in guns: ids.add id
|
||||
ids.sort()
|
||||
for i, id in ids:
|
||||
if i > 0: result.add ","
|
||||
result.add $id
|
||||
|
||||
proc emit(name, value, source: string) =
|
||||
echo "[env] ", name, " = ", value, " (source: ", source, ")"
|
||||
|
||||
proc readProcFile(path: string): string =
|
||||
## Best-effort /proc read; never raises, returns "" when unavailable.
|
||||
try: readFile(path)
|
||||
except CatchableError: ""
|
||||
|
||||
proc ppidFromStatus(): int =
|
||||
for line in readProcFile("/proc/self/status").splitLines():
|
||||
if line.startsWith("PPid:"):
|
||||
let parts = line.splitWhitespace()
|
||||
if parts.len >= 2:
|
||||
try: return parseInt(parts[1])
|
||||
except ValueError: return -1
|
||||
-1
|
||||
|
||||
proc cmdlineOf(pid: int): string =
|
||||
## NUL-separated argv from /proc/<pid>/cmdline, flattened for display.
|
||||
if pid < 0: return "(unavailable)"
|
||||
let raw = readProcFile("/proc/" & $pid & "/cmdline")
|
||||
if raw.len == 0: return "(unavailable)"
|
||||
result = raw.replace('\0', ' ').strip()
|
||||
if result.len == 0: result = "(available but empty)"
|
||||
|
||||
proc selfCmdline(): string =
|
||||
let raw = readProcFile("/proc/self/cmdline")
|
||||
if raw.len == 0:
|
||||
return getAppFilename()
|
||||
result = raw.replace('\0', ' ').strip()
|
||||
if result.len == 0: result = getAppFilename()
|
||||
|
||||
# ── section A: the raw process environment + process identity ────────────────
|
||||
|
||||
proc printRawEnvironment() =
|
||||
echo "[env] --- A. raw process environment (what THIS process really has) ---"
|
||||
|
||||
var rawVars: seq[(string, string)]
|
||||
var total = 0
|
||||
for key, val in envPairs():
|
||||
inc total
|
||||
if key.startsWith("TR_") or key.startsWith("GUN_"):
|
||||
rawVars.add (key, val)
|
||||
rawVars.sort(proc(a, b: (string, string)): int = cmp(a[0], b[0]))
|
||||
|
||||
for (key, val) in rawVars:
|
||||
echo "[env] ", key, "=", val
|
||||
|
||||
echo "[env] raw: ", rawVars.len, " TR_*/GUN_* of ", total,
|
||||
" total environment variables"
|
||||
|
||||
if rawVars.len == 0:
|
||||
echo "[env] WARNING: this process has NO TR_*/GUN_* variables - the env you " &
|
||||
"exported did NOT reach the bot. See docs/env_reference.md."
|
||||
|
||||
# Process identity. The parent is the process that SPAWNED the bot: while
|
||||
# running under the GUI it is the SERVER/GUI, never the user's interactive
|
||||
# shell — which is exactly why exporting in an unrelated terminal does nothing.
|
||||
let pid = getCurrentProcessId()
|
||||
let ppid = ppidFromStatus()
|
||||
echo "[env] pid=", pid, " ppid=", ppid
|
||||
echo "[env] cwd=", getCurrentDir()
|
||||
echo "[env] self_cmd=", selfCmdline()
|
||||
echo "[env] parent_cmd=", cmdlineOf(ppid)
|
||||
|
||||
# ── section B: effective values ──────────────────────────────────────────────
|
||||
|
||||
proc printEffectiveValues(ctx: EnvReportContext) =
|
||||
echo "[env] --- B. effective values (resolved at boot) ---"
|
||||
|
||||
# ── movement / process-level ModularBot knobs ─────────────────────────────
|
||||
# Only `tfil_ring` selects the ring engine; every other value runs `tfil`, so
|
||||
# report the RESOLVED engine rather than the raw string.
|
||||
let effectiveMovement = if ctx.movementName == "tfil_ring": "tfil_ring" else: "tfil"
|
||||
emit("TR_MOVEMENT", effectiveMovement, sourceOf("TR_MOVEMENT"))
|
||||
emit("TR_MOVEMENT_LOG", onOff(MovementLog), sourceOfPresence("TR_MOVEMENT_LOG"))
|
||||
emit("TR_VBULLET_ADMIT_ONLY", onOff(ctx.vBulletAdmitOnly),
|
||||
sourceOf("TR_VBULLET_ADMIT_ONLY"))
|
||||
emit("TR_POWER_LOG", onOff(ctx.powerLog), sourceOfPresence("TR_POWER_LOG"))
|
||||
emit("TR_RECORD_WORLDSTATE", onOff(ctx.recordWorldState),
|
||||
sourceOfPresence("TR_RECORD_WORLDSTATE"))
|
||||
emit("TR_RADAR_FORCE_SPIN", onOff(ctx.radarForceSpin),
|
||||
sourceOfPresence("TR_RADAR_FORCE_SPIN"))
|
||||
emit("TR_RADAR_SCANLOG", onOff(ctx.radarScanLog),
|
||||
sourceOfPresence("TR_RADAR_SCANLOG"))
|
||||
emit("TR_RADAR_SCAN_LOG_PATH", ctx.radarScanLogPath,
|
||||
sourceOf("TR_RADAR_SCAN_LOG_PATH"))
|
||||
emit("TR_TRACKER_PROBE", onOff(ctx.trackerProbe),
|
||||
sourceOfPresence("TR_TRACKER_PROBE"))
|
||||
emit("TR_TRACKER_PROBE_PATH", ctx.trackerProbePath,
|
||||
sourceOf("TR_TRACKER_PROBE_PATH"))
|
||||
emit("GUN_STATS_PATH", ctx.gunStatsPath, sourceOf("GUN_STATS_PATH"))
|
||||
emit("GUN_SHOTLOG_PATH", ctx.shotLogPath, sourceOf("GUN_SHOTLOG_PATH"))
|
||||
let seed = getEnv("GUN_SELECTOR_SEED", "")
|
||||
if seed.len > 0:
|
||||
emit("GUN_SELECTOR_SEED", seed, "env")
|
||||
else:
|
||||
emit("GUN_SELECTOR_SEED", "(unset -> time+pid)", "default")
|
||||
|
||||
# ── rack ──────────────────────────────────────────────────────────────────
|
||||
emit("GUN_RACK_DISABLE", disabledGunsString(ctx.disabledGuns),
|
||||
sourceOf("GUN_RACK_DISABLE"))
|
||||
for i in 0..<len(RackGunNames):
|
||||
let name = "TR_RACK_" & RackGunNames[i]
|
||||
emit(name, rackMembershipName(ActiveRackMembership[i]), sourceOf(name))
|
||||
echo "[env] rack active 1v1 = ", rackActive(ActiveRackMembership, rm1v1)
|
||||
echo "[env] rack active melee = ", rackActive(ActiveRackMembership, rmMelee)
|
||||
|
||||
# ── the selector (virtual-bullet fitness) ─────────────────────────────────
|
||||
emit("GUN_VBULLET_METRIC", metricName(ActiveMetric), sourceOf("GUN_VBULLET_METRIC"))
|
||||
emit("GUN_SELECTOR_MODE", selectorModeName(ActiveSelectorMode), sourceOf("GUN_SELECTOR_MODE"))
|
||||
emit("GUN_SELECTOR_WINDOW", $ActiveWindow, sourceOf("GUN_SELECTOR_WINDOW"))
|
||||
emit("GUN_SELECTOR_MINOBS", $ActiveMinObs, sourceOf("GUN_SELECTOR_MINOBS"))
|
||||
emit("GUN_SELECTOR_TIE", $ActiveRelTie, sourceOf("GUN_SELECTOR_TIE"))
|
||||
emit("GUN_SELECTOR_FLOOR", $ActiveFloorFrac, sourceOf("GUN_SELECTOR_FLOOR"))
|
||||
emit("GUN_SELECTOR_POOL", onOff(ActivePooled), sourceOf("GUN_SELECTOR_POOL"))
|
||||
emit("GUN_SELECTOR_RANK", rankName(ActiveRank), sourceOf("GUN_SELECTOR_RANK"))
|
||||
emit("GUN_SELECTOR_SHRINK", $ActiveShrink, sourceOf("GUN_SELECTOR_SHRINK"))
|
||||
emit("GUN_SELECTOR_DWELL", $ActiveDwellTicks, sourceOf("GUN_SELECTOR_DWELL"))
|
||||
emit("GUN_SELECTOR_MARGIN", $ActiveSwitchMargin, sourceOf("GUN_SELECTOR_MARGIN"))
|
||||
emit("GUN_SELECTOR_TIEBREAK", tieBreakName(ActiveTieBreak), sourceOf("GUN_SELECTOR_TIEBREAK"))
|
||||
emit("GUN_SELECTOR_POINT_TIE", $ActivePointTie, sourceOf("GUN_SELECTOR_POINT_TIE"))
|
||||
|
||||
# ── the power policy ──────────────────────────────────────────────────────
|
||||
emit("TR_POWER_POLICY", onOff(PowerPolicyEnabled), sourceOf("TR_POWER_POLICY"))
|
||||
emit("TR_POWER_FAR_DIST", $PowerFarDist, sourceOf("TR_POWER_FAR_DIST"))
|
||||
emit("TR_POWER_FAR_CAP", $PowerFarCap, sourceOf("TR_POWER_FAR_CAP"))
|
||||
emit("TR_POWER_MID_CAP", $PowerMidCap, sourceOf("TR_POWER_MID_CAP"))
|
||||
emit("TR_POWER_REF", $PowerRefFixed, sourceOf("TR_POWER_REF"))
|
||||
emit("TR_POWER_ENERGY_HI", $PowerEnergyHi, sourceOf("TR_POWER_ENERGY_HI"))
|
||||
emit("TR_POWER_ENERGY_LO", $PowerEnergyLo, sourceOf("TR_POWER_ENERGY_LO"))
|
||||
emit("TR_POWER_ENERGY_MIN", $PowerEnergyMin, sourceOf("TR_POWER_ENERGY_MIN"))
|
||||
emit("TR_POWER_ENERGY_MAX", $PowerEnergyMax, sourceOf("TR_POWER_ENERGY_MAX"))
|
||||
emit("TR_POWER_FINISH_KILL", onOff(PowerFinishKill),
|
||||
sourceOf("TR_POWER_FINISH_KILL"))
|
||||
|
||||
# ── movement (TFIL ring) ──────────────────────────────────────────────────
|
||||
emit("TR_TFIL_RANGE_LO", $RangeLo, sourceOf("TR_TFIL_RANGE_LO"))
|
||||
emit("TR_TFIL_RANGE_HI", $RangeHi, sourceOf("TR_TFIL_RANGE_HI"))
|
||||
emit("TR_TFIL_RANGE_TEMP", $RangeTemp, sourceOf("TR_TFIL_RANGE_TEMP"))
|
||||
emit("TR_TFIL_RANGE_K", $RangeK, sourceOf("TR_TFIL_RANGE_K"))
|
||||
emit("TR_TFIL_CORRIDOR_HEAT", $CorridorHeat, sourceOf("TR_TFIL_CORRIDOR_HEAT"))
|
||||
emit("TR_TFIL_WALL_HOTNESS", $WallHotness, sourceOf("TR_TFIL_WALL_HOTNESS"))
|
||||
|
||||
# ── ramming ───────────────────────────────────────────────────────────────
|
||||
emit("TR_RAM_OPPORTUNITY", onOff(RamOppEnabled), sourceOf("TR_RAM_OPPORTUNITY"))
|
||||
emit("TR_RAM_OPP_DIST", $RamOppDist, sourceOf("TR_RAM_OPP_DIST"))
|
||||
emit("TR_RAM_OPP_MARGIN", $RamOppMargin, sourceOf("TR_RAM_OPP_MARGIN"))
|
||||
emit("TR_RAM_ABORT_DMG", $RamAbortDmg, sourceOf("TR_RAM_ABORT_DMG"))
|
||||
emit("TR_RAM_PLAN", onOff(RamPlanEnabled), sourceOf("TR_RAM_PLAN"))
|
||||
emit("TR_RAM_PLAN_DIST", $RamPlanDist, sourceOf("TR_RAM_PLAN_DIST"))
|
||||
emit("TR_RAM_PLAN_MARGIN", $RamPlanMargin, sourceOf("TR_RAM_PLAN_MARGIN"))
|
||||
emit("TR_RAM_PLAN_HITRATE", $RamPlanHitRate, sourceOf("TR_RAM_PLAN_HITRATE"))
|
||||
emit("TR_RAM_LOG", onOff(RamLog), sourceOfPresence("TR_RAM_LOG"))
|
||||
|
||||
# ── the horizon TM gun ────────────────────────────────────────────────────
|
||||
# `resetLearning`/`targetChanged` resolve the lazily-read fields at round
|
||||
# start; force them here on a COPY so the report reads real resolved values
|
||||
# without touching the live gun's state.
|
||||
var tmh = ctx.tmHorizon
|
||||
discard tmh.targetChanged(-1)
|
||||
emit("TR_TMHORIZON_SHIFT", $tmh.shiftDeg, sourceOf("TR_TMHORIZON_SHIFT"))
|
||||
emit("TR_TMHORIZON_BIG_MULT", $tmh.bigMult, sourceOf("TR_TMHORIZON_BIG_MULT"))
|
||||
emit("TR_TMHORIZON_LOG", onOff(tmh.logEnabled), sourceOfPresence("TR_TMHORIZON_LOG"))
|
||||
emit("TR_TMHORIZON_RESET_ON_TARGET", onOff(tmh.resetOnTarget),
|
||||
sourceOf("TR_TMHORIZON_RESET_ON_TARGET"))
|
||||
emit("TR_TMHORIZON_NSTATES", $tmh.nStates, sourceOf("TR_TMHORIZON_NSTATES"))
|
||||
emit("TR_TMHORIZON_WINDOW", $tmh.windowN, sourceOf("TR_TMHORIZON_WINDOW"))
|
||||
emit("TR_TMHORIZON_RESET_DROP", $tmh.resetDrop, sourceOf("TR_TMHORIZON_RESET_DROP"))
|
||||
emit("TR_TMHORIZON_ACCURVE", onOff(tmh.accurveEnabled),
|
||||
sourceOfPresence("TR_TMHORIZON_ACCURVE"))
|
||||
emit("TR_TMHORIZON_RETRAIN_EVERY", $tmh.retrainEvery,
|
||||
sourceOf("TR_TMHORIZON_RETRAIN_EVERY"))
|
||||
emit("TR_TMHORIZON_EPOCHS", $tmh.retrainEpochs, sourceOf("TR_TMHORIZON_EPOCHS"))
|
||||
|
||||
# ── Pattern radial knobs ──────────────────────────────────────────────────
|
||||
# These are resolved lazily inside `predict` (which has not run at boot), so
|
||||
# unless something already forced them we report the raw env value and say so.
|
||||
if ctx.patternMatcher.radConfigured:
|
||||
emit("TR_PATTERN_RAD_OFFSET", $ctx.patternMatcher.radOffset, sourceOf("TR_PATTERN_RAD_OFFSET"))
|
||||
emit("TR_PATTERN_RAD_SCALE", $ctx.patternMatcher.radScale, sourceOf("TR_PATTERN_RAD_SCALE"))
|
||||
else:
|
||||
echo "[env] TR_PATTERN_RAD_OFFSET = ",
|
||||
getEnv("TR_PATTERN_RAD_OFFSET", "0.0"), " (raw - not resolved here)"
|
||||
echo "[env] TR_PATTERN_RAD_SCALE = ",
|
||||
getEnv("TR_PATTERN_RAD_SCALE", "1.0"), " (raw - not resolved here)"
|
||||
|
||||
# ── the report's own switch ───────────────────────────────────────────────
|
||||
emit(EnvReportEnableEnvVar, getEnv(EnvReportEnableEnvVar, "1"),
|
||||
sourceOf(EnvReportEnableEnvVar))
|
||||
|
||||
# ── build identity (which binary is the GUI actually running?) ───────────────
|
||||
|
||||
proc printBuildIdentity() =
|
||||
## The user has been bitten by a stale binary. CompileDate/CompileTime are
|
||||
## available at runtime (even if a reproducible-build toolchain pins them),
|
||||
## and the binary path + mtime + size pin down the exact artifact.
|
||||
echo "[env] --- build identity ---"
|
||||
echo "[env] build nim=", NimVersion,
|
||||
" compileDate=", CompileDate, " compileTime=", CompileTime
|
||||
let bin = getAppFilename()
|
||||
var size = -1
|
||||
var mtime = "unknown"
|
||||
if fileExists(bin):
|
||||
try:
|
||||
size = getFileSize(bin)
|
||||
mtime = $getLastModificationTime(bin)
|
||||
except CatchableError:
|
||||
discard
|
||||
echo "[env] build binary=", bin, " size=", size, " mtime=", mtime
|
||||
|
||||
# ── the entry point ──────────────────────────────────────────────────────────
|
||||
|
||||
var envReportDone = false
|
||||
|
||||
proc printEnvReport*(ctx: EnvReportContext) =
|
||||
## Print the whole report exactly once per process, to stdout. Suppressed
|
||||
## (with one visible note) by TR_ENV_REPORT=0/false/no/off.
|
||||
if envReportDone: return
|
||||
envReportDone = true
|
||||
|
||||
let raw = getEnv(EnvReportEnableEnvVar, "").strip().toLowerAscii()
|
||||
if raw in ["0", "false", "no", "off"]:
|
||||
echo "[env] report suppressed by ", EnvReportEnableEnvVar, "=", raw,
|
||||
" (unset or 1 to enable)"
|
||||
return
|
||||
|
||||
echo "[env] === ENVIRONMENT (boot report) ==="
|
||||
printRawEnvironment()
|
||||
printEffectiveValues(ctx)
|
||||
printBuildIdentity()
|
||||
echo "[env] === END ENVIRONMENT ==="
|
||||
+99
-1
@@ -9,6 +9,94 @@ Every knob the bot reads. **All are read ONCE at process start.**
|
||||
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
|
||||
@@ -22,6 +110,7 @@ can never silently change behaviour, it warns on stderr and falls back.
|
||||
| `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 |
|
||||
|
||||
---
|
||||
|
||||
@@ -32,6 +121,7 @@ can never silently change behaviour, it warns on stderr and falls back.
|
||||
| `TR_RACK_<GUN>` | `PATTERN=both`, all others `off` | membership: `both` \| `1v1` \| `melee` \| `off` |
|
||||
| `GUN_RACK_DISABLE` | empty | comma-separated gun **ids** to remove entirely (older mechanism; the gun never even spawns virtual bullets) |
|
||||
| `GUN_STATS_PATH` | `/tmp/gun_stats.jsonl` | where the per-round per-gun stats are written |
|
||||
| `GUN_SHOTLOG_PATH` | `/tmp/shot_log.jsonl` | where the per-shot JSONL (Task A) is written |
|
||||
|
||||
`<GUN>` names: `HEADON LINEAR TSETLIN CIRCULAR GUESSFACTOR PATTERN WALLBOUNCE ACCEL
|
||||
STOPSHOT DISPLACE AVGLEAD DECAYGF KNN TMSELECT TMPATTERN TMHORIZON`.
|
||||
@@ -56,8 +146,14 @@ If **every** gun is `off`, the rack falls back to admitting all of them.
|
||||
| `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 |
|
||||
|
||||
@@ -130,7 +226,7 @@ only path that converts.
|
||||
| `TR_TMHORIZON_BIG_MULT` | `1.5` | multiplier when the predicted correction is BIG |
|
||||
| `TR_TMHORIZON_NSTATES` | `64` | **automata inertia** — the "mood". Lower = adapts faster, higher = more stubborn (2…4096) |
|
||||
| `TR_TMHORIZON_RESET_ON_TARGET` | on | wipe the model when the target changes (no-op in 1v1) |
|
||||
| `TR_TMHORIZON_WINDOW` | `0` | sliding window: retrain on only the most recent N samples. `0` = keep everything |
|
||||
| `TR_TMHORIZON_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`.** |
|
||||
| `TR_TMHORIZON_RESET_DROP` | `0` | change detection: if rolling accuracy falls this many points below its own peak, treat it as "the enemy changed" and re-learn |
|
||||
| `TR_TMHORIZON_ACCURVE` | off | `1` = log the rolling accuracy periodically |
|
||||
| `TR_TMHORIZON_RETRAIN_EVERY` | `50` | samples between windowed retrains |
|
||||
@@ -156,6 +252,7 @@ bearing-only, so a purely radial offset is invisible. Measured byte-identical on
|
||||
| variable | default | what it does |
|
||||
|---|---|---|
|
||||
| `TR_VBULLET_ADMIT_ONLY` | `1` | only admitted guns spawn virtual bullets. **Turning it on gave +68% tick rate** (87→146 ticks/s); `0` restores the old behaviour |
|
||||
| `TR_ENV_REPORT` | `1` | print the boot-time `[env]` report to stdout; `0` suppresses it |
|
||||
| `TR_RECORD_WORLDSTATE` | off | append every tick's WorldState to `/tmp/worldstate_record.jsonl` for offline replay |
|
||||
| `TR_RADAR_FORCE_SPIN` | off | force the old stateless full-spin melee radar (for A/B on one binary) |
|
||||
| `TR_RADAR_SCANLOG` | off | append per-round radar scan/coverage JSON |
|
||||
@@ -181,6 +278,7 @@ bearing-only, so a purely radial offset is invisible. Measured byte-identical on
|
||||
| `-d:TMH_STALE_MAX=N` | 8 | a sample is dropped if the enemy was not seen this recently |
|
||||
| `-d:TM_S_DEF=x` | `"3.0"` | `guns/tm_pattern.nim` specificity (`s`). **Higher = LONGER clauses**, measured. `s=1.0` is degenerate |
|
||||
| `-d:TM_NSTATES=N` | 64 | `guns/tm_pattern.nim` |
|
||||
| `-d:TM_NCLAUSES=N` | 40 | `guns/tm_pattern.nim` — clauses per class |
|
||||
|
||||
## Adding / finding knobs
|
||||
|
||||
|
||||
Reference in New Issue
Block a user