test(range): restore the 12/12 offline==online proof; measure TM clause readability
Task 1 - the acceptance proof was unrunnable because RecordWorldState was a
compile-time const set to false. It is now a RUNTIME switch
(let RecordWorldState* = existsEnv("TR_RECORD_WORLDSTATE")), default OFF, so
ordinary runs write no fixture, and acceptance_offline_vs_online.nim enables
it for the battle it spawns and clears it afterwards. Restored and run twice:
12/12 deterministic guns match exactly (128-tick and 546-tick battles), with
Tsetlin reported separately as stochastic. Both nimble build variants clean.
Task 2 - does a compact encoding turn the TM's 99.35% into a READABLE rule?
Measured across window sizes (fixed seed, no tuning):
frames TEST acc eff.lits/clause firing clauses counterfactual low/high/mean
10 99.35% 152.8 37 100/24/62.4%
3 95.94% 54.9 35 96/20/58.6%
2 99.48% 39.6 38 95/25/60.7%
1 98.30% 19.2 45 100/24/62.3%
So 2 frames is strictly better than 10 on BOTH axes: +0.13 accuracy for 4x
smaller clauses. The 3-frame dip is non-monotonic and left unexplained rather
than smoothed over.
A readable rule WAS partially recovered. Five clauses carry the exact Gray
form !g10 ^ !g9 ^ !g8; g10 is inert in this data, so the effective rule is the
2-literal proposition !g9 ^ !g8, i.e. energy < 25.6. That is a genuine
threshold in readable propositional form - but at 25.6, NOT the labelled 30,
because 256 is a power-of-two Gray boundary expressible in two literals while
300 needs a longer conjunction. The TM found the nearest SIMPLE threshold.
The honest caveat: that threshold is not the ensemble's decision mechanism.
The counterfactual follow rate (high 24%, mean 62.3%) is statistically
identical at 1, 2 and 10 frames, so compactness did not make the model read
energy - its vote is carried by co-occurring bearing/velocity/heading/wall
literals. Also identified: clauses containing all 11 Gray energy bits are
satisfied at exactly one raw value (50, the dataset floor), so they are
'energy has hit the floor' detectors, not thresholds.
Methodological fix worth keeping: the earlier single-frame counterfactual
wrote energy into all 10 frame slots including the zeroed ones, reviving dead
clauses and producing a spurious 2% high-follow rate. setEnergyFrames now
rewrites only the exposed frames; the corrected figure is 24%.
This commit is contained in:
@@ -38,10 +38,12 @@ const DebugCircular = false
|
||||
## Task A instrumentation: log every real shot + its eventual outcome to
|
||||
## /tmp/shot_log.jsonl. Set to false to compile all shot-log machinery out.
|
||||
const ShotLog = true
|
||||
## Offline range recorder: when true, append the exact WorldState the bot builds
|
||||
## each tick to /tmp/worldstate_record.jsonl so it can be replayed offline.
|
||||
## Mirrors the ShotLog idiom: false compiles the machinery out entirely.
|
||||
const RecordWorldState = false
|
||||
## Offline range recorder: when the TR_RECORD_WORLDSTATE env var is set, append
|
||||
## the exact WorldState the bot builds each tick to /tmp/worldstate_record.jsonl
|
||||
## so it can be replayed offline. This is a RUNTIME switch (not a compile-time
|
||||
## const) so the normal build never writes a fixture, while the acceptance test
|
||||
## can enable recording for just the battle it spawns by exporting the env var.
|
||||
let RecordWorldState* = existsEnv("TR_RECORD_WORLDSTATE")
|
||||
const WorldStateRecordPath = "/tmp/worldstate_record.jsonl"
|
||||
const GunNames = ["HeadOn", "Linear", "Tsetlin", "Circular", "GuessFactor", "Pattern", "WallBounce", "Accel", "StopShot", "Displace", "AvgLead", "DecayGF", "KNN"]
|
||||
|
||||
@@ -192,7 +194,7 @@ proc printConfig(bot: ModularBot, changed: string = "") =
|
||||
|
||||
proc startWorldStateRecord(bot: ModularBot) =
|
||||
## Truncate the fixture and write the meta line at round start.
|
||||
when RecordWorldState:
|
||||
if RecordWorldState:
|
||||
try:
|
||||
let f = open(WorldStateRecordPath, fmWrite)
|
||||
f.writeLine($(%*{"meta": {
|
||||
@@ -210,7 +212,7 @@ proc finishWorldStateRecord(bot: ModularBot) =
|
||||
## Append the end marker so the offline replay can reproduce the live
|
||||
## resolver's final-tick behaviour: if the target died during the last go()
|
||||
## the live aim block was skipped, so no bullet resolved on that tick.
|
||||
when RecordWorldState:
|
||||
if RecordWorldState:
|
||||
var died = false
|
||||
if bot.lastKnownTargetId >= 0 and bot.enemyTracker.enemies.contains(bot.lastKnownTargetId):
|
||||
died = not bot.enemyTracker.enemies[bot.lastKnownTargetId].alive
|
||||
@@ -225,7 +227,7 @@ proc recordWorldState(bot: ModularBot, ws: WorldState) =
|
||||
## Append one tick of the state the bot ACTUALLY built (including the stale
|
||||
## tracker positions between radar scans) so the offline replay sees the same
|
||||
## information the guns saw online.
|
||||
when RecordWorldState:
|
||||
if RecordWorldState:
|
||||
let tid = bot.currentTargetId
|
||||
var lst = -1
|
||||
if tid >= 0 and bot.enemyTracker.enemies.contains(tid):
|
||||
@@ -268,7 +270,7 @@ proc buildState(bot: ModularBot, ex, ey, espeed, eheading, eenergy: float): Worl
|
||||
tick: bot.tick,
|
||||
enemies: ei,
|
||||
)
|
||||
when RecordWorldState:
|
||||
if RecordWorldState:
|
||||
bot.recordWorldState(result)
|
||||
|
||||
method onScannedBot*(bot: ModularBot, e: ScannedBotEvent) =
|
||||
@@ -310,7 +312,7 @@ method onHitByBullet*(bot: ModularBot, e: HitByBulletEvent) =
|
||||
|
||||
method onRoundEnded*(bot: ModularBot, e: RoundEndedEventForBot) =
|
||||
## Dump per-gun virtual bullet stats to /tmp/gun_stats.jsonl (one line per round).
|
||||
when RecordWorldState:
|
||||
if RecordWorldState:
|
||||
bot.finishWorldStateRecord()
|
||||
# Use lastKnownTargetId: currentTargetId is -1 if enemy died before round end
|
||||
let targetId = if bot.currentTargetId >= 0: bot.currentTargetId else: bot.lastKnownTargetId
|
||||
@@ -371,7 +373,7 @@ method onRoundEnded*(bot: ModularBot, e: RoundEndedEventForBot) =
|
||||
|
||||
method onRoundStarted*(bot: ModularBot, e: RoundStartedEvent) =
|
||||
bot.roundNumber = e.roundNumber
|
||||
when RecordWorldState:
|
||||
if RecordWorldState:
|
||||
bot.startWorldStateRecord()
|
||||
bot.realShotsFired = 0
|
||||
bot.realHits = 0
|
||||
|
||||
Reference in New Issue
Block a user