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.
|
||||
## Movement: OscillatorModule (perpendicular strafing).
|
||||
|
||||
import std/[math, os, strformat, tables, json]
|
||||
import std/[math, os, strformat, tables, sets, json]
|
||||
import robocode_tankroyale_botapi
|
||||
import radar_harness/radar_interface
|
||||
import radars/radar_lock_module
|
||||
@@ -90,9 +90,39 @@ type
|
||||
roundNumber: int
|
||||
realShotsFired: 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]
|
||||
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 = "") =
|
||||
let gunName = GunNames[bot.currentGun]
|
||||
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) =
|
||||
inc bot.realShotsFired
|
||||
bot.registerOwnBullet(e.bullet.bulletId)
|
||||
bot.moveTracker.shadows.addBullet(e.bullet.x, e.bullet.y,
|
||||
degToRad(e.bullet.direction), e.bullet.power)
|
||||
|
||||
method onBulletHit*(bot: ModularBot, e: BulletHitBotEvent) =
|
||||
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)
|
||||
|
||||
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.mover.removeBulletNear(e.bullet.x, e.bullet.y)
|
||||
|
||||
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.mover.removeBulletNear(e.bullet.x, e.bullet.y)
|
||||
|
||||
@@ -192,13 +229,19 @@ method onRoundEnded*(bot: ModularBot, e: RoundEndedEventForBot) =
|
||||
for k in 0..<n:
|
||||
if fw.hits[k]: inc totalHits
|
||||
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(%*{
|
||||
"id": gid,
|
||||
"name": GunNames[gid],
|
||||
"vShots": totalShots,
|
||||
"vHits": totalHits,
|
||||
"hitRate": (hitRate * 100.0).int,
|
||||
"selected": bot.gunSelectionCount[gid]
|
||||
"selected": bot.gunSelectionCount[gid],
|
||||
"realShots": rShots,
|
||||
"realHits": rHits,
|
||||
"realHitRate": rRate.round(1)
|
||||
})
|
||||
|
||||
let row = %*{
|
||||
@@ -219,7 +262,13 @@ method onRoundStarted*(bot: ModularBot, e: RoundStartedEvent) =
|
||||
bot.realShotsFired = 0
|
||||
bot.realHits = 0
|
||||
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)
|
||||
setAdjustRadarForBodyTurn(true)
|
||||
setAdjustRadarForGunTurn(true)
|
||||
@@ -523,7 +572,10 @@ method run*(bot: ModularBot) =
|
||||
elif normDelta < -180.0: normDelta += 360.0
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
let fit = t.fitnessFor(targetId)
|
||||
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):
|
||||
let rate = fit[gunId].bins[binIdx].hitRate()
|
||||
if rate >= MinHitRate or fit[gunId].bins[binIdx].count == 0:
|
||||
|
||||
Reference in New Issue
Block a user