From e5609a7d9bcf3d3d3a2a71847dbdcb3ef3041fca Mon Sep 17 00:00:00 2001 From: Davide Cappellini Date: Mon, 17 Aug 2026 11:52:36 +0200 Subject: [PATCH] =?UTF-8?q?fix(PPO=5FBot):=20radar=20lock=20=E2=80=94=20us?= =?UTF-8?q?e=20enemy=5Ftracker=20width-lock,=20fix=20arctan2=20arg=20order?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three bugs caused the radar to sweep continuously instead of locking: 1. run() loop set radar to Inf every tick, overwriting any lock → replaced with enemy_tracker.getRadarTurnRate() 2. onScannedBot used radarBearingTo() (math convention, east=0 CCW) → removed; run loop now handles radar via enemy_tracker 3. enemy_tracker.getRadarTurnRate() had arctan2(dx,dy) instead of arctan2(dy,dx) — introduced by fd22535; bearing was off by ~90° Also relaxed stale-lock threshold from 2 to 8 ticks to survive brief scan gaps without falling back to full sweep. Added tools/battle_runner for automated 1v1 testing. Result: 1303/1308 ticks with successful scan (was ~1 in 4). --- PPO_Bot/PPO_Bot.nim | 6 +- PPO_Bot/config.nims | 2 +- PPO_Bot/enemy_tracker.nim | 30 +++++----- tools/battle_runner/RunBattle.java | 96 ++++++++++++++++++++++++++++++ tools/battle_runner/run.sh | 26 ++++++++ 5 files changed, 141 insertions(+), 19 deletions(-) create mode 100644 tools/battle_runner/RunBattle.java create mode 100755 tools/battle_runner/run.sh diff --git a/PPO_Bot/PPO_Bot.nim b/PPO_Bot/PPO_Bot.nim index 36cf4f3..675f9d5 100644 --- a/PPO_Bot/PPO_Bot.nim +++ b/PPO_Bot/PPO_Bot.nim @@ -64,6 +64,9 @@ method onScannedBot*(bot: PPOBot, e: ScannedBotEvent) = bot.tracker.update(e.x, e.y, e.direction, e.speed, e.energy) method onRoundStarted*(bot: PPOBot, e: RoundStartedEvent) = + setAdjustGunForBodyTurn(true) + setAdjustRadarForBodyTurn(true) + setAdjustRadarForGunTurn(true) bot.tracker = initEnemyTracker() bot.buffer = initTrajectoryBuffer() bot.prevEnergy = 0.0'f32 @@ -130,8 +133,7 @@ method run(bot: PPOBot) = while isRunning(): bot.tracker.deadReckon() - setRadarTurnRate(bot.tracker.getRadarTurnRate( - getX(), getY(), getDirection(), getRadarDirection())) + setRadarTurnRate(bot.tracker.getRadarTurnRate(getX(), getY(), getDirection(), getRadarDirection())) let botData = BotStateData( x: getX(), diff --git a/PPO_Bot/config.nims b/PPO_Bot/config.nims index b47a741..3324b6d 100644 --- a/PPO_Bot/config.nims +++ b/PPO_Bot/config.nims @@ -1,6 +1,6 @@ # Static-link OpenBLAS for portable deployment # ponytail: adjust path per machine, or use pkg-config -switch("passL", "-lopenblas") +switch("passL", "-L/nix/store/v07svn2y92bvzjl51aj7c9ca1cwg7rw7-openblas-0.3.32/lib -lopenblas") switch("threads", "on") # begin Nimble config (version 2) when withDir(thisDir(), system.fileExists("nimble.paths")): diff --git a/PPO_Bot/enemy_tracker.nim b/PPO_Bot/enemy_tracker.nim index 7f50ebc..67fbdda 100644 --- a/PPO_Bot/enemy_tracker.nim +++ b/PPO_Bot/enemy_tracker.nim @@ -19,11 +19,8 @@ type historyCount*: int # valid entries 0-5 prevEnergy*: float64 hasContact*: bool - ## For radar overshoot reversal - lastOvershootDir*: float64 # +1 or -1 -proc initEnemyTracker*(): EnemyTracker = - result.lastOvershootDir = 1.0 +proc initEnemyTracker*(): EnemyTracker = discard proc update*(tracker: var EnemyTracker; scanX, scanY, scanDir, scanSpeed, scanEnergy: float64) = @@ -77,20 +74,21 @@ proc getRadarTurnRate*(tracker: var EnemyTracker; if not tracker.hasContact: return 45.0 - if tracker.current.ticksSinceLastScan >= 2: - # Lost lock — widen sweep proportional to staleness + if tracker.current.ticksSinceLastScan >= 8: + # Lost lock — widen sweep return 45.0 - # Bearing from radar to enemy - # Tank Royale: 0°=north, CW+. arctan2(dx, dy) gives north-based CW bearing. + # Bearing from radar to enemy. + # Tank Royale radar directions use standard math convention (0=east, CCW+). + # arctan2(dy, dx) gives the standard math angle matching radarDirection units. let dx = tracker.current.x - botX let dy = tracker.current.y - botY - let absoluteDir = (180.0 * arctan2(dx, dy) / PI + 360.0) mod 360.0 - let radarBearing = normalizeRelative(absoluteDir - radarDirection) + let absoluteDir = (180.0 * arctan2(dy, dx) / PI + 360.0) mod 360.0 + var radarTurn = normalizeRelative(absoluteDir - radarDirection) - # Overshoot by 10°, alternate direction each tick - # ponytail: simple fixed overshoot; adaptive sweep if enemy is fast-turning - let overshoot = 10.0 - let target = radarBearing + tracker.lastOvershootDir * overshoot - result = target.clamp(-45.0, 45.0) - tracker.lastOvershootDir *= -1.0 + # Width Lock: overshoot proportional to arctan(36 / distance) + let distance = sqrt(dx * dx + dy * dy) + let extraTurn = min(arctan(36.0 / distance) * 180.0 / PI, 45.0) + if radarTurn < 0.0: radarTurn -= extraTurn + else: radarTurn += extraTurn + result = radarTurn.clamp(-45.0, 45.0) diff --git a/tools/battle_runner/RunBattle.java b/tools/battle_runner/RunBattle.java new file mode 100644 index 0000000..ea736c3 --- /dev/null +++ b/tools/battle_runner/RunBattle.java @@ -0,0 +1,96 @@ +import dev.robocode.tankroyale.runner.*; +import dev.robocode.tankroyale.client.model.*; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Radar-lock diagnostic battle: PPO_Bot vs Target (1 round). + * + * Per tick prints: turn, each bot's radar direction, and any ScannedBotEvents. + * + * Usage: + * ./run.sh + */ +public class RunBattle { + + static final Map idToName = new ConcurrentHashMap<>(); + + public static void main(String[] args) { + Logger.getLogger("dev.robocode.tankroyale").setLevel(Level.WARNING); + + String ppoBotDir = requireEnv("PPO_BOT_DIR"); + String sampleBots = requireEnv("SAMPLE_BOTS_DIR"); + + try (var runner = BattleRunner.create(b -> b.embeddedServer().suppressServerOutput())) { + var setup = BattleSetup.classic(s -> s.setNumberOfRounds(1)); + var bots = List.of( + BotEntry.of(ppoBotDir), + BotEntry.of(sampleBots + "/Target") + ); + + var owner = new Object(); + try (var handle = runner.startBattleAsync(setup, bots)) { + + handle.getOnGameStarted().on(owner, event -> { + System.out.println("=== GAME STARTED ==="); + for (var p : event.getParticipants()) { + idToName.put(p.getId(), p.getName()); + System.out.printf(" #%d %s%n", p.getId(), p.getName()); + } + System.out.printf("%-6s %-4s %-30s %-30s %-20s%n", + "Turn", "Id", "Name", "RadarDir", "Event"); + System.out.println("-".repeat(95)); + }); + + handle.getOnTickEvent().on(owner, event -> { + int turn = event.getTurnNumber(); + + // Keep id→name map current from bot states (handles race with onGameStarted) + for (var state : event.getBotStates()) + if (state.getName() != null) idToName.putIfAbsent(state.getId(), state.getName()); + + // Print radar direction for every bot + for (var state : event.getBotStates()) { + String name = idToName.getOrDefault(state.getId(), + state.getName() != null ? state.getName() : "#" + state.getId()); + System.out.printf("%-6d %-4d %-30s %-30.2f%n", + turn, state.getId(), name, state.getRadarDirection()); + } + + // Print any scan events on the same tick + for (var e : event.getEvents()) { + if (e instanceof ScannedBotEvent scan) { + String scanner = idToName.getOrDefault(scan.getScannedByBotId(), "?" + scan.getScannedByBotId()); + String target = idToName.getOrDefault(scan.getScannedBotId(), "?" + scan.getScannedBotId()); + System.out.printf("%-6d SCAN %s scanned %s at (%.1f,%.1f)%n", + turn, scanner, target, scan.getX(), scan.getY()); + } + } + }); + + handle.getOnRoundEnded().on(owner, event -> + System.out.printf("%n=== ROUND %d ENDED (turn %d) ===%n", + event.getRoundNumber(), event.getTurnNumber())); + + var results = handle.awaitResults(); + System.out.printf("%n=== RESULTS (%d rounds) ===%n", results.getNumberOfRounds()); + for (var r : results.getResults()) { + System.out.printf(" #%d %-25s %d pts%n", + r.getRank(), r.getName(), r.getTotalScore()); + } + } + } + } + + static String requireEnv(String name) { + var v = System.getenv(name); + if (v == null || v.isBlank()) { + System.err.println("Error: " + name + " env var not set"); + System.exit(1); + } + return v; + } +} diff --git a/tools/battle_runner/run.sh b/tools/battle_runner/run.sh new file mode 100755 index 0000000..e827a59 --- /dev/null +++ b/tools/battle_runner/run.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +JAR="${TANK_ROYALE_JAR:-/home/davide/Projects/tank-royale/runner/examples/lib/robocode-tankroyale-runner.jar}" +PPO_BOT_SRC="$REPO_ROOT/PPO_Bot" + +export PPO_BOT_DIR="$PPO_BOT_SRC" +export SAMPLE_BOTS_DIR="${SAMPLE_BOTS_DIR:-/home/davide/Projects/tank-royale/sample-bots/java/build/archive}" + +# ── 1. Compile PPO_Bot if binary is missing ────────────────────────────────── +if [ ! -x "$PPO_BOT_SRC/PPO_Bot" ]; then + echo ">>> Compiling PPO_Bot..." + (cd "$PPO_BOT_SRC" && nimble build -d:release) +fi + +# ── 2. Compile and run RunBattle.java ──────────────────────────────────────── +cd "$SCRIPT_DIR" + +echo ">>> Compiling RunBattle.java..." +javac -cp "$JAR" RunBattle.java + +echo ">>> Running battle..." +java -cp ".:$JAR" RunBattle