diff --git a/common_libs/tankroyale_botapi/tankroyale_botapi.nim b/common_libs/tankroyale_botapi/tankroyale_botapi.nim
deleted file mode 100644
index 7ae784d..0000000
--- a/common_libs/tankroyale_botapi/tankroyale_botapi.nim
+++ /dev/null
@@ -1,260 +0,0 @@
-## Main entry-point module for Robocode Tank Royale Nim bot API.
-##
-## Usage:
-## import tankroyale_botapi
-##
-## type MyBot = ref object of Bot
-## method run(bot: MyBot) =
-## forward(100)
-## ...
-##
-## var bot = MyBot()
-## start(bot, "MyBot.json")
-
-import std/[os, json]
-
-import ./tankroyale_botapi/constants
-import ./tankroyale_botapi/color
-import ./tankroyale_botapi/schemas
-import ./tankroyale_botapi/utils
-import ./tankroyale_botapi/bot_info
-import ./tankroyale_botapi/ws_client
-import ./tankroyale_botapi/json_parse
-import ./tankroyale_botapi/event_queue
-import ./tankroyale_botapi/bot
-import ./tankroyale_botapi/graphics
-
-export constants
-export color
-export schemas
-export utils
-export bot_info
-export json_parse
-export event_queue
-export bot
-export graphics
-
-# ---------------------------------------------------------------------------
-# WebSocket receive loop (main thread)
-# ---------------------------------------------------------------------------
-
-proc handleServerHandshake(ws: SyncWebSocket; node: JsonNode; info: BotInfo; secret: string) =
- let sessionId = node{"sessionId"}.getStr
- setServerInfo(node{"variant"}.getStr, node{"version"}.getStr)
-
- # Build bot handshake
- var h = newJObject()
- h["type"] = %"BotHandshake"
- h["sessionId"] = %sessionId
- h["name"] = %info.name
- h["version"] = %info.version
- h["authors"] = %info.authors
- h["description"] = %info.description
- h["homepage"] = %info.homepage
- h["countryCodes"] = %info.countryCodes
- h["gameTypes"] = %info.gameTypes
- h["platform"] = %info.platform
- h["programmingLang"]= %info.programmingLang
- h["isDroid"] = %info.isDroid
- if secret.len > 0:
- h["secret"] = %secret
- let ip = info.initialPosition
- if ip.x != 0.0 or ip.y != 0.0 or ip.direction != 0.0:
- var ipObj = newJObject()
- if ip.x != 0.0: ipObj["x"] = %ip.x
- if ip.y != 0.0: ipObj["y"] = %ip.y
- if ip.direction != 0.0: ipObj["direction"] = %ip.direction
- h["initialPosition"] = ipObj
- ws.send($h)
-
-proc parseGameSetup(node: JsonNode): GameSetup =
- if node.isNil: return
- result.gameType = node{"gameType"}.getStr("classic")
- result.arenaWidth = node{"arenaWidth"}.getInt(800)
- result.isArenaWidthLocked = node{"isArenaWidthLocked"}.getBool(false)
- result.arenaHeight = node{"arenaHeight"}.getInt(600)
- result.isArenaHeightLocked = node{"isArenaHeightLocked"}.getBool(false)
- result.numberOfRounds = node{"numberOfRounds"}.getInt(10)
- result.isNumberOfRoundsLocked = node{"isNumberOfRoundsLocked"}.getBool(false)
- result.minNumberOfParticipants = node{"minNumberOfParticipants"}.getInt(2)
- result.isMinNumberOfParticipantsLocked = node{"isMinNumberOfParticipantsLocked"}.getBool(false)
- result.maxNumberOfParticipants = node{"maxNumberOfParticipants"}.getInt(10)
- result.isMaxNumberOfParticipantsLocked = node{"isMaxNumberOfParticipantsLocked"}.getBool(false)
- result.gunCoolingRate = node{"gunCoolingRate"}.getFloat(0.1)
- result.isGunCoolingRateLocked = node{"isGunCoolingRateLocked"}.getBool(false)
- result.maxInactivityTurns = node{"maxInactivityTurns"}.getInt(450)
- result.isMaxInactivityTurnsLocked = node{"isMaxInactivityTurnsLocked"}.getBool(false)
- result.turnTimeout = node{"turnTimeout"}.getInt(30000)
- result.isTurnTimeoutLocked = node{"isTurnTimeoutLocked"}.getBool(false)
- result.readyTimeout = node{"readyTimeout"}.getInt(1000000)
- result.isReadyTimeoutLocked = node{"isReadyTimeoutLocked"}.getBool(false)
- result.defaultTurnsPerSecond = node{"defaultTurnsPerSecond"}.getInt(30)
-
-proc handleGameStarted(ws: SyncWebSocket; node: JsonNode) =
- let setup = parseGameSetup(node{"gameSetup"})
-
- var teammateIds: seq[int] = @[]
- if not node{"teammateIds"}.isNil and node["teammateIds"].kind == JArray:
- for id in node["teammateIds"]: teammateIds.add id.getInt
-
- let myId = node{"myId"}.getInt
- setGameStarted(myId, setup, teammateIds)
-
- # Build event object manually — GameStartedEventForBot has no turnNumber
- let e = GameStartedEventForBot(
- `type`: "GameStartedEventForBot",
- myId: myId,
- startX: node{"startX"}.getFloat(0.0),
- startY: node{"startY"}.getFloat(0.0),
- startDirection: node{"startDirection"}.getFloat(0.0),
- teammateIds: teammateIds,
- gameSetup: setup
- )
- gBot.onGameStarted(e)
-
- # Send BotReady
- ws.send("""{"type":"BotReady"}""")
-
-proc handleTick(node: JsonNode) =
- # Build TickEventForBot manually to handle optional fields safely
- var tick: TickEventForBot
- tick.`type` = "TickEventForBot"
- tick.turnNumber = node{"turnNumber"}.getInt(0)
- tick.roundNumber = node{"roundNumber"}.getInt(0)
- tick.botState = parseBotState(node{"botState"})
- tick.bulletStates = @[]
- if not node{"bulletStates"}.isNil and node["bulletStates"].kind == JArray:
- for bs in node["bulletStates"]:
- tick.bulletStates.add parseBulletState(bs)
- tick.events = @[] # sub-events parsed separately into typed BotEvent
-
- # Parse embedded events into typed BotEvent for priority-based dispatch
- var events: seq[BotEvent] = @[]
- let myId = getMyId()
- if node.hasKey("events") and node["events"].kind == JArray:
- for ev in node["events"]:
- events.add parseBotEvent(ev, myId)
-
- signalTick(tick, events) # update shared state
- processTickOnMainThread() # motion tracking (while bot is blocked)
- wakeBotThread() # wake bot — state + motion ready
-
-proc runReceiveLoop*(ws: SyncWebSocket; info: BotInfo; secret: string; serverUrl: string) =
- ## Main WebSocket receive loop. Blocks until disconnected.
- while ws.connected:
- var msg: string
- try:
- msg = ws.receive()
- except Exception as e:
- stderr.writeLine "[ws] receive error: " & e.msg
- gBot.onConnectionError(ConnectionErrorEvent(serverUrl: serverUrl, error: e.msg))
- break
-
- if msg.len == 0:
- break # connection closed
-
- var node: JsonNode
- try:
- node = parseJson(msg)
- except Exception as e:
- stderr.writeLine "[ws] json parse error: " & e.msg
- continue
-
- let msgType = node{"type"}.getStr
- try:
- case msgType
- of "ServerHandshake":
- handleServerHandshake(ws, node, info, secret)
- of "GameStartedEventForBot":
- handleGameStarted(ws, node)
- of "RoundStartedEvent":
- let e = node.to(RoundStartedEvent)
- debugLog("[NS-ENTER] round=" & $e.roundNumber & " tid=" & $getThreadId())
- # Start (or restart) the bot thread each round
- startRound()
- startBotThread()
- gBot.onRoundStarted(e)
- debugLog("[NS-EXIT] round=" & $e.roundNumber & " tid=" & $getThreadId())
- of "TickEventForBot":
- handleTick(node)
- of "RoundEndedEventForBot":
- setRunning(false)
- let e = node.to(RoundEndedEventForBot)
- debugLog("[RE-ENTER] round=" & $e.roundNumber & " tid=" & $getThreadId())
- signalStop() # unblock bot thread blocked in go()
- debugLog("[WT-ENTER] round=" & $e.roundNumber & " tid=" & $getThreadId())
- waitForBotThread()
- debugLog("[WT-EXIT] round=" & $e.roundNumber & " tid=" & $getThreadId())
- debugLog("[DR-ENTER] round=" & $e.roundNumber & " tid=" & $getThreadId())
- drainTickChan() # drain stop signal if bot exited via isRunning() check
- drainIntentChan() # drain AFTER thread joined — no more writes possible
- drainEventChan() # drop any unconsumed tick events (stale into next round)
- debugLog("[DR-EXIT] round=" & $e.roundNumber & " tid=" & $getThreadId())
- debugLog("[ONRE-ENTER] round=" & $e.roundNumber & " tid=" & $getThreadId())
- gBot.onRoundEnded(e)
- debugLog("[ONRE-EXIT] round=" & $e.roundNumber & " tid=" & $getThreadId())
- of "GameEndedEventForBot":
- setRunning(false)
- let e = node.to(GameEndedEventForBot)
- drainIntentChan()
- gBot.onGameEnded(e)
- of "GameAbortedEvent":
- setRunning(false)
- signalStop() # unblock bot thread (game aborted mid-round)
- waitForBotThread()
- drainTickChan() # drain stop signal if bot exited via isRunning() check
- drainIntentChan() # drain AFTER thread joined — no more writes possible
- drainEventChan() # drop any unconsumed tick events (stale into next round)
- gBot.onGameAborted()
- of "SkippedTurnEvent":
- let e = node.to(SkippedTurnEvent)
- gBot.onSkippedTurn(e)
- of "BotListUpdate":
- updateBotNames(node)
- else:
- discard # unknown message type — ignore
- except Exception as e:
- # A raised handler/callback (e.g. an OSError from sync training inside
- # onRoundEnded) must not kill the receive loop — that is the silent
- # corpse path (process lives, no intents ever again). Log and continue.
- stderr.writeLine "[ws] handler error (" & msgType & "): " & e.msg
- debugLog("[WS-HANDLER-ERR] " & msgType & ": " & e.msg)
-
- # Loop exited: server disconnected or ws error. Make sure the bot thread is
- # stopped and joined so the process exits cleanly instead of hanging forever
- # with a blocked bot (corpse). ponytail: signalStop + join; the bot's go()
- # consumes the stop as a non-tick and exits via the isRunning() check.
- if isRunning():
- debugLog("[DBG] receive loop exited while running — stopping bot thread")
- setRunning(false)
- signalStop()
- waitForBotThread()
- drainTickChan()
- drainIntentChan()
- drainEventChan()
- gBot.onDisconnected(DisconnectedEvent(serverUrl: serverUrl))
-
-# ---------------------------------------------------------------------------
-# Public start() procedure
-# ---------------------------------------------------------------------------
-
-proc start*(bot: Bot; jsonFile: string = "") =
- ## Connect to the server and start the bot.
- ## jsonFile: path to bot JSON profile (optional; falls back to env vars).
- gBot = bot
- gBotInfo = loadBotInfo(jsonFile)
- initGlobals()
-
- let serverUrl = getEnv("SERVER_URL", "ws://localhost:7654")
- let serverSecret = getEnv("SERVER_SECRET", "")
-
- try:
- gWs = newSyncWebSocket(serverUrl)
- except Exception as e:
- stderr.writeLine "[start] Cannot connect to " & serverUrl & ": " & e.msg
- quit(1)
-
- bot.onConnected(ConnectedEvent(serverUrl: serverUrl))
- startSenderThread()
- runReceiveLoop(gWs, gBotInfo, serverSecret, serverUrl)
- stopSenderThread()
diff --git a/common_libs/tankroyale_botapi/tankroyale_botapi.nimble b/common_libs/tankroyale_botapi/tankroyale_botapi.nimble
deleted file mode 100644
index 76b6ef7..0000000
--- a/common_libs/tankroyale_botapi/tankroyale_botapi.nimble
+++ /dev/null
@@ -1,11 +0,0 @@
-# Package
-version = "1.0.1"
-author = "Davide Cappellini"
-description = "Nim bot API for Robocode Tank Royale"
-license = "Apache-2.0"
-srcDir = "src"
-skipDirs = @["sample_bots"]
-
-# Dependencies
-requires "nim >= 2.0.0"
-requires "jsony >= 1.1.5"
diff --git a/common_libs/tankroyale_botapi/tankroyale_botapi/bot.nim b/common_libs/tankroyale_botapi/tankroyale_botapi/bot.nim
deleted file mode 100644
index f41f03a..0000000
--- a/common_libs/tankroyale_botapi/tankroyale_botapi/bot.nim
+++ /dev/null
@@ -1,1026 +0,0 @@
-## Core bot implementation for Robocode Tank Royale Nim bot API.
-##
-## Threading model
-## ---------------
-## Main thread: WebSocket receive loop. Receives all server messages,
-## updates shared state, runs processTurn, then wakes bot thread.
-## Bot thread: Wakes, dispatches events, runs user's `run()` / go() loop,
-## sends intent JSON via gIntentChan.
-## Sender thread: Owns all WebSocket writes during gameplay — reads from
-## gIntentChan and calls ws.send.
-##
-## Synchronisation uses two channels:
-## tickChan main → bot (true = new tick ready; false = stop)
-## intentChan bot → sender (JSON string to send to server; "" = stop)
-
-import std/[json, locks, math, os, posix, syncio, tables]
-import ./constants
-import ./schemas
-import ./color
-import ./utils as botutils
-import ./ws_client
-import ./bot_info
-import ./event_queue
-import ./graphics
-
-proc toInfiniteValue(rate: float): float {.inline.} =
- if rate > 0.0: Inf
- elif rate < 0.0: NegInf
- else: 0.0
-
-# ---------------------------------------------------------------------------
-# Types
-# ---------------------------------------------------------------------------
-
-type
- Bot* = ref object of RootObj
- ## Override `run()` and event handler methods in your bot subtype.
-
- BotState* = object
- ## Snapshot of shared state, read by the bot thread.
-
-# ---------------------------------------------------------------------------
-# Global mutable state (module-level; single bot per process)
-# ---------------------------------------------------------------------------
-
-# WebSocket
-var gWs*: SyncWebSocket
-
-# Thread handles
-var gBotThread: Thread[void]
-var gSenderThread: Thread[void]
-var gFirstTickOfRound: bool # main-thread only, no lock needed
-
-# Channels — must be opened before use
-var gTickChan: Channel[bool] # main → bot: tick arrived (false = stop)
-var gIntentChan: Channel[string] # bot → main: send this JSON
-
-# Shared state protected by lock
-var gLock: Lock
-var gRunning {.guard: gLock.}: bool
-var gMyId {.guard: gLock.}: int
-var gRound {.guard: gLock.}: int
-var gTurn {.guard: gLock.}: int
-var gEnemyCount {.guard: gLock.}: int
-var gState {.guard: gLock.}: schemas.BotState
-var gBullets {.guard: gLock.}: seq[BulletState]
-var gGameSetup {.guard: gLock.}: GameSetup
-var gTeammateIds{.guard: gLock.}: seq[int]
-var gVariant {.guard: gLock.}: string
-var gServerVersion {.guard: gLock.}: string
-var gBotNames {.guard: gLock.}: Table[int, string]
-# Events handed main -> bot thread. Channel move, NOT a locked shared seq:
-# the old locked seq[BotEvent] copied GC'd payloads (strings/teamMessages)
-# across threads on every tick -> refcount churn under ORC + --threads:on ->
-# heap freeList corruption + SIGSEGV inside prepareSeqAddUninit at tps=-1
-# (gdb-confirmed). Channels move ownership: zero cross-thread refcount traffic.
-var gEventChan: Channel[seq[BotEvent]]
-
-# Saved state for stop/resume
-var gStopped: bool
-var gSavedTurnRate: float
-var gSavedGunTurnRate: float
-var gSavedRadarTurnRate: float
-var gSavedTargetSpeed: float
-
-# Motion tracking (bot thread only)
-var gDistanceRemaining: float
-var gTurnRemaining: float
-var gGunTurnRemaining: float
-var gRadarTurnRemaining: float
-var gPreviousDirection: float
-var gPreviousGunDirection: float
-var gPreviousRadarDirection: float
-var gOverrideTurnRate: bool
-var gOverrideGunTurnRate: bool
-var gOverrideRadarTurnRate: bool
-var gOverrideTargetSpeed: bool
-var gContinuousTurnRate: float
-var gContinuousGunTurnRate: float
-var gContinuousRadarTurnRate: float
-var gContinuousTargetSpeed: float
-var gIsOverDriving: bool
-
-var gMaxSpeed = MAX_SPEED
-var gMaxTurnRate = MAX_TURN_RATE
-var gMaxGunTurnRate = MAX_GUN_TURN_RATE
-var gMaxRadarTurnRate = MAX_RADAR_TURN_RATE
-
-# The bot instance (set by start())
-var gBot*: Bot
-
-var gEventQueue: EventQueue # bot-thread-only, no lock needed
-var gInterrupted: bool # flag-based interruptibility (checked by blocking calls)
-
-# BotInfo (set by start())
-var gBotInfo*: BotInfo
-
-# ---------------------------------------------------------------------------
-# Safe readers (can be called from bot thread without lock in most cases
-# because they read after the tick channel signal — happens-before is enough)
-# ---------------------------------------------------------------------------
-
-proc getMyId*(): int = withLock(gLock): result = gMyId
-proc getRound*(): int = withLock(gLock): result = gRound
-proc getTurn*(): int = withLock(gLock): result = gTurn
-proc getEnemyCount*(): int = withLock(gLock): result = gEnemyCount
-proc getEnergy*(): float = withLock(gLock): result = gState.energy
-proc getX*(): float = withLock(gLock): result = gState.x
-proc getY*(): float = withLock(gLock): result = gState.y
-proc getDirection*(): float = withLock(gLock): result = gState.direction
-proc getGunDirection*(): float = withLock(gLock): result = gState.gunDirection
-proc getRadarDirection*(): float= withLock(gLock): result = gState.radarDirection
-proc getRadarSweep*(): float = withLock(gLock): result = gState.radarSweep
-proc getSpeed*(): float = withLock(gLock): result = gState.speed
-proc getTurnRate*(): float = withLock(gLock): result = gState.turnRate
-proc getGunTurnRate*(): float = withLock(gLock): result = gState.gunTurnRate
-proc getRadarTurnRate*(): float = withLock(gLock): result = gState.radarTurnRate
-proc getGunHeat*(): float = withLock(gLock): result = gState.gunHeat
-proc getBodyColor*(): Color = withLock(gLock): result = gState.bodyColor
-proc getTurretColor*(): Color = withLock(gLock): result = gState.turretColor
-proc getRadarColor*(): Color = withLock(gLock): result = gState.radarColor
-proc getBulletColor*(): Color = withLock(gLock): result = gState.bulletColor
-proc getScanColor*(): Color = withLock(gLock): result = gState.scanColor
-proc getTracksColor*(): Color = withLock(gLock): result = gState.tracksColor
-proc getGunColor*(): Color = withLock(gLock): result = gState.gunColor
-proc getArenaWidth*(): int = withLock(gLock): result = gGameSetup.arenaWidth
-proc getArenaHeight*(): int = withLock(gLock): result = gGameSetup.arenaHeight
-proc getGameType*(): string = withLock(gLock): result = gGameSetup.gameType
-proc getNumberOfRounds*(): int = withLock(gLock): result = gGameSetup.numberOfRounds
-proc getGunCoolingRate*(): float= withLock(gLock): result = gGameSetup.gunCoolingRate
-proc getMaxInactivityTurns*(): int = withLock(gLock): result = gGameSetup.maxInactivityTurns
-proc getTurnTimeout*(): int = withLock(gLock): result = gGameSetup.turnTimeout
-proc getTimeLeft*(): int = getTurnTimeout() # ponytail: returns turnTimeout as ceiling; precise impl needs tick timestamp + elapsed tracking
-proc getVariant*(): string = withLock(gLock): result = gVariant
-proc getServerVersion*(): string= withLock(gLock): result = gServerVersion
-proc isRunning*(): bool = withLock(gLock): result = gRunning
-proc isDroid*(): bool = withLock(gLock): result = gState.isDroid
-proc isDisabled*(): bool = getEnergy() == 0.0
-proc isStopped*(): bool = gStopped
-proc getBulletStates*(): seq[BulletState] = withLock(gLock): result = gBullets
-proc getTeammateIds*(): seq[int]= withLock(gLock): result = gTeammateIds
-proc isTeammate*(botId: int): bool =
- withLock(gLock): result = gTeammateIds.contains(botId)
-
-proc getDistanceRemaining*(): float = gDistanceRemaining
-proc getTurnRemaining*(): float = gTurnRemaining
-proc getGunTurnRemaining*(): float = gGunTurnRemaining
-proc getRadarTurnRemaining*(): float = gRadarTurnRemaining
-
-proc getBotName*(id: int): string =
- ## Lookup bot name by id from the last BotListUpdate. Returns "" if unknown.
- withLock(gLock): result = gBotNames.getOrDefault(id, "")
-
-proc updateBotNames*(node: JsonNode) =
- ## Update the id → name table from a BotListUpdate message (full replacement).
- withLock(gLock):
- gBotNames.clear()
- if node.isNil: return
- let botsNode = node{"bots"}
- if botsNode.isNil or botsNode.kind != JArray: return
- for b in botsNode:
- let name = b{"name"}.getStr("")
- if name.len == 0: continue
- var id = -1
- if not b{"id"}.isNil:
- id = b{"id"}.getInt(-1)
- if id == -1 and not b{"botId"}.isNil:
- id = b{"botId"}.getInt(-1)
- if id == -1: continue
- gBotNames[id] = name
-
-proc getMaxSpeed*(): float = gMaxSpeed
-proc getMaxTurnRate*(): float = gMaxTurnRate
-proc getMaxGunTurnRate*(): float = gMaxGunTurnRate
-proc getMaxRadarTurnRate*(): float = gMaxRadarTurnRate
-
-proc setMaxSpeed*(v: float) = gMaxSpeed = v.clamp(0, MAX_SPEED)
-proc setMaxTurnRate*(v: float) = gMaxTurnRate = v.clamp(0, MAX_TURN_RATE)
-proc setMaxGunTurnRate*(v: float) = gMaxGunTurnRate = v.clamp(0, MAX_GUN_TURN_RATE)
-proc setMaxRadarTurnRate*(v: float)= gMaxRadarTurnRate = v.clamp(0, MAX_RADAR_TURN_RATE)
-
-# ---------------------------------------------------------------------------
-# Intent building
-# ---------------------------------------------------------------------------
-
-# Intent fields (bot thread writes, main thread reads + clears)
-var gIntentTurnRate: float = 0.0
-var gIntentGunTurnRate: float = 0.0
-var gIntentRadarTurnRate: float = 0.0
-var gIntentTargetSpeed: float = 0.0
-var gIntentFirepower: float = 0.0
-var gIntentRescan: bool = false
-var gIntentFireAssist: bool = false
-var gIntentBodyColor: Color = Color(0)
-var gIntentTurretColor: Color = Color(0)
-var gIntentRadarColor: Color = Color(0)
-var gIntentBulletColor: Color = Color(0)
-var gIntentScanColor: Color = Color(0)
-var gIntentTracksColor: Color = Color(0)
-var gIntentGunColor: Color = Color(0)
-var gIntentAdjGunBody: bool = false
-var gIntentAdjRadarBody: bool = false
-var gIntentAdjRadarGun: bool = false
-# ponytail: static buffers instead of strings/seq. go()'s stop path returns
-# before buildIntentJson clears these, so a round's last tick can leave heap
-# blocks owned by the exiting bot thread -> the fresh next-round thread
-# reallocs a dead allocator block (rawDealloc SIGSEGV). Static storage:
-# no heap block crosses threads.
-const INTENT_STDOUT_CAP = 4096
-const INTENT_STDERR_CAP = 4096
-const INTENT_MSG_CAP = 16
-
-var gIntentTeamMessages: array[INTENT_MSG_CAP, TeamMessage]
-var gIntentTeamMsgsLen: int
-var gIntentStdOut: array[INTENT_STDOUT_CAP, char]
-var gIntentStdOutLen: int
-var gIntentStdErr: array[INTENT_STDERR_CAP, char]
-var gIntentStdErrLen: int
-
-proc buildIntentJson*(): string =
- ## Serialise current intent to JSON for sending to server.
- var obj = newJObject()
- obj["type"] = %"BotIntent"
- obj["turnRate"] = %gIntentTurnRate
- obj["gunTurnRate"] = %gIntentGunTurnRate
- obj["radarTurnRate"] = %gIntentRadarTurnRate
- obj["targetSpeed"] = %gIntentTargetSpeed
- if gIntentFirepower > 0.0:
- obj["firepower"] = %gIntentFirepower
- if gIntentRescan:
- obj["rescan"] = %true
- gIntentRescan = false # one-shot
- if gIntentFireAssist:
- obj["fireAssist"] = %true
- if gIntentAdjGunBody:
- obj["adjustGunForBodyTurn"] = %true
- if gIntentAdjRadarBody:
- obj["adjustRadarForBodyTurn"] = %true
- if gIntentAdjRadarGun:
- obj["adjustRadarForGunTurn"] = %true
- if gIntentBodyColor != Color(0):
- obj["bodyColor"] = %gIntentBodyColor.toHex
- if gIntentTurretColor != Color(0):
- obj["turretColor"] = %gIntentTurretColor.toHex
- if gIntentRadarColor != Color(0):
- obj["radarColor"] = %gIntentRadarColor.toHex
- if gIntentBulletColor != Color(0):
- obj["bulletColor"] = %gIntentBulletColor.toHex
- if gIntentScanColor != Color(0):
- obj["scanColor"] = %gIntentScanColor.toHex
- if gIntentTracksColor != Color(0):
- obj["tracksColor"] = %gIntentTracksColor.toHex
- if gIntentGunColor != Color(0):
- obj["gunColor"] = %gIntentGunColor.toHex
- if gIntentTeamMsgsLen > 0:
- var msgs = newJArray()
- for i in 0 ..< gIntentTeamMsgsLen:
- let m = gIntentTeamMessages[i]
- var mo = newJObject()
- mo["message"] = %m.message
- mo["messageType"] = %m.messageType
- if m.receiverId != 0:
- mo["receiverId"] = %m.receiverId
- msgs.add mo
- obj["teamMessages"] = msgs
- for i in 0 ..< gIntentTeamMsgsLen: gIntentTeamMessages[i].reset
- gIntentTeamMsgsLen = 0
- if gIntentStdOutLen > 0:
- obj["stdOut"] = %($gIntentStdOut[0 ..< gIntentStdOutLen])
- gIntentStdOutLen = 0
- if gIntentStdErrLen > 0:
- obj["stdErr"] = %($gIntentStdErr[0 ..< gIntentStdErrLen])
- gIntentStdErrLen = 0
- let svg = svgOutput()
- if svg.len > 0:
- obj["debugGraphics"] = %svg
- result = $obj
-
-# ---------------------------------------------------------------------------
-# Intent setters (bot thread)
-# ---------------------------------------------------------------------------
-
-proc setTurnRate*(rate: float) =
- gIntentTurnRate = rate.clamp(-gMaxTurnRate, gMaxTurnRate)
- gOverrideTurnRate = false
- gContinuousTurnRate = rate
- gTurnRemaining = toInfiniteValue(rate)
-
-proc setGunTurnRate*(rate: float) =
- gIntentGunTurnRate = rate.clamp(-gMaxGunTurnRate, gMaxGunTurnRate)
- gOverrideGunTurnRate = false
- gContinuousGunTurnRate = rate
- gGunTurnRemaining = toInfiniteValue(rate)
-
-proc setRadarTurnRate*(rate: float) =
- gIntentRadarTurnRate = rate.clamp(-gMaxRadarTurnRate, gMaxRadarTurnRate)
- gOverrideRadarTurnRate = false
- gContinuousRadarTurnRate = rate
- gRadarTurnRemaining = toInfiniteValue(rate)
-
-proc setTargetSpeed*(speed: float) =
- gIntentTargetSpeed = speed.clamp(-gMaxSpeed, gMaxSpeed)
- gOverrideTargetSpeed = false
- gContinuousTargetSpeed = speed
- if speed > 0:
- gDistanceRemaining = Inf
- elif speed < 0:
- gDistanceRemaining = NegInf
- else:
- gDistanceRemaining = 0.0
-
-proc setFire*(firepower: float): bool =
- let fp = firepower.clamp(MIN_FIRE_POWER, MAX_FIRE_POWER)
- if getEnergy() < fp or getGunHeat() > 0.0:
- return false
- gIntentFirepower = fp
- return true
-
-proc setRescan*() = gIntentRescan = true
-
-proc setBodyColor*(color: Color) = gIntentBodyColor = color
-proc setTurretColor*(color: Color) = gIntentTurretColor = color
-proc setRadarColor*(color: Color) = gIntentRadarColor = color
-proc setBulletColor*(color: Color) = gIntentBulletColor = color
-proc setScanColor*(color: Color) = gIntentScanColor = color
-proc setTracksColor*(color: Color) = gIntentTracksColor = color
-proc setGunColor*(color: Color) = gIntentGunColor = color
-
-proc printToStdOut*(s: string) =
- ## Append s to this tick's stdOut payload (sent to server in BotIntent).
- let n = min(s.len, INTENT_STDOUT_CAP - gIntentStdOutLen)
- for i in 0 ..< n: gIntentStdOut[gIntentStdOutLen + i] = s[i]
- inc gIntentStdOutLen, n
-
-proc printToStdErr*(s: string) =
- ## Append s to this tick's stdErr payload (sent to server in BotIntent).
- let n = min(s.len, INTENT_STDERR_CAP - gIntentStdErrLen)
- for i in 0 ..< n: gIntentStdErr[gIntentStdErrLen + i] = s[i]
- inc gIntentStdErrLen, n
-
-proc broadcastTeamMessage*(message: string) =
- ## Send a message to all teammates this tick.
- if gIntentTeamMsgsLen < INTENT_MSG_CAP:
- gIntentTeamMessages[gIntentTeamMsgsLen] = TeamMessage(message: message, messageType: "String")
- inc gIntentTeamMsgsLen
-
-proc sendTeamMessage*(botId: int; message: string) =
- ## Send a message to a specific teammate this tick.
- if gIntentTeamMsgsLen < INTENT_MSG_CAP:
- gIntentTeamMessages[gIntentTeamMsgsLen] = TeamMessage(message: message, messageType: "String", receiverId: botId)
- inc gIntentTeamMsgsLen
-
-proc setAdjustGunForBodyTurn*(v: bool) = gIntentAdjGunBody = v
-proc setAdjustRadarForBodyTurn*(v: bool) = gIntentAdjRadarBody = v
-proc setFireAssist*(enable: bool) = gIntentFireAssist = enable
-proc setAdjustRadarForGunTurn*(v: bool) =
- gIntentAdjRadarGun = v
- setFireAssist(not v)
-
-proc isAdjustGunForBodyTurn*(): bool = gIntentAdjGunBody
-proc isAdjustRadarForBodyTurn*(): bool = gIntentAdjRadarBody
-proc isAdjustRadarForGunTurn*(): bool = gIntentAdjRadarGun
-
-proc getTargetSpeed*(): float = gIntentTargetSpeed
-proc getFirepower*(): float = gIntentFirepower
-
-# Convenience math re-exports (state-aware wrappers; pure-math helpers come from utils)
-proc calcBearing*(direction: float): float = botutils.calcDeltaAngle(direction, getDirection())
-proc calcGunBearing*(direction: float): float = botutils.calcDeltaAngle(direction, getGunDirection())
-proc calcRadarBearing*(direction: float): float = botutils.calcDeltaAngle(direction, getRadarDirection())
-proc bearingTo*(x, y: float): float = botutils.bearingTo(getX(), getY(), getDirection(), x, y)
-proc gunBearingTo*(x, y: float): float = botutils.bearingTo(getX(), getY(), getGunDirection(), x, y)
-proc radarBearingTo*(x, y: float): float = botutils.normalizeRelativeAngle(botutils.directionTo(getX(), getY(), x, y) - getRadarDirection())
-proc directionTo*(x, y: float): float = botutils.directionTo(getX(), getY(), x, y)
-proc distanceTo*(x, y: float): float = botutils.distanceTo(getX(), getY(), x, y)
-
-# ---------------------------------------------------------------------------
-# Bot motion processing (called on first turn and each subsequent turn)
-# NOTE: must be defined before go() so it can be called inside go()
-# ---------------------------------------------------------------------------
-
-var gDebugLog: File # nil unless PPOB_DEBUG_LOG=1 (debug-only knob)
-var gDebugLogBytes: int64 # written bytes since last truncation
-const gDebugLogCap = 100 * 1024 * 1024 # 100 MB ceiling
-const gDebugLogPath = "/tmp/walls_debug.log"
-
-proc resetDebugLog() =
- ## Truncate the debug log to zero (Nim's stdlib has no File truncate, so do
- ## it by path via POSIX — the open fmAppend handle stays valid).
- discard truncate(gDebugLogPath, 0)
- gDebugLogBytes = 0
-
-proc debugLog*(msg: string) =
- ## Debug tracing, only active when PPOB_DEBUG_LOG=1. Called from both the
- ## main and bot threads, so writes are serialized under gLock (File writes
- ## are not thread-safe); the log is truncated and restarted at the cap so a
- ## long campaign can't grow a GB-scale file again.
- if gDebugLog == nil: return
- withLock(gLock):
- if gDebugLogBytes >= gDebugLogCap:
- resetDebugLog()
- gDebugLog.writeLine(msg)
- gDebugLog.flushFile()
- gDebugLogBytes += int64(msg.len) + 1
-
-proc clearRemaining*() =
- gDistanceRemaining = 0.0
- gTurnRemaining = 0.0
- gGunTurnRemaining = 0.0
- gRadarTurnRemaining = 0.0
- gContinuousTurnRate = 0.0
- gContinuousGunTurnRate = 0.0
- gContinuousRadarTurnRate = 0.0
- gContinuousTargetSpeed = 0.0
- # Reset override flags — prevents stale state carrying across rounds
- gOverrideTurnRate = false
- gOverrideGunTurnRate = false
- gOverrideRadarTurnRate = false
- gOverrideTargetSpeed = false
- gIsOverDriving = false
- # Reset intent values to zero for a clean slate each round
- gIntentTurnRate = 0.0
- gIntentGunTurnRate = 0.0
- gIntentRadarTurnRate = 0.0
- gIntentTargetSpeed = 0.0
- # Reset stop/resume state — stale gStopped=true would hijack forward/turn calls
- gStopped = false
- gSavedTurnRate = 0.0
- gSavedGunTurnRate = 0.0
- gSavedRadarTurnRate = 0.0
- gSavedTargetSpeed = 0.0
- # Reset prevDir to current tick values — prevents wrong delta on first processTurn
- gPreviousDirection = getDirection()
- gPreviousGunDirection = getGunDirection()
- gPreviousRadarDirection = getRadarDirection()
- # Reset event queue state for new round
- gEventQueue.clear()
- gInterrupted = false
-
-proc updateTurnRemaining() =
- let delta = calcDeltaAngle(getDirection(), gPreviousDirection)
- gPreviousDirection = getDirection()
- if not gOverrideTurnRate:
- gIntentTurnRate = gContinuousTurnRate.clamp(-gMaxTurnRate, gMaxTurnRate)
- return
- if abs(gTurnRemaining) <= abs(delta):
- gTurnRemaining = 0.0
- else:
- gTurnRemaining -= delta
- if isNearZero(gTurnRemaining): gTurnRemaining = 0.0
- gIntentTurnRate = gTurnRemaining.clamp(-gMaxTurnRate, gMaxTurnRate)
-
-proc updateGunTurnRemaining() =
- let delta = calcDeltaAngle(getGunDirection(), gPreviousGunDirection)
- gPreviousGunDirection = getGunDirection()
- if not gOverrideGunTurnRate:
- gIntentGunTurnRate = gContinuousGunTurnRate.clamp(-gMaxGunTurnRate, gMaxGunTurnRate)
- return
- if abs(gGunTurnRemaining) <= abs(delta):
- gGunTurnRemaining = 0.0
- else:
- gGunTurnRemaining -= delta
- if isNearZero(gGunTurnRemaining): gGunTurnRemaining = 0.0
- gIntentGunTurnRate = gGunTurnRemaining.clamp(-gMaxGunTurnRate, gMaxGunTurnRate)
-
-proc updateRadarTurnRemaining() =
- let delta = calcDeltaAngle(getRadarDirection(), gPreviousRadarDirection)
- gPreviousRadarDirection = getRadarDirection()
- if not gOverrideRadarTurnRate:
- gIntentRadarTurnRate = gContinuousRadarTurnRate.clamp(-gMaxRadarTurnRate, gMaxRadarTurnRate)
- return
- if abs(gRadarTurnRemaining) <= abs(delta):
- gRadarTurnRemaining = 0.0
- else:
- gRadarTurnRemaining -= delta
- if isNearZero(gRadarTurnRemaining): gRadarTurnRemaining = 0.0
- gIntentRadarTurnRate = gRadarTurnRemaining.clamp(-gMaxRadarTurnRate, gMaxRadarTurnRate)
-
-proc updateMovement() =
- if not gOverrideTargetSpeed:
- gIntentTargetSpeed = gContinuousTargetSpeed.clamp(-gMaxSpeed, gMaxSpeed)
- if abs(gDistanceRemaining) < abs(getSpeed()):
- gDistanceRemaining = 0.0
- else:
- gDistanceRemaining -= getSpeed()
- elif gDistanceRemaining == Inf:
- gIntentTargetSpeed = gMaxSpeed
- elif gDistanceRemaining == NegInf:
- gIntentTargetSpeed = -gMaxSpeed
- else:
- let dist = gDistanceRemaining
- let newSpeed = getNewTargetSpeed(gMaxSpeed, getSpeed(), dist)
- gIntentTargetSpeed = newSpeed.clamp(-gMaxSpeed, gMaxSpeed)
-
- if isNearZero(newSpeed) and gIsOverDriving:
- gDistanceRemaining = 0.0
- gIsOverDriving = false
- else:
- if math.sgn(dist * newSpeed).float != -1.0:
- gIsOverDriving = getDistanceTraveledUntilStop(gMaxSpeed, newSpeed) > abs(dist)
- gDistanceRemaining = dist - newSpeed
-
-proc processTurn*() =
- ## Update motion tracking at the start of each tick (called from go() and
- ## botThreadEntry after each tick signal).
- if isDisabled():
- clearRemaining()
- else:
- updateTurnRemaining()
- updateGunTurnRemaining()
- updateRadarTurnRemaining()
- updateMovement()
-
-# ---------------------------------------------------------------------------
-# Default event handlers (no-ops; override in your Bot subtype)
-# NOTE: must be defined before dispatchEvent() below
-# ---------------------------------------------------------------------------
-
-method run*(bot: Bot) {.base.} = discard
-method onConnected*(bot: Bot, e: ConnectedEvent) {.base.} = discard
-method onDisconnected*(bot: Bot, e: DisconnectedEvent) {.base.} = discard
-method onConnectionError*(bot: Bot, e: ConnectionErrorEvent) {.base.} = discard
-method onGameStarted*(bot: Bot, e: GameStartedEventForBot) {.base.} = discard
-method onGameEnded*(bot: Bot, e: GameEndedEventForBot) {.base.} = discard
-method onGameAborted*(bot: Bot) {.base.} = discard
-method onRoundStarted*(bot: Bot, e: RoundStartedEvent) {.base.} = discard
-method onRoundEnded*(bot: Bot, e: RoundEndedEventForBot) {.base.} = discard
-method onTick*(bot: Bot, e: TickEventForBot) {.base.} = discard
-method onSkippedTurn*(bot: Bot, e: SkippedTurnEvent) {.base.} = discard
-method onBotDeath*(bot: Bot, e: BotDeathEvent) {.base.} = discard
-method onBulletFired*(bot: Bot, e: BulletFiredEvent) {.base.} = discard
-method onBulletHit*(bot: Bot, e: BulletHitBotEvent) {.base.} = discard
-method onBulletHitBullet*(bot: Bot, e: BulletHitBulletEvent) {.base.} = discard
-method onBulletHitWall*(bot: Bot, e: BulletHitWallEvent) {.base.} = discard
-method onHitByBullet*(bot: Bot, e: HitByBulletEvent) {.base.} = discard
-method onHitBot*(bot: Bot, e: BotHitBotEvent) {.base.} = discard
-method onHitWall*(bot: Bot, e: BotHitWallEvent) {.base.} = discard
-method onScannedBot*(bot: Bot, e: ScannedBotEvent) {.base.} = discard
-method onWonRound*(bot: Bot, e: WonRoundEvent) {.base.} = discard
-method onTeamMessage*(bot: Bot, e: TeamMessageEvent) {.base.} = discard
-method onDeath*(bot: Bot, e: BotDeathEvent) {.base.} = discard
-method onCustomEvent*(bot: Bot, e: Condition) {.base.} = discard
-
-# ---------------------------------------------------------------------------
-# Event dispatch (called from bot thread)
-# NOTE: must be defined before go() below
-# ---------------------------------------------------------------------------
-
-proc dispatchSingleEvent(bot: Bot; e: BotEvent) =
- ## Dispatch a typed BotEvent to the appropriate handler.
- case e.kind
- of ekTick: bot.onTick(e.tick)
- of ekSkippedTurn: bot.onSkippedTurn(e.skippedTurn)
- of ekBotDeath: bot.onBotDeath(e.botDeath)
- of ekDeath: bot.onDeath(e.death)
- of ekBulletFired:
- bot.onBulletFired(e.bulletFired)
- gIntentFirepower = 0.0
- of ekBulletHitBot: bot.onBulletHit(e.bulletHitBot)
- of ekBulletHitBullet: bot.onBulletHitBullet(e.bulletHitBullet)
- of ekBulletHitWall: bot.onBulletHitWall(e.bulletHitWall)
- of ekHitByBullet: bot.onHitByBullet(e.hitByBullet)
- of ekHitBot:
- if e.hitBot.rammed: gDistanceRemaining = 0.0
- bot.onHitBot(e.hitBot)
- of ekHitWall:
- gDistanceRemaining = 0.0
- bot.onHitWall(e.hitWall)
- of ekScannedBot: bot.onScannedBot(e.scannedBot)
- of ekWonRound: bot.onWonRound(e.wonRound)
- of ekTeamMessage: bot.onTeamMessage(e.teamMessage)
- of ekCustom: bot.onCustomEvent(e.condition)
-
-proc dispatchPendingEvents*(bot: Bot) =
- var pending: seq[BotEvent]
- let (hasEvents, evs) = gEventChan.tryRecv() # non-blocking: stop signals carry no events
- if hasEvents: pending = evs
- for e in pending:
- gEventQueue.addEvent(e)
- let turnNumber = getTurn()
- gEventQueue.addCustomEvents(turnNumber)
- gEventQueue.removeOldEvents(turnNumber)
- gEventQueue.sortEvents()
- while gEventQueue.eventsLen > 0:
- let e = gEventQueue.events[0]
- let p = gEventQueue.getPriority(e.kind)
- if p < gEventQueue.currentTopPriority:
- break
- if p == gEventQueue.currentTopPriority:
- if gEventQueue.currentTopEventKind in gEventQueue.interruptible:
- gEventQueue.setInterruptible(gEventQueue.currentTopEventKind, false)
- gInterrupted = true
- break
- discard gEventQueue.popFirst()
- let oldPriority = gEventQueue.currentTopPriority
- let oldKind = gEventQueue.currentTopEventKind
- gEventQueue.currentTopPriority = p
- gEventQueue.currentTopEventKind = e.kind
- try: dispatchSingleEvent(bot, e)
- except Exception as ex:
- stderr.writeLine "[bot] event dispatch error: " & ex.msg
- gInterrupted = false
- gEventQueue.currentTopPriority = oldPriority
- gEventQueue.currentTopEventKind = oldKind
-
-# ---------------------------------------------------------------------------
-# go() — send intent and wait for next tick
-# ---------------------------------------------------------------------------
-
-var gDroppedIntents: int # bot-thread only; rate-limits drop logging
-
-proc go*() =
- ## Send current intent to the server and block until the next tick arrives.
- ## This is the fundamental time-step primitive — all blocking methods use it.
- if not isRunning():
- raise newException(CatchableError, "Bot is not running")
- # Stop-signal check BEFORE emitting an intent: a pending `false` means the
- # round/game ended while we were computing. Consume it and bail without an
- # intent so a stop is never treated as a tick (kills the duplicate-intent /
- # duplicate-GO-RECV storm at round boundaries).
- let (hasStop, stopVal) = gTickChan.tryRecv()
- if hasStop and not stopVal:
- debugLog("[GO-STOP] pending stop consumed — no intent emitted")
- return
- let json = buildIntentJson()
- debugLog("[GO-SEND] turn=" & $getTurn() &
- " intentTR=" & $gIntentTurnRate &
- " intentGTR=" & $gIntentGunTurnRate &
- " intentSpd=" & $gIntentTargetSpeed &
- " turnRem=" & $gTurnRemaining &
- " gunTurnRem=" & $gGunTurnRemaining &
- " distRem=" & $gDistanceRemaining &
- " overTR=" & $gOverrideTurnRate &
- " overGTR=" & $gOverrideGunTurnRate)
- clearGraphics() # reset SVG buffer and style state for next tick
- # ponytail: trySend, drop if the cap-1 channel is full — a stalled sender
- # must never wedge the bot thread mid-round (corpse signature). Sender
- # drains unconditionally (see senderThreadEntry); the drop is pure belt.
- if not gIntentChan.trySend(json):
- inc gDroppedIntents
- if gDroppedIntents mod 100 == 1:
- debugLog("[GO-DROP] intent chan full (sender stalled/dead) — dropped " &
- $gDroppedIntents & " intents")
- let gotTick = gTickChan.recv() # block until main thread finishes processTurn + wake
- if not gotTick:
- # Stop signal: round/game ended while we were blocked. No tick dispatch,
- # no further intent — the caller's isRunning() check exits the loop.
- debugLog("[GO-STOP] stop consumed in recv — no dispatch")
- return
- debugLog("[GO-RECV] turn=" & $getTurn() &
- " dir=" & $getDirection() &
- " gunDir=" & $getGunDirection() &
- " spd=" & $getSpeed() &
- " prevDir=" & $gPreviousDirection &
- " prevGunDir=" & $gPreviousGunDirection)
- # processTurn already ran on main thread — just dispatch events
- dispatchPendingEvents(gBot) # fire event handlers for this tick
-
-# ---------------------------------------------------------------------------
-# Stop / Resume
-# ---------------------------------------------------------------------------
-
-proc setStop*(overwrite: bool = false) =
- ## Non-blocking: save current movement state (IBaseBot API).
- if not gStopped or overwrite:
- gStopped = true
- gSavedTurnRate = gIntentTurnRate
- gSavedGunTurnRate = gIntentGunTurnRate
- gSavedRadarTurnRate = gIntentRadarTurnRate
- gSavedTargetSpeed = gIntentTargetSpeed
- gIntentTurnRate = 0.0
- gIntentGunTurnRate = 0.0
- gIntentRadarTurnRate = 0.0
- gIntentTargetSpeed = 0.0
-
-proc setResume*() =
- ## Non-blocking: restore saved movement state (IBaseBot API).
- if gStopped:
- gIntentTurnRate = gSavedTurnRate
- gIntentGunTurnRate = gSavedGunTurnRate
- gIntentRadarTurnRate = gSavedRadarTurnRate
- gIntentTargetSpeed = gSavedTargetSpeed
- gStopped = false
-
-proc stop*(overwrite: bool = false) =
- ## Blocking: save movement state, then call go() (IBot API).
- setStop(overwrite)
-
-proc resume*() =
- ## Blocking: restore saved movement state, then call go() (IBot API).
- setResume()
-
-# ---------------------------------------------------------------------------
-# Blocking movement methods
-# ---------------------------------------------------------------------------
-
-proc setForward*(distance: float) =
- gOverrideTargetSpeed = true
- let speed = getNewTargetSpeed(gMaxSpeed, getSpeed(), distance)
- gIntentTargetSpeed = speed.clamp(-gMaxSpeed, gMaxSpeed)
- gDistanceRemaining = distance
-
-proc setTurnLeft*(degrees: float) =
- gOverrideTurnRate = true
- gTurnRemaining = degrees
- gIntentTurnRate = degrees.clamp(-gMaxTurnRate, gMaxTurnRate)
-
-proc setTurnRight*(degrees: float) = setTurnLeft(-degrees)
-proc setBack*(distance: float) = setForward(-distance)
-
-proc setTurnGunLeft*(degrees: float) =
- gOverrideGunTurnRate = true
- gGunTurnRemaining = degrees
- gIntentGunTurnRate = degrees.clamp(-gMaxGunTurnRate, gMaxGunTurnRate)
-
-proc setTurnGunRight*(degrees: float) = setTurnGunLeft(-degrees)
-
-proc setTurnRadarLeft*(degrees: float) =
- gOverrideRadarTurnRate = true
- gRadarTurnRemaining = degrees
- gIntentRadarTurnRate = degrees.clamp(-gMaxRadarTurnRate, gMaxRadarTurnRate)
-
-proc setTurnRadarRight*(degrees: float) = setTurnRadarLeft(-degrees)
-
-proc forward*(distance: float) =
- debugLog("[FORWARD] distance=" & $distance & " dir=" & $getDirection())
- if gStopped:
- go()
- else:
- setForward(distance)
- while isRunning() and not gInterrupted and
- not (gDistanceRemaining == 0.0 and getSpeed() == 0.0):
- go()
- debugLog("[FORWARD-DONE] dir=" & $getDirection() & " distRem=" & $gDistanceRemaining)
-
-proc back*(distance: float) = forward(-distance)
-
-proc turnLeft*(degrees: float) =
- debugLog("[TURNLEFT] degrees=" & $degrees & " dir=" & $getDirection() & " gunDir=" & $getGunDirection())
- if gStopped:
- go()
- else:
- setTurnLeft(degrees)
- while isRunning() and not gInterrupted and gTurnRemaining != 0.0:
- go()
- debugLog("[TURNLEFT-DONE] dir=" & $getDirection() & " gunDir=" & $getGunDirection() & " turnRem=" & $gTurnRemaining)
-
-proc turnRight*(degrees: float) = turnLeft(-degrees)
-
-proc turnGunLeft*(degrees: float) =
- debugLog("[GUNLEFT] degrees=" & $degrees & " gunDir=" & $getGunDirection())
- if gStopped:
- go()
- else:
- setTurnGunLeft(degrees)
- while isRunning() and not gInterrupted and gGunTurnRemaining != 0.0:
- go()
- debugLog("[GUNLEFT-DONE] gunDir=" & $getGunDirection() & " gunTurnRem=" & $gGunTurnRemaining)
-
-proc turnGunRight*(degrees: float) = turnGunLeft(-degrees)
-
-proc turnRadarLeft*(degrees: float) =
- if gStopped:
- go()
- else:
- setTurnRadarLeft(degrees)
- while isRunning() and not gInterrupted and gRadarTurnRemaining != 0.0:
- go()
-
-proc turnRadarRight*(degrees: float) = turnRadarLeft(-degrees)
-
-proc fire*(firepower: float) =
- discard setFire(firepower)
- go()
-
-proc rescan*() =
- setRescan()
- go()
-
-proc waitFor*(condition: proc(): bool) =
- while isRunning() and not condition():
- go()
-
-# ---------------------------------------------------------------------------
-# Event queue public API
-# ---------------------------------------------------------------------------
-
-proc addCustomEvent*(name: string; test: proc(): bool) =
- ## Register a custom event condition. Evaluated each tick; fires onCustomEvent when true.
- gEventQueue.addCondition(Condition(name: name, test: test))
-
-proc removeCustomEvent*(name: string) =
- ## Remove a custom event condition by name.
- gEventQueue.removeConditionByName(name)
-
-proc setInterruptible*(v: bool) =
- ## Mark the current event handler as interruptible by same-priority events.
- if v: gEventQueue.interruptible.incl gEventQueue.currentTopEventKind
- else: gEventQueue.interruptible.excl gEventQueue.currentTopEventKind
-
-proc getEventPriority*(kind: EventKind): int =
- ## Get the dispatch priority for an event kind.
- gEventQueue.getPriority(kind)
-
-proc setEventPriority*(kind: EventKind; p: int) =
- ## Set the dispatch priority for an event kind.
- gEventQueue.setPriority(kind, p)
-
-proc getEvents*(): seq[BotEvent] =
- ## Get all events currently in the queue.
- gEventQueue.getEvents()
-
-proc clearEvents*() =
- ## Clear all events from the queue.
- gEventQueue.clearEvents()
-
-# ---------------------------------------------------------------------------
-# Bot thread entry point
-# ---------------------------------------------------------------------------
-
-proc botThreadEntry() {.thread.} =
- ## Runs `bot.run()` after waiting for the first tick.
- {.cast(gcsafe).}:
- # Wait for first tick signal from main thread
- # (clearRemaining + processTurn already ran on main thread)
- let firstTick = gTickChan.recv()
- if not firstTick:
- debugLog("[DBG] botThreadEntry: stop before first tick — exiting")
- return
- debugLog("[DBG] botThreadEntry: first tick" &
- " dir=" & $getDirection() &
- " gunDir=" & $getGunDirection() &
- " prevDir=" & $gPreviousDirection &
- " prevGunDir=" & $gPreviousGunDirection)
-
- # Reset graphics + intent buffers on the thread that owns them. go()'s
- # stop path (round end) returns before buildIntentJson/clearGraphics, so
- # the previous round's thread can leave content behind; resetting here
- # keeps it from leaking into this round's first intent.
- clearGraphics()
- gIntentStdOutLen = 0
- gIntentStdErrLen = 0
- for i in 0 ..< gIntentTeamMsgsLen: gIntentTeamMessages[i].reset
- gIntentTeamMsgsLen = 0
-
- dispatchPendingEvents(gBot) # dispatch events embedded in the first tick
-
- try:
- gBot.run()
- except Exception as e:
- stderr.writeLine "[bot] run() exception: " & e.msg
-
- # After run() exits, keep calling go() to skip turns until game ends
- while isRunning():
- try: go()
- except: break
-
-# ---------------------------------------------------------------------------
-# Exported initialiser (called from start() in tankroyale_botapi.nim)
-# ---------------------------------------------------------------------------
-
-proc initGlobals*() =
- gTickChan.open(1)
- gIntentChan.open(1)
- gEventChan.open(8)
- initLock(gLock)
- gEventQueue = initEventQueue()
- withLock(gLock):
- gBotNames = initTable[int, string]()
- # Debug log is opt-in: it is written every tick from two threads, so leaving
- # it on by default is a disk hog and an I/O stall source. Enable with
- # PPOB_DEBUG_LOG=1 to debug; starts fresh (truncated) each run.
- if getEnv("PPOB_DEBUG_LOG", "") == "1":
- gDebugLog = open(gDebugLogPath, fmAppend)
- resetDebugLog()
- gDebugLog.writeLine("=== PROCESS START pid=" & $getpid() & " ===")
- gDebugLog.flushFile()
-
-proc setServerInfo*(variant, version: string) =
- withLock(gLock):
- gVariant = variant
- gServerVersion = version
-
-proc setGameStarted*(myId: int; setup: GameSetup; teammateIds: seq[int]) =
- withLock(gLock):
- gMyId = myId
- gGameSetup = setup
- gTeammateIds = teammateIds
- debugLog("=== GAME START myId=" & $myId & " ===")
-
-proc startRound*() =
- withLock(gLock):
- gRunning = true
- gTurn = 0
- gFirstTickOfRound = true
-
-proc setRunning*(v: bool) =
- withLock(gLock): gRunning = v
-
-proc signalTick*(tick: TickEventForBot; events: seq[BotEvent]) =
- ## Called from main thread when a new tick arrives.
- ## Updates shared state only — caller must call processTickOnMainThread + wakeBotThread.
- withLock(gLock):
- gTurn = tick.turnNumber
- gRound = tick.roundNumber
- gEnemyCount = tick.botState.enemyCount # enemyCount lives in BotState
- gState = tick.botState
- gBullets = tick.bulletStates
- # Prepend tick event, then sub-events — queue sorts by priority.
- # Moved through a Channel: ownership transfer, no cross-thread refcounts.
- # Send happens-before wakeBotThread's tickChan signal, so the bot thread
- # always finds its events waiting when it wakes.
- var pending = @[BotEvent(kind: ekTick, turnNumber: tick.turnNumber, tick: tick)]
- pending.add events
- gEventChan.send(move(pending))
-
-proc processTickOnMainThread*() =
- ## Run motion tracking on the main thread while bot is blocked.
- ## Must be called after signalTick and before wakeBotThread.
- if gFirstTickOfRound:
- clearRemaining()
- gFirstTickOfRound = false
- processTurn()
-
-proc wakeBotThread*() =
- ## Wake the bot thread after state + motion tracking are ready.
- gTickChan.send(true)
-
-var gWsFailed = false # set by sender thread when a ws.send dies
-
-proc senderThreadEntry() {.thread.} =
- ## Sender thread: owns all WebSocket writes during gameplay.
- ## ponytail: never exits — keeps draining gIntentChan so the bot thread's
- ## trySend never wedges. A dead socket just discards intents (the main
- ## receive loop detects the broken connection and exits cleanly).
- {.cast(gcsafe).}:
- while true:
- let json = gIntentChan.recv()
- if json.len == 0: break # sentinel: stop
- try:
- gWs.send(json)
- except Exception as e:
- gWsFailed = true
- stderr.writeLine "[sender] send error (keeping drain): " & e.msg
- debugLog("[SENDER-ERR] " & e.msg)
- # drain without blocking: bot's go() uses trySend, so the channel is
- # either empty or has at most one fresh intent — the next recv takes it
- continue
-
-proc startSenderThread*() =
- createThread(gSenderThread, senderThreadEntry)
-
-proc stopSenderThread*() =
- gIntentChan.send("") # sentinel
- joinThread(gSenderThread)
-
-proc signalStop*() =
- ## Unblock the bot thread when a round or the game ends.
- ## Sends a dummy false to gTickChan so the bot wakes from go().
- ## gTickChan has capacity 1 and the server can deliver the final
- ## TickEventForBot + RoundEndedEventForBot back-to-back, leaving the tick's
- ## `true` unconsumed: the bot exits via `raise` on the isRunning() check in
- ## the next go() instead of another recv(), so send(false) would block
- ## forever and freeze the main receive loop (silent corpse). The main thread
- ## is the only gTickChan sender, so draining right before the send cannot
- ## race; [true,false] and [false] orderings both unblock cleanly.
- debugLog("[SS-ENTER] tid=" & $getThreadId())
- let (drained, val) = gTickChan.tryRecv()
- debugLog("[SS-DRAIN] drained=" & $drained & " val=" & $val & " tid=" & $getThreadId())
- gTickChan.send(false)
- debugLog("[SS-SENT] false tid=" & $getThreadId())
- debugLog("[SS-EXIT] tid=" & $getThreadId())
-
-proc recvIntent*(): string =
- ## Called by main thread after signalling a tick.
- ## Blocks until the bot thread sends its intent JSON via go().
- let (ok, json) = gIntentChan.tryRecv()
- if ok: return json
- return gIntentChan.recv()
-
-proc drainIntentChan*() =
- ## Discard any pending intent (used when round/game ends).
- discard gIntentChan.tryRecv()
-
-proc drainTickChan*() =
- ## Discard any pending tick/stop signal left in gTickChan.
- ## Needed when the bot thread exits via isRunning() check instead of
- ## consuming the stop signal from go() — the false sits in the channel
- ## and would be mistaken for the first real tick of the next round.
- let (drained, val) = gTickChan.tryRecv()
- if drained:
- debugLog("[DBG] drainTickChan: drained signal=" & $val)
-
-proc drainEventChan*() =
- ## Discard any unconsumed tick events left in gEventChan (round/game end).
- ## Called after the bot thread joined — no more recv's possible, so this
- ## prevents stale previous-round events leaking into the next round.
- while true:
- let (hasEvents, _) = gEventChan.tryRecv()
- if not hasEvents: break
-
-proc startBotThread*() =
- createThread(gBotThread, botThreadEntry)
-
-proc waitForBotThread*() =
- joinThread(gBotThread)
diff --git a/common_libs/tankroyale_botapi/tankroyale_botapi/bot_info.nim b/common_libs/tankroyale_botapi/tankroyale_botapi/bot_info.nim
deleted file mode 100644
index 21b9349..0000000
--- a/common_libs/tankroyale_botapi/tankroyale_botapi/bot_info.nim
+++ /dev/null
@@ -1,79 +0,0 @@
-## BotInfo: bot identification loaded from a JSON file or environment variables.
-
-import std/[os, json, strutils, sequtils]
-import ./schemas
-
-type
- BotInfo* = object
- name*: string
- version*: string
- authors*: seq[string]
- description*: string
- homepage*: string
- countryCodes*: seq[string]
- gameTypes*: seq[string]
- platform*: string
- programmingLang*: string
- initialPosition*: InitialPosition
- isDroid*: bool
-
-proc botInfoFromJson*(path: string): BotInfo =
- let data = parseJson(readFile(path))
- result.name = data{"name"}.getStr
- result.version = data{"version"}.getStr
- if data.hasKey("authors"):
- for a in data["authors"]: result.authors.add a.getStr
- result.description = data{"description"}.getStr
- result.homepage = data{"homepage"}.getStr
- if data.hasKey("countryCodes"):
- for c in data["countryCodes"]: result.countryCodes.add c.getStr
- if data.hasKey("gameTypes"):
- for g in data["gameTypes"]: result.gameTypes.add g.getStr
- result.platform = data{"platform"}.getStr("Nim " & NimVersion)
- result.programmingLang = data{"programmingLang"}.getStr("Nim")
- if data.hasKey("initialPosition"):
- let ip = data["initialPosition"]
- result.initialPosition.x = ip{"x"}.getFloat
- result.initialPosition.y = ip{"y"}.getFloat
- result.initialPosition.direction = ip{"direction"}.getFloat
- result.isDroid = data{"isDroid"}.getBool(false)
-
-proc botInfoFromEnv*(): BotInfo =
- ## Fall back to environment variables when no JSON file is given.
- result.name = getEnv("BOT_NAME", "Unnamed Bot")
- result.version = getEnv("BOT_VERSION", "1.0")
- let authorsStr = getEnv("BOT_AUTHORS", "Unknown")
- result.authors = authorsStr.split(',').mapIt(it.strip)
- result.description = getEnv("BOT_DESCRIPTION", "")
- result.homepage = getEnv("BOT_HOMEPAGE", "")
- let ccStr = getEnv("BOT_COUNTRY_CODES", "")
- if ccStr.len > 0:
- result.countryCodes = ccStr.split(',').mapIt(it.strip)
- let gtStr = getEnv("BOT_GAME_TYPES", "classic,melee,1v1")
- result.gameTypes = gtStr.split(',').mapIt(it.strip)
- result.platform = getEnv("BOT_PLATFORM", "Nim " & NimVersion)
- result.programmingLang = getEnv("BOT_PROGRAMMING_LANG", "Nim")
- result.isDroid = getEnv("BOT_IS_DROID", "false").toLowerAscii == "true"
-
-proc loadBotInfo*(jsonFile: string = ""): BotInfo =
- var resolved = ""
- if jsonFile.len > 0:
- if fileExists(jsonFile):
- resolved = jsonFile
- else:
- # Try alongside the executable
- let appPath = getAppDir() / jsonFile
- if fileExists(appPath):
- resolved = appPath
-
- if resolved.len > 0:
- result = botInfoFromJson(resolved)
- else:
- result = botInfoFromEnv()
- # Ensure gameTypes has at least one entry
- if result.gameTypes.len == 0:
- result.gameTypes = @["classic", "melee", "1v1"]
- if result.platform.len == 0:
- result.platform = "Nim " & NimVersion
- if result.programmingLang.len == 0:
- result.programmingLang = "Nim"
diff --git a/common_libs/tankroyale_botapi/tankroyale_botapi/color.nim b/common_libs/tankroyale_botapi/tankroyale_botapi/color.nim
deleted file mode 100644
index 02afcfe..0000000
--- a/common_libs/tankroyale_botapi/tankroyale_botapi/color.nim
+++ /dev/null
@@ -1,205 +0,0 @@
-## Color type for Robocode Tank Royale — RGBA packed as uint32 (R<<24|G<<16|B<<8|A),
-## matching the layout of the Java Color class.
-
-import std/strutils
-
-type Color* = distinct uint32
-
-# ---------------------------------------------------------------------------
-# Factory procs
-# ---------------------------------------------------------------------------
-
-proc fromRgb*(r, g, b: uint8): Color {.inline.} =
- Color((r.uint32 shl 24) or (g.uint32 shl 16) or (b.uint32 shl 8) or 0xFF)
-
-proc fromRgba*(r, g, b, a: uint8): Color {.inline.} =
- Color((r.uint32 shl 24) or (g.uint32 shl 16) or (b.uint32 shl 8) or a.uint32)
-
-proc fromHex*(s: string): Color =
- ## Parse "#RRGGBB" or "#RRGGBBAA". Raises ValueError on bad input.
- let h = if s.len > 0 and s[0] == '#': s[1..^1] else: s
- case h.len
- of 6:
- let v = parseHexInt(h)
- result = Color((v.uint32 shl 8) or 0xFF)
- of 8:
- result = Color(parseHexInt(h).uint32)
- else:
- raise newException(ValueError, "invalid color string: " & s)
-
-# ---------------------------------------------------------------------------
-# Accessors
-# ---------------------------------------------------------------------------
-
-proc r*(c: Color): uint8 {.inline.} = uint8(c.uint32 shr 24)
-proc g*(c: Color): uint8 {.inline.} = uint8((c.uint32 shr 16) and 0xFF)
-proc b*(c: Color): uint8 {.inline.} = uint8((c.uint32 shr 8) and 0xFF)
-proc a*(c: Color): uint8 {.inline.} = uint8(c.uint32 and 0xFF)
-
-# ---------------------------------------------------------------------------
-# Serialisation
-# ---------------------------------------------------------------------------
-
-proc toHex*(c: Color): string =
- ## Returns "#RRGGBB" when alpha=255, "#RRGGBBAA" otherwise.
- if c.a == 0xFF:
- result = '#' & toHex(c.r.int, 2) & toHex(c.g.int, 2) & toHex(c.b.int, 2)
- else:
- result = '#' & toHex(c.r.int, 2) & toHex(c.g.int, 2) & toHex(c.b.int, 2) & toHex(c.a.int, 2)
-
-proc `$`*(c: Color): string = c.toHex
-
-proc `==`*(a, b: Color): bool {.borrow.}
-
-# ---------------------------------------------------------------------------
-# Backward compat: implicit conversion from string literal / variable
-# ---------------------------------------------------------------------------
-
-converter toColor*(s: string): Color = fromHex(s)
-
-# ---------------------------------------------------------------------------
-# Named constants (all 141 from Java Color class)
-# ---------------------------------------------------------------------------
-
-const
- TRANSPARENT* = fromRgba(255, 255, 255, 0)
- ALICE_BLUE* = fromRgb(240, 248, 255)
- ANTIQUE_WHITE* = fromRgb(250, 235, 215)
- AQUA* = fromRgb(0, 255, 255)
- AQUAMARINE* = fromRgb(127, 255, 212)
- AZURE* = fromRgb(240, 255, 255)
- BEIGE* = fromRgb(245, 245, 220)
- BISQUE* = fromRgb(255, 228, 196)
- BLACK* = fromRgb(0, 0, 0)
- BLANCHED_ALMOND* = fromRgb(255, 235, 205)
- BLUE* = fromRgb(0, 0, 255)
- BLUE_VIOLET* = fromRgb(138, 43, 226)
- BROWN* = fromRgb(165, 42, 42)
- BURLY_WOOD* = fromRgb(222, 184, 135)
- CADET_BLUE* = fromRgb(95, 158, 160)
- CHARTREUSE* = fromRgb(127, 255, 0)
- CHOCOLATE* = fromRgb(210, 105, 30)
- CORAL* = fromRgb(255, 127, 80)
- CORNFLOWER_BLUE* = fromRgb(100, 149, 237)
- CORNSILK* = fromRgb(255, 248, 220)
- CRIMSON* = fromRgb(220, 20, 60)
- CYAN* = fromRgb(0, 255, 255)
- DARK_BLUE* = fromRgb(0, 0, 139)
- DARK_CYAN* = fromRgb(0, 139, 139)
- DARK_GOLDENROD* = fromRgb(184, 134, 11)
- DARK_GRAY* = fromRgb(169, 169, 169)
- DARK_GREEN* = fromRgb(0, 100, 0)
- DARK_KHAKI* = fromRgb(189, 183, 107)
- DARK_MAGENTA* = fromRgb(139, 0, 139)
- DARK_OLIVE_GREEN* = fromRgb(85, 107, 47)
- DARK_ORANGE* = fromRgb(255, 140, 0)
- DARK_ORCHID* = fromRgb(153, 50, 204)
- DARK_RED* = fromRgb(139, 0, 0)
- DARK_SALMON* = fromRgb(233, 150, 122)
- DARK_SEA_GREEN* = fromRgb(143, 188, 139)
- DARK_SLATE_BLUE* = fromRgb(72, 61, 139)
- DARK_SLATE_GRAY* = fromRgb(47, 79, 79)
- DARK_TURQUOISE* = fromRgb(0, 206, 209)
- DARK_VIOLET* = fromRgb(148, 0, 211)
- DEEP_PINK* = fromRgb(255, 20, 147)
- DEEP_SKY_BLUE* = fromRgb(0, 191, 255)
- DIM_GRAY* = fromRgb(105, 105, 105)
- DODGER_BLUE* = fromRgb(30, 144, 255)
- FIREBRICK* = fromRgb(178, 34, 34)
- FLORAL_WHITE* = fromRgb(255, 250, 240)
- FOREST_GREEN* = fromRgb(34, 139, 34)
- FUCHSIA* = fromRgb(255, 0, 255)
- GAINSBORO* = fromRgb(220, 220, 220)
- GHOST_WHITE* = fromRgb(248, 248, 255)
- GOLD* = fromRgb(255, 215, 0)
- GOLDENROD* = fromRgb(218, 165, 32)
- GRAY* = fromRgb(128, 128, 128)
- GREEN* = fromRgb(0, 128, 0)
- GREEN_YELLOW* = fromRgb(173, 255, 47)
- HONEYDEW* = fromRgb(240, 255, 240)
- HOT_PINK* = fromRgb(255, 105, 180)
- INDIAN_RED* = fromRgb(205, 92, 92)
- INDIGO* = fromRgb(75, 0, 130)
- IVORY* = fromRgb(255, 255, 240)
- KHAKI* = fromRgb(240, 230, 140)
- LAVENDER* = fromRgb(230, 230, 250)
- LAVENDER_BLUSH* = fromRgb(255, 240, 245)
- LAWN_GREEN* = fromRgb(124, 252, 0)
- LEMON_CHIFFON* = fromRgb(255, 250, 205)
- LIGHT_BLUE* = fromRgb(173, 216, 230)
- LIGHT_CORAL* = fromRgb(240, 128, 128)
- LIGHT_CYAN* = fromRgb(224, 255, 255)
- LIGHT_GOLDENROD_YELLOW* = fromRgb(250, 250, 210)
- LIGHT_GRAY* = fromRgb(211, 211, 211)
- LIGHT_GREEN* = fromRgb(144, 238, 144)
- LIGHT_PINK* = fromRgb(255, 182, 193)
- LIGHT_SALMON* = fromRgb(255, 160, 122)
- LIGHT_SEA_GREEN* = fromRgb(32, 178, 170)
- LIGHT_SKY_BLUE* = fromRgb(135, 206, 250)
- LIGHT_SLATE_GRAY* = fromRgb(119, 136, 153)
- LIGHT_STEEL_BLUE* = fromRgb(176, 196, 222)
- LIGHT_YELLOW* = fromRgb(255, 255, 224)
- LIME* = fromRgb(0, 255, 0)
- LIME_GREEN* = fromRgb(50, 205, 50)
- LINEN* = fromRgb(250, 240, 230)
- MAGENTA* = fromRgb(255, 0, 255)
- MAROON* = fromRgb(128, 0, 0)
- MEDIUM_AQUAMARINE* = fromRgb(102, 205, 170)
- MEDIUM_BLUE* = fromRgb(0, 0, 205)
- MEDIUM_ORCHID* = fromRgb(186, 85, 211)
- MEDIUM_PURPLE* = fromRgb(147, 112, 219)
- MEDIUM_SEA_GREEN* = fromRgb(60, 179, 113)
- MEDIUM_SLATE_BLUE* = fromRgb(123, 104, 238)
- MEDIUM_SPRING_GREEN* = fromRgb(0, 250, 154)
- MEDIUM_TURQUOISE* = fromRgb(72, 209, 204)
- MEDIUM_VIOLET_RED* = fromRgb(199, 21, 133)
- MIDNIGHT_BLUE* = fromRgb(25, 25, 112)
- MINT_CREAM* = fromRgb(245, 255, 250)
- MISTY_ROSE* = fromRgb(255, 228, 225)
- MOCCASIN* = fromRgb(255, 228, 181)
- NAVAJO_WHITE* = fromRgb(255, 222, 173)
- NAVY* = fromRgb(0, 0, 128)
- OLD_LACE* = fromRgb(253, 245, 230)
- OLIVE* = fromRgb(128, 128, 0)
- OLIVE_DRAB* = fromRgb(107, 142, 35)
- ORANGE* = fromRgb(255, 165, 0)
- ORANGE_RED* = fromRgb(255, 69, 0)
- ORCHID* = fromRgb(218, 112, 214)
- PALE_GOLDENROD* = fromRgb(238, 232, 170)
- PALE_GREEN* = fromRgb(152, 251, 152)
- PALE_TURQUOISE* = fromRgb(175, 238, 238)
- PALE_VIOLET_RED* = fromRgb(219, 112, 147)
- PAPAYA_WHIP* = fromRgb(255, 239, 213)
- PEACH_PUFF* = fromRgb(255, 218, 185)
- PERU* = fromRgb(205, 133, 63)
- PINK* = fromRgb(255, 192, 203)
- PLUM* = fromRgb(221, 160, 221)
- POWDER_BLUE* = fromRgb(176, 224, 230)
- PURPLE* = fromRgb(128, 0, 128)
- RED* = fromRgb(255, 0, 0)
- ROSY_BROWN* = fromRgb(188, 143, 143)
- ROYAL_BLUE* = fromRgb(65, 105, 225)
- SADDLE_BROWN* = fromRgb(139, 69, 19)
- SALMON* = fromRgb(250, 128, 114)
- SANDY_BROWN* = fromRgb(244, 164, 96)
- SEA_GREEN* = fromRgb(46, 139, 87)
- SEA_SHELL* = fromRgb(255, 245, 238)
- SIENNA* = fromRgb(160, 82, 45)
- SILVER* = fromRgb(192, 192, 192)
- SKY_BLUE* = fromRgb(135, 206, 235)
- SLATE_BLUE* = fromRgb(106, 90, 205)
- SLATE_GRAY* = fromRgb(112, 128, 144)
- SNOW* = fromRgb(255, 250, 250)
- SPRING_GREEN* = fromRgb(0, 255, 127)
- STEEL_BLUE* = fromRgb(70, 130, 180)
- TAN* = fromRgb(210, 180, 140)
- TEAL* = fromRgb(0, 128, 128)
- THISTLE* = fromRgb(216, 191, 216)
- TOMATO* = fromRgb(255, 99, 71)
- TURQUOISE* = fromRgb(64, 224, 208)
- VIOLET* = fromRgb(238, 130, 238)
- WHEAT* = fromRgb(245, 222, 179)
- WHITE* = fromRgb(255, 255, 255)
- WHITE_SMOKE* = fromRgb(245, 245, 245)
- YELLOW* = fromRgb(255, 255, 0)
- YELLOW_GREEN* = fromRgb(154, 205, 50)
diff --git a/common_libs/tankroyale_botapi/tankroyale_botapi/constants.nim b/common_libs/tankroyale_botapi/tankroyale_botapi/constants.nim
deleted file mode 100644
index 28c4c16..0000000
--- a/common_libs/tankroyale_botapi/tankroyale_botapi/constants.nim
+++ /dev/null
@@ -1,44 +0,0 @@
-## Game constants for Robocode Tank Royale Nim bot API
-
-const
- # Infinity helpers
- POSITIVE_INFINITY* = high(float)
- NEGATIVE_INFINITY* = low(float)
-
- # Event queue limits
- MAX_QUEUE_SIZE* = 256
- MAX_EVENTS_AGE* = 2
- MIN_VALUE* = low(int32)
-
- # Event priorities (higher = processed first)
- PRIORITY_WON_ROUND* = 150
- PRIORITY_SKIPPED_TURN* = 140
- PRIORITY_TICK* = 130
- PRIORITY_CUSTOM* = 120
- PRIORITY_TEAM_MESSAGE* = 110
- PRIORITY_BOT_DEATH* = 100
- PRIORITY_BULLET_HIT_WALL* = 90
- PRIORITY_BULLET_HIT_BULLET* = 80
- PRIORITY_BULLET_HIT_BOT* = 70
- PRIORITY_BULLET_FIRED* = 60
- PRIORITY_HIT_BY_BULLET* = 50
- PRIORITY_HIT_WALL* = 40
- PRIORITY_HIT_BOT* = 30
- PRIORITY_SCANNED_BOT* = 20
- PRIORITY_DEATH* = 10
-
- # Physics
- ACCELERATION* = 1.0
- DECELERATION* = -2.0
- ABS_DECELERATION* = 2.0
-
- MAX_SPEED* = 8.0
- MAX_TURN_RATE* = 10.0
- MAX_GUN_TURN_RATE* = 20.0
- MAX_RADAR_TURN_RATE* = 45.0
-
- MAX_FIRE_POWER* = 3.0
- MIN_FIRE_POWER* = 0.1
-
- BOT_RADIUS* = 18.0
- RADAR_RADIUS* = 1200.0
diff --git a/common_libs/tankroyale_botapi/tankroyale_botapi/event_queue.nim b/common_libs/tankroyale_botapi/tankroyale_botapi/event_queue.nim
deleted file mode 100644
index 112dfcc..0000000
--- a/common_libs/tankroyale_botapi/tankroyale_botapi/event_queue.nim
+++ /dev/null
@@ -1,167 +0,0 @@
-## Priority-based event queue for Robocode Tank Royale bot API.
-## Typed BotEvent variants wrapping schema types, priority-sorted dispatch.
-
-import std/[algorithm, tables]
-import ./constants
-import ./schemas
-
-type
- EventKind* = enum
- ekTick
- ekSkippedTurn
- ekBotDeath
- ekDeath ## self-death; isCritical=true
- ekBulletFired
- ekBulletHitBot
- ekBulletHitBullet
- ekBulletHitWall
- ekHitByBullet
- ekHitBot
- ekHitWall
- ekScannedBot
- ekWonRound
- ekTeamMessage
- ekCustom
-
- Condition* = object
- name*: string
- test*: proc(): bool {.closure.}
-
- BotEvent* = object
- turnNumber*: int
- case kind*: EventKind
- of ekTick: tick*: TickEventForBot
- of ekSkippedTurn: skippedTurn*: SkippedTurnEvent
- of ekBotDeath: botDeath*: BotDeathEvent
- of ekDeath: death*: BotDeathEvent
- of ekBulletFired: bulletFired*: BulletFiredEvent
- of ekBulletHitBot: bulletHitBot*: BulletHitBotEvent
- of ekBulletHitBullet: bulletHitBullet*: BulletHitBulletEvent
- of ekBulletHitWall: bulletHitWall*: BulletHitWallEvent
- of ekHitByBullet: hitByBullet*: HitByBulletEvent
- of ekHitBot: hitBot*: BotHitBotEvent
- of ekHitWall: hitWall*: BotHitWallEvent
- of ekScannedBot: scannedBot*: ScannedBotEvent
- of ekWonRound: wonRound*: WonRoundEvent
- of ekTeamMessage: teamMessage*: TeamMessageEvent
- of ekCustom: condition*: Condition
-
- EventQueue* = object
- # ponytail: fixed static storage instead of a seq. The queue outlives bot
- # threads (a fresh thread runs each round), so a heap seq's backing array
- # is realloc'd by a *different* dead thread's allocator mid-round ->
- # rawDealloc SIGSEGV in addEvent (7 gdb-confirmed dumps). Static array:
- # no heap block crosses threads, realloc can never happen.
- events*: array[MAX_QUEUE_SIZE, BotEvent]
- eventsLen*: int
- priorities: Table[EventKind, int] ## runtime-mutable overrides
- interruptible*: set[EventKind]
- currentTopEventKind*: EventKind
- currentTopPriority*: int
- conditions*: seq[Condition]
-
-proc priorityOf*(kind: EventKind): int =
- case kind
- of ekTick: PRIORITY_TICK
- of ekSkippedTurn: PRIORITY_SKIPPED_TURN
- of ekBotDeath: PRIORITY_BOT_DEATH
- of ekDeath: PRIORITY_DEATH
- of ekBulletFired: PRIORITY_BULLET_FIRED
- of ekBulletHitBot: PRIORITY_BULLET_HIT_BOT
- of ekBulletHitBullet: PRIORITY_BULLET_HIT_BULLET
- of ekBulletHitWall: PRIORITY_BULLET_HIT_WALL
- of ekHitByBullet: PRIORITY_HIT_BY_BULLET
- of ekHitBot: PRIORITY_HIT_BOT
- of ekHitWall: PRIORITY_HIT_WALL
- of ekScannedBot: PRIORITY_SCANNED_BOT
- of ekWonRound: PRIORITY_WON_ROUND
- of ekTeamMessage: PRIORITY_TEAM_MESSAGE
- of ekCustom: PRIORITY_CUSTOM
-
-proc isCritical*(e: BotEvent): bool =
- e.kind in {ekDeath, ekWonRound, ekSkippedTurn}
-
-proc initEventQueue*(): EventQueue =
- result.currentTopPriority = MIN_VALUE
-
-proc getPriority*(eq: EventQueue; kind: EventKind): int =
- eq.priorities.getOrDefault(kind, priorityOf(kind))
-
-proc setPriority*(eq: var EventQueue; kind: EventKind; p: int) =
- eq.priorities[kind] = p
-
-proc addEvent*(eq: var EventQueue; e: BotEvent) =
- if eq.eventsLen < MAX_QUEUE_SIZE:
- eq.events[eq.eventsLen] = e
- inc eq.eventsLen
-
-proc clear*(eq: var EventQueue) =
- for i in 0 ..< eq.eventsLen:
- eq.events[i].reset # destroy refcounted payloads before len drops to 0
- eq.eventsLen = 0
- eq.currentTopPriority = MIN_VALUE
-
-proc clearEvents*(eq: var EventQueue) =
- clear(eq)
-
-proc removeOldEvents*(eq: var EventQueue; turnNumber: int) =
- var i = 0
- while i < eq.eventsLen:
- if eq.events[i].turnNumber < turnNumber - MAX_EVENTS_AGE and
- not eq.events[i].isCritical:
- for j in i ..< eq.eventsLen - 1:
- eq.events[j] = eq.events[j + 1]
- dec eq.eventsLen
- eq.events[eq.eventsLen].reset
- else:
- inc i
-
-proc popFirst*(eq: var EventQueue): BotEvent =
- ## Remove and return the head element (replaces seq delete(0)).
- if eq.eventsLen == 0: return
- result = eq.events[0]
- for i in 0 ..< eq.eventsLen - 1:
- eq.events[i] = eq.events[i + 1]
- dec eq.eventsLen
- eq.events[eq.eventsLen].reset
-
-proc addCustomEvents*(eq: var EventQueue; turnNumber: int) =
- for c in eq.conditions:
- try:
- if c.test():
- eq.addEvent(BotEvent(kind: ekCustom, turnNumber: turnNumber, condition: c))
- except: discard
-
-proc sortEvents*(eq: var EventQueue) =
- # ponytail: copy priorities table for closure capture (cheap, overrides are rare)
- let prio = eq.priorities
- if eq.eventsLen > 1:
- eq.events.toOpenArray(0, eq.eventsLen - 1).sort(proc(a, b: BotEvent): int =
- let dc = b.isCritical.int - a.isCritical.int
- if dc != 0: return dc
- let dt = a.turnNumber - b.turnNumber
- if dt != 0: return dt
- let pa = prio.getOrDefault(a.kind, priorityOf(a.kind))
- let pb = prio.getOrDefault(b.kind, priorityOf(b.kind))
- pb - pa
- )
-
-proc setInterruptible*(eq: var EventQueue; kind: EventKind; v: bool) =
- if v: eq.interruptible.incl kind
- else: eq.interruptible.excl kind
-
-proc isInterruptible*(eq: EventQueue; kind: EventKind): bool =
- kind in eq.interruptible
-
-proc addCondition*(eq: var EventQueue; c: Condition) =
- eq.conditions.add c
-
-proc removeConditionByName*(eq: var EventQueue; name: string) =
- for i in countdown(eq.conditions.high, 0):
- if eq.conditions[i].name == name:
- eq.conditions.del i
- return
-
-proc getEvents*(eq: EventQueue): seq[BotEvent] =
- for i in 0 ..< eq.eventsLen:
- result.add eq.events[i]
diff --git a/common_libs/tankroyale_botapi/tankroyale_botapi/graphics.nim b/common_libs/tankroyale_botapi/tankroyale_botapi/graphics.nim
deleted file mode 100644
index 1483e41..0000000
--- a/common_libs/tankroyale_botapi/tankroyale_botapi/graphics.nim
+++ /dev/null
@@ -1,108 +0,0 @@
-## SVG debug graphics API for Robocode Tank Royale Nim bot API.
-## Module-level procs write SVG into a buffer that is flushed into
-## BotIntent.debugGraphics each tick and cleared afterward.
-
-import std/strformat
-import ./color
-
-# ---------------------------------------------------------------------------
-# Module-level state (single bot per process)
-# ---------------------------------------------------------------------------
-
-# ponytail: static char array + length instead of a heap string. A fresh bot
-# thread runs each round; a module-level string grown by thread N and cleared
-# ("") by thread N+1 free/reallocs a dead thread's allocator block ->
-# rawDealloc SIGSEGV (same crash class as the event queue seq; gdb-confirmed
-# in drawText mid-campaign). Static storage: no heap block crosses threads.
-const SVG_BUFFER_CAP = 16384
-
-var gSvgLen: int
-var gSvgBuffer: array[SVG_BUFFER_CAP, char]
-var gStrokeColor: Color = WHITE
-var gFillColor: Color = WHITE
-var gStrokeWidth: float = 1.0
-var gFontFamily: string = "Arial" # never rebound at runtime (setFont unused)
-var gFontSize: float = 12.0
-
-proc appendSvg(s: string) =
- ## Append an SVG fragment, dropping anything past the static cap.
- let room = SVG_BUFFER_CAP - gSvgLen
- if room > 0:
- let n = min(room, s.len)
- for i in 0 ..< n: gSvgBuffer[gSvgLen + i] = s[i]
- inc gSvgLen, n
-
-# ---------------------------------------------------------------------------
-# Internal helpers
-# ---------------------------------------------------------------------------
-
-proc svgAttrs(): string =
- ## Current stroke/fill/width as SVG attribute string.
- &"stroke=\"{gStrokeColor.toHex}\" fill=\"{gFillColor.toHex}\" stroke-width=\"{gStrokeWidth}\""
-
-proc svgOutput*(): string =
- ## Returns the SVG fragment for this tick, or "" if nothing was drawn.
- if gSvgLen == 0: return ""
- "" & $gSvgBuffer[0 ..< gSvgLen] & ""
-
-proc clearGraphics*() =
- ## Reset buffer and all style globals to defaults. Called after each tick.
- gSvgLen = 0
- gStrokeColor = WHITE
- gFillColor = WHITE
- gStrokeWidth = 1.0
- gFontFamily = "Arial"
- gFontSize = 12.0
-
-# ---------------------------------------------------------------------------
-# State setters
-# ---------------------------------------------------------------------------
-
-proc setStrokeColor*(c: Color) = gStrokeColor = c
-proc setFillColor*(c: Color) = gFillColor = c
-proc setStrokeWidth*(w: float) = gStrokeWidth = w
-proc setFont*(family: string; size: float) =
- gFontFamily = family
- gFontSize = size
-
-# ---------------------------------------------------------------------------
-# Draw procs — append raw SVG elements
-# ---------------------------------------------------------------------------
-
-proc drawLine*(x1, y1, x2, y2: float) =
- appendSvg(&"")
-
-proc drawRectangle*(x, y, w, h: float) =
- let attrs = &"stroke=\"{gStrokeColor.toHex}\" fill=\"none\" stroke-width=\"{gStrokeWidth}\""
- appendSvg(&"")
-
-proc fillRectangle*(x, y, w, h: float) =
- let attrs = &"stroke=\"none\" fill=\"{gFillColor.toHex}\""
- appendSvg(&"")
-
-proc drawCircle*(x, y, r: float) =
- let attrs = &"stroke=\"{gStrokeColor.toHex}\" fill=\"none\" stroke-width=\"{gStrokeWidth}\""
- appendSvg(&"")
-
-proc fillCircle*(x, y, r: float) =
- let attrs = &"stroke=\"none\" fill=\"{gFillColor.toHex}\""
- appendSvg(&"")
-
-proc drawText*(text: string; x, y: float) =
- appendSvg(&"{text}")
-
-proc drawPolygon*(points: seq[(float, float)]) =
- var pts = ""
- for (px, py) in points:
- if pts.len > 0: pts.add ' '
- pts.add &"{px},{py}"
- let attrs = &"stroke=\"{gStrokeColor.toHex}\" fill=\"none\" stroke-width=\"{gStrokeWidth}\""
- appendSvg(&"")
-
-proc fillPolygon*(points: seq[(float, float)]) =
- var pts = ""
- for (px, py) in points:
- if pts.len > 0: pts.add ' '
- pts.add &"{px},{py}"
- let attrs = &"stroke=\"none\" fill=\"{gFillColor.toHex}\""
- appendSvg(&"")
diff --git a/common_libs/tankroyale_botapi/tankroyale_botapi/json_parse.nim b/common_libs/tankroyale_botapi/tankroyale_botapi/json_parse.nim
deleted file mode 100644
index 9dc896f..0000000
--- a/common_libs/tankroyale_botapi/tankroyale_botapi/json_parse.nim
+++ /dev/null
@@ -1,106 +0,0 @@
-## Safe JSON-to-type parsing for Robocode Tank Royale protocol.
-##
-## Uses the {} accessor (returns nil on missing keys) and typed getters with
-## default values so that optional schema fields never raise KeyError.
-
-import std/json
-import ./schemas
-import ./color
-import ./event_queue
-
-proc parseBulletState*(node: JsonNode): BulletState =
- ## Parse a BulletState from JSON; missing optional fields default to zero.
- if node.isNil: return
- result.bulletId = node{"bulletId"}.getInt(0)
- result.ownerId = node{"ownerId"}.getInt(0)
- result.power = node{"power"}.getFloat(0.0)
- result.x = node{"x"}.getFloat(0.0)
- result.y = node{"y"}.getFloat(0.0)
- result.direction = node{"direction"}.getFloat(0.0)
- let bulletColorStr = node{"color"}.getStr("")
- result.color = if bulletColorStr.len > 0: fromHex(bulletColorStr) else: Color(0)
-
-proc parseBotState*(node: JsonNode): BotState =
- ## Parse a BotState from JSON; optional colour/flag fields default to empty/false.
- if node.isNil: return
- result.isDroid = node{"isDroid"}.getBool(false)
- result.energy = node{"energy"}.getFloat(0.0)
- result.x = node{"x"}.getFloat(0.0)
- result.y = node{"y"}.getFloat(0.0)
- result.direction = node{"direction"}.getFloat(0.0)
- result.gunDirection = node{"gunDirection"}.getFloat(0.0)
- result.radarDirection = node{"radarDirection"}.getFloat(0.0)
- result.radarSweep = node{"radarSweep"}.getFloat(0.0)
- result.speed = node{"speed"}.getFloat(0.0)
- result.turnRate = node{"turnRate"}.getFloat(0.0)
- result.gunTurnRate = node{"gunTurnRate"}.getFloat(0.0)
- result.radarTurnRate = node{"radarTurnRate"}.getFloat(0.0)
- result.gunHeat = node{"gunHeat"}.getFloat(0.0)
- result.enemyCount = node{"enemyCount"}.getInt(0)
- template parseColor(field: untyped) =
- let s = node{astToStr(field)}.getStr("")
- result.field = if s.len > 0: fromHex(s) else: Color(0)
- parseColor(bodyColor)
- parseColor(turretColor)
- parseColor(radarColor)
- parseColor(bulletColor)
- parseColor(scanColor)
- parseColor(tracksColor)
- parseColor(gunColor)
-
-proc parseBotEvent*(node: JsonNode; myId: int): BotEvent =
- ## Parse a JSON event node into a typed BotEvent.
- let typeStr = node{"type"}.getStr
- let tn = node{"turnNumber"}.getInt(0)
- case typeStr
- of "BotDeathEvent":
- let victimId = node{"victimId"}.getInt(0)
- if victimId == myId:
- result = BotEvent(kind: ekDeath, turnNumber: tn,
- death: BotDeathEvent(`type`: typeStr, turnNumber: tn, victimId: victimId))
- else:
- result = BotEvent(kind: ekBotDeath, turnNumber: tn,
- botDeath: BotDeathEvent(`type`: typeStr, turnNumber: tn, victimId: victimId))
- of "BulletFiredEvent":
- result = BotEvent(kind: ekBulletFired, turnNumber: tn,
- bulletFired: BulletFiredEvent(`type`: typeStr, turnNumber: tn,
- bullet: parseBulletState(node{"bullet"})))
- of "BulletHitBotEvent":
- let victimId = node{"victimId"}.getInt(0)
- let bullet = parseBulletState(node{"bullet"})
- let damage = node{"damage"}.getFloat(0.0)
- let energy = node{"energy"}.getFloat(0.0)
- if victimId == myId:
- result = BotEvent(kind: ekHitByBullet, turnNumber: tn,
- hitByBullet: HitByBulletEvent(`type`: "HitByBulletEvent", turnNumber: tn,
- bullet: bullet, damage: damage, energy: energy))
- else:
- result = BotEvent(kind: ekBulletHitBot, turnNumber: tn,
- bulletHitBot: BulletHitBotEvent(`type`: typeStr, turnNumber: tn,
- victimId: victimId, bullet: bullet, damage: damage, energy: energy))
- of "BulletHitBulletEvent":
- result = BotEvent(kind: ekBulletHitBullet, turnNumber: tn,
- bulletHitBullet: BulletHitBulletEvent(`type`: typeStr, turnNumber: tn,
- bullet: parseBulletState(node{"bullet"}),
- hitBullet: parseBulletState(node{"hitBullet"})))
- of "BulletHitWallEvent":
- result = BotEvent(kind: ekBulletHitWall, turnNumber: tn,
- bulletHitWall: BulletHitWallEvent(`type`: typeStr, turnNumber: tn,
- bullet: parseBulletState(node{"bullet"})))
- of "BotHitBotEvent":
- result = BotEvent(kind: ekHitBot, turnNumber: tn,
- hitBot: node.to(BotHitBotEvent))
- of "BotHitWallEvent":
- result = BotEvent(kind: ekHitWall, turnNumber: tn,
- hitWall: node.to(BotHitWallEvent))
- of "ScannedBotEvent":
- result = BotEvent(kind: ekScannedBot, turnNumber: tn,
- scannedBot: node.to(ScannedBotEvent))
- of "WonRoundEvent":
- result = BotEvent(kind: ekWonRound, turnNumber: tn,
- wonRound: WonRoundEvent(`type`: typeStr, turnNumber: tn))
- of "TeamMessageEvent":
- result = BotEvent(kind: ekTeamMessage, turnNumber: tn,
- teamMessage: node.to(TeamMessageEvent))
- else:
- discard
diff --git a/common_libs/tankroyale_botapi/tankroyale_botapi/schemas.nim b/common_libs/tankroyale_botapi/tankroyale_botapi/schemas.nim
deleted file mode 100644
index c2ad48b..0000000
--- a/common_libs/tankroyale_botapi/tankroyale_botapi/schemas.nim
+++ /dev/null
@@ -1,275 +0,0 @@
-## Schema types for Robocode Tank Royale protocol messages.
-## These mirror the YAML schemas in schema/schemas/.
-
-import ./color
-
-type
- # ---- Shared / embedded types -----------------------------------------------
-
- InitialPosition* = object
- x*: float
- y*: float
- direction*: float
-
- GameSetup* = object
- gameType*: string
- arenaWidth*: int
- isArenaWidthLocked*: bool
- arenaHeight*: int
- isArenaHeightLocked*: bool
- minNumberOfParticipants*: int
- isMinNumberOfParticipantsLocked*: bool
- maxNumberOfParticipants*: int
- isMaxNumberOfParticipantsLocked*: bool
- numberOfRounds*: int
- isNumberOfRoundsLocked*: bool
- gunCoolingRate*: float
- isGunCoolingRateLocked*: bool
- maxInactivityTurns*: int
- isMaxInactivityTurnsLocked*: bool
- turnTimeout*: int # microseconds
- isTurnTimeoutLocked*: bool
- readyTimeout*: int # microseconds
- isReadyTimeoutLocked*: bool
- defaultTurnsPerSecond*: int
-
- BotState* = object
- isDroid*: bool
- energy*: float
- x*: float
- y*: float
- direction*: float
- gunDirection*: float
- radarDirection*: float
- radarSweep*: float
- speed*: float
- turnRate*: float
- gunTurnRate*: float
- radarTurnRate*: float
- gunHeat*: float
- enemyCount*: int
- bodyColor*: Color
- turretColor*: Color
- radarColor*: Color
- bulletColor*: Color
- scanColor*: Color
- tracksColor*: Color
- gunColor*: Color
-
- BulletState* = object
- bulletId*: int
- ownerId*: int
- power*: float
- x*: float
- y*: float
- direction*: float
- color*: Color
-
- ResultsForBot* = object
- rank*: int
- survival*: int
- lastSurvivorBonus*: int
- bulletDamage*: int
- bulletKillBonus*: int
- ramDamage*: int
- ramKillBonus*: int
- totalScore*: int
- firstPlaces*: int
- secondPlaces*: int
- thirdPlaces*: int
-
- TeamMessage* = object
- message*: string
- messageType*: string
- receiverId*: int
-
- # ---- Connection event types ------------------------------------------------
-
- ConnectedEvent* = object
- serverUrl*: string
-
- DisconnectedEvent* = object
- serverUrl*: string
- remote*: bool
- statusCode*: int ## 0 when not provided
- reason*: string
-
- ConnectionErrorEvent* = object
- serverUrl*: string
- error*: string
-
- # ---- Server → Bot messages -------------------------------------------------
-
- ServerHandshake* = object
- `type`*: string
- sessionId*: string
- name*: string
- variant*: string
- version*: string
- gameTypes*: seq[string]
-
- GameStartedEventForBot* = object
- `type`*: string
- myId*: int
- startX*: float
- startY*: float
- startDirection*: float
- teammateIds*: seq[int]
- gameSetup*: GameSetup
-
- RoundStartedEvent* = object
- `type`*: string
- roundNumber*: int
-
- RoundEndedEventForBot* = object
- `type`*: string
- turnNumber*: int
- roundNumber*: int
- results*: ResultsForBot
-
- GameEndedEventForBot* = object
- `type`*: string
- numberOfRounds*: int
- results*: ResultsForBot
-
- GameAbortedEvent* = object
- `type`*: string
-
- SkippedTurnEvent* = object
- `type`*: string
- turnNumber*: int
-
- # Events inside TickEventForBot.events
- BotDeathEvent* = object
- `type`*: string
- turnNumber*: int
- victimId*: int
-
- BotHitBotEvent* = object
- `type`*: string
- turnNumber*: int
- victimId*: int
- botId*: int
- energy*: float
- x*: float
- y*: float
- rammed*: bool
-
- BotHitWallEvent* = object
- `type`*: string
- turnNumber*: int
- victimId*: int
-
- BulletFiredEvent* = object
- `type`*: string
- turnNumber*: int
- bullet*: BulletState
-
- BulletHitBotEvent* = object
- `type`*: string
- turnNumber*: int
- victimId*: int
- bullet*: BulletState
- damage*: float
- energy*: float
-
- BulletHitBulletEvent* = object
- `type`*: string
- turnNumber*: int
- bullet*: BulletState
- hitBullet*: BulletState
-
- BulletHitWallEvent* = object
- `type`*: string
- turnNumber*: int
- bullet*: BulletState
-
- HitByBulletEvent* = object
- `type`*: string
- turnNumber*: int
- bullet*: BulletState
- damage*: float
- energy*: float
-
- ScannedBotEvent* = object
- `type`*: string
- turnNumber*: int
- scannedByBotId*: int
- scannedBotId*: int
- energy*: float
- x*: float
- y*: float
- direction*: float
- speed*: float
-
- WonRoundEvent* = object
- `type`*: string
- turnNumber*: int
-
- TeamMessageEvent* = object
- `type`*: string
- turnNumber*: int
- message*: string
- messageType*: string
- senderId*: int
-
- TickEventForBot* = object
- `type`*: string
- turnNumber*: int
- roundNumber*: int
- botState*: BotState
- bulletStates*: seq[BulletState]
- events*: seq[RawEvent] # heterogeneous; decoded by type field
-
- # A raw event with just a type field, for first-pass dispatch
- RawEvent* = object
- `type`*: string
- turnNumber*: int
-
- # ---- Bot → Server messages -------------------------------------------------
-
- BotHandshake* = object
- `type`*: string
- sessionId*: string
- name*: string
- version*: string
- authors*: seq[string]
- description*: string
- homepage*: string
- countryCodes*: seq[string]
- gameTypes*: seq[string]
- platform*: string
- programmingLang*: string
- initialPosition*: InitialPosition
- teamId*: int
- teamName*: string
- teamVersion*: string
- isDroid*: bool
- secret*: string
-
- BotReady* = object
- `type`*: string
-
- BotIntent* = object
- `type`*: string
- turnRate*: float
- gunTurnRate*: float
- radarTurnRate*: float
- targetSpeed*: float
- firepower*: float
- adjustGunForBodyTurn*: bool
- adjustRadarForBodyTurn*: bool
- adjustRadarForGunTurn*: bool
- rescan*: bool
- fireAssist*: bool
- bodyColor*: Color
- turretColor*: Color
- radarColor*: Color
- bulletColor*: Color
- scanColor*: Color
- tracksColor*: Color
- gunColor*: Color
- stdOut*: string
- stdErr*: string
- teamMessages*: seq[TeamMessage]
- debugGraphics*: string
diff --git a/common_libs/tankroyale_botapi/tankroyale_botapi/utils.nim b/common_libs/tankroyale_botapi/tankroyale_botapi/utils.nim
deleted file mode 100644
index 93dbdf8..0000000
--- a/common_libs/tankroyale_botapi/tankroyale_botapi/utils.nim
+++ /dev/null
@@ -1,75 +0,0 @@
-## Math / geometry utilities for Robocode Tank Royale bot API.
-
-import std/math
-import ./constants
-
-proc isNearZero*(value: float): bool {.inline.} =
- abs(value) < 0.00001
-
-proc normalizeRelativeAngle*(angle: float): float =
- ## Normalise angle to [-180, 180).
- result = angle mod 360.0
- if result >= 180.0:
- result -= 360.0
- elif result < -180.0:
- result += 360.0
-
-proc normalizeAbsoluteAngle*(angle: float): float {.inline.} =
- ## Normalise angle to [0, 360).
- (angle mod 360.0 + 360.0) mod 360.0
-
-proc calcDeltaAngle*(targetAngle, sourceAngle: float): float {.inline.} =
- normalizeRelativeAngle(targetAngle - sourceAngle)
-
-proc calcMaxTurnRate*(speed: float): float {.inline.} =
- ## Maximum body turn rate at a given speed.
- MAX_TURN_RATE - 0.75 * abs(speed.clamp(-MAX_SPEED, MAX_SPEED))
-
-proc calcBulletSpeed*(firepower: float): float {.inline.} =
- 20.0 - 3.0 * firepower.clamp(MIN_FIRE_POWER, MAX_FIRE_POWER)
-
-proc calcGunHeat*(firepower: float): float {.inline.} =
- 1.0 + firepower.clamp(MIN_FIRE_POWER, MAX_FIRE_POWER) / 5.0
-
-proc directionTo*(fromX, fromY, toX, toY: float): float {.inline.} =
- normalizeAbsoluteAngle(180.0 * arctan2(toY - fromY, toX - fromX) / PI)
-
-proc distanceTo*(fromX, fromY, toX, toY: float): float {.inline.} =
- let dx = toX - fromX
- let dy = toY - fromY
- sqrt(dx * dx + dy * dy)
-
-proc bearingTo*(fromX, fromY, fromDir, toX, toY: float): float {.inline.} =
- normalizeRelativeAngle(directionTo(fromX, fromY, toX, toY) - fromDir)
-
-# ---- Speed / distance helpers -----------------------------------------------
-
-proc getMaxDeceleration(speed: float): float =
- let decelerationTime = speed / ABS_DECELERATION
- let accelerationTime = 1.0 - decelerationTime
- min(1.0, decelerationTime) * ABS_DECELERATION + max(0.0, accelerationTime) * ACCELERATION
-
-proc getMaxSpeed(distance: float): float =
- let decelerationTime = max(1.0, ceil((sqrt(4.0 * 2.0 / ABS_DECELERATION * distance + 1.0) - 1.0) / 2.0))
- if decelerationTime == Inf: return MAX_SPEED
- let decelerationDistance = (decelerationTime / 2.0) * (decelerationTime - 1.0) * ABS_DECELERATION
- ((decelerationTime - 1.0) * ABS_DECELERATION) + ((distance - decelerationDistance) / decelerationTime)
-
-proc getNewTargetSpeed*(maxSpeed, speed, distance: float): float =
- if distance < 0.0:
- return -getNewTargetSpeed(maxSpeed, -speed, -distance)
- let targetSpeed =
- if distance == Inf: maxSpeed
- else: min(maxSpeed, getMaxSpeed(distance))
- if speed >= 0.0:
- targetSpeed.clamp(speed - ABS_DECELERATION, speed + ACCELERATION)
- else:
- targetSpeed.clamp(speed - ACCELERATION, speed + getMaxDeceleration(-speed))
-
-proc getDistanceTraveledUntilStop*(maxSpeed, speed: float): float =
- var s = abs(speed)
- var dist = 0.0
- while s > 0.0:
- s = getNewTargetSpeed(maxSpeed, s, 0.0)
- dist += s
- dist
diff --git a/common_libs/tankroyale_botapi/tankroyale_botapi/ws_client.nim b/common_libs/tankroyale_botapi/tankroyale_botapi/ws_client.nim
deleted file mode 100644
index 02e9476..0000000
--- a/common_libs/tankroyale_botapi/tankroyale_botapi/ws_client.nim
+++ /dev/null
@@ -1,164 +0,0 @@
-## Minimal synchronous WebSocket client for Robocode Tank Royale bot API.
-## Uses std/net (blocking TCP) so the bot thread model stays simple.
-## Implements only what is needed: connect, send text frame, receive text frame.
-
-import std/[net, base64, random, strutils, uri]
-
-type
- SyncWebSocket* = ref object
- socket*: Socket
- connected*: bool
-
- WebSocketError* = object of IOError
-
-# ---- WebSocket frame helpers -------------------------------------------------
-
-proc genKey(): string =
- var raw = newString(16)
- for i in 0 ..< 16:
- raw[i] = char(rand(255))
- base64.encode(raw)
-
-proc encodeTextFrame*(payload: string, masked: bool = true): string =
- ## Encode a WebSocket text frame (opcode 0x1, FIN=1).
- ## Clients MUST mask frames.
- var frame = ""
- frame.add(char(0x81)) # FIN + opcode=text
-
- let plen = payload.len
- var maskKey: array[4, uint8]
- if masked:
- for i in 0 ..< 4: maskKey[i] = uint8(rand(255))
-
- if plen <= 125:
- frame.add(char(if masked: 0x80 or plen else: plen))
- elif plen <= 65535:
- frame.add(char(if masked: 0x80 or 126 else: 126))
- frame.add(char((plen shr 8) and 0xFF))
- frame.add(char(plen and 0xFF))
- else:
- frame.add(char(if masked: 0x80 or 127 else: 127))
- for shift in [56, 48, 40, 32, 24, 16, 8, 0]:
- frame.add(char((plen shr shift) and 0xFF))
-
- if masked:
- for b in maskKey: frame.add(char(b))
- for i, c in payload:
- frame.add(char(uint8(c) xor maskKey[i mod 4]))
- else:
- frame.add(payload)
-
- result = frame
-
-proc decodeFrame*(socket: Socket): string =
- ## Read one complete WebSocket frame from the socket and return the payload.
- ## Handles text frames; pings are responded to automatically.
- while true:
- var header: array[2, uint8]
- discard socket.recv(addr header[0], 2)
- let fin = (header[0] and 0x80) != 0
- let opcode = header[0] and 0x0F
- let masked = (header[1] and 0x80) != 0
- var plen = int(header[1] and 0x7F)
-
- if plen == 126:
- var ext: array[2, uint8]
- discard socket.recv(addr ext[0], 2)
- plen = int(ext[0]) shl 8 or int(ext[1])
- elif plen == 127:
- var ext: array[8, uint8]
- discard socket.recv(addr ext[0], 8)
- plen = 0
- for b in ext: plen = (plen shl 8) or int(b)
-
- var maskKey: array[4, uint8]
- if masked:
- discard socket.recv(addr maskKey[0], 4)
-
- var payload = newString(plen)
- if plen > 0:
- discard socket.recv(addr payload[0], plen)
-
- if masked:
- for i in 0 ..< plen:
- payload[i] = char(uint8(payload[i]) xor maskKey[i mod 4])
-
- case opcode
- of 0x8: # close
- result = ""
- return
- of 0x9: # ping — send pong
- let pong = char(0x8A) & char(plen) & payload
- socket.send(pong)
- continue
- of 0xA: # pong — ignore
- continue
- else:
- if not fin:
- raise newException(WebSocketError, "Fragmented frames not supported")
- result = payload
- return
-
-# ---- Public API --------------------------------------------------------------
-
-proc newSyncWebSocket*(url: string): SyncWebSocket =
- ## Connect to a WebSocket server. url must be ws:// or wss://.
- randomize()
- let u = parseUri(url)
- let host = u.hostname
- let port = if u.port == "": "7654" else: u.port
- let path = if u.path == "": "/" else: u.path
-
- let sock = newSocket()
- sock.connect(host, Port(parseInt(port)))
-
- # HTTP upgrade handshake
- let key = genKey()
- let request = "GET " & path & " HTTP/1.1\r\n" &
- "Host: " & host & ":" & port & "\r\n" &
- "Upgrade: websocket\r\n" &
- "Connection: Upgrade\r\n" &
- "Sec-WebSocket-Key: " & key & "\r\n" &
- "Sec-WebSocket-Version: 13\r\n\r\n"
- sock.send(request)
-
- # Read HTTP 101 response
- var line = ""
- var upgraded = false
- while true:
- line = sock.recvLine()
- if line == "\r\n" or line == "":
- break
- if line.startsWith("HTTP/1.1 101"):
- upgraded = true
-
- if not upgraded:
- raise newException(WebSocketError, "WebSocket upgrade failed for " & url)
-
- result = SyncWebSocket(socket: sock, connected: true)
-
-proc send*(ws: SyncWebSocket, text: string) =
- ## Send a text message (masked, as required for clients).
- if not ws.connected:
- raise newException(WebSocketError, "WebSocket is not connected")
- let frame = encodeTextFrame(text, masked = true)
- ws.socket.send(frame)
-
-proc receive*(ws: SyncWebSocket): string =
- ## Block until a text message arrives. Returns "" on close.
- if not ws.connected:
- return ""
- try:
- result = decodeFrame(ws.socket)
- except Exception as e:
- ws.connected = false
- raise newException(WebSocketError, "Receive failed: " & e.msg)
-
-proc close*(ws: SyncWebSocket) =
- if ws.connected:
- ws.connected = false
- try:
- # Send close frame
- ws.socket.send(char(0x88) & char(0))
- except: discard
- ws.socket.close()