# DevControlBot — Garage Agent Guide > ## ⚠️ READ THIS FIRST — THIS GARAGE IS *INTENTIONALLY* NOT THE ROOT TEMPLATE > > **The layout and format described in the repo-root [`../../AGENTS.md`](../../AGENTS.md) > template do NOT apply to `DevControlBot_garage/`.** The root template documents a > `src/.nim` / `tests/` / `out/` convention for garages. **This garage deviates > from it on purpose and must stay that way.** > > **Do NOT, even to "fix" a perceived inconsistency:** > - create `src/`, `tests/`, or `out/` > - add a `config.nims`, `nimble.paths`, `--path` flags, or a vendor/ directory > - restructure `DevControlBot/` to match the root template > > Builds go to a `mktemp -d` throwaway dir that is removed on exit — nothing is > ever written here. The root `AGENTS.md` and its folder conventions are for the > **other** garages; here it is context only (issue tracker, Nim conventions), and > is **NOT a layout contract for this folder**. If the root template and this file > disagree about layout, **this file wins.** > ## ⚠️ DO NOT TOUCH `PLAN.md` > > **[`PLAN.md`](PLAN.md) is the user's personal working notes file.** The user > maintains it and edits it exclusively. > > - Agents must **NEVER** modify, rewrite, reformat, move, delete or commit > changes to `PLAN.md` — unless the user explicitly asks in that conversation. > - Reading it is **not** assumed or expected; do not open it by default. > - It **IS** tracked in git and is intended to be committed (deliberately added > in commit `59fad00b`). Do **NOT** gitignore it and do **NOT** > `git rm --cached` it. ## Start here This file is the working rules for the bot. The human-facing description lives in [README.md](README.md) — read it first. Garage-specific rules for `DevControlBot_garage/`. Repo-wide rules live in [`../../AGENTS.md`](../../AGENTS.md) (issue tracker, Nim conventions, test framework) — read it for that context, but note again: **its folder-layout template does not govern this garage.** ## Conventions - This bot's code lives **only** inside `DevControlBot_garage/`. Do not edit other garages to make this bot work. - Do not reuse or copy another bot's game logic (movement/scanning/firing, radar tables, targeting heuristics) without explicit permission from the user. Shared, bot-agnostic code belongs in `common_libs/`, and only with permission. - The radar is the one part currently driven. **All of it is `DevControlBot/modules/radar.nim`** — two procs (`onScan`, `commandRadar`), three variables (the target's position, whether we have one, ticks since it was last scanned), three constants. Nothing else; do not split it back into harness/lock/melee modules. `commandRadar()` issues exactly one `setRadarTurnRate` per tick on every path: lock when `getEnemyCount() == 1` and we hold a target scanned within `FreshScanTurns`, the 45 deg/tick sweep otherwise (that same sweep is both the search and the multi-enemy answer — there is no separate melee mode). No counter, no seen-id set, no mode enum; the server's enemy count drives the choice, so a death switches modes on the spot and no tick is ever left without a command. The lock commands the delta to the target's bearing **plus `OvershootDeg` past it**, so the radar sweeps across the bearing every tick and re-scans the target every tick. That overshoot is the whole reason the lock works, and it is why `setRescan()` is not needed (the radar is never idle). **Never** turn by the bare error and never hold at rate 0: that parks the radar on the bearing, the scans stop, and the target is lost until a sweep re-finds it. A lock is only taken on a target seen within `FreshScanTurns`; a working lock re-scans every tick, so that threshold can never throw a good lock away, and it prevents a blind slew at a stale position just after the count drops to 1. `run()` commands the radar before every `go()`, so the command is part of the tick's intent; `onScannedBot` is a one-liner, `onScan(e.x, e.y)`, which remembers the target's absolute position and issues no command. Angles are 0° = east, positive = counter-clockwise = **left** (see README for the API proof). Never the blocking `rescan()`. Provenance: ModularBot / common_libs (Davide Cappellini, Apache-2.0); see README. ## Build artifacts / binaries - **Rule:** never write a binary, log or build output into this garage. The `runBot` task builds into a `mktemp -d` throwaway dir and removes it with an `EXIT`/`INT`/`TERM` trap, so **no `out/` directory is ever created** here. - **Rule:** never commit binaries or nimble cache dirs (`~/.nimble`, `nimbledeps/`). `nimbledeps/` only appears with `nimble develop`; ignore it. - **Rule:** dependencies come from the global nimble store. Do **not** vendor a copy or add `nimble.paths`/`nimble develop` — `nimble install robocode_tankroyale_botapi` is the only setup step. (The package was renamed from `tankroyale_botapi`; the old name is gone.) Exception: the `runBot` task *derives* `--path:` flags at run time from `nimble path `, because the compiler's bundled `nimblepath="$home/.nimble/pkgs2/"` only resolves when `$HOME` is set. That is resolution, not vendoring; keep it. - Build/run with the folder task, never raw `nim c`: ```sh cd DevControlBot_garage/DevControlBot nimble runBot ``` - Note: the root `.gitignore` claims "all builds go to `*_garage/out/`". That is no longer true for this garage — do not rely on that rule, and do not add an `out/` directory just to satisfy it. (Root `.gitignore` is not this garage's file; leave it alone unless the user asks.) ## Running the bot `DevControlBot/DevControlBot.sh` is the canonical entry point and works from any cwd — it resolves `SCRIPT_DIR` from `BASH_SOURCE`, `cd`s to the `.nimble` dir, forwards args and signals to `nimble runBot`, and propagates the exit code. ```sh ./DevControlBot/DevControlBot.sh # from the garage root: bundled metadata ./DevControlBot/DevControlBot.sh /path.json # alternate metadata (first arg, must end in .json) ./DevControlBot/DevControlBot.sh --debug # classic debug build ./DevControlBot/DevControlBot.sh --help # usage, exits 0 without building ``` - **Rule: no arguments means RUN, not usage.** A bare `./DevControlBot.sh` performs a quiet RELEASE build and runs the bot with the bundled `DevControlBot.json`, exactly as if that path had been passed. Usage is printed **only** for an explicit `-h` / `--help`, which exits 0 without building. (An earlier version printed usage and exited 0 with no arguments — that was the bug, and it is fixed.) - There is **no `--json` flag**: the metadata path is the first positional argument and is used when it ends in `.json`. Anything else is forwarded to the bot verbatim. - **Build mode:** default is **release** (`nim c -d:release`, quiet: no compiler hints, no dot-progress line, just `[devcontrolbot] build ok (release)`). `--debug` selects the classic debug build (`nim c`, full hints/diagnostics). The flag is stripped by the wrapper in any position and reaches the `runBot` task through the exported env var `$DEVCONTROLBOT_DEBUG` (`debug`/`release`), never as a bot argument. Compiler errors are shown in **both** modes; a failed release build replays the captured compiler log, so it is never silent. - With no arguments (or `--help`) the wrapper prints usage and exits 0 without building. - **Gotcha:** `nimble runBot` must be invoked from `DevControlBot/`, the directory containing the `.nimble`. From `DevControlBot_garage/` it fails with `Could not find a file with a .nimble extension inside the specified directory`. - `compile`/`build` are reserved Nimble builtin names — hence `runBot`. - Without a Tank Royale server at `SERVER_URL` the run fails with `[start] Cannot connect ... Connection refused` and exit code 1. That is expected during local verification, not a bug. ## Configuration = environment variables (the API's own mechanism) - **Rule: never add env-reading code to `DevControlBot.nim`.** The bot API already reads its configuration from the process environment inside `start()`: `SERVER_URL` and `SERVER_SECRET` (`robocode_tankroyale_botapi.nim:424-425`, documented at `:396-397`) and `BOT_NAME` / `BOT_VERSION` / `BOT_AUTHORS` / … in `bot_info.nim:117-130` (used when no JSON metadata is found). That is the supported mechanism the official BotLauncher uses; `DevControlBot.nim` calls plain `start(bot, jsonPath)` and needs no change. - The `runBot` task makes those variables **present** for local runs (see the `.env` section). That is all it does; it does not interpret them. ## Optional `.env` (local runs only) `DevControlBot/.env` holds the local configuration (typically `SERVER_URL` and `SERVER_SECRET`). Rules: - **It is optional.** If it is missing the run is unchanged: no error, no non-zero exit, just one informational line and the API defaults. - **Loaded in the `runBot` task's shell**, not in `DevControlBot.sh`, so it works for both `./DevControlBot.sh` and a direct `nimble runBot` and the wrapper stays a thin wrapper. `$DEVCONTROLBOT_ENV_FILE` overrides the path. - **Precedence — CRITICAL: the environment always wins.** A variable already present in the environment is **never** overridden by `.env`, so the official BotLauncher's values keep winning. It is achieved by *filtering*: a `NAME=value` line is dropped when `NAME` is already set (`[ -n "${NAME+x}" ]`, set-but-maybe-empty), and only the still-unset names are `set -a`-exported and sourced. Do **not** "simplify" this into a plain `set -a; . .env` — sourcing does the opposite and would let the file beat the launcher. - Accepts `FOO=bar` and `export FOO=bar`, strips `CR` (CRLF), skips blanks, `#` comments and any line that is not a plain `NAME=value` identifier. - **Never print values** and never use `set -x`: report variable *names* only. - **Never commit it** — it holds secrets. The root `.gitignore` covers `ModularBot_garage/.env` only and does **not** ignore this one; the rule to add (ask the user before touching the root file) is `DevControlBot_garage/DevControlBot/.env`. ## Failure reporting / exit codes - **Rule:** an expected failure must never surface as a Nimble/NimScript exception. `exec` raises on any non-zero exit, printing a stack trace and an escaped copy of `runScript`. So the shell always exits 0 and writes the real code to `$DEVCONTROLBOT_STATUS_FILE`; `DevControlBot.sh` exports that path and re-exits with the code. Never mask it to 0, never let the shell's code be swallowed. - Exit codes (keep them distinct, they are what BotLauncher branches on): `0` clean, `1` no game server at `SERVER_URL` (expected), `2` dependency not installed, `3` compile failure, anything else = the bot's own code. Each prints its own short, actionable line; compile failures keep the raw compiler errors. - Running `nimble runBot` directly (no status file) intentionally falls back to the legacy behaviour: the shell exits with the real code and Nimble raises. ## Test organization - **There are currently no tests in this garage.** No `tests/` directory, no `tests/config.nims`, and the `.nimble` has exactly one task (`runBot`) — the old `test`, `compileBot` and `setupVendor` tasks are gone. `nimble test` does not exist; do not invoke it and do not document it. - Convention **when** tests are added (repo-wide, see [`../../AGENTS.md`](../../AGENTS.md) and `common_libs/test_framework/README.md`): - create `DevControlBot_garage/tests/` with `tests/config.nims` containing `--path:"../../common_libs"` (depth must match the garage location), one `test.nim` per concern (e.g. `test_basic_battle.nim`); - add a `test` task to `DevControlBot.nimble` running `nim c -r --path:../common_libs tests/test_basic_battle.nim`; - guard first, import second — tests must pass with no Java present: ```nim import std/os if not existsEnv("TR_SERVER_JAR") or not existsEnv("TR_BATTLE_RUNNER"): echo "Skipping: TR_SERVER_JAR / TR_BATTLE_RUNNER not set" quit(0) import test_framework/test_framework ``` - shared adversaries: `common_libs/test_framework/adversaries/SittingDuck` (passive) and `OscillatorBot` (fights back); standard call is `runBattle(@[myBotDir, adversaryDir], rounds = 10)`. - env vars: `TR_SERVER_JAR`, `TR_BATTLE_RUNNER`. Failures surface as `OSError` (bot compile failed), `IOError` (runner non-zero), `TimeoutError` (server 15s / compile 30s per bot / battle runner). ## Onboarding — full sequence ```sh # 1. dependency (once; already global if nimble install says "already installed") nimble install robocode_tankroyale_botapi # 2. run the bot (compiles to a temp dir, runs, cleans up; nothing left behind) ./DevControlBot/DevControlBot.sh # 2b. same thing via the task — must be run from DevControlBot/ cd DevControlBot && nimble runBot # 3. tests — none exist yet; see "Test organization" above ``` Checklist after touching build config: - `cd DevControlBot && nimble runBot` compiles (fails only on "Cannot connect" without a server) and leaves the garage tree unchanged (`ls DevControlBot_garage` still shows only `AGENTS.md`, `README.md`, `DevControlBot/`). ## Layout of this folder ``` DevControlBot_garage/ ├── AGENTS.md # this file ├── README.md # human-facing description └── DevControlBot/ ├── DevControlBot.nim # bot type + entry point (isMainModule) ├── DevControlBot.json # bot metadata ├── DevControlBot.nimble # single task: runBot ├── DevControlBot.sh # BotLauncher entry point (thin wrapper) └── .env # OPTIONAL local config; secrets, never commit ``` ## Coordinates & angles (source: robocode.dev/articles/coordinates-and-angles.html) Source: https://robocode.dev/articles/coordinates-and-angles.html (Tank Royale docs) - Cartesian coordinate system; (0, 0) is the bottom-left corner of the arena. - Y up is implied by the origin being bottom-left (page never states it explicitly); X east is confirmed via the angle rules below. - Angles follow classic trig (unlike original Robocode, whose 0/360 was north and 90 east). - 0°/360° = east; 90° = north; 180° = west; 270° = south. - Positive angles go counterclockwise; turning right decreases the angle (clockwise). - Full turn = 360°. - NOT stated on this page: distance formula, bearing/angle-between-two-points formula, angle normalization/wrapping, worked example numbers. ## Behaviour diagrams **CURRENT STATE, not a design sketch.** These describe the code as it is today. **Diagram 1 — one tick. Owned by `DevControlBot/DevControlBot.nim` (`run`, lines 34-40) and `DevControlBot/modules/radar.nim` (`onScan`, `commandRadar`) — update this diagram when either file changes.** The ordering is the whole point: the command is issued BEFORE `go()` so it travels with this tick's intent, and events arrive AFTER, so a scan can only ever steer the NEXT tick. ```mermaid sequenceDiagram participant Run as run loop participant Radar as radar.nim participant Bot as bot + API participant Srv as server Run->>Radar: commandRadar
# one command, every tick, no idle branch Radar-->>Bot: setRadarTurnRate
LOCK turn + 5deg overshoot,
or +45deg sweep Run->>Bot: go
# intent sent here, FIRST Bot->>Srv: tick intent
radar turn only,
no move, no fire Srv-->>Bot: tick result
+ pending ScannedBotEvent Bot->>Radar: onScannedBot -> onScan x, y
# AFTER go: remembers only Radar-->>Radar: target position stored,
tick counter reset Note over Run,Radar: that remembered target is used
by the NEXT commandRadar ``` **Diagram 2 — radar behaviour. Owned by `DevControlBot/modules/radar.nim` (`commandRadar`, lines 70-81) — update this diagram when that file changes.** SWEEP is not a separate lifecycle: it is the same single command the lock replaces, so switching costs no ticks. ```mermaid stateDiagram-v2 [*] --> SWEEP state "SWEEP
360 sweep at 45 deg/tick,
full revolution every 8 ticks" as SWEEP state "LOCKED
servo onto target bearing
PLUS 5 deg overshoot" as LOCKED SWEEP --> LOCKED: "getEnemyCount is 1
AND target seen within
FreshScanTurns 1" LOCKED --> SWEEP: "enemy count is not 1,
or target stale beyond 10 ticks,
or lost" LOCKED --> LOCKED: "scan every tick
because the radar crosses
the bearing" ``` The 5 deg overshoot is the reason the lock works: the radar sweeps ACROSS the bearing instead of parking on it, so a fresh `ScannedBotEvent` arrives every tick. Turning by the bare error, or holding at rate 0 inside a deadzone, stops the scans and loses the target until a sweep re-finds it. Never replace the overshoot with a deadzone hold. ## Physics (source: robocode.dev/articles/physics.html) Source: https://robocode.dev/articles/physics.html (Tank Royale docs) - Turns/rounds 1-based; each round restarts at turn 1. Distance units: floating-point double. - Acceleration: +1 unit/turn; deceleration: 2 units/turn (braking 2x faster than accelerating). - Speed: v = a * t; max speed 8 units/turn. Velocity direction == bot heading. - Distance: d = v * t. - Rotation in degrees. Base max turn rate: 10 - 3/4*|v| deg/turn (10 deg/turn standing still; 4 deg/turn at max speed 8). - Gun rotation max 20 deg/turn (added to bot's current rate). - Radar rotation max 45 deg/turn (added to gun's current rate). - Firepower: min 0.1, max 3; energy cost subtracted from bot energy. - Bullet damage: 4 * firepower, plus 2 * (firepower - 1) if firepower > 1. - Bullet speed: 20 - 3 * firepower units/turn (19.7 at fp 0.1; 11 at fp 3); constant per shot. - Gun heat gain: 1 + firepower/5. Cannot fire while heat > 0. Gun starts at heat 3 each round. - Energy gain on bullet hit: 3 * firepower. - Collision with bot/wall stops the bot, except when moving away from the bot that hit it. - Bot-vs-bot collision: 0.6 damage to each. - Ramming (moving forward into another bot): both take damage; rammer gets ramming kill bonus. Wall damage: |v|/2 - 1, clamped at 0 if negative. - NOT stated on this page: coordinate system/axes/units, Y up or down, angle origin & clockwise/CCW sense, distance/bearing formulas, angle normalization/wrapping, max energy, per-turn energy regain from inactivity.