Files
SirRoboGarage/ModularBot_garage/src/env_report.nim
T
SirStone 38fbc6ecd1 j152 (default-off): geometry shapes the tile DRAW (TR_TFIL_GEO_MODE/TAU) + the offline sweep with the diversity cost
The owner: choose the tile pool not only from the heat point but from the
geometric position too. Heat stays the hard filter; the draw over the
survivors is re-weighted by turn and/or distance. Three weighting forms
(soft softmax / top-K third / rejection band), one env name carrying both
axes. Default = off = today's uniform draw, byte-for-byte (golden parity).

WHAT DIFFERS FROM j9 (TR_TFIL_TURN_BIAS, a live null): that was a tiebreak
weight among the non-empty safe set only. This runs on the WHOLE pool the
draw already runs on, including the 2 promoted least-hot tiles the ~65%
forced picks choose from.

OFFLINE (8 fixtures x 3 seeds, no java): headline 'tile actually reached at
tta' 4.5% -> 16.9% at TR_TFIL_GEO_MODE=both-soft TR_TFIL_GEO_TAU=45, with
no diversity collapse (distinct tiles 220 -> 219, normalised entropy
0.87 -> 0.86, top-tile share 7.0% -> 8.6%). perpE — the perpendicular rate
in the FORCED population — does not move in ANY arm: that pool is 2 tiles
ranked by heat alone and the only lever is a coin flip.

Also registers j150's TR_TFIL_DIAG / TR_TFIL_DANGER_THRESHOLD in
env_report + knownEnvNames + .env.example (test_env_report was red) and
documents DANGER_THRESHOLD as quantisation-limited: heat comes in 5s, so
the effective steps are 10/15/20 and 10-14 admits zero extra tiles.

No battle, no server, no A/B run.
2026-09-27 10:22:24 +02:00

831 lines
42 KiB
Nim

## 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 env_dotenv
import module_switches
import gun_harness/virtual_bullets
import gun_harness/selector
import movements/ram_decision
import movement_harness/fire_tracker
import movements/the_floor_is_lava
import movements/the_floor_is_lava_ring
import movements/strafe
import movements/wave_surfer
import movements/learned_surfer
import guns/tm_horizon
import guns/lead_gain
import guns/pattern_matcher
import vbullet_draw
import geo_overlay
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
vBulletDebug*: bool
vBulletDebugGun*: string
vBulletDebugMax*: int
recordWorldState*: bool
geoDebug*: bool
debugDraw*: bool
radarForceSpin*: bool
radarScanLog*: bool
radarScanLogPath*: string
trackerProbe*: bool
trackerProbePath*: string
powerLog*: bool
resultLog*: bool
gunStatsPath*: string
shotLogPath*: string
disabledGuns*: HashSet[int]
tmHorizon*: TmHorizonGun
leadGain*: LeadGainGun
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". A value that came from the resolved .env file is labelled
## `.env` even though the file applied it with putEnv (so `getEnv` finds it).
if isFromEnvFile(name): ".env"
elif 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). A value that
## came from the .env file is labelled `.env`.
if isFromEnvFile(name): ".env"
elif existsEnv(name): "env"
else: "default"
proc legacyNameFor(name: string): string =
## The legacy `TR_BITBRAIN_<X>` alias for a `TR_LEADGAIN_<X>` knob, or "" when
## the knob has no legacy name. This gun is the sole owner of the
## `TR_BITBRAIN_*` prefix (the ADE+SBC gun that briefly shared it was retired).
if not name.startsWith(NewPrefix): return ""
let suffix = name[NewPrefix.len .. ^1]
for s in LegacyKnobEnvNames:
if s == suffix: return LegacyPrefix & suffix
""
proc sourceOfLg(name: string): string =
## `sourceOf`, plus credit for the legacy `TR_BITBRAIN_*` alias. Tagged
## `legacy` so a migrated name is distinguishable from an unmigrated one.
let legacy = legacyNameFor(name)
if legacy.len > 0 and getEnv(name, "").len == 0 and getEnv(legacy, "").len > 0:
if isFromEnvFile(legacy): "legacy/.env" else: "legacy"
else:
sourceOf(name)
proc sourceOfLgPresence(name: string): string =
let legacy = legacyNameFor(name)
if legacy.len > 0 and not existsEnv(name) and existsEnv(legacy):
if isFromEnvFile(legacy): "legacy/.env" else: "legacy"
else:
sourceOfPresence(name)
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` and `strafe` select a non-default engine; every other value
# runs `tfil`, so report the RESOLVED engine rather than the raw string.
# `resolveMoveEngine` applies the per-engine TR_MODULE_MOVE_* switches, so
# this line reports the engine the tick will actually run (identical to the
# old name-mapping when every switch is unset).
let effectiveMovement = resolveMoveEngine(ctx.movementName)
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_VBULLET_DEBUG", onOff(ctx.vBulletDebug),
sourceOfPresence(VBulletDebugEnv))
emit("TR_VBULLET_DEBUG_GUN",
(if ctx.vBulletDebugGun.len == 0: "(unset -> selected gun)"
else: ctx.vBulletDebugGun),
sourceOf(VBulletDebugGunEnv))
emit("TR_VBULLET_DEBUG_MAX", $ctx.vBulletDebugMax,
sourceOf(VBulletDebugMaxEnv))
# global debug-draw switch (default on) + the draw-only geometry overlay
emit(DebugDrawEnv, onOff(ctx.debugDraw), sourceOfPresence(DebugDrawEnv))
emit(GeoDebugEnv, onOff(ctx.geoDebug), sourceOfPresence(GeoDebugEnv))
emit("TR_POWER_LOG", onOff(ctx.powerLog), sourceOfPresence("TR_POWER_LOG"))
emit("TR_RESULT_LOG", onOff(ctx.resultLog), sourceOf("TR_RESULT_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 heat shape + ring) ─────────────────────────────────────
# NOTE (j119): `TR_TFIL_CORRIDOR_HEAT` / `TR_TFIL_WALL_HOTNESS` drive BOTH
# movers now — the ring's own copies (reported here) AND the shipped base
# field (the_floor_is_lava, shipped defaults 20/30/10). The two modules export
# the same symbol names, so the reads below are module-qualified.
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", $the_floor_is_lava_ring.CorridorHeat,
sourceOf("TR_TFIL_CORRIDOR_HEAT"))
emit("TR_TFIL_CORRIDOR_TICKS", $the_floor_is_lava.TfilCorridorTicks,
sourceOf("TR_TFIL_CORRIDOR_TICKS"))
emit("TR_TFIL_ARRIVE_TICKS", $the_floor_is_lava.TfilArriveTicks,
sourceOf("TR_TFIL_ARRIVE_TICKS"))
emit("TR_TFIL_HOLD_WHEN_TRAPPED", onOff(the_floor_is_lava.TfilHoldWhenTrapped),
sourceOfPresence("TR_TFIL_HOLD_WHEN_TRAPPED"))
emit("TR_TFIL_WALL_HOTNESS", $the_floor_is_lava_ring.WallHotness,
sourceOf("TR_TFIL_WALL_HOTNESS"))
emit("TR_TFIL_WALL_RADIANCE", $the_floor_is_lava.WallRadiance,
sourceOf("TR_TFIL_WALL_RADIANCE"))
emit("TR_TFIL_BULLET_CORE", $the_floor_is_lava.BulletCore,
sourceOf("TR_TFIL_BULLET_CORE"))
emit("TR_TFIL_BULLET_AURA", $the_floor_is_lava.BulletAura,
sourceOf("TR_TFIL_BULLET_AURA"))
emit("TR_TFIL_TILE_REPLAN", tileReplanName(TfilTileReplanMode),
sourceOf("TR_TFIL_TILE_REPLAN"))
emit("TR_TFIL_COMMIT_TICKS", $TfilCommitTicks, sourceOf("TR_TFIL_COMMIT_TICKS"))
emit("TR_TFIL_NO_REV", onOff(TfilNoRev), sourceOfPresence("TR_TFIL_NO_REV"))
emit("TR_TFIL_COMMIT_LOG", TfilCommitLogPath, sourceOfPresence("TR_TFIL_COMMIT_LOG"))
emit("TR_TFIL_COMMIT_ARRIVAL", onOff(TfilCommitArrival),
sourceOf("TR_TFIL_COMMIT_ARRIVAL"))
emit("TR_TFIL_COMMIT_MARGIN", $TfilCommitMargin,
sourceOf("TR_TFIL_COMMIT_MARGIN"))
emit("TR_TFIL_NOREV_SPEED", $TfilNoRevSpeed, sourceOf("TR_TFIL_NOREV_SPEED"))
emit("TR_TFIL_DANGER_THRESHOLD", $the_floor_is_lava.TfilDangerThreshold,
sourceOf("TR_TFIL_DANGER_THRESHOLD"))
emit("TR_TFIL_DIAG", onOff(the_floor_is_lava.TfilDiag),
sourceOfPresence("TR_TFIL_DIAG"))
emit("TR_TFIL_GEO_MODE", $the_floor_is_lava.TfilGeoMode,
sourceOf("TR_TFIL_GEO_MODE"))
emit("TR_TFIL_GEO_TAU", $the_floor_is_lava.TfilGeoTau,
sourceOf("TR_TFIL_GEO_TAU"))
emit("TR_TFIL_TURN_BIAS", $TfilTurnBias, sourceOf("TR_TFIL_TURN_BIAS"))
emit("TR_TFIL_TURN_REF_DEG", $TfilTurnRefDeg, sourceOf("TR_TFIL_TURN_REF_DEG"))
# time-indexed bullet heat (default off = shipped flat model)
emit("TR_TFIL_HEAT_TIME", onOff(TfilHeatTime), sourceOf("TR_TFIL_HEAT_TIME"))
emit("TR_TFIL_HEAT_TAU", $TfilHeatTau, sourceOf("TR_TFIL_HEAT_TAU"))
emit("TR_TFIL_HEAT_POWER_GAIN", $TfilHeatPowerGain,
sourceOf("TR_TFIL_HEAT_POWER_GAIN"))
emit("TR_TFIL_PILLAR_ON", onOff(PillarHotness > 0.0),
sourceOf("TR_TFIL_PILLAR_ON"))
# ── movement (STRAFE) ─────────────────────────────────────────────────────
emit("TR_STRAFE_BAND", $StrafeBand, sourceOf("TR_STRAFE_BAND"))
emit("TR_STRAFE_SPREAD", $StrafeSpread, sourceOf("TR_STRAFE_SPREAD"))
emit("TR_STRAFE_REACH", $StrafeReach, sourceOf("TR_STRAFE_REACH"))
emit("TR_STRAFE_DWELL_MIN", $StrafeDwellMin, sourceOf("TR_STRAFE_DWELL_MIN"))
emit("TR_STRAFE_DWELL_MAX", $StrafeDwellMax, sourceOf("TR_STRAFE_DWELL_MAX"))
emit("TR_STRAFE_LOG", onOff(StrafeLog), sourceOfPresence("TR_STRAFE_LOG"))
emit("TR_STRAFE_RANGE", $StrafeRange, sourceOf("TR_STRAFE_RANGE"))
emit("TR_STRAFE_RANGE_TOL", $StrafeRangeTol, sourceOf("TR_STRAFE_RANGE_TOL"))
emit("TR_STRAFE_TILT_MAX", $StrafeTiltMax, sourceOf("TR_STRAFE_TILT_MAX"))
emit("TR_STRAFE_TILT_GAIN", $StrafeTiltGain, sourceOf("TR_STRAFE_TILT_GAIN"))
# curved wings + wall escape (j112)
emit("TR_STRAFE_KAPPA", $StrafeKappa, sourceOf("TR_STRAFE_KAPPA"))
emit("TR_STRAFE_WALL_MARGIN", $StrafeWallMargin, sourceOf("TR_STRAFE_WALL_MARGIN"))
emit("TR_STRAFE_WING_MAX", $StrafeWingMax, sourceOf("TR_STRAFE_WING_MAX"))
emit("TR_STRAFE_WALL_BIAS", $StrafeWallBias, sourceOf("TR_STRAFE_WALL_BIAS"))
emit("TR_STRAFE_WALL_SAFE", $StrafeWallSafe, sourceOf("TR_STRAFE_WALL_SAFE"))
emit("TR_STRAFE_ESCAPE", onOff(StrafeEscape), sourceOf("TR_STRAFE_ESCAPE"))
emit("TR_STRAFE_FIRE_FIX", onOff(StrafeFireFix), sourceOf("TR_STRAFE_FIRE_FIX"))
# j134: the global switch the shared `movement_harness/fire_tracker` reads.
# Every mover's effective value is this AND (for STRAFE) TR_STRAFE_FIRE_FIX.
emit("TR_FIRE_FIX", onOff(TfilFireFix), sourceOf("TR_FIRE_FIX"))
# j134 TASK B diagnostic (off by default): per-reading tick/raw/correction trace.
emit("TR_FIRE_DIAG", onOff(StrafeFireDiag), sourceOf("TR_FIRE_DIAG"))
# j147: the back-date (ticks) applied to every detected enemy fire at spawn.
# 0 = shipped (the ghost is born at the scanned enemy position).
emit("TR_FIRE_LAG", $FireLag, sourceOf("TR_FIRE_LAG"))
emit("TR_STRAFE_HEAT_GRID", onOff(StrafeHeatGrid), sourceOf("TR_STRAFE_HEAT_GRID"))
# STRAFE's heat shape is its own RETUNE (bullet 20/10, corridor 10, wall
# 15/5), override-able per run so the shipped field can be A/B'd on one
# binary at boot.
emit("TR_STRAFE_BULLET_CORE", $StrafeBulletCore, sourceOf("TR_STRAFE_BULLET_CORE"))
emit("TR_STRAFE_BULLET_AURA", $StrafeBulletAura, sourceOf("TR_STRAFE_BULLET_AURA"))
emit("TR_STRAFE_CORRIDOR_HEAT", $StrafeCorridorHeat, sourceOf("TR_STRAFE_CORRIDOR_HEAT"))
emit("TR_STRAFE_CORRIDOR_TICKS", $StrafeCorridorTicks, sourceOf("TR_STRAFE_CORRIDOR_TICKS"))
emit("TR_STRAFE_WALL_HOTNESS", $StrafeWallHotness, sourceOf("TR_STRAFE_WALL_HOTNESS"))
emit("TR_STRAFE_WALL_RADIANCE", $StrafeWallRadiance, sourceOf("TR_STRAFE_WALL_RADIANCE"))
# ── movement (WAVE SURFER) ────────────────────────────────────────────────
emit(SurfPrefDistEnv, $SurfPrefDist, sourceOf(SurfPrefDistEnv))
emit(SurfDistBandEnv, $SurfDistBand, sourceOf(SurfDistBandEnv))
emit(SurfWallMarginEnv, $SurfWallMargin, sourceOf(SurfWallMarginEnv))
emit(SurfRadialFracEnv, $SurfRadialFrac, sourceOf(SurfRadialFracEnv))
emit(SurfLogEnv, onOff(SurfLog), sourceOfPresence(SurfLogEnv))
# ── 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"))
# ── the LEADGAIN per-range-band lead-gain corrector ───────────────────────
# All knobs are resolved at gun construction (`initLeadGainGun`), so the
# context holds the real values. `sourceOfLg`/`sourceOfLgPresence` also
# credit the LEGACY `TR_BITBRAIN_*` alias, so a pre-rename `.env` shows up as
# coming from the env (tagged `legacy`) instead of silently reading default.
let lg = ctx.leadGain
emit("TR_LEADGAIN_MEM", memModeName(lg.memMode), sourceOfLg(LG_MEM_ENV))
emit("TR_LEADGAIN_GAINS", lgGainsString(lg.cands), sourceOfLg(LG_GAINS_ENV))
emit("TR_LEADGAIN_N", $lg.nClasses, sourceOfLg(LG_N_ENV))
emit("TR_LEADGAIN_NADE", $lg.nAde, sourceOfLg(LG_NADE_ENV))
emit("TR_LEADGAIN_RANGE", $lg.maxDeg, sourceOfLg(LG_RANGE_ENV))
emit("TR_LEADGAIN_LOG", onOff(lg.logEnabled), sourceOfLgPresence(LG_LOG_ENV))
emit("TR_LEADGAIN_MIN_OBS", $lg.minObs, sourceOfLg(LG_MIN_OBS_ENV))
emit("TR_LEADGAIN_WARMUP", $lg.warmupN, sourceOfLg(LG_WARMUP_ENV))
emit("TR_LEADGAIN_ADAPT", $lg.adaptEvery, sourceOfLg(LG_ADAPT_ENV))
emit("TR_LEADGAIN_CALIB", $lg.calibEvery, sourceOfLg(LG_CALIB_ENV))
emit("TR_LEADGAIN_DECAY", $lg.decayEvery, sourceOfLg(LG_DECAY_ENV))
emit("TR_LEADGAIN_DECAY_FRAC", $lg.decayFrac, sourceOfLg(LG_DECAY_FRAC_ENV))
emit("TR_LEADGAIN_SEED", $lg.seed, sourceOfLg(LG_SEED_ENV))
emit("TR_LEADGAIN_RESET_ON_TARGET", onOff(lg.resetOnTarget),
sourceOfLg(LG_RESET_ON_TARGET_ENV))
# The deprecation line the legacy alias layer can print, in the report so an
# owner staring at a `[depr]` line can see exactly which knob fired.
let dep = lgDeprecationLine()
if dep.len > 0: emit("legacy TR_BITBRAIN_* aliases honoured", "yes", "legacy")
else: emit("legacy TR_BITBRAIN_* aliases honoured", "no", "default")
# ── Pattern match-shape + 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.paramConfigured:
emit("TR_PATTERN_LEN", $ctx.patternMatcher.patternLen, sourceOf("TR_PATTERN_LEN"))
emit("TR_PATTERN_DEPTH", $ctx.patternMatcher.histDepth, sourceOf("TR_PATTERN_DEPTH"))
else:
echo "[env] TR_PATTERN_LEN = ",
getEnv("TR_PATTERN_LEN", $PatternLen), " (raw - not resolved here)"
echo "[env] TR_PATTERN_DEPTH = ",
getEnv("TR_PATTERN_DEPTH", $PatternDepth), " (raw - not resolved here)"
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))
# ── section C: the module on/off inventory ───────────────────────────────────
#
# One line per module, always the same shape:
# [modules] gun.pattern = ON (source: default)
# so `grep '^\[modules\]'` is the complete answer to "what is switched on right
# now". The always-on core is listed too, marked `source: always-on`, rather
# than silently omitted. Existing off switches (TR_RACK_<GUN>, TR_POWER_POLICY,
# TR_FIRE_FIX, TR_RADAR_FORCE_SPIN, TR_VBULLET_DEBUG, TR_GEO_DEBUG,
# TR_DEBUG_DRAW, TR_STRAFE_HEAT_GRID, TR_TFIL_HEAT_TIME) are surfaced under
# their ONE name — no duplicate knobs are invented.
proc emitModule(name: string, on: bool, source: string, note = "") =
var line = "[modules] " & name
while line.len < len("[modules] ") + 20: line.add ' '
line.add "= "
line.add(if on: "ON" else: "off")
line.add " (source: "
line.add source
if note.len > 0:
line.add "; "
line.add note
line.add ")"
echo line
proc printModules(ctx: EnvReportContext) =
echo "[modules] --- C. module on/off inventory (grep '^\\[modules\\]' for all) ---"
# Guns. Rack membership IS the on/off switch: `off` = disabled, any other
# mode = enabled in that rack.
for i in 0..<len(RackGunNames):
var src = sourceOf(RackEnvPrefix & RackGunNames[i])
if src == "default":
for (key, gid) in RackLegacyAlias:
if gid == i and getEnv(key, "").len > 0:
src = if isFromEnvFile(key): "legacy/.env" else: "legacy"
emitModule("gun." & RackGunNames[i].toLowerAscii(),
ActiveRackMembership[i] != rmOff,
src,
"rack: " & rackMembershipName(ActiveRackMembership[i]))
# Movement engines + the engine the tick actually runs.
for e in MoveEngines:
emitModule("move." & e.name, e.enabled, sourceOf(e.envVar))
echo "[modules] move.effective = ", resolveMoveEngine(ctx.movementName),
" (requested: TR_MOVEMENT=", ctx.movementName, ")"
# New TR_MODULE_* switches (j136).
emitModule("vbullets.tracker", ModuleVBullets, sourceOf(ModuleVBulletsEnvVar),
"off: no predict/spawn; selector falls back to its floor gun")
emitModule("ram.decider", ModuleRam, sourceOf(ModuleRamEnvVar),
"off: never ram; the movement engine alone drives")
# Existing off switches, surfaced as modules under their ONE existing name.
emitModule("radar.adaptive", not ctx.radarForceSpin,
sourceOfPresence("TR_RADAR_FORCE_SPIN"),
"off via TR_RADAR_FORCE_SPIN: full 360 spin")
emitModule("radar.lock", true, "always-on",
"1v1 radar; without it the bot cannot function")
emitModule("power.policy", PowerPolicyEnabled, sourceOf(PowerPolicyEnvVar),
"off via TR_POWER_POLICY: uncapped")
emitModule("fire.fix", TfilFireFix, sourceOf("TR_FIRE_FIX"),
"off: shipped prev-energy detector")
emitModule("dbg.debug_draw", ctx.debugDraw, sourceOfPresence(DebugDrawEnv),
"off: every mover debugGraphics=false")
emitModule("dbg.vbullet", ctx.vBulletDebug, sourceOfPresence(VBulletDebugEnv))
emitModule("dbg.geo", ctx.geoDebug, sourceOfPresence(GeoDebugEnv))
emitModule("dbg.heat_grid", StrafeHeatGrid, sourceOf("TR_STRAFE_HEAT_GRID"))
emitModule("dbg.heat_time", TfilHeatTime, sourceOf("TR_TFIL_HEAT_TIME"),
"off: flat (time-independent) bullet heat")
# Always-on core, listed so the inventory is complete.
emitModule("core.enemy_tracker", true, "always-on",
"without it the bot has no enemy positions")
emitModule("core.target_select", true, "always-on",
"picks which enemy to aim at")
emitModule("core.gun_selector", true, "always-on",
"ranks the rack; empty fitness floor = first admitted gun")
emitModule("core.body", true, "always-on",
"Tank Royale API: drives and turns the tank; without it nothing moves")
# ── 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
# ── Part 2: warnings about env mistakes ──────────────────────────────────────
#
# This is the point of the report. A typo must be LOUD, not a silent no-op.
# Two mistakes are caught:
#
# 1. an unknown TR_*/GUN_* name (e.g. TR_TMHORIZON_NSTATE, missing S);
# 2. a compile-time `{.intdefine.}`/`{.strdefine.}`/`{.booldefine.}` symbol
# exported as an env var (e.g. `TMH_NSTATES=2`), which is NEVER read at
# runtime and therefore does nothing.
#
# The known-name set is assembled from the modules' exported env-name
# constants; the remaining inline reads are listed once here. The guard test
# `common_libs/tests/test_env_report.nim` scans the tree for env reads and for
# intdefine/strdefine/booldefine symbols and fails loudly if a name read
# anywhere (or a new define) is missing from these tables, so neither can
# silently drift.
proc knownEnvNames*(): seq[string] =
## Every TR_*/GUN_* name this project reads at runtime. Order is irrelevant;
## callers put it in a set. Keep in sync with the tree scan in the guard test.
result = @[
# exported constants — the single source of truth (do not re-spell these)
MetricEnvVar, SelectorModeEnvVar, TieBreakEnvVar,
RackShareEnvVar,
# module on/off switches (j136) — the new TR_MODULE_* prefix
ModuleVBulletsEnvVar, ModuleRamEnvVar,
ModuleMoveTfilEnvVar, ModuleMoveRingEnvVar, ModuleMoveStrafeEnvVar,
ModuleMoveSurfEnvVar, ModuleMoveLearnedEnvVar,
PowerPolicyEnvVar, PowerFarDistEnvVar, PowerFarCapEnvVar, PowerMidCapEnvVar,
PowerRefEnvVar, PowerEnergyHiEnvVar, PowerEnergyLoEnvVar,
PowerEnergyMinEnvVar, PowerEnergyMaxEnvVar, PowerFinishKillEnvVar,
PatternRadOffsetEnvVar, PatternRadScaleEnvVar,
PatternLenEnvVar, PatternDepthEnvVar,
TMH_SHIFT_ENV, TMH_BIG_MULT_ENV, TMH_LOG_ENV, TMH_RESET_ON_TARGET_ENV,
TMH_WINDOW_ENV, TMH_RESET_DROP_ENV, TMH_NSTATES_ENV, TMH_ACCURVE_ENV,
TMH_RETRAIN_EVERY_ENV, TMH_EPOCHS_ENV,
LG_MEM_ENV, LG_GAINS_ENV, LG_N_ENV, LG_NADE_ENV, LG_RANGE_ENV, LG_LOG_ENV,
LG_MIN_OBS_ENV, LG_WARMUP_ENV, LG_ADAPT_ENV, LG_CALIB_ENV, LG_DECAY_ENV,
LG_DECAY_FRAC_ENV, LG_SEED_ENV, LG_RESET_ON_TARGET_ENV,
# LEGACY aliases of the corrector knobs above, honoured UNCONDITIONALLY
# (this gun is the sole owner of the `TR_BITBRAIN_*` prefix). Registered so
# the tree-scan guard does not flag the owner's pre-rename .env as unknown.
"TR_BITBRAIN_GAINS", "TR_BITBRAIN_MEM", "TR_BITBRAIN_MIN_OBS",
"TR_BITBRAIN_DECAY", "TR_BITBRAIN_DECAY_FRAC", "TR_BITBRAIN_LOG",
"TR_BITBRAIN_RESET_ON_TARGET", "TR_BITBRAIN_N", "TR_BITBRAIN_NADE",
"TR_BITBRAIN_RANGE", "TR_BITBRAIN_WARMUP", "TR_BITBRAIN_ADAPT",
"TR_BITBRAIN_CALIB", "TR_BITBRAIN_SEED",
# common_libs/bitbrain (the SBC LIBRARY, which STAYS — `learned_surfer`
# imports bitbrain/sbc) reads these three inline; they are the library's own
# knobs, not the retired ADE+SBC gun's (that gun, rack id 17, was removed).
"TR_BITBRAIN_MODE", "TR_BITBRAIN_DECAY_EVERY", "TR_BITBRAIN_DECAY_SHIFT",
]
# rack names are constructed from the prefix + gun table, not spelled out
for g in RackGunNames:
result.add RackEnvPrefix & g
# the legacy rack alias `TR_RACK_BITBRAIN` is no longer in `RackGunNames`
# (it is LEADGAIN's old name), so register it explicitly. See
# gun_harness/selector's RackLegacyAlias.
for (key, _) in RackLegacyAlias:
result.add key
# inline reads with no exported constant (the guard test scans for these)
for n in [
"GUN_SELECTOR_WINDOW", "GUN_SELECTOR_MINOBS", "GUN_SELECTOR_TIE",
"GUN_SELECTOR_FLOOR", "GUN_SELECTOR_POOL", "GUN_SELECTOR_RANK",
"GUN_SELECTOR_SHRINK", "GUN_SELECTOR_DWELL", "GUN_SELECTOR_MARGIN",
"GUN_SELECTOR_POINT_TIE", "GUN_SELECTOR_SEED",
"GUN_RACK_DISABLE", "GUN_STATS_PATH", "GUN_SHOTLOG_PATH",
"TR_MOVEMENT", "TR_MOVEMENT_LOG", "TR_RECORD_WORLDSTATE",
"TR_RADAR_FORCE_SPIN", "TR_RADAR_SCANLOG", "TR_RADAR_SCAN_LOG_PATH",
"TR_TRACKER_PROBE", "TR_TRACKER_PROBE_PATH", "TR_VBULLET_ADMIT_ONLY",
VBulletDebugEnv, VBulletDebugGunEnv, VBulletDebugMaxEnv,
DebugDrawEnv, GeoDebugEnv,
"TR_POWER_LOG",
"TR_RESULT_LOG",
# the .env loader's own environment hook (TR_ENV_FILE names the file)
"TR_ENV_FILE",
"TR_RAM_OPPORTUNITY", "TR_RAM_OPP_DIST", "TR_RAM_OPP_MARGIN",
"TR_RAM_ABORT_DMG", "TR_RAM_PLAN", "TR_RAM_PLAN_DIST",
"TR_RAM_PLAN_MARGIN", "TR_RAM_PLAN_HITRATE", "TR_RAM_LOG",
"TR_TFIL_RANGE_LO", "TR_TFIL_RANGE_HI", "TR_TFIL_RANGE_TEMP",
"TR_TFIL_RANGE_K", "TR_TFIL_CORRIDOR_HEAT", "TR_TFIL_WALL_HOTNESS",
"TR_TFIL_CORRIDOR_TICKS", "TR_TFIL_ARRIVE_TICKS",
"TR_TFIL_HOLD_WHEN_TRAPPED",
"TR_TFIL_DANGER_THRESHOLD", "TR_TFIL_DIAG",
"TR_TFIL_GEO_MODE", "TR_TFIL_GEO_TAU",
"TR_TFIL_WALL_RADIANCE", "TR_TFIL_BULLET_CORE", "TR_TFIL_BULLET_AURA",
"TR_TFIL_TILE_REPLAN", "TR_TFIL_COMMIT_TICKS", "TR_TFIL_NO_REV",
"TR_TFIL_COMMIT_LOG", "TR_TFIL_COMMIT_ARRIVAL", "TR_TFIL_COMMIT_MARGIN",
"TR_TFIL_NOREV_SPEED", "TR_TFIL_TURN_BIAS", "TR_TFIL_TURN_REF_DEG",
"TR_TFIL_HEAT_TIME", "TR_TFIL_HEAT_TAU", "TR_TFIL_HEAT_POWER_GAIN",
"TR_TFIL_PILLAR_ON",
"TR_STRAFE_BAND", "TR_STRAFE_SPREAD", "TR_STRAFE_REACH",
"TR_STRAFE_DWELL_MIN", "TR_STRAFE_DWELL_MAX", "TR_STRAFE_LOG",
"TR_STRAFE_RANGE", "TR_STRAFE_RANGE_TOL", "TR_STRAFE_TILT_MAX",
"TR_STRAFE_TILT_GAIN",
"TR_STRAFE_KAPPA", "TR_STRAFE_WALL_MARGIN", "TR_STRAFE_WING_MAX",
"TR_STRAFE_WALL_BIAS", "TR_STRAFE_WALL_SAFE", "TR_STRAFE_ESCAPE",
"TR_STRAFE_FIRE_FIX",
"TR_FIRE_FIX",
"TR_FIRE_DIAG",
"TR_FIRE_LAG",
"TR_STRAFE_HEAT_GRID", "TR_STRAFE_BULLET_CORE", "TR_STRAFE_BULLET_AURA",
"TR_STRAFE_CORRIDOR_HEAT", "TR_STRAFE_WALL_HOTNESS",
"TR_STRAFE_CORRIDOR_TICKS",
"TR_STRAFE_WALL_RADIANCE",
SurfPrefDistEnv, SurfDistBandEnv, SurfWallMarginEnv, SurfRadialFracEnv,
SurfLogEnv,
# movements/learned_surfer.nim (the state-conditional learned danger mover)
LearnedDecayEveryEnv, LearnedDecayShiftEnv, LearnedAlphaEnv,
LearnedTravelEnv, LearnedReversalEnv, LearnedPrefDistEnv,
LearnedDistBandEnv, LearnedRadialFracEnv, LearnedWallMarginEnv,
LearnedGlobalEnv, LearnedLabelEnv, LearnedRealEventsEnv, LearnedLogEnv,
# harness vars (read by the test framework, inherited by the bot, so they
# must NOT be reported as typos)
"TR_SERVER_JAR", "TR_BATTLE_RUNNER", "TR_BATTLE_RUNNER_DIR",
# the report's own switch
EnvReportEnableEnvVar,
]:
result.add n
proc unknownEnvNames*(pairs: openArray[(string, string)]): seq[string] =
## Pure: every TR_*/GUN_* name in `pairs` the bot does not read, sorted.
## Anything else (e.g. PATH, JAVA_HOME) is deliberately ignored.
let known = knownEnvNames().toHashSet()
for (key, _) in pairs:
if (key.startsWith("TR_") or key.startsWith("GUN_")) and key notin known:
result.add key
result.sort()
type
DefineMapping* = object
## A `{.intdefine.}`/`{.strdefine.}`/`{.booldefine.}` symbol and the runtime
## env var (if any) that actually controls the same knob. `suggestedEnv` is
## the env name a user would naturally guess, used only for the
## "does not exist" text when `runtimeEnv` is empty.
defineName*: string
runtimeEnv*: string
suggestedEnv*: string
## Derived by grepping the tree for intdefine/strdefine/booldefine (see the
## guard test, which re-greps and fails if this table drifts):
## tm_horizon.nim:101,102,103,106,108,139 — TMH_* ; only NSTATES has an env
## tm_pattern.nim:50,53,55,57,59,62,65,68,69,85,87,92,94,96 — TM_*
## tsetlin.nim:29,116,118,119,120 — TM_*
## docs/env_reference.md documents the same table.
const CompileTimeDefineMap*: seq[DefineMapping] = @[
# tm_horizon.nim — the ONLY define with a runtime twin (`TR_TMHORIZON_NSTATES`)
DefineMapping(defineName: "TMH_NSTATES", runtimeEnv: "TR_TMHORIZON_NSTATES"),
DefineMapping(defineName: "TMH_NCLAUSES", suggestedEnv: "TR_TMHORIZON_NCLAUSES"),
DefineMapping(defineName: "TMH_S_DEF", suggestedEnv: "TR_TMHORIZON_S_DEF"),
DefineMapping(defineName: "TMH_MIN_OBS", suggestedEnv: "TR_TMHORIZON_MIN_OBS"),
DefineMapping(defineName: "TMH_STALE_MAX", suggestedEnv: "TR_TMHORIZON_STALE_MAX"),
# tm_pattern.nim — no env form at all; do not invent a plausible one
DefineMapping(defineName: "TM_NCLAUSES"),
DefineMapping(defineName: "TM_NSTATES"),
DefineMapping(defineName: "TM_MIN_OBS"),
DefineMapping(defineName: "TM_CLASSES"),
DefineMapping(defineName: "TM_CONF_MARGIN_DEF"),
DefineMapping(defineName: "TM_SHRINK_DEF"),
DefineMapping(defineName: "TM_GF_MODE"),
DefineMapping(defineName: "TM_SOFT_BETA_DEF"),
DefineMapping(defineName: "TM_RADIAL_RANGE_DEF"),
DefineMapping(defineName: "TM_RAD_MARGIN_DEF"),
DefineMapping(defineName: "TM_REV_TURN_DEG_DEF"),
DefineMapping(defineName: "TM_REV_MARGIN_DEF"),
DefineMapping(defineName: "TM_REV_GAIN_DEF"),
# tsetlin.nim — no env form at all
DefineMapping(defineName: "TM_N_CLAUSES"),
DefineMapping(defineName: "TM_N_STATES"),
DefineMapping(defineName: "TM_T_DEF"),
DefineMapping(defineName: "TM_WINDOW_SIZE"),
# TM_S_DEF is shared by tm_pattern.nim AND tsetlin.nim (one -d sets both)
DefineMapping(defineName: "TM_S_DEF"),
]
proc isCompileTimeDefine*(name: string): bool =
## True when `name` is one of our -d: symbols (never a runtime env var).
for m in CompileTimeDefineMap:
if m.defineName == name: return true
proc setCompileTimeDefines*(pairs: openArray[(string, string)]): seq[string] =
## Pure: the -d: symbols present as env vars, sorted.
for (key, _) in pairs:
if isCompileTimeDefine(key): result.add key
result.sort()
proc defineWarningMessage*(defineName, value: string): string =
## The exact warning text for one -d: symbol set as an env var. "" when the
## name is not a known define.
for m in CompileTimeDefineMap:
if m.defineName != defineName: continue
let head = "[env] WARNING: " & defineName & "=" & value &
" is a COMPILE-TIME define (-d:" & defineName & "=" & value &
") and is NOT read at runtime; "
if m.runtimeEnv.len > 0:
return head & "the runtime equivalent is " & m.runtimeEnv & "=" & value & "."
if m.suggestedEnv.len > 0:
return head & m.suggestedEnv &
" does not exist - this knob is compile-time only."
return head & "no runtime env equivalent exists - this knob is " &
"compile-time only."
return ""
proc printMistakeWarnings*() =
## The two warning paths, boot-only. Emits nothing when the env is clean, so
## a correct run keeps the documented A/B/build shape.
var pairs: seq[(string, string)]
for key, val in envPairs():
pairs.add (key, val)
let unknown = unknownEnvNames(pairs)
let defines = setCompileTimeDefines(pairs)
if unknown.len == 0 and defines.len == 0: return
echo "[env] --- warnings: misnamed variables ---"
for key in unknown:
echo "[env] WARNING: ", key,
" is not a recognized TR_*/GUN_* variable; it is IGNORED. " &
"Check the spelling (see docs/env_reference.md)."
for key in defines:
echo defineWarningMessage(key, getEnv(key, ""))
# ── 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) ==="
echo "[env] env file: ",
(if envFilePath().len > 0: envFilePath() & " (source: " & envFileSource() & ")"
else: "none")
printRawEnvironment()
printMistakeWarnings()
printEffectiveValues(ctx)
printModules(ctx)
printBuildIdentity()
echo "[env] === END ENVIRONMENT ==="