j124 spinner claim: add ConstantSpinner adversary + spinner A/B arms, panel, analyzer

This commit is contained in:
2026-09-26 04:05:36 +02:00
parent 2a98aba91b
commit 06da7abb0e
11 changed files with 739 additions and 0 deletions
@@ -0,0 +1,11 @@
{
"name": "ConstantSpinner",
"version": "0.1.0",
"authors": ["Davide Cappellini"],
"description": "True constant-turn spinner adversary — constant speed + constant body turn rate, head-on firing",
"homepage": "",
"countryCodes": ["IT"],
"gameTypes": ["classic", "1v1"],
"platform": "Nim",
"programmingLang": "Nim"
}
@@ -0,0 +1,85 @@
# ConstantSpinner — a TRUE constant-turn spinner.
#
# Purpose: the legacy roster has no constant-turn spinner. `SpinBot` (the sample
# bot) and the in-repo movers are periodic/oscillating, but this fixture is the
# degenerate case the owner's gun claim is really about: a body that moves at a
# CONSTANT forward speed while turning at a CONSTANT rate, forever. Its path is a
# perfect circle of radius v / omega, i.e. a single-frequency, low-entropy,
# perfectly periodic movement — the easiest possible target for a fast-adapting
# or pattern-matching gun.
#
# It is a MOVEMENT FIXTURE, not a fighter: the gun is plain head-on, fired at low
# power when the turret happens to be on target. No adaptation, no randomness, no
# wall logic (the server's wall collision is the only non-periodic perturbation).
#
# Env (read at round start, so one binary serves every spin rate):
# SPIN_TURN_RATE body turn rate, deg/tick (default 6.0)
# SPIN_SPEED forward speed, units/tick (default 5.0)
# SPIN_FIRE_POWER bullet power (default 1.0)
#
# The requested turn rate is clamped to the maximum reachable at the target
# speed (`MAX_TURN_RATE - 0.75 * speed`), so the ACTUAL turn rate stays exactly
# constant instead of being server-clamped while the bot accelerates or slides
# along a wall.
import std/[math, os, strutils]
import robocode_tankroyale_botapi
# The identity JSON. A generated spin-rate variant directory (see
# `make_variant.sh`) sets BOT_JSON so the bot handshakes under the variant's own
# name, which is what the Tank Royale runner matches against `<dir>/<dir>.json`.
const defaultBotJsonPath = currentSourcePath().parentDir / "ConstantSpinner.json"
proc envFloat(name: string, default: float): float =
let v = getEnv(name)
if v.len == 0: return default
try:
parseFloat(v)
except ValueError:
default
type ConstantSpinner = ref object of Bot
turnRate: float
speed: float
firePower: float
method onRoundStarted*(bot: ConstantSpinner, e: RoundStartedEvent) =
# Gun and radar stay independent of the (continuously turning) body.
setAdjustGunForBodyTurn(true)
setAdjustRadarForBodyTurn(true)
setAdjustRadarForGunTurn(true)
bot.speed = envFloat("SPIN_SPEED", 5.0)
bot.turnRate = envFloat("SPIN_TURN_RATE", 6.0)
bot.firePower = envFloat("SPIN_FIRE_POWER", 1.0)
# A turn rate above the speed-dependent maximum would be clamped by the
# server, making the rate non-constant during acceleration / wall slides.
let maxAtSpeed = MAX_TURN_RATE - 0.75 * abs(bot.speed)
if bot.turnRate > maxAtSpeed:
bot.turnRate = maxAtSpeed
if bot.turnRate < 0.0:
bot.turnRate = 0.0
method onScannedBot*(bot: ConstantSpinner, e: ScannedBotEvent) =
# Head-on: aim at where the enemy is right now, fire low power whenever the
# gun is cool. It is a movement fixture, so the shot goes wherever the turret
# happens to point; the radar sweep keeps scans coming.
let bearing = directionTo(getX(), getY(), e.x, e.y)
let gunDelta = normalizeRelativeAngle(bearing - getGunDirection())
setGunTurnRate(gunDelta.clamp(-MAX_GUN_TURN_RATE, MAX_GUN_TURN_RATE))
if getGunHeat() <= 0.0:
discard setFire(bot.firePower)
method run*(bot: ConstantSpinner) =
while isRunning():
# The whole movement: constant turn, constant speed. Nothing else.
setRadarTurnRate(45.0)
setTurnRate(bot.turnRate)
setTargetSpeed(bot.speed)
go()
when isMainModule:
var bot = ConstantSpinner()
let jsonPath = getEnv("BOT_JSON", defaultBotJsonPath)
start(bot, jsonPath)