From 06e68522cf19b36efeac40c7f813d27b837129fe Mon Sep 17 00:00:00 2001 From: Davide Cappellini Date: Thu, 27 Aug 2026 18:45:56 +0200 Subject: [PATCH] Remove deprecated components from Tank Royale Bot API - Deleted constants.nim, event_queue.nim, graphics.nim, json_parse.nim, schemas.nim, utils.nim, and ws_client.nim files. - This cleanup removes unused code and simplifies the codebase, focusing on essential functionalities. --- .../tankroyale_botapi/tankroyale_botapi.nim | 260 ----- .../tankroyale_botapi.nimble | 11 - .../tankroyale_botapi/bot.nim | 1026 ----------------- .../tankroyale_botapi/bot_info.nim | 79 -- .../tankroyale_botapi/color.nim | 205 ---- .../tankroyale_botapi/constants.nim | 44 - .../tankroyale_botapi/event_queue.nim | 167 --- .../tankroyale_botapi/graphics.nim | 108 -- .../tankroyale_botapi/json_parse.nim | 106 -- .../tankroyale_botapi/schemas.nim | 275 ----- .../tankroyale_botapi/utils.nim | 75 -- .../tankroyale_botapi/ws_client.nim | 164 --- 12 files changed, 2520 deletions(-) delete mode 100644 common_libs/tankroyale_botapi/tankroyale_botapi.nim delete mode 100644 common_libs/tankroyale_botapi/tankroyale_botapi.nimble delete mode 100644 common_libs/tankroyale_botapi/tankroyale_botapi/bot.nim delete mode 100644 common_libs/tankroyale_botapi/tankroyale_botapi/bot_info.nim delete mode 100644 common_libs/tankroyale_botapi/tankroyale_botapi/color.nim delete mode 100644 common_libs/tankroyale_botapi/tankroyale_botapi/constants.nim delete mode 100644 common_libs/tankroyale_botapi/tankroyale_botapi/event_queue.nim delete mode 100644 common_libs/tankroyale_botapi/tankroyale_botapi/graphics.nim delete mode 100644 common_libs/tankroyale_botapi/tankroyale_botapi/json_parse.nim delete mode 100644 common_libs/tankroyale_botapi/tankroyale_botapi/schemas.nim delete mode 100644 common_libs/tankroyale_botapi/tankroyale_botapi/utils.nim delete mode 100644 common_libs/tankroyale_botapi/tankroyale_botapi/ws_client.nim 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()