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