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.
This commit is contained in:
@@ -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()
|
|
||||||
@@ -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"
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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"
|
|
||||||
@@ -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)
|
|
||||||
@@ -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
|
|
||||||
@@ -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]
|
|
||||||
@@ -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 ""
|
|
||||||
"<g>" & $gSvgBuffer[0 ..< gSvgLen] & "</g>"
|
|
||||||
|
|
||||||
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(&"<line x1=\"{x1}\" y1=\"{y1}\" x2=\"{x2}\" y2=\"{y2}\" {svgAttrs()}/>")
|
|
||||||
|
|
||||||
proc drawRectangle*(x, y, w, h: float) =
|
|
||||||
let attrs = &"stroke=\"{gStrokeColor.toHex}\" fill=\"none\" stroke-width=\"{gStrokeWidth}\""
|
|
||||||
appendSvg(&"<rect x=\"{x}\" y=\"{y}\" width=\"{w}\" height=\"{h}\" {attrs}/>")
|
|
||||||
|
|
||||||
proc fillRectangle*(x, y, w, h: float) =
|
|
||||||
let attrs = &"stroke=\"none\" fill=\"{gFillColor.toHex}\""
|
|
||||||
appendSvg(&"<rect x=\"{x}\" y=\"{y}\" width=\"{w}\" height=\"{h}\" {attrs}/>")
|
|
||||||
|
|
||||||
proc drawCircle*(x, y, r: float) =
|
|
||||||
let attrs = &"stroke=\"{gStrokeColor.toHex}\" fill=\"none\" stroke-width=\"{gStrokeWidth}\""
|
|
||||||
appendSvg(&"<circle cx=\"{x}\" cy=\"{y}\" r=\"{r}\" {attrs}/>")
|
|
||||||
|
|
||||||
proc fillCircle*(x, y, r: float) =
|
|
||||||
let attrs = &"stroke=\"none\" fill=\"{gFillColor.toHex}\""
|
|
||||||
appendSvg(&"<circle cx=\"{x}\" cy=\"{y}\" r=\"{r}\" {attrs}/>")
|
|
||||||
|
|
||||||
proc drawText*(text: string; x, y: float) =
|
|
||||||
appendSvg(&"<text x=\"{x}\" y=\"{y}\" font-family=\"{gFontFamily}\" font-size=\"{gFontSize}\">{text}</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(&"<polygon points=\"{pts}\" {attrs}/>")
|
|
||||||
|
|
||||||
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(&"<polygon points=\"{pts}\" {attrs}/>")
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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()
|
|
||||||
Reference in New Issue
Block a user