Files
SirRoboGarage/DevControlBot_garage/AGENTS.md
T
SirStone caa096d6c9 Add radar lock and melee sweep to DevControlBot
The bot now drives its radar every tick. It still never moves and never
fires: body, turret and gun stay white ("not programmed"), while the
radar paints itself black at init to mark itself as working.

- modules/radar.nim replaces the earlier multi-module radar design with a
  single module holding the whole of it: target memory, mode selection
  and both commands
- two commands, picked from the only fact the bot is sure of (the API's
  getEnemyCount()): one enemy plus a target in hand -> LOCK, servo onto
  its bearing; anything else -> SWEEP at 45 deg/tick, the radar's physics
  cap, a full revolution every 8 ticks
- the lock turns by the delta to the target's bearing plus 5 deg PAST it,
  so the radar CROSSES the bearing every tick instead of settling on it.
  That crossing is what produces a fresh ScannedBotEvent every tick: a
  scan per tick, with no rescan to wait for. Turning by the bare error
  parks the radar on the bearing and the scans stop
- commandRadar() is called before go() on every path, so there is no idle
  branch to fall into and switching modes costs zero turns
- idea derived from ModularBot's radar (Apache-2.0) via common_libs/;
  common_libs/ is not edited in place
- AGENTS.md gains mermaid behaviour diagrams plus pointers to the physics
  and coordinate references
- version bumped to 1.1.0 in both DevControlBot.nimble and
  DevControlBot.json so the two agree

Compiled against robocode_tankroyale_botapi 1.0.7.
2026-10-04 16:22:42 +02:00

18 KiB

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 template do NOT apply to DevControlBot_garage/. The root template documents a src/<bot>.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 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 — read it first.

Garage-specific rules for DevControlBot_garage/. Repo-wide rules live in ../../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 <pkg>, 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:
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, cds to the .nimble dir, forwards args and signals to nimble runBot, and propagates the exit code.

./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 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<concern>.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:

      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

# 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.

sequenceDiagram
    participant Run as run loop
    participant Radar as radar.nim
    participant Bot as bot + API
    participant Srv as server
    Run->>Radar: commandRadar<br/># one command, every tick, no idle branch
    Radar-->>Bot: setRadarTurnRate<br/>LOCK turn + 5deg overshoot,<br/>or +45deg sweep
    Run->>Bot: go<br/># intent sent here, FIRST
    Bot->>Srv: tick intent<br/>radar turn only,<br/>no move, no fire
    Srv-->>Bot: tick result<br/>+ pending ScannedBotEvent
    Bot->>Radar: onScannedBot -> onScan x, y<br/># AFTER go: remembers only
    Radar-->>Radar: target position stored,<br/>tick counter reset
    Note over Run,Radar: that remembered target is used<br/>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.

stateDiagram-v2
    [*] --> SWEEP
    state "SWEEP<br/>360 sweep at 45 deg/tick,<br/>full revolution every 8 ticks" as SWEEP
    state "LOCKED<br/>servo onto target bearing<br/>PLUS 5 deg overshoot" as LOCKED
    SWEEP --> LOCKED: "getEnemyCount is 1<br/>AND target seen within<br/>FreshScanTurns 1"
    LOCKED --> SWEEP: "enemy count is not 1,<br/>or target stale beyond 10 ticks,<br/>or lost"
    LOCKED --> LOCKED: "scan every tick<br/>because the radar crosses<br/>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.