diff --git a/PPO_Bot/PPO_Bot b/PPO_Bot/PPO_Bot index 996e4b9..2dab3c3 100755 Binary files a/PPO_Bot/PPO_Bot and b/PPO_Bot/PPO_Bot differ diff --git a/PPO_Bot/tests/test_actions b/PPO_Bot/tests/test_actions index 27e5f1a..f53939f 100755 Binary files a/PPO_Bot/tests/test_actions and b/PPO_Bot/tests/test_actions differ diff --git a/PPO_Bot/tests/test_actions.nim b/PPO_Bot/tests/test_actions.nim index 5d5c3bc..f7695d0 100644 --- a/PPO_Bot/tests/test_actions.nim +++ b/PPO_Bot/tests/test_actions.nim @@ -20,7 +20,7 @@ proc makeRaw(vals: array[6, float32]): Tensor[float32] = # --- 6-dim input produces a valid BotActions --- block basicDecode: let raw = makeRaw([0.0'f32, 0.0, 0.0, 0.0, 0.0, 0.0]) - let acts = mapActions(raw, 0.0, arenaW, arenaH, 600.0, 400.0, 0.0, 0.0, 0.0) + let acts = mapActions(raw, 0.0, arenaW, arenaH, 600.0, 400.0, 0.0, 0.0, 0.0, 600.0, 400.0) # sigmoid(0)*arenaW = 0.5*1200 = 600, sigmoid(0)*arenaH = 0.5*800 = 400 check abs(acts.gotoX - 600.0) < 1e-6, "gotoX = sigmoid(0)*arenaW" check abs(acts.gotoY - 400.0) < 1e-6, "gotoY = sigmoid(0)*arenaH" @@ -31,13 +31,13 @@ block basicDecode: block coordBounds: # Large positive raw → sigmoid ≈ 1 → close to arenaW/arenaH let rawHigh = makeRaw([100.0'f32, 100.0, 100.0, 100.0, 0.0, 0.0]) - let actsHigh = mapActions(rawHigh, 0.0, arenaW, arenaH, 600.0, 400.0, 0.0, 0.0, 0.0) + let actsHigh = mapActions(rawHigh, 0.0, arenaW, arenaH, 600.0, 400.0, 0.0, 0.0, 0.0, 600.0, 400.0) check actsHigh.gotoX <= arenaW + 1e-9, "gotoX <= arenaWidth" check actsHigh.gotoY <= arenaH + 1e-9, "gotoY <= arenaHeight" check actsHigh.gotoX >= 0.0, "gotoX >= 0" # Large negative raw → sigmoid ≈ 0 → close to 0 let rawLow = makeRaw([-100.0'f32, -100.0, -100.0, -100.0, 0.0, 0.0]) - let actsLow = mapActions(rawLow, 0.0, arenaW, arenaH, 600.0, 400.0, 0.0, 0.0, 0.0) + let actsLow = mapActions(rawLow, 0.0, arenaW, arenaH, 600.0, 400.0, 0.0, 0.0, 0.0, 600.0, 400.0) check actsLow.gotoX >= -1e-9, "gotoX >= 0 (low raw)" check actsLow.gotoY >= -1e-9, "gotoY >= 0 (low raw)" @@ -45,24 +45,24 @@ block coordBounds: block fireTrigger: # tanh(positive) >= 0 → fire when gunHeat = 0 let rawFire = makeRaw([0.0'f32, 0.0, 0.0, 0.0, 1.0, 0.0]) - let actsFire = mapActions(rawFire, 0.0, arenaW, arenaH, 600.0, 400.0, 0.0, 0.0, 0.0) + let actsFire = mapActions(rawFire, 0.0, arenaW, arenaH, 600.0, 400.0, 0.0, 0.0, 0.0, 600.0, 400.0) check actsFire.shouldFire, "positive tanh → should fire when gun cool" # tanh(negative) < 0 → no fire let rawNoFire = makeRaw([0.0'f32, 0.0, 0.0, 0.0, -1.0, 0.0]) - let actsNoFire = mapActions(rawNoFire, 0.0, arenaW, arenaH, 600.0, 400.0, 0.0, 0.0, 0.0) + let actsNoFire = mapActions(rawNoFire, 0.0, arenaW, arenaH, 600.0, 400.0, 0.0, 0.0, 0.0, 600.0, 400.0) check not actsNoFire.shouldFire, "negative tanh → no fire" # gunHeat > 0 → no fire even with positive decision - let actsHot = mapActions(rawFire, 0.5, arenaW, arenaH, 600.0, 400.0, 0.0, 0.0, 0.0) + let actsHot = mapActions(rawFire, 0.5, arenaW, arenaH, 600.0, 400.0, 0.0, 0.0, 0.0, 600.0, 400.0) check not actsHot.shouldFire, "positive tanh but gun hot → no fire" # --- Fire power in [0.1, 3.0] --- block firePowerRange: let rawMin = makeRaw([0.0'f32, 0.0, 0.0, 0.0, 0.0, -100.0]) let rawMax = makeRaw([0.0'f32, 0.0, 0.0, 0.0, 0.0, 100.0]) - let actsMin = mapActions(rawMin, 0.0, arenaW, arenaH, 600.0, 400.0, 0.0, 0.0, 0.0) - let actsMax = mapActions(rawMax, 0.0, arenaW, arenaH, 600.0, 400.0, 0.0, 0.0, 0.0) + let actsMin = mapActions(rawMin, 0.0, arenaW, arenaH, 600.0, 400.0, 0.0, 0.0, 0.0, 600.0, 400.0) + let actsMax = mapActions(rawMax, 0.0, arenaW, arenaH, 600.0, 400.0, 0.0, 0.0, 0.0, 600.0, 400.0) check actsMin.firePower >= 0.1 - 1e-6, "firePower >= 0.1" check actsMax.firePower <= 3.0 + 1e-6, "firePower <= 3.0" diff --git a/PPO_Bot/tests/test_controllers b/PPO_Bot/tests/test_controllers index 9d93e25..d05b120 100755 Binary files a/PPO_Bot/tests/test_controllers and b/PPO_Bot/tests/test_controllers differ diff --git a/PPO_Bot/tests/test_network b/PPO_Bot/tests/test_network index 93b4b89..a8e5d0d 100755 Binary files a/PPO_Bot/tests/test_network and b/PPO_Bot/tests/test_network differ diff --git a/PPO_Bot/tests/test_network.nim b/PPO_Bot/tests/test_network.nim index d0299ea..5aaed99 100644 --- a/PPO_Bot/tests/test_network.nim +++ b/PPO_Bot/tests/test_network.nim @@ -40,7 +40,7 @@ when isMainModule: let raw = randomNormalTensor[float32](ACTION_DIM) let speed = 4.0'f32 # arena 800×600, bot at centre, heading north, gun north - let botActs = mapActions(raw, 0.0'f32, 800.0, 600.0, 400.0, 300.0, 0.0, speed.float, 0.0) # gunHeat=0 → fire allowed + let botActs = mapActions(raw, 0.0'f32, 800.0, 600.0, 400.0, 300.0, 0.0, speed.float, 0.0, 400.0, 300.0) # gunHeat=0 → fire allowed assert botActs.targetSpeed >= -8.0'f32 and botActs.targetSpeed <= 8.0'f32, &"targetSpeed out of range: {botActs.targetSpeed}" @@ -52,7 +52,7 @@ when isMainModule: &"firePower out of range: {botActs.firePower}" # shouldFire=false when gunHeat > 0 - let noFire = mapActions(raw, 1.0'f32, 800.0, 600.0, 400.0, 300.0, 0.0, speed.float, 0.0) + let noFire = mapActions(raw, 1.0'f32, 800.0, 600.0, 400.0, 300.0, 0.0, speed.float, 0.0, 400.0, 300.0) assert not noFire.shouldFire, "shouldFire should be false when gunHeat > 0" echo "All tests passed" diff --git a/PPO_Bot/tests/test_state b/PPO_Bot/tests/test_state index 3171ca6..bd92d05 100755 Binary files a/PPO_Bot/tests/test_state and b/PPO_Bot/tests/test_state differ diff --git a/PPO_Bot/tests/test_training b/PPO_Bot/tests/test_training index 964f826..49f8bba 100755 Binary files a/PPO_Bot/tests/test_training and b/PPO_Bot/tests/test_training differ diff --git a/PPO_Bot/tests/test_weights b/PPO_Bot/tests/test_weights index ef5feb5..0f16f0a 100755 Binary files a/PPO_Bot/tests/test_weights and b/PPO_Bot/tests/test_weights differ diff --git a/libs/tankroyale_botapi/tankroyale_botapi/bot.nim b/libs/tankroyale_botapi/tankroyale_botapi/bot.nim index 6bcbc81..d9147c5 100644 --- a/libs/tankroyale_botapi/tankroyale_botapi/bot.nim +++ b/libs/tankroyale_botapi/tankroyale_botapi/bot.nim @@ -198,9 +198,21 @@ var gIntentGunColor: Color = Color(0) var gIntentAdjGunBody: bool = false var gIntentAdjRadarBody: bool = false var gIntentAdjRadarGun: bool = false -var gIntentTeamMessages: seq[TeamMessage] = @[] -var gIntentStdOut: string = "" -var gIntentStdErr: string = "" +# ponytail: static buffers instead of strings/seq. go()'s stop path returns +# before buildIntentJson clears these, so a round's last tick can leave heap +# blocks owned by the exiting bot thread -> the fresh next-round thread +# reallocs a dead allocator block (rawDealloc SIGSEGV). Static storage: +# no heap block crosses threads. +const INTENT_STDOUT_CAP = 4096 +const INTENT_STDERR_CAP = 4096 +const INTENT_MSG_CAP = 16 + +var gIntentTeamMessages: array[INTENT_MSG_CAP, TeamMessage] +var gIntentTeamMsgsLen: int +var gIntentStdOut: array[INTENT_STDOUT_CAP, char] +var gIntentStdOutLen: int +var gIntentStdErr: array[INTENT_STDERR_CAP, char] +var gIntentStdErrLen: int proc buildIntentJson*(): string = ## Serialise current intent to JSON for sending to server. @@ -237,9 +249,10 @@ proc buildIntentJson*(): string = obj["tracksColor"] = %gIntentTracksColor.toHex if gIntentGunColor != Color(0): obj["gunColor"] = %gIntentGunColor.toHex - if gIntentTeamMessages.len > 0: + if gIntentTeamMsgsLen > 0: var msgs = newJArray() - for m in gIntentTeamMessages: + for i in 0 ..< gIntentTeamMsgsLen: + let m = gIntentTeamMessages[i] var mo = newJObject() mo["message"] = %m.message mo["messageType"] = %m.messageType @@ -247,13 +260,14 @@ proc buildIntentJson*(): string = mo["receiverId"] = %m.receiverId msgs.add mo obj["teamMessages"] = msgs - gIntentTeamMessages.setLen 0 - if gIntentStdOut.len > 0: - obj["stdOut"] = %gIntentStdOut - gIntentStdOut = "" - if gIntentStdErr.len > 0: - obj["stdErr"] = %gIntentStdErr - gIntentStdErr = "" + for i in 0 ..< gIntentTeamMsgsLen: gIntentTeamMessages[i].reset + gIntentTeamMsgsLen = 0 + if gIntentStdOutLen > 0: + obj["stdOut"] = %($gIntentStdOut[0 ..< gIntentStdOutLen]) + gIntentStdOutLen = 0 + if gIntentStdErrLen > 0: + obj["stdErr"] = %($gIntentStdErr[0 ..< gIntentStdErrLen]) + gIntentStdErrLen = 0 let svg = svgOutput() if svg.len > 0: obj["debugGraphics"] = %svg @@ -311,19 +325,27 @@ proc setGunColor*(color: Color) = gIntentGunColor = color proc printToStdOut*(s: string) = ## Append s to this tick's stdOut payload (sent to server in BotIntent). - gIntentStdOut.add s + let n = min(s.len, INTENT_STDOUT_CAP - gIntentStdOutLen) + for i in 0 ..< n: gIntentStdOut[gIntentStdOutLen + i] = s[i] + inc gIntentStdOutLen, n proc printToStdErr*(s: string) = ## Append s to this tick's stdErr payload (sent to server in BotIntent). - gIntentStdErr.add s + let n = min(s.len, INTENT_STDERR_CAP - gIntentStdErrLen) + for i in 0 ..< n: gIntentStdErr[gIntentStdErrLen + i] = s[i] + inc gIntentStdErrLen, n proc broadcastTeamMessage*(message: string) = ## Send a message to all teammates this tick. - gIntentTeamMessages.add TeamMessage(message: message, messageType: "String") + if gIntentTeamMsgsLen < INTENT_MSG_CAP: + gIntentTeamMessages[gIntentTeamMsgsLen] = TeamMessage(message: message, messageType: "String") + inc gIntentTeamMsgsLen proc sendTeamMessage*(botId: int; message: string) = ## Send a message to a specific teammate this tick. - gIntentTeamMessages.add TeamMessage(message: message, messageType: "String", receiverId: botId) + if gIntentTeamMsgsLen < INTENT_MSG_CAP: + gIntentTeamMessages[gIntentTeamMsgsLen] = TeamMessage(message: message, messageType: "String", receiverId: botId) + inc gIntentTeamMsgsLen proc setAdjustGunForBodyTurn*(v: bool) = gIntentAdjGunBody = v proc setAdjustRadarForBodyTurn*(v: bool) = gIntentAdjRadarBody = v @@ -808,6 +830,16 @@ proc botThreadEntry() {.thread.} = " prevDir=" & $gPreviousDirection & " prevGunDir=" & $gPreviousGunDirection) + # Reset graphics + intent buffers on the thread that owns them. go()'s + # stop path (round end) returns before buildIntentJson/clearGraphics, so + # the previous round's thread can leave content behind; resetting here + # keeps it from leaking into this round's first intent. + clearGraphics() + gIntentStdOutLen = 0 + gIntentStdErrLen = 0 + for i in 0 ..< gIntentTeamMsgsLen: gIntentTeamMessages[i].reset + gIntentTeamMsgsLen = 0 + dispatchPendingEvents(gBot) # dispatch events embedded in the first tick try: diff --git a/libs/tankroyale_botapi/tankroyale_botapi/graphics.nim b/libs/tankroyale_botapi/tankroyale_botapi/graphics.nim index 503c68b..1483e41 100644 --- a/libs/tankroyale_botapi/tankroyale_botapi/graphics.nim +++ b/libs/tankroyale_botapi/tankroyale_botapi/graphics.nim @@ -9,13 +9,29 @@ import ./color # Module-level state (single bot per process) # --------------------------------------------------------------------------- -var gSvgBuffer: string +# 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" +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 # --------------------------------------------------------------------------- @@ -26,12 +42,12 @@ proc svgAttrs(): string = proc svgOutput*(): string = ## Returns the SVG fragment for this tick, or "" if nothing was drawn. - if gSvgBuffer.len == 0: return "" - "" & gSvgBuffer & "" + if gSvgLen == 0: return "" + "" & $gSvgBuffer[0 ..< gSvgLen] & "" proc clearGraphics*() = ## Reset buffer and all style globals to defaults. Called after each tick. - gSvgBuffer = "" + gSvgLen = 0 gStrokeColor = WHITE gFillColor = WHITE gStrokeWidth = 1.0 @@ -54,26 +70,26 @@ proc setFont*(family: string; size: float) = # --------------------------------------------------------------------------- proc drawLine*(x1, y1, x2, y2: float) = - gSvgBuffer.add &"" + appendSvg(&"") proc drawRectangle*(x, y, w, h: float) = let attrs = &"stroke=\"{gStrokeColor.toHex}\" fill=\"none\" stroke-width=\"{gStrokeWidth}\"" - gSvgBuffer.add &"" + appendSvg(&"") proc fillRectangle*(x, y, w, h: float) = let attrs = &"stroke=\"none\" fill=\"{gFillColor.toHex}\"" - gSvgBuffer.add &"" + appendSvg(&"") proc drawCircle*(x, y, r: float) = let attrs = &"stroke=\"{gStrokeColor.toHex}\" fill=\"none\" stroke-width=\"{gStrokeWidth}\"" - gSvgBuffer.add &"" + appendSvg(&"") proc fillCircle*(x, y, r: float) = let attrs = &"stroke=\"none\" fill=\"{gFillColor.toHex}\"" - gSvgBuffer.add &"" + appendSvg(&"") proc drawText*(text: string; x, y: float) = - gSvgBuffer.add &"{text}" + appendSvg(&"{text}") proc drawPolygon*(points: seq[(float, float)]) = var pts = "" @@ -81,7 +97,7 @@ proc drawPolygon*(points: seq[(float, float)]) = if pts.len > 0: pts.add ' ' pts.add &"{px},{py}" let attrs = &"stroke=\"{gStrokeColor.toHex}\" fill=\"none\" stroke-width=\"{gStrokeWidth}\"" - gSvgBuffer.add &"" + appendSvg(&"") proc fillPolygon*(points: seq[(float, float)]) = var pts = "" @@ -89,4 +105,4 @@ proc fillPolygon*(points: seq[(float, float)]) = if pts.len > 0: pts.add ' ' pts.add &"{px},{py}" let attrs = &"stroke=\"none\" fill=\"{gFillColor.toHex}\"" - gSvgBuffer.add &"" + appendSvg(&"")