Add DevControlBot garage: skeleton bot with release/debug build script

A minimal Tank Royale bot that boots and stands still. Body, gun and radar
are set to white once at init (team convention: white = not programmed yet);
the tick loop only calls go().

- DevControlBot/ holds the single bot module, its metadata JSON, the
  BotLauncher entry script and the nimble file
- DevControlBot.sh is a thin wrapper around the runBot nimble task:
  release build by default, --debug for the classic diagnostic build
- dependencies resolve at run time from the global nimble store via
  `nimble path`; optional .env (gitignored) supplies env vars such as
  SERVER_URL, with existing environment variables taking precedence
- build happens in a throwaway mktemp dir removed on exit; no artifacts
This commit is contained in:
2026-10-03 16:32:27 +02:00
parent 7632aaba06
commit 59fad00b5c
8 changed files with 820 additions and 0 deletions
+228
View File
@@ -0,0 +1,228 @@
# 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/<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.**
## 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.
- Body, gun and radar are `WHITE` (team convention: that part is not programmed
yet). Colors are set once at initialization, never in the tick loop. Keep it
that way: the tick loop must do nothing but `go()` until real logic is written.
## 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`:
```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 on `ws://localhost:7654` 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 on 7654 (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<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:
```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
```
@@ -0,0 +1,2 @@
# User-authored environment variables (e.g. SERVER_URL) — never committed
.env
@@ -0,0 +1,9 @@
{
"name": "DevControlBot",
"version": "0.1.0",
"authors": ["Davide Cappellini"],
"description": "Control skeleton bot — does nothing, bright colors",
"gameTypes": ["classic", "1v1"],
"platform": "Nim",
"programmingLang": "Nim"
}
@@ -0,0 +1,32 @@
## DevControlBot — control skeleton: boots, stands still, does nothing.
##
## Team convention: white means "not programmed yet", so body, gun and radar are
## all plain white. Colors are applied once at initialization, never per tick.
##
## This module is both the bot type and the program entry point (see isMainModule
## below); there is no separate top-level DevControlBot.nim.
##
## API: robocode_tankroyale_botapi 1.0.7 (the renamed tankroyale_botapi package).
import std/os
import robocode_tankroyale_botapi
type DevControlBot* = ref object of Bot
proc newDevControlBot*(): DevControlBot =
result = DevControlBot()
setBodyColor(WHITE)
setTurretColor(WHITE)
setGunColor(WHITE)
setRadarColor(WHITE)
method run*(bot: DevControlBot) =
while isRunning():
go()
when isMainModule:
# argv[1] may override the metadata file (used by DevControlBot/DevControlBot.sh);
# otherwise the JSON next to this source, resolved at compile time so any cwd works.
let jsonPath = if paramCount() >= 1: paramStr(1)
else: currentSourcePath().parentDir / "DevControlBot.json"
var bot = newDevControlBot()
start(bot, jsonPath)
@@ -0,0 +1,238 @@
# Package
version = "0.1.0"
author = "Davide Cappellini"
description = "DevControlBot — control-skeleton bot that does nothing but show up bright"
license = "MIT"
srcDir = "."
bin = @[]
# Dependencies
# Installed globally with `nimble install`; nothing is vendored locally and
# no nimble.paths / vendor/ is used. `runBot` resolves each package's real
# location at run time with `nimble path` and passes explicit --path: flags,
# so the build does not rely on $HOME or on the compiler's bundled nimblepath.
requires "nim >= 2.0.0"
requires "robocode_tankroyale_botapi >= 1.0.7"
requires "jsony >= 1.1.5"
# The build-and-run logic lives here, not in DevControlBot.sh.
#
# NimScript cannot install signal handlers, so the temp build dir is owned by a
# small POSIX shell program (below) that Nimble executes via `exec` (which goes
# through the shell). That program traps EXIT/INT/TERM and always removes the
# throwaway build dir, including when compilation fails or the run is
# interrupted. Nothing is ever written to a persistent out/ directory.
#
# `exec` turns ANY non-zero exit into a NimScript exception, which prints a
# stack trace plus an escaped copy of the whole script. That is nonsense for the
# expected "no game server on 7654" outcome. So the shell always exits 0 and
# hands the real code back through the file named by $DEVCONTROLBOT_STATUS_FILE,
# which DevControlBot.sh exports and then re-exits with. Direct `nimble runBot`
# (no such variable) keeps the old behaviour: the shell exits with the real code
# and Nimble raises.
#
# DevControlBot.sh is a thin wrapper: it locates itself, chdirs here so Nimble
# finds this file, forwards caller args, calls `runBot` and forwards signals.
import std/os
const
runScript = """
# Resolve the globally-installed dependencies at run time. Plain `nim c` only
# finds them when $HOME is set (the Nim distribution's nim.cfg carries
# `nimblepath="$home/.nimble/pkgs2/"`), which is NOT true in bare environments
# (env -i, nix build sandboxes, CI). `nimble path` is the reliable,
# cwd-independent way to ask for their real location.
# Distinct exit codes, so a caller can tell the failure modes apart.
EXIT_NO_SERVER=1 # bot could not connect to the game server (expected)
EXIT_NO_DEPS=2 # a dependency is not installed
EXIT_COMPILE=3 # compilation failed
STATUS_FILE="${DEVCONTROLBOT_STATUS_FILE:-}"
# report <code>: write the real exit code to the status file. Returns 1 when
# there is no status file, i.e. the caller wants it as the shell's own code.
report() {
[ -n "$STATUS_FILE" ] || return 1
printf '%s\n' "$1" > "$STATUS_FILE"
}
# die <code>: finish with <code> as the run's real result — quietly (exit 0,
# the status file carries the code) or, with no listener, by exiting with it.
die() {
if report "$1"; then exit 0; else exit "$1"; fi
}
DEPS=""
for pkg in robocode_tankroyale_botapi jsony; do
pkgdir="$(nimble path "$pkg" 2>/dev/null | head -n 1)"
if [ -z "$pkgdir" ] || [ ! -d "$pkgdir" ]; then
echo "[devcontrolbot] COMPILE BLOCKED: dependency '$pkg' is not installed." >&2
echo "[devcontrolbot] fix with: nimble install $pkg" >&2
die $EXIT_NO_DEPS
fi
echo "[devcontrolbot] dependency $pkg -> $pkgdir"
DEPS="$DEPS '--path:$pkgdir'"
done
# Build mode: $DEVCONTROLBOT_DEBUG is set to "debug" by DevControlBot.sh when
# the caller passed --debug; anything else (including an unset variable, i.e. a
# direct `nimble runBot`) means release. Release is the quiet default.
BUILD_MODE="${DEVCONTROLBOT_DEBUG:-release}"
BUILD_DIR="$(mktemp -d "${TMPDIR:-/tmp}/devcontrolbot.XXXXXX")"
BINARY="$BUILD_DIR/DevControlBot"
# ---- optional .env ------------------------------------------------------------
# Configuration reaches the bot through the ENVIRONMENT, not through any code
# of ours: robocode_tankroyale_botapi's start() reads SERVER_URL and
# SERVER_SECRET (robocode_tankroyale_botapi.nim:424-425, documented at :396-397)
# and loadBotInfo() reads BOT_NAME / BOT_VERSION / ... from the environment when
# the JSON is absent (bot_info.nim:117-130). That is the documented, supported
# mechanism the official BotLauncher uses, so the only thing we do here is make
# those variables PRESENT in the bot's process environment for local runs.
#
# Loaded HERE, in the runBot task, and not in DevControlBot.sh, so it applies both
# to `./DevControlBot.sh` and to a direct `nimble runBot` (one implementation, and
# the wrapper stays a thin wrapper).
# - optional: a missing file is not an error, nothing is printed but a note,
# the bot just uses the API defaults.
# - CRLF-tolerant: CRs are stripped so a Windows-edited file does not end up
# with the value "bar\r".
# - both `FOO=bar` and `export FOO=bar` styles.
# - never echoed, never `set -x`: only the file NAME is reported.
# - $DEVCONTROLBOT_ENV_FILE overrides the default path (".env" next to the
# .nimble, which is the cwd because DevControlBot.sh chdirs there).
#
# PRECEDENCE — CRITICAL. The .env file must NEVER override a variable that is
# already present in the environment: the official BotLauncher's values (and the
# caller's explicit `FOO=bar ./DevControlBot.sh`) must always win, otherwise this
# convenience file could silently break a BotLauncher run. Sourcing the file
# directly would do the opposite, because an assignment in a sourced file
# overwrites an already-exported variable of the same name. So the file is
# FILTERED line by line: a `NAME=value` line is dropped when NAME is already set
# in the environment (tested with `[ -n "${NAME+x}" ]`, i.e. set-but-maybe-empty,
# not non-empty), and only the remaining, still-unset names are sourced with
# `set -a`. The filtering is what implements the precedence, not the source order.
ENV_FILE="${DEVCONTROLBOT_ENV_FILE:-.env}"
if [ -f "$ENV_FILE" ]; then
ENV_RAW="$BUILD_DIR/env.raw"
ENV_KEEP="$BUILD_DIR/env.keep"
ENV_ADDED=""
# CRLF tolerance: drop the CRs, keep everything else verbatim.
tr -d '\r' < "$ENV_FILE" > "$ENV_RAW"
: > "$ENV_KEEP"
while IFS= read -r line || [ -n "$line" ]; do
case "$line" in
''|\#*) continue ;; # blank / comment
export\ *|export=*) line="${line#export }" ;;
esac
name="${line%%=*}"
# Only plain NAME=value assignments; anything else is ignored, so the file
# can never smuggle in commands. `name` is validated as an identifier
# BEFORE being used in the eval below.
printf '%s' "$name" | grep -q '^[A-Za-z_][A-Za-z_0-9]*$' || continue
# PRECEDENCE: skip if the variable already exists in the environment.
if eval "[ -n \"\${$name+x}\" ]"; then
continue
fi
printf '%s\n' "$line" >> "$ENV_KEEP"
ENV_ADDED="$ENV_ADDED $name"
done < "$ENV_RAW"
if [ -n "$ENV_ADDED" ]; then
set -a
. "$ENV_KEEP"
set +a
fi
rm -f "$ENV_RAW" "$ENV_KEEP"
# Variable NAMES only, never values.
echo "[devcontrolbot] env file loaded: $ENV_FILE (names added:${ENV_ADDED:- none}; values never printed)"
echo "[devcontrolbot] variables already in the environment win and were left untouched"
else
echo "[devcontrolbot] no env file at $ENV_FILE (optional, using API defaults)"
fi
cleanup() {
rc=$?
trap - EXIT INT TERM
rm -rf "$BUILD_DIR"
if [ -d "$BUILD_DIR" ]; then
echo "[devcontrolbot] cleanup failed: $BUILD_DIR still present" >&2
exit 1
fi
exit $rc
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
# Release: hints and the progress/dot lines are noise, so the compiler output is
# captured to a log and only shown if the build fails (errors are ALWAYS shown).
# Debug: classic build, full compiler output (config-file hints, diagnostics).
LOG="$BUILD_DIR/build.log"
if [ "$BUILD_MODE" = "debug" ]; then
echo "[devcontrolbot] compiling DevControlBot.nim (DEBUG BUILD) -> $BINARY"
# `eval` re-parses $DEPS so the embedded quotes do the word splitting: the
# dependency paths survive spaces, without relying on glob or brace expansion.
# No pipe: POSIX sh has no PIPESTATUS, and `nim | tee` would report tee's
# status, not the compiler's. Buffer first, then replay the log verbatim.
eval nim c $DEPS --out:"$BINARY" DevControlBot.nim >"$LOG" 2>&1
compile_rc=$?
cat "$LOG"
else
echo "[devcontrolbot] compiling DevControlBot.nim -> $BINARY"
eval nim c -d:release --hints:off $DEPS --out:"$BINARY" DevControlBot.nim >"$LOG" 2>&1
compile_rc=$?
fi
if [ $compile_rc -ne 0 ]; then
if [ "$BUILD_MODE" != "debug" ]; then
echo "[devcontrolbot] --- compiler output ---" >&2
cat "$LOG" >&2
echo "[devcontrolbot] --- end compiler output ---" >&2
fi
echo "[devcontrolbot] COMPILE FAILED (nim c exited $compile_rc) — errors above." >&2
die $EXIT_COMPILE
fi
rm -f "$LOG"
if [ "$BUILD_MODE" = "debug" ]; then
echo "[devcontrolbot] build ok (debug)"
else
echo "[devcontrolbot] build ok (release)"
fi
# The bot reads argv[1] as its metadata JSON path. If the caller already
# supplied a JSON, use that instead of the bundled one.
if [ -n "${1:-}" ] && [ "${1##*.}" = "json" ]; then
echo "[devcontrolbot] using caller-supplied metadata: $1"
"$BINARY" "$@"
else
# NOTE: no `exec` here: the shell must survive so the EXIT trap can clean up.
echo "[devcontrolbot] running $BINARY DevControlBot.json $*"
"$BINARY" DevControlBot.json "$@"
fi
bot_rc=$?
if [ $bot_rc -ne 0 ]; then
echo "[devcontrolbot] bot exited with code $bot_rc." >&2
if [ $bot_rc -eq $EXIT_NO_SERVER ]; then
# The Tank Royale client answers a refused connection with this exact
# exit code, so it means "no server", not "broken bot".
echo "[devcontrolbot] No game server on ${SERVER_URL:-ws://localhost:7654} (SERVER_URL)." >&2
echo "[devcontrolbot] Start the Tank Royale server, or check it is listening on port 7654." >&2
else
echo "[devcontrolbot] The bot ran but failed at runtime (crash or bad metadata)." >&2
fi
fi
die $bot_rc
"""
task runBot, "Compile, run and clean up the bot":
# Nimble passes every CLI argument here (its own flags, then the task name,
# then the caller's arguments). Ours are the ones after the task name.
var args = ""
var seen = false
for a in commandLineParams():
if seen:
if args != "": args = args & " "
args = args & quoteShell(a)
elif a == "runBot":
seen = true
exec "sh -c " & quoteShell(runScript) & " devcontrolbot " & args
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
# BotLauncher entry point for DevControlBot.
#
# Thin wrapper: all build/run/cleanup logic lives in the `runBot` nimble task
# of DevControlBot.nimble. This script only locates itself, moves next to the
# .nimble so Nimble finds it, forwards caller arguments, calls the task and
# propagates the real exit code. Signals are forwarded to Nimble so the task's
# own EXIT/INT/TERM cleanup always runs.
#
# Usage: DevControlBot.sh [--debug] [<metadata.json>] [extra args for the bot]
#
# (no args) RELEASE build + run with the bundled DevControlBot.json
# (no flag) RELEASE build (nim c -d:release), quiet output
# --debug classic DEBUG build (plain nim c): compiler hints + diagnostics
#
# --debug is a build-mode flag for this wrapper only: it is stripped here and
# never reaches the bot binary or nimble's task arguments. It travels to the
# runBot task as $DEVCONTROLBOT_DEBUG (exported), because an env var cannot be
# confused with a bot argument, cannot collide with the status file mechanism
# and keeps the task's argument parsing untouched. Repeated --debug is harmless.
#
# Configuration (SERVER_URL, SERVER_SECRET, ...) is NOT handled here at all: the
# API's start() reads those from the environment, and the optional .env file is
# loaded by the runBot task, so it also works with a direct `nimble runBot`.
set -uo pipefail
set -m # job control: the background job gets its own process group
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
usage() {
cat <<EOF
Usage: $(basename "$0") [--debug] [<metadata.json>] [extra args for the bot]
--debug classic debug build: shows compiler hints and diagnostics
(default is a quiet RELEASE build, nim c -d:release)
-h,--help show this help
Examples:
$(basename "$0") # release build + run, bundled DevControlBot.json
$(basename "$0") my.json # release build + run with custom metadata
$(basename "$0") -h # this help
$(basename "$0") --debug # debug build
$(basename "$0") --debug my.json # debug build with custom metadata
EOF
}
# Split caller args: everything except --debug is forwarded verbatim.
BUILD_MODE=release
FORWARDED=()
for a in "$@"; do
case "$a" in
--debug) BUILD_MODE=debug ;;
*) FORWARDED+=("$a") ;;
esac
done
set -- ${FORWARDED+"${FORWARDED[@]}"}
# No arguments is the normal case: build and run with the bundled metadata.
# Usage is printed ONLY for an explicit -h/--help.
case "${1:-}" in
-h|--help) usage; exit 0 ;;
esac
export DEVCONTROLBOT_DEBUG="$BUILD_MODE"
# The runBot task's shell always exits 0 (otherwise Nimble raises a NimScript
# exception with a stack trace and a dump of the whole script) and writes the
# bot's real exit code here instead. We read it back and exit with it, so
# BotLauncher still sees the faithful status: 1 = no game server on 7654,
# 2 = a dependency is missing, 3 = compile failure, anything else = the bot's
# own code, 0 = clean run.
STATUS_FILE="$(mktemp "${TMPDIR:-/tmp}/devcontrolbot.status.XXXXXX")"
export DEVCONTROLBOT_STATUS_FILE="$STATUS_FILE"
trap 'rm -f "$STATUS_FILE"' EXIT
# Forward to the whole job process group (nimble + the shell it spawned) so the
# task's own EXIT/INT/TERM cleanup runs. No cleanup logic here.
forward() { kill -s "$1" -- "-$NIMBLE_PID" 2>/dev/null; }
trap 'forward TERM' TERM
trap 'forward INT' INT
nimble runBot ${1+"$@"} &
NIMBLE_PID=$!
wait "$NIMBLE_PID"
rc=$?
trap - TERM INT
# Prefer the code the run actually produced; fall back to Nimble's own status if
# the task died before it could report (e.g. Nimble itself failed).
real_rc="$(head -n 1 "$STATUS_FILE" 2>/dev/null | tr -d '[:space:]')"
[ -n "$real_rc" ] || real_rc=$rc
exit "$real_rc"
+6
View File
@@ -0,0 +1,6 @@
1. Create a tidy Bot garage folder
2. Make inside a folder ready to be exported and shared with others
3. Create a basic bot with all white colors and does nothing else
4. make a nice runnable in bash script fro the RT official launcher
5. add nice radars: 1vs1 and melee
5. decide the gun to develop
+213
View File
@@ -0,0 +1,213 @@
# DevControlBot
Control skeleton bot: it boots, participates in the battle, and does **nothing**
per tick except `go()` — no movement logic, no scanning, no firing.
Body, gun and radar are plain **white**, which by team convention means "this part
is not programmed yet". Colors are set **once at initialization** (`newDevControlBot`),
never inside the tick loop.
## Layout
```
DevControlBot_garage/
├── AGENTS.md
├── README.md
└── DevControlBot/
├── DevControlBot.nim # bot type + entry point (isMainModule)
├── DevControlBot.json # bot metadata
├── DevControlBot.nimble # the single `runBot` task
├── DevControlBot.sh # BotLauncher entry point
└── .env # OPTIONAL local config (secrets; never commit)
```
There is no `src/`, no `tests/`, no `out/`, no `config.nim`, no `nimble.paths`
and no vendor directory. `DevControlBot.nim` is both the bot implementation and
the program entry point.
## Dependencies
Only two, both installed **globally** with Nimble and resolved from the global
package store — nothing vendored, no `nimble.paths`, no `nimble develop`:
- `robocode_tankroyale_botapi >= 1.0.7` (the renamed `tankroyale_botapi`)
- `jsony >= 1.1.5`
```sh
nimble install robocode_tankroyale_botapi
```
The `runBot` task asks Nimble where each of those packages actually lives
(`nimble path <pkg>`) and passes explicit `--path:` flags to `nim c`. This is
required because the compiler's own `nimblepath="$home/.nimble/pkgs2/"` only
works when `$HOME` is set — with an empty environment (`env -i`, Nix build
sandbox, CI) the packages are otherwise not on the search path. If a package is
not installed, the build stops with `run: nimble install <pkg>` instead of a
compiler error.
## Run
```sh
./DevControlBot/DevControlBot.sh
```
`DevControlBot.sh` is the **BotLauncher entry point**. It is a thin wrapper: it
locates itself, `chdir`s to the directory holding the `.nimble` file, forwards
arguments and signals to `nimble runBot`, and propagates the real exit code.
All build/run/cleanup logic lives in the `runBot` task of `DevControlBot.nimble`,
which creates a throwaway `mktemp -d` build dir, compiles there, runs, and always
removes the dir via an `EXIT`/`INT`/`TERM` trap. **Nothing is left behind** — this
bot never creates an `out/` directory.
`runBot` also accepts a metadata path: **as the first positional argument**. If it
ends in `.json` it is used instead of the bundled `DevControlBot.json`; there is
no `--json` flag (an argument that does not end in `.json` is forwarded to the bot
as-is).
```sh
./DevControlBot/DevControlBot.sh # bundled metadata (default)
./DevControlBot/DevControlBot.sh /path/to/my.json # alt metadata
./DevControlBot/DevControlBot.sh --debug /path/to/my.json # alt metadata, debug build
```
With **no arguments at all** the wrapper does *not* print usage: it 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` (exit 0, nothing is built).
## Configuration comes from the environment — the API's own mechanism
No configuration code exists in `DevControlBot.nim`, and none is needed. The
bot API reads its own environment variables inside `start()`:
| variable | read at | default |
|---|---|---|
| `SERVER_URL` | `robocode_tankroyale_botapi.nim:424` (documented at `:396`) | `ws://localhost:7654` |
| `SERVER_SECRET` | `robocode_tankroyale_botapi.nim:425` | `""` |
| `BOT_NAME`, `BOT_VERSION`, `BOT_AUTHORS`, … | `bot_info.nim:117-130`, used only when no JSON is found | see that file |
This is the supported mechanism the official BotLauncher uses: it puts those
variables in the bot's process environment before starting the binary. So the
only thing the build/run wrapper does is make sure the variables are *present*
in the bot's environment for local runs.
## Optional `.env` (a convenience for local runs only)
`DevControlBot/.env` is **optional**. If it is absent nothing happens: no error,
no warning beyond one informational line, the run behaves exactly as before and
the API defaults apply. It is not required, not generated, and not committed.
When present it is loaded by the **`runBot` task**, not by `DevControlBot.sh`, so
it applies identically to `./DevControlBot.sh` and to a direct `nimble runBot`
and the wrapper stays a thin wrapper. Properties:
- **Format:** plain `NAME=value` and `export NAME=value` lines; blank lines and
`#` comments ignored; anything that is not a simple `NAME=value` identifier
assignment is ignored (a `.env` can never smuggle in a command).
- **CRLF tolerant:** `CR` characters are stripped, so a Windows-edited file does
not turn the value into `bar\r`.
- **Nothing is ever printed.** Only the *names* taken from the file are reported,
never a value, and `set -x` is not used.
- **Overridable path:** `$DEVCONTROLBOT_ENV_FILE` points the loader at a
different file (useful for testing).
### Precedence: the environment always wins
A variable **already present in the environment is never overridden** by the
`.env` file. This matters because the official BotLauncher's values must win:
if a `.env` sitting in the checkout could overwrite them, this convenience would
silently break a real BotLauncher run.
The `.env` file is therefore **not** simply sourced — sourcing it would do the
opposite, because an assignment in a sourced file overwrites an already-exported
variable of the same name. Instead the file is **filtered line by line**: a
`NAME=value` line is dropped when `NAME` is already set in the environment
(tested with `[ -n "${NAME+x}" ]`, i.e. *set but possibly empty*, not merely
non-empty), and only the still-unset names are then exported with `set -a` and
sourced. The filtering is what implements the precedence.
```sh
SERVER_URL=ws://host:1234 ./DevControlBot.sh # shell wins, .env ignored for SERVER_URL
./DevControlBot.sh # .env supplies SERVER_URL
```
`.env` contains secrets, so it must never be committed; see the note in
[AGENTS.md](AGENTS.md) about the root `.gitignore`.
## Build modes: release by default, `--debug` for the classic build
| invocation | build | output |
|---|---|---|
| `DevControlBot.sh …` (default) | `nim c -d:release --hints:off` | quiet: dependency lines, one `build ok (release)`, one `running` line |
| `DevControlBot.sh --debug …` | plain `nim c` | classic diagnostics: `Hint: used config file …`, dot-progress line, `DEBUG BUILD` hint |
`--debug` is stripped by the wrapper wherever it appears in the arguments (a
repeated `--debug` is harmless) and is handed to the `runBot` task via the
exported env var `$DEVCONTROLBOT_DEBUG` (`debug` / `release`) rather than as a
task argument: an env var can never be mistaken for a bot argument, cannot
collide with the `$DEVCONTROLBOT_STATUS_FILE` exit-code mechanism, and leaves the
task's argument parsing untouched. Every other argument is forwarded verbatim.
In release mode the compiler's stdout/stderr is captured to a log and replayed
**only if the build fails**, so a failed build is always loud in both modes while
a successful release build stays short (10 lines including the no-server report,
vs 16 with `--debug`). `--help` / `-h` prints usage and exits 0 **without
building**; no arguments does the opposite — it builds and runs.
The bot needs a running Tank Royale server on `ws://localhost:7654`; without one
the run fails with `[start] Cannot connect to ws://localhost:7654` and exits 1
(expected during local verification).
## Failure reporting and exit codes
Nimble's `exec` raises a NimScript exception — stack trace, escaped copy of the
whole script — for *any* non-zero exit, which made "no game server" look like a
catastrophic build crash. The `runBot` shell therefore always exits 0 and writes
the real code to the status file named by `$DEVCONTROLBOT_STATUS_FILE`, which
`DevControlBot.sh` exports and then re-exits with. The real code is preserved,
not masked.
| exit | meaning |
|---|---|
| `0` | clean run |
| `1` | bot could not connect to the game server on `ws://localhost:7654` (expected locally) |
| `2` | a dependency is not installed — `nimble install <pkg>` |
| `3` | compilation failed — the compiler's own errors are printed above |
| other | the bot's own exit code (runtime crash, bad metadata) |
Each mode prints its own short line, e.g.
```
[start] Cannot connect to ws://localhost:7654: Connection refused
[devcontrolbot] bot exited with code 1.
[devcontrolbot] No game server on ws://localhost:7654.
[devcontrolbot] Start the Tank Royale server, or check it is listening on port 7654.
```
No `Exception raised during nimble script execution`, no stack trace, no source
dump. `nimble runBot` run directly (no status file) keeps the old behaviour and
still raises.
Interrupt handling is unchanged: SIGINT/SIGTERM are forwarded, the build dir is
removed by the `EXIT` trap and the wrapper exits 143 (SIGTERM) / 130 (SIGINT).
## Gotcha: `nimble runBot` must be run from `DevControlBot/`
The `.nimble` file lives in `DevControlBot/`, so the task is only visible from
there:
```sh
cd DevControlBot && nimble runBot # works
cd DevControlBot_garage && nimble runBot
# Error: Could not find a file with a .nimble extension inside the specified
# directory: .../DevControlBot_garage
```
`DevControlBot.sh` handles this for you by `cd`-ing itself. Note also that
`compile`/`build` are reserved Nimble builtin names — hence the `runBot` name.
## Test
There are **no tests** in this garage. The `test` task and the `tests/` directory
were removed along with the old build layout; see
[AGENTS.md](AGENTS.md) for the convention to follow when tests are added.