diff --git a/ModularBot_garage/ModularBot b/ModularBot_garage/ModularBot index b54f681..15feb17 100755 Binary files a/ModularBot_garage/ModularBot and b/ModularBot_garage/ModularBot differ diff --git a/ModularBot_garage/src/ModularBot.nim b/ModularBot_garage/src/ModularBot.nim index fda81e8..e99a396 100644 --- a/ModularBot_garage/src/ModularBot.nim +++ b/ModularBot_garage/src/ModularBot.nim @@ -1,5 +1,5 @@ ## ModularBot — plugin gun architecture tracer bullet. -## Guns: HeadOnGun (0), LinearGun (1), TsetlinGun (2), CircularGun (3), GFGun (4), PatternMatcherGun (5) via GunHarness. +## Guns: HeadOnGun (0), LinearGun (1), TsetlinGun (2), CircularGun (3), GFGun (4), PatternMatcherGun (5), AntiSurferGun (6) via GunHarness. ## Radar: RadarLockModule via radar harness. ## Movement: OscillatorModule (perpendicular strafing). @@ -16,12 +16,13 @@ import guns/circular import guns/tsetlin import guns/guess_factor import guns/pattern_matcher +import guns/anti_surfer import movements/phantom_meteor const botJsonPath = currentSourcePath().parentDir / "ModularBot.json" const DebugVBullets = false const DebugCircular = false -const GunNames = ["HeadOn", "Linear", "Tsetlin", "Circular", "GuessFactor", "Pattern"] +const GunNames = ["HeadOn", "Linear", "Tsetlin", "Circular", "GuessFactor", "Pattern", "AntiSurf"] type ModularBot = ref object of Bot @@ -36,6 +37,7 @@ type tsetlin: TsetlinGun guessFactor: GFGun patternMatcher: PatternMatcherGun + antiSurfer: AntiSurferGun mover: PhantomMeteorModule virtualHits: int virtualMiss: int @@ -72,6 +74,7 @@ method onScannedBot*(bot: ModularBot, e: ScannedBotEvent) = var circPreds: array[len(PowerBins), GunPrediction] var gfPreds: array[len(PowerBins), GunPrediction] var pmPreds: array[len(PowerBins), GunPrediction] + var asPreds: array[len(PowerBins), GunPrediction] for i in 0.. enScore: "ModularBot" else: enemyName + echo fmt" ModularBot: {mbScore} {enemyName}: {enScore} → {winner} wins" + echo fmt" Rounds: {r.rounds.len}" + +runAndReport("Battle 1/3: vs WaveSurfer", "WaveSurfer", waveSurferDir) +runAndReport("Battle 2/3: vs SpinBot", "SpinBot", spinBotDir) +runAndReport("Battle 3/3: vs Crazy", "Crazy", crazyDir) +echo "\nGauntlet complete." diff --git a/ModularBot_garage/tests/test_gauntlet_guns.nim b/ModularBot_garage/tests/test_gauntlet_guns.nim new file mode 100644 index 0000000..7bf016b --- /dev/null +++ b/ModularBot_garage/tests/test_gauntlet_guns.nim @@ -0,0 +1,68 @@ +## Gauntlet with [gun] line capture via raw runner output. +## nim c -r --path:../../common_libs tests/test_gauntlet_guns.nim + +import std/[os, strutils, strformat, tables] +import test_framework/server_manager +import test_framework/bot_compiler +import test_framework/runner_process +import test_framework/battle_result + +const + modularBotDir = currentSourcePath().parentDir.parentDir + samplesDir = "/home/davide/Downloads/robocode-tankroyale/sample-bots-nim-linux-1.0.7" + waveSurferDir = "/home/davide/Projects/SirRoboGarage/WaveSurfer_garage" + spinBotDir = samplesDir / "SpinBot" + crazyDir = samplesDir / "Crazy" + +if not fileExists("/home/davide/Projects/tank-royale/server/build/libs/robocode-tankroyale-server-0.35.5-all.jar") or + not fileExists("/home/davide/Projects/tank-royale/runner/examples/lib/robocode-tankroyale-runner.jar"): + echo "Skipping: Tank Royale JARs not found" + quit(0) + +proc gunCounts(raw: string): Table[string, int] = + ## Count [gun] switched: occurrences + for line in raw.splitLines(): + if "[gun] switched:" in line: + let parts = line.split(":") + if parts.len >= 2: + let name = parts[^1].strip() + result[name] = result.getOrDefault(name, 0) + 1 + +proc runAndReport(label, enemyName, enemyDir: string) = + let sep = "=".repeat(50) + echo "\n" & sep + echo " " & label + echo sep + let bots = @[modularBotDir, enemyDir] + discard compileBots(bots) + let raw = runBattleRunner(getServerUrl(), bots, 10, 240000, true) + let r = parseServerOutput(raw) + + var mbScore = 0; var enScore = 0 + for b in r.results: + if b.name == "ModularBot": mbScore = b.totalScore + if b.name == enemyName: enScore = b.totalScore + let winner = if mbScore > enScore: "ModularBot" else: enemyName + echo " Score: ModularBot " & $mbScore & " " & enemyName & " " & $enScore & " -> " & winner & " wins" + + let guns = gunCounts(raw) + if guns.len > 0: + echo " Gun switches:" + for name, count in guns.pairs: + echo fmt" [gun] {name}: {count}x" + else: + echo " (no [gun] switch lines in output)" + + # Show all [gun] lines for visibility + var gunLines: seq[string] + for line in raw.splitLines(): + if line.contains("[gun]"): gunLines.add(line.strip()) + if gunLines.len > 0: + echo fmt" First 20 [gun] lines:" + for l in gunLines[0..min(19, gunLines.high)]: echo fmt" {l}" + +ensureServer() +runAndReport("Battle 1/3: vs WaveSurfer", "WaveSurfer", waveSurferDir) +runAndReport("Battle 2/3: vs SpinBot", "SpinBot", spinBotDir) +runAndReport("Battle 3/3: vs Crazy", "Crazy", crazyDir) +echo "\nGauntlet complete." diff --git a/common_libs/guns/anti_surfer.nim b/common_libs/guns/anti_surfer.nim new file mode 100644 index 0000000..592b629 --- /dev/null +++ b/common_libs/guns/anti_surfer.nim @@ -0,0 +1,110 @@ +## Anti-surfer gun: inverse of GF histogram — aim at the VALLEY bin. +## Wave surfers dodge to where they predict bullets won't be (the GF peak). +## They congregate at the valley → aim there to punish their dodge. +## Bins: 31, same as GFGun. Learns from virtual bullet outcomes. + +import std/[math, strformat] +import gun_harness/gun_interface + +const + ASBins = 31 + ASPrior = 0.1 + DebugAS* = false + +type + ASWave = object + fireX, fireY: float + fireBearing: float + + AntiSurferGun* = object + bins: array[ASBins, float] + waves: seq[ASWave] + cachedTick: int + cachedWaveStored: bool + +proc initAntiSurferGun*(): AntiSurferGun = + result.cachedTick = -1 + # Same triangular head-on prior as GFGun — cold-start aims head-on. + let center = (ASBins - 1) div 2 # = 15 + for i in 0.. PI: bearingDelta -= 2.0*PI + while bearingDelta < -PI: bearingDelta += 2.0*PI + + let gf = if mea > 1e-10: clamp(bearingDelta / mea, -1.0, 1.0) else: 0.0 + let centerIdx = asToIndex(gf) + + when DebugAS: + echo fmt"[as-dbg] onResult: delta={radToDeg(bearingDelta):.1f}° MEA={radToDeg(mea):.1f}° GF={gf:.2f} bin={centerIdx}" + + for i in 0.. bestRatio: + bestRatio = ratio + result = i + +proc computeMove*(m: var WaveSurferModule, ws: WorldState): MoveCommand = + let botX = ws.selfX + let botY = ws.selfY + let enemyX = ws.enemyX + let enemyY = ws.enemyY + + # Fire detection + let drop = m.prevEnergy - ws.enemyEnergy + m.prevEnergy = ws.enemyEnergy + if drop >= 0.1 and drop <= 3.0: + let bspeed = 20.0 - 3.0 * drop + let bearing = arctan2(botY - enemyY, botX - enemyX) + let d = sqrt((botX - enemyX)^2 + (botY - enemyY)^2) + m.waves.add WSWave( + originX: enemyX, originY: enemyY, + bearing: bearing, + speed: bspeed, + radius: 0.0, + startDist: d, + ) + + # Advance waves; collect hits + var i = 0 + while i < m.waves.len: + m.waves[i].radius += m.waves[i].speed + let d = sqrt((botX - m.waves[i].originX)^2 + (botY - m.waves[i].originY)^2) + if m.waves[i].radius >= d: + let toBot = arctan2(botY - m.waves[i].originY, botX - m.waves[i].originX) + var off = toBot - m.waves[i].bearing + while off > PI: off -= 2.0*PI + while off < -PI: off += 2.0*PI + let maxA = mea(m.waves[i].speed) + if maxA >= 1e-9: + let gf = clamp(off / maxA, -1.0, 1.0) + m.bins[gfToBin(gf)] += 1.0 + m.waves.del(i) + else: + inc i + + # Dodge logic: find nearest wave, pick safest GF bin direction + let nearest = m.nearestWave(botX, botY) + if nearest >= 0: + let w = m.waves[nearest] + let toBot = arctan2(botY - w.originY, botX - w.originX) + var off = toBot - w.bearing + while off > PI: off -= 2.0*PI + while off < -PI: off += 2.0*PI + let maxA = mea(w.speed) + let curGF = if maxA >= 1e-9: clamp(off / maxA, -1.0, 1.0) else: 0.0 + let curBin = gfToBin(curGF) + + # Average danger + var total = 0.0 + for b in m.bins: total += b + let avg = total / float64(WS_BINS) + + if m.bins[curBin] > avg: + # Find lowest-danger bin + var bestBin = 0 + for j in 1..= curGF: 1.0 else: -1.0 + + # Perpendicular strafe (same body trick as phantom_meteor) + let enemyBearingRad = arctan2(enemyY - botY, enemyX - botX) + # Perpendicular in chosen strafe direction + let perpAngle = + if m.strafeDir >= 0.0: enemyBearingRad + PI * 0.5 + else: enemyBearingRad - PI * 0.5 + let desiredDeg = radToDeg(perpAngle) + + var delta = desiredDeg - ws.selfHeading + while delta > 180.0: delta -= 360.0 + while delta < -180.0: delta += 360.0 + + let goForward = abs(delta) <= 90.0 + if not goForward: + delta = if delta >= 0.0: delta - 180.0 else: delta + 180.0 + + (speed: if goForward: 8.0 else: -8.0, + turnRate: delta.clamp(-10.0, 10.0)) diff --git a/out/test_gauntlet b/out/test_gauntlet new file mode 100755 index 0000000..4a9b274 Binary files /dev/null and b/out/test_gauntlet differ diff --git a/out/test_gauntlet_guns b/out/test_gauntlet_guns new file mode 100755 index 0000000..3c00575 Binary files /dev/null and b/out/test_gauntlet_guns differ