diff --git a/ModularBot_garage/src/ModularBot.nim b/ModularBot_garage/src/ModularBot.nim index 707ecd9..9028d13 100644 --- a/ModularBot_garage/src/ModularBot.nim +++ b/ModularBot_garage/src/ModularBot.nim @@ -7,6 +7,7 @@ import env_boot # MUST be first: applies the .env file before any other # module's top-level `let` reads the environment. import std/[math, os, strformat, tables, sets, json, random, strutils] import robocode_tankroyale_botapi +import module_switches import radar_harness/radar_interface import radars/radar_lock_module import radars/adaptive_melee_radar @@ -126,6 +127,10 @@ let VBulletAdmitOnly* = ## tfil_ring — the new RANGE-WEIGHTED mover (movements/the_floor_is_lava_ring). ## UNPROVEN: it must never become the default silently. let MovementName* = getEnv("TR_MOVEMENT", "strafe").strip().toLowerAscii() +## The engine the tick ACTUALLY runs after the per-engine on/off switches +## (`TR_MODULE_MOVE_*`, default all on) are applied. With every switch unset this +## equals `MovementName`, so the dispatch below is byte-identical to before. +let EffectiveMovementName* = resolveMoveEngine(MovementName) ## TEMPORARY j134 diagnostic (TASK B): when `TR_FIRE_DIAG` is set, print the tick ## of every enemy-hit / our-hit EVENT so it can be checked against the energy ## reading tick logged by the mover. OFF by default; observability only. @@ -379,13 +384,13 @@ proc printConfig(bot: ModularBot, forceAll: bool = false) = let tc = if bot.currentTargetId != bot.prevTarget or forceAll: CLR_CHANGE else: "" let radarName = if bot.radarMode == 0: "radar_lock" else: "adaptive_melee" let moveName = - if MovementName == "strafe": + if EffectiveMovementName == "strafe": if bot.isRamming: "strafe(ram)" else: "strafe" - elif MovementName == "tfil_ring": + elif EffectiveMovementName == "tfil_ring": if bot.isRamming: "tfil_ring(ram)" else: "tfil_ring" - elif MovementName == "surf": + elif EffectiveMovementName == "surf": if bot.isRamming: "surf(ram)" else: "surf" - elif MovementName == "learned": + elif EffectiveMovementName == "learned": if bot.isRamming: "learned(ram)" else: "learned" elif bot.isRamming: "rammer" else: "tfil" @@ -1150,8 +1155,8 @@ method run*(bot: ModularBot) = let ramReason = ramTrigger(RamInputs( dist: ramDist, selfEnergy: ws.selfEnergy, enemyEnergy: ws.enemyEnergy, gunHitRate: gunHitRate)) - let triggerRam = bot.currentTargetId >= 0 and bot.ramCooldownTicks == 0 and - ramReason != rrNone + let triggerRam = ModuleRam and bot.currentTargetId >= 0 and + bot.ramCooldownTicks == 0 and ramReason != rrNone # Abort an IN-PROGRESS ram on sustained incoming fire. The trigger itself is # unaffected, so a firefight before the approach never vetoes the start. let abortRam = shouldAbortRam(bot.isRamming, ramDmgRate) @@ -1195,13 +1200,13 @@ method run*(bot: ModularBot) = # through the single ring engine; the default `tfil` keeps the old # two-engine behaviour (tfil when holding, rammer when ramming). var spd, tr: float - if MovementName == "strafe" and not shouldRam: + if EffectiveMovementName == "strafe" and not shouldRam: (spd, tr) = bot.strafeMover.computeMove(ws) - elif MovementName == "surf" and not shouldRam: + elif EffectiveMovementName == "surf" and not shouldRam: (spd, tr) = bot.surfMover.computeMove(ws) - elif MovementName == "learned" and not shouldRam: + elif EffectiveMovementName == "learned" and not shouldRam: (spd, tr) = bot.learnedMover.computeMove(ws) - elif MovementName == "tfil_ring": + elif EffectiveMovementName == "tfil_ring": bot.ringMover.band = if shouldRam: (lo: 0.0, hi: 50.0) else: (lo: RangeLo, hi: RangeHi) @@ -1282,7 +1287,8 @@ method run*(bot: ModularBot) = # is 0, so TR_VBULLET_ADMIT_ONLY=0 reproduces the exact pre-change rack. var admit: array[17, bool] for gi in 0..<17: - admit[gi] = vBulletAdmitted(gi, bot.rackMode, ActiveRackMembership, + admit[gi] = ModuleVBullets and + vBulletAdmitted(gi, bot.rackMode, ActiveRackMembership, VBulletAdmitOnly or gi == TmPatternId or gi == TmHorizonId or gi == BitBrainId) for i in 0.., 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.. TR_RADAR_FORCE_SPIN (on = force the old full spin) +## power policy -> TR_POWER_POLICY +## fire fix -> TR_FIRE_FIX +## time-indexed heat -> TR_TFIL_HEAT_TIME +## virtual-bullet overlay -> TR_VBULLET_DEBUG +## geometry overlay -> TR_GEO_DEBUG +## global debug draw -> TR_DEBUG_DRAW +## strafe heat grid -> TR_STRAFE_HEAT_GRID +## every gun -> TR_RACK_ (membership `off` = disabled) +## +## Everything NEW below uses one uniform, greppable prefix: `TR_MODULE_`. +## Every switch defaults to ON, so an unset environment is byte-for-byte the +## shipped bot. "OFF" always means the simpler/older fallback, never a broken +## bot: +## +## TR_MODULE_VBULLETS=0 no gun predicts or spawns virtual bullets; the +## selector falls back to its floor gun. +## TR_MODULE_RAM=0 no ramming at all; the movement engine alone +## drives (the finisher/desperation/plan/opportunity +## triggers all stop firing). +## TR_MODULE_MOVE_=0 that engine is no longer selectable. The +## EFFECTIVE engine falls back to the first enabled +## engine, with `tfil` (the long-shipped mover) as +## the designated fallback. There is no "no +## movement": if every engine is switched off the +## bot still runs `tfil`. +## +## Read once at module init, like every other knob, so one compiled binary can +## A/B every arm by environment alone. + +import std/[os, strutils] + +# ── the new switches ───────────────────────────────────────────────────────── + +const + ModuleVBulletsEnvVar* = "TR_MODULE_VBULLETS" + ModuleRamEnvVar* = "TR_MODULE_RAM" + ModuleMoveTfilEnvVar* = "TR_MODULE_MOVE_TFIL" + ModuleMoveRingEnvVar* = "TR_MODULE_MOVE_TFIL_RING" + ModuleMoveStrafeEnvVar* = "TR_MODULE_MOVE_STRAFE" + ModuleMoveSurfEnvVar* = "TR_MODULE_MOVE_SURF" + ModuleMoveLearnedEnvVar* = "TR_MODULE_MOVE_LEARNED" + +proc moduleFlagOn*(name: string, default = true): bool = + ## Value-based, exactly like `TR_POWER_POLICY`: unset/empty -> `default`; + ## explicit 0/false/no/off -> off; any other value -> on. + let v = getEnv(name, "").strip().toLowerAscii() + if v.len == 0: return default + v notin ["0", "false", "no", "off"] + +let ModuleVBullets* = moduleFlagOn(ModuleVBulletsEnvVar) + ## OFF: the virtual-bullet tracker stops. The selector's fitness stays empty + ## and `chooseFromFit` takes its floor path (the first admitted gun), so the + ## bot keeps aiming and firing — just without the fitness signal. +let ModuleRam* = moduleFlagOn(ModuleRamEnvVar) + ## OFF: `triggerRam` is never true, so `isRamming` stays false and the + ## movement engine drives every tick (the pre-rammer fallback). + +type + MoveEngine* = object + name*: string ## the `TR_MOVEMENT` value + envVar*: string ## its `TR_MODULE_MOVE_*` switch + enabled*: bool + +let MoveEngines*: array[5, MoveEngine] = [ + MoveEngine(name: "tfil", envVar: ModuleMoveTfilEnvVar, + enabled: moduleFlagOn(ModuleMoveTfilEnvVar)), + MoveEngine(name: "tfil_ring", envVar: ModuleMoveRingEnvVar, + enabled: moduleFlagOn(ModuleMoveRingEnvVar)), + MoveEngine(name: "strafe", envVar: ModuleMoveStrafeEnvVar, + enabled: moduleFlagOn(ModuleMoveStrafeEnvVar)), + MoveEngine(name: "surf", envVar: ModuleMoveSurfEnvVar, + enabled: moduleFlagOn(ModuleMoveSurfEnvVar)), + MoveEngine(name: "learned", envVar: ModuleMoveLearnedEnvVar, + enabled: moduleFlagOn(ModuleMoveLearnedEnvVar)), +] + +proc moveEngineEnvVar*(name: string): string = + ## The switch name for `name`, or "" when it is not a known engine. + for e in MoveEngines: + if e.name == name: return e.envVar + "" + +proc moveEngineEnabled*(name: string): bool = + for e in MoveEngines: + if e.name == name: return e.enabled + false + +proc resolveMoveEngine*(requested: string): string = + ## The EFFECTIVE engine the live tick will run. + ## * an unknown/empty `requested` means `tfil` (the shipped fallback); + ## * the requested engine is used when its switch is on; + ## * otherwise the first enabled engine in `MoveEngines` (tfil first); + ## * if every engine is off, `tfil` is forced — there is no no-movement. + var req = requested.strip().toLowerAscii() + if moveEngineEnvVar(req).len == 0: req = "tfil" + if moveEngineEnabled(req): return req + for e in MoveEngines: + if e.enabled: return e.name + "tfil" diff --git a/docs/env_reference.md b/docs/env_reference.md index 4bb2243..bb177ac 100644 --- a/docs/env_reference.md +++ b/docs/env_reference.md @@ -230,6 +230,55 @@ shooting *look* like missing). --- +## 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: + +```sh +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_` = `both`/`1v1`/`melee` | that gun may be selected | `TR_RACK_=off`: the gun is removed from the rack | +| `TR_POWER_POLICY` | energy-aware power caps (default) | uncapped: the gun's own preferred power | +| `TR_FIRE_FIX` | the corrected enemy-fire detector (default) | the shipped `prev - energy` detector | +| `TR_RADAR_FORCE_SPIN` | force the old stateless full-spin melee radar (**off by default**) | the adaptive arc-narrowing radar (default) | +| `TR_TFIL_HEAT_TIME` | time-indexed bullet heat (**off by default**) | flat, time-independent heat (default) | +| `TR_VBULLET_DEBUG` | draw the virtual-bullet overlay (**off by default**) | nothing drawn | +| `TR_GEO_DEBUG` | draw the geometry overlay (**off by default**) | nothing drawn | +| `TR_DEBUG_DRAW` | per-mover debug graphics (default) | the movers draw nothing | +| `TR_STRAFE_HEAT_GRID` | draw the full heat grid in STRAFE (default) | the heat grid is hidden | + +Always on, listed in `[modules]` but with no switch, because the bot cannot work +without them: the enemy tracker, target selection, the gun selector and the 1v1 +radar lock. + +--- + ## 1. Gun rack — which gun(s) may be chosen | variable | default | what it does |