Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| de0dd50d6e | |||
| caa096d6c9 | |||
| 305c3977a6 | |||
| 59fad00b5c |
@@ -0,0 +1,2 @@
|
|||||||
|
# User-authored environment variables (e.g. SERVER_URL) — never committed
|
||||||
|
.env
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
# 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/` or `out/`, or put anything inside `DevControlBot/` for testing
|
||||||
|
> (the one exception is the intended `DevControlBot_garage/tests/`, see
|
||||||
|
> "Layout of this folder" — it does not exist yet, create it only when the
|
||||||
|
> first test is actually written)
|
||||||
|
> - add a `config.nims`, `nimble.paths`, `--path` flags, or a vendor/ directory
|
||||||
|
> - restructure `DevControlBot/` to match the root template
|
||||||
|
>
|
||||||
|
> Builds go to a `mktemp -d` throwaway dir that is removed on exit — nothing is
|
||||||
|
> ever written here. The root `AGENTS.md` and its folder conventions are for the
|
||||||
|
> **other** garages; here it is context only (issue tracker, Nim conventions), and
|
||||||
|
> is **NOT a layout contract for this folder**. If the root template and this file
|
||||||
|
> disagree about layout, **this file wins.**
|
||||||
|
|
||||||
|
> ## ⚠️ DO NOT TOUCH `PLAN.md`
|
||||||
|
>
|
||||||
|
> **[`PLAN.md`](PLAN.md) is the user's personal working notes file.** The user
|
||||||
|
> maintains it and edits it exclusively.
|
||||||
|
>
|
||||||
|
> - Agents must **NEVER** modify, rewrite, reformat, move, delete or commit
|
||||||
|
> changes to `PLAN.md` — unless the user explicitly asks in that conversation.
|
||||||
|
> - Reading it is **not** assumed or expected; do not open it by default.
|
||||||
|
> - It **IS** tracked in git and is intended to be committed (deliberately added
|
||||||
|
> in commit `59fad00b`). Do **NOT** gitignore it and do **NOT**
|
||||||
|
> `git rm --cached` it.
|
||||||
|
|
||||||
|
## Start here
|
||||||
|
|
||||||
|
The code is the source of truth; this file is an inventory of what exists and how
|
||||||
|
the pieces interact — not a description of what the code does. If the two
|
||||||
|
disagree, the code wins and this file is stale.
|
||||||
|
|
||||||
|
The human-facing description lives in [README.md](README.md). 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
|
||||||
|
|
||||||
|
### Git rule for this garage
|
||||||
|
|
||||||
|
- **EVERYTHING** under `DevControlBot_garage/` gets committed — sources, modules,
|
||||||
|
docs, `PLAN.md` — **unless it is explicitly listed in a `.gitignore`**.
|
||||||
|
- This includes files that look like scratch or generated output, and files an agent
|
||||||
|
did not create: if not gitignored, they belong in the commit.
|
||||||
|
- Do not silently leave a file untracked, and do **not** add entries to
|
||||||
|
`.gitignore` to avoid committing something. The only things that stay out are
|
||||||
|
those the user has deliberately gitignored (e.g. `.env`).
|
||||||
|
|
||||||
|
### Version bumps and tags — the user's decision
|
||||||
|
|
||||||
|
Agents **MUST NOT** bump the version in `DevControlBot.nimble` or
|
||||||
|
`DevControlBot.json`, and **MUST NOT** create git tags. A version bump and its tag
|
||||||
|
are honoured **only** when the user explicitly asks in that conversation; a bump is
|
||||||
|
never a side effect of ordinary work or of a commit. Never amend, rewrite or retag
|
||||||
|
an existing commit or tag.
|
||||||
|
|
||||||
|
### Scope and colour
|
||||||
|
|
||||||
|
- This bot's code lives **only** inside `DevControlBot_garage/`. Do not edit other
|
||||||
|
garages to make this bot work, and do not reuse another bot's game logic
|
||||||
|
without explicit permission; shared, bot-agnostic code belongs in
|
||||||
|
`common_libs/`, and only with permission.
|
||||||
|
- Team colour convention: **white = not programmed, black = radar active**.
|
||||||
|
- Provenance: ModularBot / common_libs (Davide Cappellini, Apache-2.0).
|
||||||
|
|
||||||
|
### Constraints worth remembering
|
||||||
|
|
||||||
|
Not explanations — real invariants that broke things when violated:
|
||||||
|
|
||||||
|
- **Command the radar BEFORE `go()`** — a real bug the other way round.
|
||||||
|
- All radar logic is `modules/radar.nim`, in one piece. Do not split it back into
|
||||||
|
harness/lock/melee modules.
|
||||||
|
|
||||||
|
## Layout of this folder
|
||||||
|
|
||||||
|
```
|
||||||
|
DevControlBot_garage/
|
||||||
|
├── AGENTS.md # this file
|
||||||
|
├── README.md # human-facing description
|
||||||
|
├── PLAN.md # the user's own notes — never edit (see header)
|
||||||
|
├── .gitignore # ignores .env
|
||||||
|
├── 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
|
||||||
|
│ └── modules/
|
||||||
|
│ └── radar.nim # all radar logic
|
||||||
|
└── tests/ # INTENDED, does not exist yet — create it on
|
||||||
|
# the first real test
|
||||||
|
```
|
||||||
|
|
||||||
|
- `DevControlBot/` holds only what the bot needs to run.
|
||||||
|
- `tests/` is a sibling of `DevControlBot/`, never inside it, and holds **test
|
||||||
|
code worth keeping**: drivers, harnesses, reusable battle/probe scripts.
|
||||||
|
- **Throwaway output does NOT go here or anywhere in the repo — it goes to
|
||||||
|
`/tmp`**: logs, temporary binaries, dumps, telemetry, scratch output.
|
||||||
|
- No `out/`, no binaries, no `nimbledeps/` here. Builds are throwaway by design.
|
||||||
|
|
||||||
|
## Build and run
|
||||||
|
|
||||||
|
Use the folder task, never raw `nim c`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd DevControlBot_garage/DevControlBot
|
||||||
|
nimble runBot # quiet release build + run
|
||||||
|
```
|
||||||
|
|
||||||
|
Or via the wrapper, which works from any cwd:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./DevControlBot/DevControlBot.sh # bundled metadata
|
||||||
|
./DevControlBot/DevControlBot.sh my.json # first positional arg, must end in .json
|
||||||
|
./DevControlBot/DevControlBot.sh --debug # classic debug build (hints + diagnostics)
|
||||||
|
./DevControlBot/DevControlBot.sh --help # usage, exits 0 without building
|
||||||
|
```
|
||||||
|
|
||||||
|
- **No arguments means RUN, not usage.** Usage prints only for `-h` / `--help`.
|
||||||
|
There is no `--json` flag; a first positional ending in `.json` overrides the
|
||||||
|
metadata, anything else is forwarded to the bot verbatim.
|
||||||
|
- **Gotcha:** `nimble runBot` must run from `DevControlBot/` (where the
|
||||||
|
`.nimble` lives); from `DevControlBot_garage/` it fails with
|
||||||
|
`Could not find a file with a .nimble extension...`. `compile`/`build` are
|
||||||
|
reserved Nimble builtin names — hence `runBot`.
|
||||||
|
- **Config is environment-only.** The bot API's `start()` reads `SERVER_URL` /
|
||||||
|
`SERVER_SECRET` itself; **never add env-reading code to `DevControlBot.nim`.**
|
||||||
|
The optional `.env` (loaded by the `runBot` task, `$DEVCONTROLBOT_ENV_FILE`
|
||||||
|
overrides the path) is local-runs-only and **never overrides a variable already
|
||||||
|
present in the environment** — the BotLauncher's values always win. Never print
|
||||||
|
its values, never `set -x`, never commit it.
|
||||||
|
- Dependency setup is `nimble install robocode_tankroyale_botapi`; dependencies
|
||||||
|
come from the global nimble store. Do not vendor, do not add
|
||||||
|
`nimble.paths` / `nimble develop`.
|
||||||
|
- Without a game server at `SERVER_URL` the run fails with
|
||||||
|
`[start] Cannot connect ... Connection refused`. That is expected locally, not a
|
||||||
|
bug.
|
||||||
|
- Exit codes are distinct because BotLauncher branches on them: `0` clean,
|
||||||
|
`1` no server (expected), `2` dependency missing, `3` compile failure,
|
||||||
|
anything else = the bot's own code.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
There are **no tests yet** — no `tests/` directory and no `test` task; `nimble test`
|
||||||
|
does not exist. When the first test is written, follow the repo-wide convention in
|
||||||
|
[`../../AGENTS.md`](../../AGENTS.md) and
|
||||||
|
`common_libs/test_framework/README.md`: `tests/config.nims` with
|
||||||
|
`--path:"../../common_libs"`, one `test<concern>.nim` per concern, a `test` task
|
||||||
|
in the `.nimble`, and a guard-first block so tests pass with no Java present.
|
||||||
|
|
||||||
|
## Coordinates & angles (source: robocode.dev/articles/coordinates-and-angles.html)
|
||||||
|
|
||||||
|
Source: https://robocode.dev/articles/coordinates-and-angles.html (Tank Royale docs)
|
||||||
|
|
||||||
|
- Cartesian coordinate system; (0, 0) is the bottom-left corner of the arena.
|
||||||
|
- Y up is implied by the origin being bottom-left (page never states it explicitly); X east is confirmed via the angle rules below.
|
||||||
|
- Angles follow classic trig (unlike original Robocode, whose 0/360 was north and 90 east).
|
||||||
|
- 0°/360° = east; 90° = north; 180° = west; 270° = south.
|
||||||
|
- Positive angles go counterclockwise; turning right decreases the angle (clockwise).
|
||||||
|
- Full turn = 360°.
|
||||||
|
- NOT stated on this page: distance formula, bearing/angle-between-two-points formula, angle normalization/wrapping, worked example numbers.
|
||||||
|
|
||||||
|
## Behaviour diagrams
|
||||||
|
|
||||||
|
**CURRENT STATE, not a design sketch.** These describe the code as it is today.
|
||||||
|
|
||||||
|
**Diagram 1 — one tick. Owned by `DevControlBot/DevControlBot.nim` (`run`, lines
|
||||||
|
34-40) and `DevControlBot/modules/radar.nim` (`onScan`, `commandRadar`) — update
|
||||||
|
this diagram when either file changes.** The ordering is the whole point: the
|
||||||
|
command is issued BEFORE `go()` so it travels with this tick's intent, and
|
||||||
|
events arrive AFTER, so a scan can only ever steer the NEXT tick.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Run as run loop
|
||||||
|
participant Radar as radar.nim
|
||||||
|
participant Bot as bot + API
|
||||||
|
participant Srv as server
|
||||||
|
Run->>Radar: commandRadar<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.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
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.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "DevControlBot",
|
||||||
|
"version": "1.1.0",
|
||||||
|
"authors": ["Davide Cappellini"],
|
||||||
|
"description": "Control skeleton bot — does nothing, bright colors",
|
||||||
|
"gameTypes": ["classic", "1v1"],
|
||||||
|
"platform": "Nim",
|
||||||
|
"programmingLang": "Nim"
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
## DevControlBot — control skeleton: boots, stands still, and drives nothing but
|
||||||
|
## the radar. It never moves and it never fires.
|
||||||
|
##
|
||||||
|
## Team convention: white means "not programmed yet". At startup body, turret, gun
|
||||||
|
## and radar are all plain WHITE. The radar then claims its OWN colours when it
|
||||||
|
## starts working: initRadar() paints the radar and the scan arc BLACK, leaving
|
||||||
|
## body/turret/gun white (this bot never moves and never fires, so they stay
|
||||||
|
## "not programmed"). Colors are applied once, never per tick.
|
||||||
|
##
|
||||||
|
## This module is both the bot type and the program entry point (see isMainModule
|
||||||
|
## below). All radar logic lives in modules/radar.nim; the bot keeps no state.
|
||||||
|
##
|
||||||
|
## The run loop drives the radar: commandRadar() is called BEFORE go() every
|
||||||
|
## tick, so the command is part of the tick's intent and is sent by THIS go().
|
||||||
|
## The order matters — go() sends the intent first and dispatches the pending
|
||||||
|
## events last, so onScannedBot only feeds the NEXT tick.
|
||||||
|
##
|
||||||
|
## API: robocode_tankroyale_botapi 1.0.7 (the renamed tankroyale_botapi package).
|
||||||
|
import std/[os]
|
||||||
|
import robocode_tankroyale_botapi
|
||||||
|
import modules/radar
|
||||||
|
|
||||||
|
type DevControlBot* = ref object of Bot
|
||||||
|
|
||||||
|
proc newDevControlBot*(): DevControlBot =
|
||||||
|
result = DevControlBot()
|
||||||
|
setBodyColor(WHITE)
|
||||||
|
setTurretColor(WHITE)
|
||||||
|
setGunColor(WHITE)
|
||||||
|
setRadarColor(WHITE)
|
||||||
|
|
||||||
|
method onScannedBot*(bot: DevControlBot, e: ScannedBotEvent) =
|
||||||
|
onScan(e.x, e.y) # remembered; commandRadar() re-aims every tick
|
||||||
|
# (e.scannedBotId, schemas.nim:305, is the
|
||||||
|
# scanned bot's identity; unused here)
|
||||||
|
|
||||||
|
method run*(bot: DevControlBot) =
|
||||||
|
initRadar() # once: radar + scan turn BLACK = programmed
|
||||||
|
while isRunning():
|
||||||
|
# One radar command per tick, unconditionally, BEFORE go() — so the command
|
||||||
|
# is part of this tick's intent and no tick is ever left without one.
|
||||||
|
commandRadar()
|
||||||
|
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 = "1.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 at SERVER_URL" 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 point SERVER_URL at a running one." >&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 at SERVER_URL,
|
||||||
|
# 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,92 @@
|
|||||||
|
## The radar — one module, the whole of it. No movement, no gun.
|
||||||
|
##
|
||||||
|
## Every tick `commandRadar()` issues EXACTLY ONE radar command, before `go()`,
|
||||||
|
## on every path. There is no "idle" branch: the 360 sweep is not a mode we
|
||||||
|
## fall back INTO, it is the command the other branch replaces. That is why
|
||||||
|
## switching costs zero turns.
|
||||||
|
##
|
||||||
|
## TWO commands, chosen by the only fact the bot knows for sure:
|
||||||
|
##
|
||||||
|
## exactly one enemy alive + a target in hand -> LOCK (servo onto its bearing)
|
||||||
|
## anything else -> SWEEP (45 deg/tick, the cap,
|
||||||
|
## a full revolution every 8 ticks)
|
||||||
|
##
|
||||||
|
## The sweep does double duty and needs no separate "melee" mode: it is both the
|
||||||
|
## search for a target we do not have yet and the right answer for 2+ enemies
|
||||||
|
## (a full revolution re-scans every enemy 8x sooner than any narrowed sweep,
|
||||||
|
## and locking one of several is worthless).
|
||||||
|
##
|
||||||
|
## Derived from ModularBot's radar (Davide Cappellini, Apache-2.0), i.e. from
|
||||||
|
## common_libs/radar_lock/radar_lock.nim (`doRadar`) and common_libs/radars/
|
||||||
|
## melee_scan.nim, which ModularBot picks between per tick in exactly this way
|
||||||
|
## (`let targetMode = if getEnemyCount() == 1: 0 else: 1` — ModularBot.nim, the
|
||||||
|
## "Auto-switch radar based on the SERVER's live enemy count" block). The one
|
||||||
|
## idea taken from it is the OVERSHOOT, and it is the whole reason the lock
|
||||||
|
## works: the rate is 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 — which
|
||||||
|
## is exactly what produces a fresh ScannedBotEvent every tick. Turning by the
|
||||||
|
## bare error (or holding inside a deadzone) parks the radar on the bearing and
|
||||||
|
## the scans stop; the target then has to be re-found by the sweep, which is the
|
||||||
|
## delay this replaces. common_libs/ is never edited in place.
|
||||||
|
##
|
||||||
|
## ANGLES: 0° = east, positive = counter-clockwise, so positive = a LEFT turn
|
||||||
|
## (proof in ../README.md; the API's own doc comment claiming "0 = North" for
|
||||||
|
## `directionTo` contradicts its code and is wrong).
|
||||||
|
|
||||||
|
import std/math
|
||||||
|
import robocode_tankroyale_botapi
|
||||||
|
|
||||||
|
const
|
||||||
|
MaxRadarTurn* = 45.0 # deg/tick, the radar's hard cap (the sweep)
|
||||||
|
OvershootDeg* = 5.0 # how far past the target's bearing the lock aims;
|
||||||
|
# guarantees the bearing is swept every tick
|
||||||
|
FreshScanTurns* = 1 # how many ticks a remembered target stays usable.
|
||||||
|
# The 5 deg overshoot makes the lock CROSS the
|
||||||
|
# target's bearing every tick, so a fresh
|
||||||
|
# ScannedBotEvent arrives every tick and this
|
||||||
|
# counter never gets past 1. Hence one tick without a
|
||||||
|
# scan means the target has genuinely left the radar
|
||||||
|
# cone: there is nothing left to wait for, sweeping
|
||||||
|
# re-finds it faster than a stale lock could.
|
||||||
|
|
||||||
|
# The target in hand: the position of the last bot we scanned. Absolute
|
||||||
|
# position, not a remembered bearing, so the delta is recomputed against the
|
||||||
|
# radar's CURRENT heading every tick and the lock keeps correcting a target
|
||||||
|
# that (and a radar that) has moved since the scan.
|
||||||
|
var
|
||||||
|
targetX, targetY: float
|
||||||
|
targetSeen: bool
|
||||||
|
scanlessTicks: int
|
||||||
|
|
||||||
|
proc onScan*(x, y: float) =
|
||||||
|
## Remember a scanned bot as the target. Issues no command: `onScannedBot`
|
||||||
|
## runs AFTER go() has already sent this tick's intent.
|
||||||
|
targetX = x
|
||||||
|
targetY = y
|
||||||
|
targetSeen = true
|
||||||
|
scanlessTicks = 0
|
||||||
|
|
||||||
|
proc initRadar*() =
|
||||||
|
## Called ONCE, before the first tick: the radar is now programmed and working,
|
||||||
|
## so it claims its own colours — radar and scan arc go BLACK.
|
||||||
|
## COLOUR CONVENTION: white = "not programmed yet", black = "programmed and
|
||||||
|
## running". The bot starts all WHITE (DevControlBot.nim); the radar paints
|
||||||
|
## ITSELF black the moment it takes control, and body/turret/gun stay white
|
||||||
|
## forever (they are still unprogrammed — this bot never moves, never fires).
|
||||||
|
## Set once here, never per tick: the colour never changes, so re-sending it
|
||||||
|
## every tick would only cost intent bandwidth.
|
||||||
|
setRadarColor(BLACK)
|
||||||
|
setScanColor(BLACK)
|
||||||
|
|
||||||
|
proc commandRadar*() =
|
||||||
|
## Command the radar for THIS tick. Called from run() before go().
|
||||||
|
if targetSeen: inc scanlessTicks
|
||||||
|
if getEnemyCount() == 1 and targetSeen and scanlessTicks <= FreshScanTurns:
|
||||||
|
var turn = normalizeRelativeAngle(
|
||||||
|
directionTo(getX(), getY(), targetX, targetY) - getRadarDirection())
|
||||||
|
if turn < 0.0: turn -= OvershootDeg
|
||||||
|
else: turn += OvershootDeg
|
||||||
|
setRadarTurnRate(turn.clamp(-MaxRadarTurn, MaxRadarTurn))
|
||||||
|
return
|
||||||
|
targetSeen = false # nothing usable to lock on: sweep
|
||||||
|
setRadarTurnRate(MaxRadarTurn)
|
||||||
@@ -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,296 @@
|
|||||||
|
# DevControlBot
|
||||||
|
|
||||||
|
Control skeleton bot: it boots, participates in the battle, and drives **no
|
||||||
|
movement and no gun**. The radar is the only driven part.
|
||||||
|
|
||||||
|
All radar logic is **one module**, `modules/radar.nim` (90 lines including its
|
||||||
|
comments; two procs, three variables, three constants). `run()` just calls
|
||||||
|
`commandRadar()`.
|
||||||
|
|
||||||
|
**Two commands, one decision.** Exactly one enemy alive *and* a target in hand ->
|
||||||
|
**lock** (servo onto the target's bearing). Anything else -> **sweep** at
|
||||||
|
`MaxRadarTurn` = 45 deg/tick, the radar's physical cap, i.e. a full revolution
|
||||||
|
every 8 turns. The sweep does double duty and needs no separate "melee" mode: it
|
||||||
|
is both the search when no target is in hand and the right answer for 2+ enemies
|
||||||
|
(a full revolution re-scans every enemy as fast as the physics allow, and locking
|
||||||
|
one of several is worthless). The choice is made from the API's
|
||||||
|
`getEnemyCount()` (bot.nim:166, from `tick.botState.enemyCount`, bot.nim:1295;
|
||||||
|
field at schemas.nim:134) every tick — the server's alive count, so it cannot
|
||||||
|
drift and it shrinks by itself when an enemy dies. No counter, no seen-id set, no
|
||||||
|
mode enum, no dispatch table.
|
||||||
|
|
||||||
|
**There is never a tick without a command.** `commandRadar()` ends in
|
||||||
|
`setRadarTurnRate(...)` on *every* path — the sweep is not a state the lock falls
|
||||||
|
back *into*, it is the single command the lock replaces. Switching costs zero
|
||||||
|
turns, in either direction, by construction.
|
||||||
|
|
||||||
|
**The lock works because of the 5° overshoot.** The commanded rate is the delta
|
||||||
|
from the radar's *current* heading to the target's bearing, plus 5° **past** it
|
||||||
|
in the direction of the turn, clamped to ±45. So the radar sweeps *across* the
|
||||||
|
bearing every tick instead of settling on it — which is what produces a fresh
|
||||||
|
`ScannedBotEvent` every tick. (Turning by the bare error, or holding inside a
|
||||||
|
deadzone, parks the radar on the bearing: the scans stop and the target has to be
|
||||||
|
re-found by the sweep, which is the delay this design removes. The `setRescan()`
|
||||||
|
trick this used to need is unnecessary here — the radar is never idle.) The
|
||||||
|
bearing is recomputed from the target's remembered **absolute position** each
|
||||||
|
tick, so the lock keeps correcting a target, and a radar, that have moved.
|
||||||
|
|
||||||
|
**Lost-target safety.** `scanlessTicks` counts the ticks since the target was last
|
||||||
|
scanned (reset by every scan) and `FreshScanTurns = 10` is how long a remembered
|
||||||
|
target stays usable — one sweep revolution plus slack. This can never throw a
|
||||||
|
good lock away: a working lock re-scans its target *every* tick, so the counter
|
||||||
|
never gets past ~1. It only bites on a target unseen for a while, where locking
|
||||||
|
would slew blindly at a stale point; the sweep re-finds it within one revolution
|
||||||
|
instead. The target is then dropped and the sweep resumes on the same tick.
|
||||||
|
|
||||||
|
`onScannedBot` is a one-liner — `onScan(e.x, e.y)` — and issues no command:
|
||||||
|
`go()` sends the tick's intent *before* dispatching the pending events, so
|
||||||
|
commanding first means the command is part of THIS tick's intent, while
|
||||||
|
`onScannedBot` only feeds the next one.
|
||||||
|
|
||||||
|
Angles are **0° = east, positive = counter-clockwise = turn LEFT**: proven from
|
||||||
|
the installed API — `directionTo` is `180 * arctan2(dy, dx) / PI`
|
||||||
|
(utils.nim:146-165, its "0 = North" doc comment contradicts its own code), and
|
||||||
|
`setTurnRadarLeft` sets a positive rate while `setTurnRadarRight` is
|
||||||
|
`setTurnRadarLeft(-degrees)` (bot.nim:1062-1070).
|
||||||
|
|
||||||
|
**Provenance.** The design is ModularBot's (Davide Cappellini, Apache-2.0), i.e.
|
||||||
|
`common_libs/radar_lock/radar_lock.nim` (`doRadar`, which is where the overshoot
|
||||||
|
comes from) and `common_libs/radars/melee_scan.nim`, picked per tick exactly this
|
||||||
|
way in `ModularBot.nim`: `let targetMode = if getEnemyCount() == 1: 0 else: 1`.
|
||||||
|
`common_libs/` is never edited in place.
|
||||||
|
|
||||||
|
**Measured** (1 round each, RadarSpy observer; scans/turn, idle = turns with no
|
||||||
|
radar command, max gap = longest run of turns without a scan):
|
||||||
|
|
||||||
|
| battle | version | scans/turn | idle turns | max scan gap |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 1v1 Walls | before | 0.513 | **391 / 509** | **33 turns** |
|
||||||
|
| 1v1 Walls | now | **0.960** | **0** | **1 turn** |
|
||||||
|
| vs 2 adversaries | before | 0.256 | 0 | 8 |
|
||||||
|
| vs 2 adversaries | now | 0.278 | 0 | 8 |
|
||||||
|
| vs 2 adversaries (3 bots, mixed) | before | 0.462 | 233 | 32 |
|
||||||
|
| vs 2 adversaries (3 bots, mixed) | now | 0.548–0.644 | **0** | **8** |
|
||||||
|
|
||||||
|
The 2-enemy rows are identical by construction (both versions issue the same
|
||||||
|
45 deg/tick sweep). When the count drops to 1 mid-round the new radar is
|
||||||
|
acquiring again within 6 turns and never parks; the old one took 29 turns and
|
||||||
|
sat at rate 0 for 26 of them.
|
||||||
|
|
||||||
|
The radar keeps moving forever: sweeping at full rate until an enemy is
|
||||||
|
scanned, servoing onto it while it is the only one alive, spinning at the cap as
|
||||||
|
soon as a second one is alive, and dropping straight back to the lock when one of
|
||||||
|
them dies. All of it is in `DevControlBot/modules/radar.nim`.
|
||||||
|
|
||||||
|
Body, turret, gun and radar all start plain **white**, which by team convention
|
||||||
|
means "not programmed yet". The radar then claims its **own** colours when it
|
||||||
|
starts working: `initRadar()` (`DevControlBot/modules/radar.nim`) paints the radar
|
||||||
|
and the scan arc **black** — black = "programmed and running" — while
|
||||||
|
body/turret/gun stay white (this bot never moves and never fires). Colors are set
|
||||||
|
**once** (startup + radar init), 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 at `SERVER_URL` (the API's documented
|
||||||
|
default is `ws://localhost:7654`); without one the run fails with
|
||||||
|
`[start] Cannot connect to <SERVER_URL>` 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 at `SERVER_URL` (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:4567: Connection refused
|
||||||
|
[devcontrolbot] bot exited with code 1.
|
||||||
|
[devcontrolbot] No game server on ws://localhost:4567 (SERVER_URL).
|
||||||
|
[devcontrolbot] Start the Tank Royale server, or point SERVER_URL at a running one.
|
||||||
|
```
|
||||||
|
|
||||||
|
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