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:
@@ -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
@@ -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"
|
||||
@@ -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
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user