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,6 +1,6 @@
|
||||
{
|
||||
"name": "DevControlBot",
|
||||
"version": "0.1.0",
|
||||
"version": "1.1.0",
|
||||
"authors": ["Davide Cappellini"],
|
||||
"description": "Control skeleton bot — does nothing, bright colors",
|
||||
"gameTypes": ["classic", "1v1"],
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
## DevControlBot — control skeleton: boots, stands still, does nothing.
|
||||
## 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", so body, gun and radar are
|
||||
## all plain white. Colors are applied once at initialization, never per tick.
|
||||
## 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); there is no separate top-level DevControlBot.nim.
|
||||
## 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 std/[os]
|
||||
import robocode_tankroyale_botapi
|
||||
import modules/radar
|
||||
|
||||
type DevControlBot* = ref object of Bot
|
||||
|
||||
@@ -19,14 +29,24 @@ proc newDevControlBot*(): DevControlBot =
|
||||
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"
|
||||
let jsonPath =
|
||||
if paramCount() >= 1: paramStr(1)
|
||||
else: currentSourcePath().parentDir / "DevControlBot.json"
|
||||
var bot = newDevControlBot()
|
||||
start(bot, jsonPath)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Package
|
||||
version = "0.1.0"
|
||||
version = "1.1.0"
|
||||
author = "Davide Cappellini"
|
||||
description = "DevControlBot — control-skeleton bot that does nothing but show up bright"
|
||||
license = "MIT"
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user