ModularBot: [result] round/battle outcome log lines (TR_RESULT_LOG, default on)
One greppable '[result]' line per round plus one at battle end, stdout only: [result] round 3/7 WE WON (enemy destroyed) | us 42.1 energy, them 0.0, 812 ticks | rounds won 3/7 [result] battle END: rounds won 4/7 Outcome is authoritative from RoundEndedEventForBot.results.rank (1 = winner); death observations (our onDeath, enemy onBotDeath) and onWonRound refine it into WE WON (enemy destroyed) / WE DIED (killed) / BOTH DIED (score decided) / TIMEOUT (score decided). Our own death is reported the instant it happens. TR_RESULT_LOG registers in the boot env report; default on, only explicit off-values disable it. No behaviour change - logging/state only.
This commit is contained in:
@@ -123,6 +123,13 @@ let ShotLogPath = getEnv("GUN_SHOTLOG_PATH", "/tmp/shot_log.jsonl")
|
||||
## shows why the cap moved. The policy itself lives in the shared gun harness
|
||||
## (`applyPowerPolicy`), so both live and offline paths see the same rule.
|
||||
let PowerLog = existsEnv("TR_POWER_LOG")
|
||||
## Round-outcome observability (TR_RESULT_LOG): one `[result]` line per round
|
||||
## (win / our death / mutual kill / timeout) plus one at battle end. Default ON;
|
||||
## only the explicit off-values disable it. Read once at process start.
|
||||
let ResultLog* =
|
||||
block:
|
||||
let v = getEnv("TR_RESULT_LOG", "1").strip().toLowerAscii()
|
||||
not (v == "0" or v == "false" or v == "no" or v == "off")
|
||||
const GunNames = ["HeadOn", "Linear", "Tsetlin", "Circular", "GuessFactor", "Pattern", "WallBounce", "Accel", "StopShot", "Displace", "AvgLead", "DecayGF", "KNN", "TMSelect", "TMPattern", "TMHorizon", "BitBrain"]
|
||||
|
||||
## Rack id of the new TM pattern gun. It defaults to `TR_RACK_TMPATTERN=off`;
|
||||
@@ -246,6 +253,18 @@ type
|
||||
# once-per-change `[rack]` log line.
|
||||
rackMode: vb.RackMode
|
||||
lastRackLogKey: string
|
||||
# Round-outcome logging (TR_RESULT_LOG). Pure observability: these fields
|
||||
# never feed a movement/gun/radar decision. Reset per round in
|
||||
# onRoundStarted (and the counters per battle in onGameStarted).
|
||||
battleRounds: int ## rounds configured for this battle (GameSetup)
|
||||
roundsWon: int ## cumulative rounds won this battle
|
||||
roundsPlayed: int ## cumulative rounds fully accounted for
|
||||
roundCounted: bool ## roundsPlayed already bumped for this round
|
||||
weDiedThisRound: bool
|
||||
enemyDiedThisRound: bool ## an enemy BotDeath was observed this round
|
||||
wonRoundSeen: bool ## onWonRound fired (authoritative "last alive")
|
||||
roundOutcomePrinted: bool ## a `[result]` line already covered this round
|
||||
roundPrintedLabel: string ## its outcome label (for reconciliation)
|
||||
|
||||
proc writeShotLog(shot: PendingShot, hit: bool, unresolved: bool) =
|
||||
## Task A: append one JSON line per resolved real shot. The write is fully
|
||||
@@ -588,8 +607,65 @@ method onHitByBullet*(bot: ModularBot, e: HitByBulletEvent) =
|
||||
bot.ramDmgAccum += bulletDamage(e.bullet.power)
|
||||
bot.moveTracker.registerHit(e.bullet.power, e.bullet.direction, getX(), getY())
|
||||
|
||||
proc enemyEnergyForReport(bot: ModularBot): float =
|
||||
## Last-scanned energy of the enemy the report is about (current target,
|
||||
## else the last known target, else any enemy in the tracker). Returns -1.0
|
||||
## when nothing has been scanned yet, so a report never invents a number;
|
||||
## returns 0.0 when that enemy is already known dead.
|
||||
let tid = if bot.currentTargetId >= 0: bot.currentTargetId else: bot.lastKnownTargetId
|
||||
if tid >= 0 and bot.enemyTracker.enemies.contains(tid):
|
||||
let es = bot.enemyTracker.enemies[tid]
|
||||
return (if es.alive: es.energy else: 0.0)
|
||||
for _, es in bot.enemyTracker.enemies:
|
||||
return (if es.alive: es.energy else: 0.0)
|
||||
-1.0
|
||||
|
||||
proc emitRoundResult(bot: ModularBot, label, detail: string, ticks: int,
|
||||
usDead, themDead: bool) =
|
||||
## The ONE `[result]` line for one round. Every line starts with the greppable
|
||||
## `[result]` prefix; the outcome word is left-padded so successive lines
|
||||
## align. `rounds won X/Y` is the cumulative count including this round.
|
||||
if not ResultLog: return
|
||||
let rn = if bot.battleRounds > 0: $bot.roundNumber & "/" & $bot.battleRounds
|
||||
else: $bot.roundNumber
|
||||
let usE = if usDead: 0.0 else: getEnergy()
|
||||
var themE = enemyEnergyForReport(bot)
|
||||
if themDead: themE = 0.0
|
||||
let themStr = if themE < 0.0: "?" else: fmt"{themE:.1f}"
|
||||
let total = if bot.battleRounds > 0: bot.battleRounds else: bot.roundsPlayed
|
||||
echo "[result] round ", rn, " ", alignLeft(label, 10), "(", detail, ")",
|
||||
" | us ", fmt"{usE:.1f}", " energy, them ", themStr, ", ",
|
||||
ticks, " ticks | rounds won ", bot.roundsWon, "/", total
|
||||
bot.roundOutcomePrinted = true
|
||||
bot.roundPrintedLabel = label
|
||||
|
||||
method onRoundEnded*(bot: ModularBot, e: RoundEndedEventForBot) =
|
||||
## Dump per-gun virtual bullet stats to /tmp/gun_stats.jsonl (one line per round).
|
||||
# Round outcome: `e.results.rank` (1 = winner) is the server's authoritative
|
||||
# win/lose signal and does not depend on BotDeathEvent. Death observations
|
||||
# (our `onDeath`, enemy `onBotDeath`) and `onWonRound` refine it into the four
|
||||
# distinct outcomes. If our death already printed this round's line (immediate
|
||||
# report), only reprint when the final label CHANGED (e.g. a mutual kill).
|
||||
if ResultLog:
|
||||
let rank1 = e.results.rank == 1
|
||||
if not bot.roundCounted:
|
||||
inc bot.roundsPlayed
|
||||
bot.roundCounted = true
|
||||
if rank1: inc bot.roundsWon
|
||||
let weDied = bot.weDiedThisRound
|
||||
let enemyGone = bot.enemyDiedThisRound or bot.wonRoundSeen
|
||||
let decided = ": " & (if rank1: "WE WON" else: "WE LOST")
|
||||
var label, detail: string
|
||||
if weDied and enemyGone:
|
||||
label = "BOTH DIED"; detail = "score decided" & decided
|
||||
elif weDied:
|
||||
label = "WE DIED"; detail = "killed"
|
||||
elif enemyGone:
|
||||
label = "WE WON"; detail = "enemy destroyed"
|
||||
else:
|
||||
label = "TIMEOUT"; detail = "score decided" & decided
|
||||
if not bot.roundOutcomePrinted or bot.roundPrintedLabel != label:
|
||||
bot.emitRoundResult(label, detail, e.turnNumber, weDied, enemyGone)
|
||||
if RecordWorldState:
|
||||
bot.finishWorldStateRecord()
|
||||
# Per-round TM-horizon summary (change-gated behind TR_TMHORIZON_LOG=1) so the
|
||||
@@ -697,6 +773,14 @@ method onRoundEnded*(bot: ModularBot, e: RoundEndedEventForBot) =
|
||||
|
||||
method onRoundStarted*(bot: ModularBot, e: RoundStartedEvent) =
|
||||
bot.roundNumber = e.roundNumber
|
||||
# Per-round outcome-log state (TR_RESULT_LOG). Reset every round so round 1
|
||||
# reports and a target/enemy change mid-round cannot leak a stale flag.
|
||||
bot.weDiedThisRound = false
|
||||
bot.enemyDiedThisRound = false
|
||||
bot.wonRoundSeen = false
|
||||
bot.roundCounted = false
|
||||
bot.roundOutcomePrinted = false
|
||||
bot.roundPrintedLabel = ""
|
||||
if RecordWorldState:
|
||||
bot.startWorldStateRecord()
|
||||
bot.realShotsFired = 0
|
||||
@@ -800,6 +884,8 @@ method onRoundStarted*(bot: ModularBot, e: RoundStartedEvent) =
|
||||
# tracker fitness persists across rounds (rolling window carries over)
|
||||
|
||||
method onBotDeath*(bot: ModularBot, e: BotDeathEvent) =
|
||||
## An ENEMY died (`ekBotDeath`; our own death goes to `onDeath`).
|
||||
bot.enemyDiedThisRound = true
|
||||
echo "[death] victimId=", e.victimId, " wasTarget=", (e.victimId == bot.currentTargetId),
|
||||
" trackerAlive=", (if bot.enemyTracker.enemies.contains(e.victimId): $bot.enemyTracker.enemies[e.victimId].alive else: "notInTracker")
|
||||
echo CLR_MOVE & "[death] victimId=" & $e.victimId & " wasTarget=" & $(e.victimId == bot.currentTargetId) & CLR_RST
|
||||
@@ -812,6 +898,25 @@ method onBotDeath*(bot: ModularBot, e: BotDeathEvent) =
|
||||
bot.isRamming = false
|
||||
bot.currentTargetId = -1
|
||||
|
||||
method onDeath*(bot: ModularBot, e: BotDeathEvent) =
|
||||
## OUR OWN death (`ekDeath`, victimId == myId). Reported the instant it
|
||||
## happens so the user tailing the GUI log sees it without waiting for the
|
||||
## round-end event (which may not arrive). If the enemy is already known dead,
|
||||
## the round is a mutual kill and is labelled as such (rank/score only arrives
|
||||
## at round end and is reconciled there if it contradicts this line).
|
||||
bot.weDiedThisRound = true
|
||||
inc bot.roundsPlayed
|
||||
bot.roundCounted = true
|
||||
if bot.enemyDiedThisRound or bot.wonRoundSeen:
|
||||
bot.emitRoundResult("BOTH DIED", "score decided", e.turnNumber, true, true)
|
||||
else:
|
||||
bot.emitRoundResult("WE DIED", "killed", e.turnNumber, true, false)
|
||||
|
||||
method onWonRound*(bot: ModularBot, e: WonRoundEvent) =
|
||||
## The server says we were the last bot alive: authoritative "enemy
|
||||
## destroyed", independent of whether a BotDeathEvent was delivered.
|
||||
bot.wonRoundSeen = true
|
||||
|
||||
method onGameStarted*(bot: ModularBot, e: GameStartedEventForBot) =
|
||||
# A NEW BATTLE begins: wipe the horizon TM's machines and learned statistics
|
||||
# (no persistence across battles). `onRoundStarted` only clears per-round
|
||||
@@ -821,6 +926,11 @@ method onGameStarted*(bot: ModularBot, e: GameStartedEventForBot) =
|
||||
bot.bitbrain.resetLearning("game_start")
|
||||
# minNumberOfParticipants == maxNumberOfParticipants for fixed battles; self is -1
|
||||
bot.initialEnemyCount = e.gameSetup.minNumberOfParticipants - 1
|
||||
# New battle: reset the cumulative outcome counters and remember how many
|
||||
# rounds the server will run (0 = unlimited; the report then shows played count).
|
||||
bot.battleRounds = e.gameSetup.numberOfRounds
|
||||
bot.roundsWon = 0
|
||||
bot.roundsPlayed = 0
|
||||
# The custom event must be registered AFTER `start()` ran `initGlobals()`,
|
||||
# which replaces the event queue (and its conditions). onGameStarted is the
|
||||
# first bot callback that runs after that, still before any round/tick.
|
||||
@@ -828,6 +938,14 @@ method onGameStarted*(bot: ModularBot, e: GameStartedEventForBot) =
|
||||
addCustomEvent("tracker_queue_probe", probeQueue)
|
||||
gProbeEventRegistered = true
|
||||
|
||||
method onGameEnded*(bot: ModularBot, e: GameEndedEventForBot) =
|
||||
## Battle summary, ONE line, after the server reports the game over.
|
||||
if not ResultLog: return
|
||||
let total = if e.numberOfRounds > 0: e.numberOfRounds
|
||||
elif bot.battleRounds > 0: bot.battleRounds
|
||||
else: bot.roundsPlayed
|
||||
echo "[result] battle END: rounds won ", bot.roundsWon, "/", total
|
||||
|
||||
proc shouldSwitchTarget(bot: ModularBot, candidateId: int): bool =
|
||||
## Hysteresis: only switch when there is a clear reason.
|
||||
if bot.currentTargetId < 0: return true
|
||||
@@ -1311,6 +1429,7 @@ when isMainModule:
|
||||
trackerProbe: TrackerProbe,
|
||||
trackerProbePath: TrackerProbePath,
|
||||
powerLog: PowerLog,
|
||||
resultLog: ResultLog,
|
||||
gunStatsPath: GunStatsPath,
|
||||
shotLogPath: ShotLogPath,
|
||||
disabledGuns: DisabledGuns,
|
||||
|
||||
@@ -48,6 +48,7 @@ type
|
||||
trackerProbe*: bool
|
||||
trackerProbePath*: string
|
||||
powerLog*: bool
|
||||
resultLog*: bool
|
||||
gunStatsPath*: string
|
||||
shotLogPath*: string
|
||||
disabledGuns*: HashSet[int]
|
||||
@@ -186,6 +187,7 @@ proc printEffectiveValues(ctx: EnvReportContext) =
|
||||
emit("TR_VBULLET_ADMIT_ONLY", onOff(ctx.vBulletAdmitOnly),
|
||||
sourceOf("TR_VBULLET_ADMIT_ONLY"))
|
||||
emit("TR_POWER_LOG", onOff(ctx.powerLog), sourceOfPresence("TR_POWER_LOG"))
|
||||
emit("TR_RESULT_LOG", onOff(ctx.resultLog), sourceOf("TR_RESULT_LOG"))
|
||||
emit("TR_RECORD_WORLDSTATE", onOff(ctx.recordWorldState),
|
||||
sourceOfPresence("TR_RECORD_WORLDSTATE"))
|
||||
emit("TR_RADAR_FORCE_SPIN", onOff(ctx.radarForceSpin),
|
||||
@@ -417,6 +419,7 @@ proc knownEnvNames*(): seq[string] =
|
||||
"TR_RADAR_FORCE_SPIN", "TR_RADAR_SCANLOG", "TR_RADAR_SCAN_LOG_PATH",
|
||||
"TR_TRACKER_PROBE", "TR_TRACKER_PROBE_PATH", "TR_VBULLET_ADMIT_ONLY",
|
||||
"TR_POWER_LOG",
|
||||
"TR_RESULT_LOG",
|
||||
"TR_RAM_OPPORTUNITY", "TR_RAM_OPP_DIST", "TR_RAM_OPP_MARGIN",
|
||||
"TR_RAM_ABORT_DMG", "TR_RAM_PLAN", "TR_RAM_PLAN_DIST",
|
||||
"TR_RAM_PLAN_MARGIN", "TR_RAM_PLAN_HITRATE", "TR_RAM_LOG",
|
||||
|
||||
Reference in New Issue
Block a user