feat(gun_harness): per-gun REAL hit attribution + bestPower cold-start fix
Attribution is proven, not guessed: the server assigns a per-round-unique bulletId (GunEngine.nextBulletId) and stamps the same id on BulletFired, BulletHitBot, BulletHitWall and BulletHitBullet. Keep a FIFO of fired gun ids, stamp bulletId -> gunId on onBulletFired, resolve through that map. Hits are deferred when onBulletHit precedes onBulletFired in the same turn (client dispatches priority 70 > 60), which recovered 14 unattributed hits. 99.9% of shots and 99.8% of hits attributed. Stats lines now carry per-gun realShots/realHits/realHitRate; the old keys and round-level totals are unchanged. bestPower: a gun with zero observations in every bin previously returned the HIGHEST bin (power 3.0) because an empty bin satisfied the 'count == 0' clause on the first countdown iteration. Cold guns now return the lowest bin as the docstring always claimed. Warm-gun path untouched.
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
## Radar: RadarLockModule (1v1) / MeleeScanModule (2+ enemies), auto-switched per tick.
|
## Radar: RadarLockModule (1v1) / MeleeScanModule (2+ enemies), auto-switched per tick.
|
||||||
## Movement: OscillatorModule (perpendicular strafing).
|
## Movement: OscillatorModule (perpendicular strafing).
|
||||||
|
|
||||||
import std/[math, os, strformat, tables, json]
|
import std/[math, os, strformat, tables, sets, json]
|
||||||
import robocode_tankroyale_botapi
|
import robocode_tankroyale_botapi
|
||||||
import radar_harness/radar_interface
|
import radar_harness/radar_interface
|
||||||
import radars/radar_lock_module
|
import radars/radar_lock_module
|
||||||
@@ -90,9 +90,39 @@ type
|
|||||||
roundNumber: int
|
roundNumber: int
|
||||||
realShotsFired: int
|
realShotsFired: int
|
||||||
realHits: int
|
realHits: int
|
||||||
|
gunRealShots: array[13, int]
|
||||||
|
gunRealHits: array[13, int]
|
||||||
|
pendingFireGuns: seq[int] ## FIFO of fired guns awaiting onBulletFired bulletId stamp
|
||||||
|
bulletGun: Table[int, int] ## bulletId -> gun id, filled on onBulletFired, drained on resolution
|
||||||
|
pendingHitBullets: HashSet[int] ## hit bulletIds seen before their onBulletFired stamp
|
||||||
gunSelectionCount: array[13, int]
|
gunSelectionCount: array[13, int]
|
||||||
lastKnownTargetId: int ## persists through death, used for round-end stats
|
lastKnownTargetId: int ## persists through death, used for round-end stats
|
||||||
|
|
||||||
|
proc registerOwnBullet(bot: ModularBot, bulletId: int) =
|
||||||
|
## onBulletFired: the server assigns a unique bulletId per round. Stamp the
|
||||||
|
## oldest pending fire with it, then credit that gun with a real shot.
|
||||||
|
## The FIFO stays aligned because we only enqueue when setFire() succeeds
|
||||||
|
## (server "bot.energy <= firepower" rejection is pre-empted by the energy guard).
|
||||||
|
if bot.pendingFireGuns.len == 0: return # unexpected: no pending fire to stamp
|
||||||
|
let gunId = bot.pendingFireGuns[0]
|
||||||
|
delete(bot.pendingFireGuns, 0)
|
||||||
|
bot.bulletGun[bulletId] = gunId
|
||||||
|
inc bot.gunRealShots[gunId]
|
||||||
|
# Same-turn fire+hit: the client sorts events by (turn asc, priority desc) and
|
||||||
|
# BulletHitBot (70) outranks BulletFired (60), so onBulletHit may have run first.
|
||||||
|
# If so, credit the hit now and drop the (already resolved) map entry.
|
||||||
|
if bulletId in bot.pendingHitBullets:
|
||||||
|
inc bot.gunRealHits[gunId]
|
||||||
|
bot.pendingHitBullets.excl(bulletId)
|
||||||
|
bot.bulletGun.del(bulletId)
|
||||||
|
|
||||||
|
proc resolveOwnBullet(bot: ModularBot, bulletId: int): int =
|
||||||
|
## Look up the gun that fired bulletId and drop it from the in-flight map.
|
||||||
|
## Returns the gun id, or -1 if the bullet was not attributed.
|
||||||
|
result = bot.bulletGun.getOrDefault(bulletId, -1)
|
||||||
|
if result >= 0:
|
||||||
|
bot.bulletGun.del(bulletId)
|
||||||
|
|
||||||
proc printConfig(bot: ModularBot, changed: string = "") =
|
proc printConfig(bot: ModularBot, changed: string = "") =
|
||||||
let gunName = GunNames[bot.currentGun]
|
let gunName = GunNames[bot.currentGun]
|
||||||
let radarName = if bot.radarMode == 0: "radar_lock" else: "melee_scan"
|
let radarName = if bot.radarMode == 0: "radar_lock" else: "melee_scan"
|
||||||
@@ -140,18 +170,25 @@ method onScannedBot*(bot: ModularBot, e: ScannedBotEvent) =
|
|||||||
|
|
||||||
method onBulletFired*(bot: ModularBot, e: BulletFiredEvent) =
|
method onBulletFired*(bot: ModularBot, e: BulletFiredEvent) =
|
||||||
inc bot.realShotsFired
|
inc bot.realShotsFired
|
||||||
|
bot.registerOwnBullet(e.bullet.bulletId)
|
||||||
bot.moveTracker.shadows.addBullet(e.bullet.x, e.bullet.y,
|
bot.moveTracker.shadows.addBullet(e.bullet.x, e.bullet.y,
|
||||||
degToRad(e.bullet.direction), e.bullet.power)
|
degToRad(e.bullet.direction), e.bullet.power)
|
||||||
|
|
||||||
method onBulletHit*(bot: ModularBot, e: BulletHitBotEvent) =
|
method onBulletHit*(bot: ModularBot, e: BulletHitBotEvent) =
|
||||||
inc bot.realHits
|
inc bot.realHits
|
||||||
|
# onBulletHit only fires for our own bullets (victimId != myId); see json_parse.nim.
|
||||||
|
let gunId = bot.resolveOwnBullet(e.bullet.bulletId)
|
||||||
|
if gunId >= 0: inc bot.gunRealHits[gunId]
|
||||||
|
else: bot.pendingHitBullets.incl(e.bullet.bulletId) # defer until onBulletFired
|
||||||
bot.moveTracker.shadows.removeBulletNear(e.bullet.x, e.bullet.y)
|
bot.moveTracker.shadows.removeBulletNear(e.bullet.x, e.bullet.y)
|
||||||
|
|
||||||
method onBulletHitWall*(bot: ModularBot, e: BulletHitWallEvent) =
|
method onBulletHitWall*(bot: ModularBot, e: BulletHitWallEvent) =
|
||||||
|
discard bot.resolveOwnBullet(e.bullet.bulletId) # miss: free the attribution slot
|
||||||
bot.moveTracker.shadows.removeBulletNear(e.bullet.x, e.bullet.y)
|
bot.moveTracker.shadows.removeBulletNear(e.bullet.x, e.bullet.y)
|
||||||
bot.mover.removeBulletNear(e.bullet.x, e.bullet.y)
|
bot.mover.removeBulletNear(e.bullet.x, e.bullet.y)
|
||||||
|
|
||||||
method onBulletHitBullet*(bot: ModularBot, e: BulletHitBulletEvent) =
|
method onBulletHitBullet*(bot: ModularBot, e: BulletHitBulletEvent) =
|
||||||
|
discard bot.resolveOwnBullet(e.bullet.bulletId) # bullet-vs-bullet: free the slot
|
||||||
bot.moveTracker.shadows.removeBulletNear(e.bullet.x, e.bullet.y)
|
bot.moveTracker.shadows.removeBulletNear(e.bullet.x, e.bullet.y)
|
||||||
bot.mover.removeBulletNear(e.bullet.x, e.bullet.y)
|
bot.mover.removeBulletNear(e.bullet.x, e.bullet.y)
|
||||||
|
|
||||||
@@ -192,13 +229,19 @@ method onRoundEnded*(bot: ModularBot, e: RoundEndedEventForBot) =
|
|||||||
for k in 0..<n:
|
for k in 0..<n:
|
||||||
if fw.hits[k]: inc totalHits
|
if fw.hits[k]: inc totalHits
|
||||||
let hitRate = if totalShots > 0: totalHits.float / totalShots.float else: 0.0
|
let hitRate = if totalShots > 0: totalHits.float / totalShots.float else: 0.0
|
||||||
|
let rShots = bot.gunRealShots[gid]
|
||||||
|
let rHits = bot.gunRealHits[gid]
|
||||||
|
let rRate = if rShots > 0: rHits.float * 100.0 / rShots.float else: 0.0
|
||||||
gunsArr.add(%*{
|
gunsArr.add(%*{
|
||||||
"id": gid,
|
"id": gid,
|
||||||
"name": GunNames[gid],
|
"name": GunNames[gid],
|
||||||
"vShots": totalShots,
|
"vShots": totalShots,
|
||||||
"vHits": totalHits,
|
"vHits": totalHits,
|
||||||
"hitRate": (hitRate * 100.0).int,
|
"hitRate": (hitRate * 100.0).int,
|
||||||
"selected": bot.gunSelectionCount[gid]
|
"selected": bot.gunSelectionCount[gid],
|
||||||
|
"realShots": rShots,
|
||||||
|
"realHits": rHits,
|
||||||
|
"realHitRate": rRate.round(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
let row = %*{
|
let row = %*{
|
||||||
@@ -219,7 +262,13 @@ method onRoundStarted*(bot: ModularBot, e: RoundStartedEvent) =
|
|||||||
bot.realShotsFired = 0
|
bot.realShotsFired = 0
|
||||||
bot.realHits = 0
|
bot.realHits = 0
|
||||||
bot.lastKnownTargetId = -1
|
bot.lastKnownTargetId = -1
|
||||||
for i in 0..<13: bot.gunSelectionCount[i] = 0
|
bot.pendingFireGuns.setLen(0)
|
||||||
|
bot.bulletGun.clear()
|
||||||
|
bot.pendingHitBullets.clear()
|
||||||
|
for i in 0..<13:
|
||||||
|
bot.gunSelectionCount[i] = 0
|
||||||
|
bot.gunRealShots[i] = 0
|
||||||
|
bot.gunRealHits[i] = 0
|
||||||
setAdjustGunForBodyTurn(true)
|
setAdjustGunForBodyTurn(true)
|
||||||
setAdjustRadarForBodyTurn(true)
|
setAdjustRadarForBodyTurn(true)
|
||||||
setAdjustRadarForGunTurn(true)
|
setAdjustRadarForGunTurn(true)
|
||||||
@@ -523,7 +572,10 @@ method run*(bot: ModularBot) =
|
|||||||
elif normDelta < -180.0: normDelta += 360.0
|
elif normDelta < -180.0: normDelta += 360.0
|
||||||
|
|
||||||
if shouldFire(gunDir, aimTarget, gunHeat):
|
if shouldFire(gunDir, aimTarget, gunHeat):
|
||||||
discard setFire(power)
|
# Enqueue the selected gun so onBulletFired can stamp the server's bulletId.
|
||||||
|
# getEnergy() > power mirrors the server's "bot.energy <= firepower" reject.
|
||||||
|
if setFire(power) and getEnergy() > power:
|
||||||
|
bot.pendingFireGuns.add(selectedGun)
|
||||||
|
|
||||||
setGunTurnRate(normDelta)
|
setGunTurnRate(normDelta)
|
||||||
|
|
||||||
|
|||||||
@@ -161,6 +161,18 @@ proc bestPower*(t: VirtualTracker, gunId: GunId, targetId: int = -1): (int, floa
|
|||||||
## Uses per-enemy fitness when targetId >= 0 and data exists; else aggregate.
|
## Uses per-enemy fitness when targetId >= 0 and data exists; else aggregate.
|
||||||
let fit = t.fitnessFor(targetId)
|
let fit = t.fitnessFor(targetId)
|
||||||
result = (0, PowerBins[0])
|
result = (0, PowerBins[0])
|
||||||
|
# Cold gun (zero observations in every bin): fall back to the lowest power bin,
|
||||||
|
# as documented. Without this the countdown loop below would hit the empty
|
||||||
|
# highest bin first and wrongly return power 3.0.
|
||||||
|
var anyObs = false
|
||||||
|
for binIdx in 0..<len(PowerBins):
|
||||||
|
if fit[gunId].bins[binIdx].count > 0:
|
||||||
|
anyObs = true
|
||||||
|
break
|
||||||
|
if not anyObs:
|
||||||
|
return (0, PowerBins[0])
|
||||||
|
# Warm gun: unchanged — return the highest power bin clearing MinHitRate
|
||||||
|
# (or an empty bin, which the existing logic treats as acceptable).
|
||||||
for binIdx in countdown(len(PowerBins) - 1, 0):
|
for binIdx in countdown(len(PowerBins) - 1, 0):
|
||||||
let rate = fit[gunId].bins[binIdx].hitRate()
|
let rate = fit[gunId].bins[binIdx].hitRate()
|
||||||
if rate >= MinHitRate or fit[gunId].bins[binIdx].count == 0:
|
if rate >= MinHitRate or fit[gunId].bins[binIdx].count == 0:
|
||||||
|
|||||||
Reference in New Issue
Block a user