Add radar lock and melee sweep to DevControlBot
The bot now drives its radar every tick. It still never moves and never
fires: body, turret and gun stay white ("not programmed"), while the
radar paints itself black at init to mark itself as working.
- modules/radar.nim replaces the earlier multi-module radar design with a
single module holding the whole of it: target memory, mode selection
and both commands
- two commands, picked from the only fact the bot is sure of (the API's
getEnemyCount()): one enemy plus a target in hand -> LOCK, servo onto
its bearing; anything else -> SWEEP at 45 deg/tick, the radar's physics
cap, a full revolution every 8 ticks
- the lock turns by the delta to the target's bearing plus 5 deg PAST it,
so the radar CROSSES the bearing every tick instead of settling on it.
That crossing is what produces a fresh ScannedBotEvent every tick: a
scan per tick, with no rescan to wait for. Turning by the bare error
parks the radar on the bearing and the scans stop
- commandRadar() is called before go() on every path, so there is no idle
branch to fall into and switching modes costs zero turns
- idea derived from ModularBot's radar (Apache-2.0) via common_libs/;
common_libs/ is not edited in place
- AGENTS.md gains mermaid behaviour diagrams plus pointers to the physics
and coordinate references
- version bumped to 1.1.0 in both DevControlBot.nimble and
DevControlBot.json so the two agree
Compiled against robocode_tankroyale_botapi 1.0.7.
This commit is contained in:
@@ -1,11 +1,93 @@
|
||||
# DevControlBot
|
||||
|
||||
Control skeleton bot: it boots, participates in the battle, and does **nothing**
|
||||
per tick except `go()` — no movement logic, no scanning, no firing.
|
||||
Control skeleton bot: it boots, participates in the battle, and drives **no
|
||||
movement and no gun**. The radar is the only driven part.
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user