## 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)