TM radial gun: registered (default OFF) + label-bias fix that removes the bias but
retracts its own earlier learning claim === TASK 1: REGISTERED AS GUN 14, DEFAULT `off` === The radial TM gun is now a first-class rack member (`TMPATTERN`, id 14), forceable alone with `TR_RACK_TMPATTERN=both` plus every other `TR_RACK_*=off`. DEFAULT IS `off`, and the justification matters: `both` would let it compete for selection AND (because the shared VirtualTracker ring is order-sensitive) shift every other gun's learning order, so it CANNOT leave the default path unchanged. With `off` its predict and spawnBullets are additionally GATED on rack admission (the only gun wired that way), so the shipped default never spawns it at all: zero cost, zero ring perturbation. Live proof: 1-round battle with only TMPATTERN racked -> `gun 14 (TMPattern): vShots=400 selected=104 other-gun selections=0`. Default-path-unchanged proof: parity checks that the 15-gun default bestGun/ selectGun equals the old 14-gun rack RNG-draw-for-RNG-draw, that gun 14 is never selected by default, and acceptance 12/12. Cost: 0.36 ms/tick (predict 0.30 + onResult 0.05) ~= 3% of the 13.16 ms budget. Tsetlin in the same harness is 1.62 ms/tick, so the new gun is ~4.5x cheaper. === TASK 2: THE LABEL-BIAS FIX - AND A RETRACTION === Root cause confirmed: under bmPoint a SHORT radial correction resolves the virtual bullet BEFORE the base arrival tick, so the label was dropped (labelMisses). Fix: defer the label in a pending queue and flush it once the arrival tick is recorded; labels still come from the BASE arrival tick. labelMisses 4,281,695 -> 0 training samples 1,071,824 -> 5,345,847 (x5) radial head acc 48.8% -> 57.0% (shuffled control 20.0%) bmPoint hit rate 9.4/5.8% -> 9.1/5.7% (unchanged, within noise) So the fix IMPROVES LEARNING but NOT the metric. **RETRACTION OF THE PREVIOUS JOB'S CLAIM.** It reported the radial head's 48.8% against a 36.7% majority baseline and concluded "conditional learning, not a constant bias". With the bias removed, the correctly-measured majority baseline is **58.2%** - so the head at 57.0% is AT/BELOW majority. The earlier apparent conditional learning was PARTLY AN ARTEFACT OF THE BIASED SAMPLE. The bmPoint metric win is real (TMRadial > Linear early 16/2 p=0.0013, overall 18/0 p<0.0001; > shuffled 18/0 p<0.0001) but it comes from a NET-POSITIVE AVERAGE RADIAL SHIFT, not from beating a majority classifier. Recorded plainly rather than left standing. Guards: test_tm_pattern_registration 20 (new), test_tm_pattern_rack_live 4 (new), test_gun_harness 39, test_vbullet_metric 11, test_power_selection 3 (the SIGSEGV is gone - the knn_gun rewrite is now committed), test_adaptive_radar 41, test_tfil_ring_weights 24, test_power_policy 26, test_ram_decision 28, test_rack_membership 38, test_selector_tiebreak 19, test_tm_pattern_learning 3, acceptance_offline_vs_online 12/12. ModularBot compiles (release). Note: `common_libs/tests/range_guns.nim` still builds 14 offline drivers (the offline sweep constructs TmPatternGun directly and acceptance only inspects ids 0..13), so nothing breaks - but a future job wanting it in the offline rack must add a 15th driver and mirror the live admission gating. gun_stats.jsonl now emits 15 rows; downstream tooling should ignore id 14.
This commit is contained in:
@@ -19,21 +19,30 @@ import virtual_bullets
|
||||
# TR_RACK_TSETLIN=1v1 -> 1v1 rack only
|
||||
# TR_RACK_DISPLACE=melee -> melee rack only
|
||||
# TR_RACK_KNN=off -> removed from both racks
|
||||
# TR_RACK_TMPATTERN=off (shipped default for the new TM pattern gun)
|
||||
#
|
||||
# The mode itself is derived from SERVER truth (`getEnemyCount()`), never from
|
||||
# the tracker's known-enemy count, by `rackMode` in virtual_bullets — the same
|
||||
# transition the radar uses. Defaults are all-`both`, so an unset environment
|
||||
# preserves the pre-change single-rack selection byte-for-byte.
|
||||
# transition the radar uses. Every gun except TMPATTERN defaults to `both`, so an
|
||||
# unset environment preserves the pre-change single-rack selection byte-for-byte;
|
||||
# TMPATTERN defaults to `off` so it cannot alter that selection.
|
||||
|
||||
const
|
||||
RackGunNames*: array[14, string] = [
|
||||
RackGunNames*: array[15, string] = [
|
||||
"HEADON", "LINEAR", "TSETLIN", "CIRCULAR", "GUESSFACTOR", "PATTERN",
|
||||
"WALLBOUNCE", "ACCEL", "STOPSHOT", "DISPLACE", "AVGLEAD", "DECAYGF",
|
||||
"KNN", "TMSELECT"]
|
||||
"KNN", "TMSELECT", "TMPATTERN"]
|
||||
RackEnvPrefix* = "TR_RACK_"
|
||||
DefaultRackMembership*: array[14, RackMembership] = [
|
||||
## Defaults are all-`both` EXCEPT the new TM pattern gun (id 14), which ships
|
||||
## `off`: it is registered and forceable (`TR_RACK_TMPATTERN=both|1v1|melee`)
|
||||
## but never spawns a virtual bullet unless explicitly enabled, so the shared
|
||||
## VirtualTracker ring head — and therefore every other gun's learning order
|
||||
## and the default selection sequence — is byte-for-byte unchanged. Defaulting
|
||||
## it to `both` would let it compete for selection and change the default rack.
|
||||
DefaultRackMembership*: array[15, RackMembership] = [
|
||||
rmBoth, rmBoth, rmBoth, rmBoth, rmBoth, rmBoth, rmBoth,
|
||||
rmBoth, rmBoth, rmBoth, rmBoth, rmBoth, rmBoth, rmBoth]
|
||||
rmBoth, rmBoth, rmBoth, rmBoth, rmBoth, rmBoth, rmBoth,
|
||||
rmOff]
|
||||
|
||||
proc parseRackMembership*(value: string): RackMembership =
|
||||
## Parse a `TR_RACK_<GUN>` value. Empty / unknown values fall back to the
|
||||
|
||||
+133
-65
@@ -70,6 +70,13 @@ const
|
||||
TM_SOFT_BETA* = parseFloat(TM_SOFT_BETA_DEF)
|
||||
TM_TRACE_SLOTS = 1024
|
||||
POS_RING = 512
|
||||
## Deferred-label queue (Task 2): a virtual bullet whose radial correction
|
||||
## aimed SHORT resolves BEFORE its BASE arrival tick, when the arrival-tick
|
||||
## position is not yet in `posRing`. Instead of dropping the sample
|
||||
## (`labelMisses`), the trace is copied here and resolved on the first later
|
||||
## `predict` tick at which the base arrival tick's position exists, so every
|
||||
## fired virtual bullet contributes an unbiased training sample.
|
||||
TM_PENDING_SLOTS = 1024
|
||||
DebugTMPattern* = false
|
||||
## ── radial head (Task 2) ────────────────────────────────────────────────
|
||||
## Radial label = (enemy radius at the BASE arrival tick) - (base fire
|
||||
@@ -125,12 +132,26 @@ type
|
||||
heading: float
|
||||
valid: bool
|
||||
|
||||
PendingResolve = object
|
||||
## A fired virtual bullet whose label was not yet resolvable at resolution
|
||||
## time. `trace` is a COPY of the fire-time trace (features + clause
|
||||
## caches), so the deferred training update is identical to an immediate
|
||||
## one, just later.
|
||||
arrivalTick: int
|
||||
powerBin: int
|
||||
power: float
|
||||
trace: TmPatternTrace
|
||||
|
||||
TmPatternGun* = object
|
||||
teams: array[TM_CLASSES, seq[int16]]
|
||||
radTeams: array[TM_CLASSES, seq[int16]]
|
||||
revTeams: array[2, seq[int16]]
|
||||
targetMode*: TmTargetMode
|
||||
traces: array[TM_TRACE_SLOTS, TmPatternTrace]
|
||||
# ── deferred labels (Task 2) ──
|
||||
pending: array[TM_PENDING_SLOTS, PendingResolve]
|
||||
pendingCount: int
|
||||
pendingDropped*: int
|
||||
# ── history ──
|
||||
posRing: array[POS_RING, PosSample]
|
||||
lastTick: int
|
||||
@@ -268,6 +289,14 @@ proc initTmPatternGun*(): TmPatternGun =
|
||||
randomize()
|
||||
result.debugGraphics = false
|
||||
|
||||
proc initTmRadialGun*(): TmPatternGun =
|
||||
## The RACK-REGISTERED instance: the RADIAL target mode, which is the
|
||||
## control-validated winner under `bmPoint` (see tm_pattern_sweep_results.md,
|
||||
## Round 2 Task 2). The gun type carries all three heads; the live rack only
|
||||
## ever selects this radial-mode instance.
|
||||
result = initTmPatternGun()
|
||||
result.targetMode = tmRadial
|
||||
|
||||
proc isWarmedUp*(g: TmPatternGun): bool {.inline.} = true
|
||||
|
||||
proc resetLearning*(g: var TmPatternGun) =
|
||||
@@ -284,6 +313,7 @@ proc resetLearning*(g: var TmPatternGun) =
|
||||
g.sinceReversal = 0
|
||||
g.radialFracSm = 0.0
|
||||
g.latPersist = 0
|
||||
g.pendingCount = 0
|
||||
|
||||
proc tmUpdateHistory(g: var TmPatternGun, state: WorldState) =
|
||||
if state.tick == g.lastTick: return
|
||||
@@ -442,6 +472,90 @@ proc tmChooseAt(votes: openArray[float], centre: int, margin: float,
|
||||
proc tmChooseClass(g: var TmPatternGun, votes: array[TM_CLASSES, float]): int =
|
||||
tmChooseAt(votes, (TM_CLASSES - 1) div 2, TM_CONF_MARGIN, g.totalObs)
|
||||
|
||||
proc tmResolveTrace(g: var TmPatternGun, t: TmPatternTrace, power: float) =
|
||||
## One label + one TM update for a fired virtual bullet, using the enemy
|
||||
## position recorded at the BASE arrival tick. `t` is a value copy of the
|
||||
## fire-time trace, so this is safe to call either from `onResult` (the label
|
||||
## is already resolvable) or from `tmFlushPending` (the label was deferred
|
||||
## because the bullet resolved before its base arrival tick).
|
||||
let s = ((t.arrivalTick mod POS_RING) + POS_RING) mod POS_RING
|
||||
let speed = bulletSpeed(power)
|
||||
let mea = arcsin(clamp(8.0 / speed, -1.0, 1.0))
|
||||
let actualBearing = arctan2(g.posRing[s].y - t.fireY, g.posRing[s].x - t.fireX)
|
||||
var delta = actualBearing - t.baseBearing
|
||||
while delta > PI: delta -= 2.0 * PI
|
||||
while delta < -PI: delta += 2.0 * PI
|
||||
let gf = if mea > 1e-10: clamp(delta / mea, -1.0, 1.0) else: 0.0
|
||||
|
||||
# The shuffled control randomises ONLY the head the current mode is claiming.
|
||||
let shuffleGF = g.shuffleLabels and g.targetMode == tmGF
|
||||
let shuffleRad = g.shuffleLabels and g.targetMode == tmRadial
|
||||
let shuffleRev = g.shuffleLabels and g.targetMode == tmReversal
|
||||
|
||||
let winner = if shuffleGF: rand(TM_CLASSES - 1) else: gfToBucket(gf)
|
||||
inc g.labelHist[winner]
|
||||
if t.warm:
|
||||
inc g.classTotal
|
||||
if winner == t.chosen: inc g.classCorrect
|
||||
|
||||
# Radial label: enemy radius at the base arrival tick minus the base fire
|
||||
# distance. Independent of our own aim, so it is a clean target.
|
||||
let actualRadius = hypot(g.posRing[s].x - t.fireX, g.posRing[s].y - t.fireY)
|
||||
let radDelta = actualRadius - t.fireDist
|
||||
let radWinner = if shuffleRad: rand(TM_CLASSES - 1) else: radToBucket(radDelta)
|
||||
inc g.radLabelHist[radWinner]
|
||||
if t.warm:
|
||||
inc g.radTotal
|
||||
if radWinner == t.radChosen: inc g.radCorrect
|
||||
|
||||
# Reversal label: net heading turn over the flight, opposite to the direction
|
||||
# the enemy was turning at fire time.
|
||||
let dh = normDeg(g.posRing[s].heading - t.fireHeading)
|
||||
let netTurn = if dh > TM_REV_TURN_DEG: 1 elif dh < -TM_REV_TURN_DEG: -1 else: 0
|
||||
let revWinner =
|
||||
if shuffleRev: rand(1)
|
||||
elif t.fireTurn != 0 and netTurn != 0 and netTurn != t.fireTurn: 1
|
||||
else: 0
|
||||
inc g.revLabelHist[revWinner]
|
||||
if t.warm:
|
||||
inc g.revTotal
|
||||
if revWinner == t.revChosen: inc g.revCorrect
|
||||
|
||||
case g.targetMode
|
||||
of tmGF:
|
||||
for c in 0..<TM_CLASSES:
|
||||
let d = if c == winner: 1.0 else: -1.0
|
||||
g.teams[c].tmLearnDir(t.lits, t.cache[c], t.votes[c], d)
|
||||
of tmRadial:
|
||||
for c in 0..<TM_CLASSES:
|
||||
let d = if c == radWinner: 1.0 else: -1.0
|
||||
g.radTeams[c].tmLearnDir(t.lits, t.radCache[c], t.radVotes[c], d)
|
||||
of tmReversal:
|
||||
for c in 0..<TM_CLASSES:
|
||||
let d = if c == winner: 1.0 else: -1.0
|
||||
g.teams[c].tmLearnDir(t.lits, t.cache[c], t.votes[c], d)
|
||||
for c in 0..<2:
|
||||
let d = if c == revWinner: 1.0 else: -1.0
|
||||
g.revTeams[c].tmLearnDir(t.lits, t.revCache[c], t.revVotes[c], d)
|
||||
inc g.totalObs
|
||||
inc g.trainCalls
|
||||
|
||||
proc tmFlushPending(g: var TmPatternGun) =
|
||||
## Resolve every deferred trace whose BASE arrival tick is now recorded in
|
||||
## `posRing`. Called once per `predict` right after `tmUpdateHistory`, so the
|
||||
## just-written current tick is visible. Entries are compacted in place.
|
||||
if g.pendingCount == 0: return
|
||||
var w = 0
|
||||
for i in 0..<g.pendingCount:
|
||||
let p = addr g.pending[i]
|
||||
let s = ((p.arrivalTick mod POS_RING) + POS_RING) mod POS_RING
|
||||
if g.posRing[s].valid and g.posRing[s].tick == p.arrivalTick:
|
||||
g.tmResolveTrace(p.trace, p.power)
|
||||
else:
|
||||
if w != i: g.pending[w] = g.pending[i]
|
||||
inc w
|
||||
g.pendingCount = w
|
||||
|
||||
proc predict*(g: var TmPatternGun, state: WorldState, bulletSpeed: float):
|
||||
GunPrediction =
|
||||
inc g.predictCalls
|
||||
@@ -454,6 +568,9 @@ proc predict*(g: var TmPatternGun, state: WorldState, bulletSpeed: float):
|
||||
g.currentTarget = tid
|
||||
|
||||
g.tmUpdateHistory(state)
|
||||
# Deferred-label flush (Task 2): resolve any fired bullet whose BASE arrival
|
||||
# tick is now in the ring, before the cold-start gate reads totalObs.
|
||||
g.tmFlushPending()
|
||||
|
||||
if bulletSpeed <= 0.0:
|
||||
return GunPrediction(x: state.enemyX, y: state.enemyY)
|
||||
@@ -560,69 +677,20 @@ proc onResult*(g: var TmPatternGun, e: FeedbackEvent) =
|
||||
|
||||
# Clean label: enemy position at the BASE arrival tick from our own history.
|
||||
let s = ((t.arrivalTick mod POS_RING) + POS_RING) mod POS_RING
|
||||
if not g.posRing[s].valid or g.posRing[s].tick != t.arrivalTick:
|
||||
inc g.labelMisses
|
||||
t.alive = false
|
||||
return
|
||||
|
||||
let speed = bulletSpeed(e.bulletPower)
|
||||
let mea = arcsin(clamp(8.0 / speed, -1.0, 1.0))
|
||||
let actualBearing = arctan2(g.posRing[s].y - t.fireY, g.posRing[s].x - t.fireX)
|
||||
var delta = actualBearing - t.baseBearing
|
||||
while delta > PI: delta -= 2.0 * PI
|
||||
while delta < -PI: delta += 2.0 * PI
|
||||
let gf = if mea > 1e-10: clamp(delta / mea, -1.0, 1.0) else: 0.0
|
||||
|
||||
# The shuffled control randomises ONLY the head the current mode is claiming.
|
||||
let shuffleGF = g.shuffleLabels and g.targetMode == tmGF
|
||||
let shuffleRad = g.shuffleLabels and g.targetMode == tmRadial
|
||||
let shuffleRev = g.shuffleLabels and g.targetMode == tmReversal
|
||||
|
||||
let winner = if shuffleGF: rand(TM_CLASSES - 1) else: gfToBucket(gf)
|
||||
inc g.labelHist[winner]
|
||||
if t.warm:
|
||||
inc g.classTotal
|
||||
if winner == t.chosen: inc g.classCorrect
|
||||
|
||||
# Radial label: enemy radius at the base arrival tick minus the base fire
|
||||
# distance. Independent of our own aim, so it is a clean target.
|
||||
let actualRadius = hypot(g.posRing[s].x - t.fireX, g.posRing[s].y - t.fireY)
|
||||
let radDelta = actualRadius - t.fireDist
|
||||
let radWinner = if shuffleRad: rand(TM_CLASSES - 1) else: radToBucket(radDelta)
|
||||
inc g.radLabelHist[radWinner]
|
||||
if t.warm:
|
||||
inc g.radTotal
|
||||
if radWinner == t.radChosen: inc g.radCorrect
|
||||
|
||||
# Reversal label: net heading turn over the flight, opposite to the direction
|
||||
# the enemy was turning at fire time.
|
||||
let dh = normDeg(g.posRing[s].heading - t.fireHeading)
|
||||
let netTurn = if dh > TM_REV_TURN_DEG: 1 elif dh < -TM_REV_TURN_DEG: -1 else: 0
|
||||
let revWinner =
|
||||
if shuffleRev: rand(1)
|
||||
elif t.fireTurn != 0 and netTurn != 0 and netTurn != t.fireTurn: 1
|
||||
else: 0
|
||||
inc g.revLabelHist[revWinner]
|
||||
if t.warm:
|
||||
inc g.revTotal
|
||||
if revWinner == t.revChosen: inc g.revCorrect
|
||||
|
||||
case g.targetMode
|
||||
of tmGF:
|
||||
for c in 0..<TM_CLASSES:
|
||||
let d = if c == winner: 1.0 else: -1.0
|
||||
g.teams[c].tmLearnDir(t.lits, t.cache[c], t.votes[c], d)
|
||||
of tmRadial:
|
||||
for c in 0..<TM_CLASSES:
|
||||
let d = if c == radWinner: 1.0 else: -1.0
|
||||
g.radTeams[c].tmLearnDir(t.lits, t.radCache[c], t.radVotes[c], d)
|
||||
of tmReversal:
|
||||
for c in 0..<TM_CLASSES:
|
||||
let d = if c == winner: 1.0 else: -1.0
|
||||
g.teams[c].tmLearnDir(t.lits, t.cache[c], t.votes[c], d)
|
||||
for c in 0..<2:
|
||||
let d = if c == revWinner: 1.0 else: -1.0
|
||||
g.revTeams[c].tmLearnDir(t.lits, t.revCache[c], t.revVotes[c], d)
|
||||
inc g.totalObs
|
||||
inc g.trainCalls
|
||||
if g.posRing[s].valid and g.posRing[s].tick == t.arrivalTick:
|
||||
g.tmResolveTrace(t[], e.bulletPower)
|
||||
else:
|
||||
# DEFER (Task 2): the bullet resolved BEFORE its BASE arrival tick, which
|
||||
# happens whenever the radial correction aimed SHORT. The arrival-tick
|
||||
# position is not recorded yet, so keep a COPY of the trace and train on it
|
||||
# once that tick is in the ring (`tmFlushPending`). Dropping it here is what
|
||||
# biased the training set toward only the resolvable (long/centre) aims.
|
||||
if g.pendingCount < TM_PENDING_SLOTS:
|
||||
g.pending[g.pendingCount] = PendingResolve(
|
||||
arrivalTick: t.arrivalTick, powerBin: binIdx,
|
||||
power: e.bulletPower, trace: t[])
|
||||
inc g.pendingCount
|
||||
else:
|
||||
inc g.pendingDropped
|
||||
inc g.labelMisses
|
||||
t.alive = false
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
## MEASUREMENT ONLY — per-tick cost of the rack-registered TM pattern gun
|
||||
## (id 14, radial mode) in isolation, following the `runPerGun` pattern of
|
||||
## `measure_parallel_vbullet_cost.nim`.
|
||||
##
|
||||
## Drives ONE gun through a real DrussGT fixture with a one-gun VirtualTracker,
|
||||
## timing `predict` (4 power bins/tick) and `onResult` (one call per resolved
|
||||
## virtual bullet) separately, then reports ms/tick against the ~13.16 ms live
|
||||
## budget (76 ticks/s). Tsetlin is measured the same way as the reference the
|
||||
## brief quotes (~5.3 ms/tick).
|
||||
##
|
||||
## Usage:
|
||||
## nim c -r -d:release common_libs/tests/measure_tm_pattern_cost.nim
|
||||
|
||||
import std/[os, strformat, tables, math, times, monotimes, random]
|
||||
import gun_harness/offline_range
|
||||
import gun_harness/gun_interface
|
||||
import gun_harness/virtual_bullets as vb
|
||||
import guns/tm_pattern
|
||||
import guns/tsetlin
|
||||
|
||||
const
|
||||
RepoRoot = currentSourcePath().parentDir.parentDir.parentDir
|
||||
FixturePath = RepoRoot / "tools" / "fixtures" / "tr_drussgt_vs_crazy.jsonl"
|
||||
|
||||
type
|
||||
CostResult = object
|
||||
predictNs: int64
|
||||
onResultNs: int64
|
||||
ticks: int
|
||||
predictCalls: int
|
||||
onCalls: int
|
||||
|
||||
proc measureGun(fx: Fixture, predictCb: proc(state: WorldState, bulletSpeed: float): GunPrediction {.closure.},
|
||||
resultCb: proc(e: FeedbackEvent) {.closure.},
|
||||
metric: BulletMetric, warmup, measure: int): CostResult =
|
||||
var tracker = vb.initTracker(1, metric)
|
||||
let acc = new(CostResult)
|
||||
let ticks = fx.states.len
|
||||
for si in 0..<ticks:
|
||||
let state = fx.states[si]
|
||||
var preds: array[len(vb.PowerBins), GunPrediction]
|
||||
let t0 = getMonoTime()
|
||||
for b in 0..<len(vb.PowerBins):
|
||||
preds[b] = predictCb(state, bulletSpeed(vb.PowerBins[b]))
|
||||
let t1 = getMonoTime()
|
||||
tracker.spawnBullets(0, preds, state, fx.enemyId)
|
||||
|
||||
var enemyPositions: Table[int, tuple[x, y: float, lastSeenTick: int, alive: bool]]
|
||||
enemyPositions[fx.enemyId] = (x: state.enemyX, y: state.enemyY,
|
||||
lastSeenTick: state.tick, alive: true)
|
||||
tracker.tickBullets(state, enemyPositions,
|
||||
proc(gunId: GunId, binIdx: int, e: FeedbackEvent) =
|
||||
let s0 = getMonoTime()
|
||||
resultCb(e)
|
||||
let s1 = getMonoTime()
|
||||
if si >= warmup and si < warmup + measure:
|
||||
acc.onResultNs += (s1 - s0).inNanoseconds
|
||||
inc acc.onCalls)
|
||||
if si >= warmup and si < warmup + measure:
|
||||
inc acc.ticks
|
||||
acc.predictNs += (t1 - t0).inNanoseconds
|
||||
inc acc.predictCalls, len(vb.PowerBins)
|
||||
result = acc[]
|
||||
|
||||
proc makeTmRadial(fx: Fixture, metric: BulletMetric, warmup, measure: int): CostResult =
|
||||
let g = new(TmPatternGun)
|
||||
g[] = initTmRadialGun()
|
||||
randomize(1)
|
||||
result = measureGun(fx,
|
||||
proc(state: WorldState, bulletSpeed: float): GunPrediction = g[].predict(state, bulletSpeed),
|
||||
proc(e: FeedbackEvent) = g[].onResult(e),
|
||||
metric, warmup, measure)
|
||||
echo fmt" TMPattern(radial): obs={g[].totalObs} labelMiss={g[].labelMisses} " &
|
||||
fmt"traceMiss={g[].traceMisses} radAcc={g[].radCorrect}/{g[].radTotal}"
|
||||
var lab = ""
|
||||
var maj = 0
|
||||
var tot = 0
|
||||
for c in 0..<TM_CLASSES:
|
||||
lab.add $g[].radLabelHist[c] & " "
|
||||
maj = max(maj, g[].radLabelHist[c])
|
||||
tot += g[].radLabelHist[c]
|
||||
echo fmt" radial label hist [{lab}] majority={maj.float / max(1, tot).float * 100:.1f}% pendingDropped={g[].pendingDropped}"
|
||||
|
||||
proc makeTsetlin(fx: Fixture, metric: BulletMetric, warmup, measure: int): CostResult =
|
||||
let g = new(TsetlinGun)
|
||||
g[] = initTsetlinGun()
|
||||
randomize(1)
|
||||
result = measureGun(fx,
|
||||
proc(state: WorldState, bulletSpeed: float): GunPrediction = g[].predict(state, bulletSpeed),
|
||||
proc(e: FeedbackEvent) = g[].onResult(e),
|
||||
metric, warmup, measure)
|
||||
|
||||
proc main() =
|
||||
if not fileExists(FixturePath):
|
||||
echo "fixture missing: ", FixturePath
|
||||
quit(1)
|
||||
let fx = loadFixture(FixturePath)
|
||||
echo fmt"fixture: {fx.meta.adversary} ticks={fx.states.len} metric=point"
|
||||
|
||||
const Warmup = 150
|
||||
const Measure = 800
|
||||
const BudgetMs = 1000.0 / 76.0 # ~76 ticks/s measured bridge throughput
|
||||
echo fmt"warmup={Warmup} measured={Measure} ticks; budget={BudgetMs:.2f} ms/tick"
|
||||
echo ""
|
||||
|
||||
let tm = makeTmRadial(fx, bmPoint, Warmup, Measure)
|
||||
let ts = makeTsetlin(fx, bmPoint, Warmup, Measure)
|
||||
|
||||
proc report(name: string, r: CostResult) =
|
||||
let pMs = r.predictNs.float / 1.0e6 / max(1, r.ticks).float
|
||||
let oMs = r.onResultNs.float / 1.0e6 / max(1, r.ticks).float
|
||||
let tot = pMs + oMs
|
||||
echo fmt"{name:<18} predict {pMs:6.2f} ms/tick onResult {oMs:6.2f} ms/tick " &
|
||||
fmt"total {tot:6.2f} ms/tick ({tot / BudgetMs * 100:5.0f}% budget) " &
|
||||
fmt"onCalls/tick={r.onCalls.float / max(1, r.ticks).float:.1f}"
|
||||
|
||||
echo "=================== PER-TICK COST (single gun, bmPoint) ==================="
|
||||
report("TMPattern(radial)", tm)
|
||||
report("Tsetlin (reference)", ts)
|
||||
|
||||
# ── pooled radial-head learning stats over the real fixture set ─────────────
|
||||
const RealFixtures = ["drussgt_vs_crazy", "drussgt_vs_spinbot",
|
||||
"drussgt_vs_drussgt", "tr_drussgt_vs_crazy", "tr_drussgt_vs_spinbot",
|
||||
"tr_drussgt_vs_modularbot"]
|
||||
echo ""
|
||||
echo "=================== POOLED RADIAL HEAD (real fixtures, bmPoint) ==================="
|
||||
var realHist: array[TM_CLASSES, int]
|
||||
var realChosen: array[TM_CLASSES, int]
|
||||
var realCorrect, realTotal = 0
|
||||
var shufHist: array[TM_CLASSES, int]
|
||||
var shufChosen: array[TM_CLASSES, int]
|
||||
var shufCorrect, shufTotal = 0
|
||||
for name in RealFixtures:
|
||||
let p = RepoRoot / "tools" / "fixtures" / (name & ".jsonl")
|
||||
if not fileExists(p): continue
|
||||
let f = loadFixture(p)
|
||||
block:
|
||||
let g = new(TmPatternGun)
|
||||
g[] = initTmRadialGun()
|
||||
randomize(1)
|
||||
discard measureGun(f,
|
||||
proc(state: WorldState, bulletSpeed: float): GunPrediction = g[].predict(state, bulletSpeed),
|
||||
proc(e: FeedbackEvent) = g[].onResult(e), bmPoint, 0, 0)
|
||||
realCorrect += g[].radCorrect
|
||||
realTotal += g[].radTotal
|
||||
for c in 0..<TM_CLASSES: realHist[c] += g[].radLabelHist[c]
|
||||
for c in 0..<TM_CLASSES: realChosen[c] += g[].radChosenHist[c]
|
||||
block:
|
||||
let g = new(TmPatternGun)
|
||||
g[] = initTmRadialGun()
|
||||
g[].shuffleLabels = true
|
||||
randomize(1)
|
||||
discard measureGun(f,
|
||||
proc(state: WorldState, bulletSpeed: float): GunPrediction = g[].predict(state, bulletSpeed),
|
||||
proc(e: FeedbackEvent) = g[].onResult(e), bmPoint, 0, 0)
|
||||
shufCorrect += g[].radCorrect
|
||||
shufTotal += g[].radTotal
|
||||
for c in 0..<TM_CLASSES: shufHist[c] += g[].radLabelHist[c]
|
||||
for c in 0..<TM_CLASSES: shufChosen[c] += g[].radChosenHist[c]
|
||||
var realMaj, shufMaj, realN, shufN = 0
|
||||
var rs, ss, rc, sc = ""
|
||||
for c in 0..<TM_CLASSES:
|
||||
realMaj = max(realMaj, realHist[c]); realN += realHist[c]
|
||||
shufMaj = max(shufMaj, shufHist[c]); shufN += shufHist[c]
|
||||
rs.add $realHist[c] & " "
|
||||
ss.add $shufHist[c] & " "
|
||||
rc.add $realChosen[c] & " "
|
||||
sc.add $shufChosen[c] & " "
|
||||
echo fmt" real: radAcc={realCorrect}/{realTotal} = {realCorrect.float/max(1,realTotal).float*100:.1f}% " &
|
||||
fmt"majority={realMaj.float/max(1,realN).float*100:.1f}% labels=[{rs}] chosen=[{rc}]"
|
||||
echo fmt" shuffled: radAcc={shufCorrect}/{shufTotal} = {shufCorrect.float/max(1,shufTotal).float*100:.1f}% " &
|
||||
fmt"majority={shufMaj.float/max(1,shufN).float*100:.1f}% labels=[{ss}] chosen=[{sc}]"
|
||||
|
||||
when isMainModule:
|
||||
main()
|
||||
@@ -139,15 +139,18 @@ proc testParseMembership() =
|
||||
|
||||
proc testDefaultsAllBoth() =
|
||||
clearRackEnv()
|
||||
# The shipped table is all-`both` EXCEPT the new TM pattern gun (id 14),
|
||||
# which defaults to `off` so it cannot alter the default selection.
|
||||
var allBoth = true
|
||||
for m in DefaultRackMembership:
|
||||
if m != rmBoth: allBoth = false
|
||||
check "defaults: the shipped membership table is all-`both`", allBoth
|
||||
for i in 0..<RackGunNames.len:
|
||||
let want = if i == 14: rmOff else: rmBoth
|
||||
if DefaultRackMembership[i] != want: allBoth = false
|
||||
check "defaults: the shipped table is all-`both` except TMPATTERN=off", allBoth
|
||||
let loaded = loadRackMembership()
|
||||
check "defaults: with a clean environment loadRackMembership() == shipped table",
|
||||
loaded == DefaultRackMembership
|
||||
check "defaults: RackGunNames covers the shipped 14-gun rack",
|
||||
RackGunNames.len == 14 and DefaultRackMembership.len == 14
|
||||
check "defaults: RackGunNames covers the shipped 15-gun rack",
|
||||
RackGunNames.len == 15 and DefaultRackMembership.len == 15
|
||||
|
||||
proc testEnvOverrides() =
|
||||
clearRackEnv()
|
||||
@@ -161,8 +164,8 @@ proc testEnvOverrides() =
|
||||
m[9] == rmOnlyMelee
|
||||
check "env: TR_RACK_HEADON=off removes only HeadOn (id 0)",
|
||||
m[0] == rmOff
|
||||
check "env: every untouched gun keeps the all-`both` default",
|
||||
m[1] == rmBoth and m[3] == rmBoth and m[13] == rmBoth
|
||||
check "env: every untouched gun keeps its shipped default",
|
||||
m[1] == rmBoth and m[3] == rmBoth and m[13] == rmBoth and m[14] == rmOff
|
||||
putEnv("TR_RACK_KNN", "banana")
|
||||
check "env: unknown values are ignored, not fatal",
|
||||
loadRackMembership()[12] == rmBoth
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
## MINIMAL LIVE CHECK for the rack-registered TM pattern gun (id 14).
|
||||
##
|
||||
## Proves the new gun is a first-class rack member that the LIVE ModularBot
|
||||
## actually predicts, spawns and can be forced alone with env vars only:
|
||||
##
|
||||
## TR_RACK_HEADON=off ... TR_RACK_TMSELECT=off TR_RACK_TMPATTERN=both
|
||||
##
|
||||
## One 1-round battle (deliberately minimal — the full A/B runs in a later job).
|
||||
## Reads /tmp/tm_pattern_rack_stats.jsonl (the bot's per-round gun dump) and
|
||||
## asserts gun 14 was selected and spawned virtual bullets while every other gun
|
||||
## was deselected.
|
||||
##
|
||||
## Run with:
|
||||
## TR_SERVER_JAR=... TR_BATTLE_RUNNER=... \
|
||||
## nim c -r common_libs/tests/test_tm_pattern_rack_live.nim
|
||||
##
|
||||
## Skips (exit 0) when the JARs are absent, matching the other live tests.
|
||||
|
||||
import std/[os, json, strutils, tables]
|
||||
import test_framework/test_framework
|
||||
|
||||
const
|
||||
repoRoot = currentSourcePath().parentDir.parentDir.parentDir
|
||||
modularBotDir = repoRoot / "ModularBot_garage"
|
||||
adversaryDir = repoRoot / "common_libs" / "test_framework" / "adversaries" / "SittingDuck"
|
||||
statsPath = "/tmp/tm_pattern_rack_stats.jsonl"
|
||||
|
||||
const TmPatternId = 14
|
||||
const RackGunNames = ["HEADON", "LINEAR", "TSETLIN", "CIRCULAR", "GUESSFACTOR",
|
||||
"PATTERN", "WALLBOUNCE", "ACCEL", "STOPSHOT", "DISPLACE", "AVGLEAD",
|
||||
"DECAYGF", "KNN", "TMSELECT", "TMPATTERN"]
|
||||
|
||||
var failures = 0
|
||||
proc check(name: string, ok: bool) =
|
||||
if ok: echo "PASS: ", name
|
||||
else: echo "FAIL: ", name; inc failures
|
||||
|
||||
proc lastRound(path: string): JsonNode =
|
||||
result = nil
|
||||
for line in lines(path):
|
||||
let s = line.strip()
|
||||
if s.len == 0: continue
|
||||
let node = parseJson(s)
|
||||
if node.hasKey("guns"): result = node
|
||||
|
||||
proc main() =
|
||||
let serverJar = getEnv("TR_SERVER_JAR", "")
|
||||
let runnerJar = getEnv("TR_BATTLE_RUNNER", "")
|
||||
if serverJar.len == 0 or runnerJar.len == 0:
|
||||
echo "Skipping: TR_SERVER_JAR / TR_BATTLE_RUNNER not set"
|
||||
quit(0)
|
||||
if not fileExists(serverJar) or not fileExists(runnerJar):
|
||||
echo "Skipping: TR JARs not found (server=", serverJar, ", runner=", runnerJar, ")"
|
||||
quit(0)
|
||||
if not fileExists(modularBotDir / "src" / "ModularBot.nim"):
|
||||
echo "Skipping: ModularBot source not found"
|
||||
quit(0)
|
||||
|
||||
if fileExists(statsPath): removeFile(statsPath)
|
||||
|
||||
# Force the new gun alone. Every other rack entry is removed; TMPATTERN is
|
||||
# admitted in both racks. With no other candidate, the selector must pick it.
|
||||
for i, name in RackGunNames:
|
||||
putEnv("TR_RACK_" & name, if i == TmPatternId: "both" else: "off")
|
||||
putEnv("GUN_VBULLET_METRIC", "point") # the metric the radial win lives on
|
||||
putEnv("GUN_STATS_PATH", statsPath)
|
||||
defer:
|
||||
for name in RackGunNames: delEnv("TR_RACK_" & name)
|
||||
delEnv("GUN_VBULLET_METRIC")
|
||||
delEnv("GUN_STATS_PATH")
|
||||
|
||||
echo "=== live: ModularBot (only TMPATTERN racked) vs SittingDuck, 1 round ==="
|
||||
let battle = runBattle(@[modularBotDir, adversaryDir], rounds = 1,
|
||||
timeout = 180000, maxSpeed = true)
|
||||
for res in battle.results:
|
||||
echo " ", res.name, " rank=", res.rank, " score=", res.totalScore
|
||||
|
||||
if not fileExists(statsPath):
|
||||
echo "FAIL: no gun_stats dump at ", statsPath
|
||||
quit(1)
|
||||
let round = lastRound(statsPath)
|
||||
check "gun_stats.jsonl has a round record", round != nil
|
||||
if round == nil: quit(1)
|
||||
|
||||
var tmShots = 0
|
||||
var tmSelected = 0
|
||||
var otherSelected = 0
|
||||
for g in round["guns"]:
|
||||
let id = g["id"].getInt()
|
||||
if id == TmPatternId:
|
||||
tmShots = g["vShots"].getInt()
|
||||
tmSelected = g["selected"].getInt()
|
||||
elif g["selected"].getInt() > 0:
|
||||
otherSelected += g["selected"].getInt()
|
||||
|
||||
echo "gun 14 (TMPattern): vShots=", tmShots, " selected=", tmSelected,
|
||||
" other-gun selections=", otherSelected
|
||||
check "the live loop SPAWNED virtual bullets for gun 14 (vShots > 0)", tmShots > 0
|
||||
check "the live selector SELECTED gun 14 (selected > 0)", tmSelected > 0
|
||||
check "no other gun was selected while TMPATTERN was forced alone",
|
||||
otherSelected == 0
|
||||
|
||||
if failures > 0:
|
||||
echo "\n", failures, " check(s) FAILED"
|
||||
quit(1)
|
||||
echo "\nTM pattern rack live check passed."
|
||||
|
||||
when isMainModule:
|
||||
main()
|
||||
@@ -0,0 +1,197 @@
|
||||
## Offline guard for the TM pattern gun rack registration (id 14) and the
|
||||
## default-path parity proof.
|
||||
##
|
||||
## Covers:
|
||||
## * RackGunNames / DefaultRackMembership carry the new gun, defaulting `off`;
|
||||
## * `TR_RACK_TMPATTERN` parses both|1v1|melee|off like every other gun;
|
||||
## * the DEFAULT membership table admits exactly the OLD 14-gun rack, so the
|
||||
## selection sequence is identical to the pre-change all-`both` rack, RNG
|
||||
## draw for RNG draw, through both `bestGun` and the live `selectGun`;
|
||||
## * `initTmRadialGun()` selects the radial target mode;
|
||||
## * the deferred-label fix (Task 2) resolves every fired virtual bullet:
|
||||
## a radial-mode replay ends with `labelMisses == 0` and non-zero training.
|
||||
##
|
||||
## Run: nim c -r common_libs/tests/test_tm_pattern_registration.nim
|
||||
|
||||
import std/[random, tables, os]
|
||||
import gun_harness/gun_interface
|
||||
import gun_harness/virtual_bullets
|
||||
import gun_harness/selector
|
||||
import gun_harness/offline_range
|
||||
import guns/tm_pattern
|
||||
|
||||
const TmPatternId = 14
|
||||
|
||||
proc toSeq(r: HSlice[int, int]): seq[int] =
|
||||
for i in r.a..r.b: result.add i
|
||||
|
||||
var failures = 0
|
||||
proc check(name: string, ok: bool) =
|
||||
if ok: echo "PASS: ", name
|
||||
else: echo "FAIL: ", name; inc failures
|
||||
|
||||
proc recordHit(fw: var FitnessWindow, hit: bool) =
|
||||
fw.hits[fw.head] = hit
|
||||
fw.head = (fw.head + 1) mod WindowSize
|
||||
inc fw.count
|
||||
|
||||
proc seedWindow(t: var VirtualTracker, targetId, gunId, binIdx, hits, misses: int) =
|
||||
if targetId notin t.fitness:
|
||||
t.fitness[targetId] = newSeq[GunFitness](t.numGuns)
|
||||
var fw = addr t.fitness[targetId][gunId].bins[binIdx]
|
||||
for _ in 0..<hits: recordHit(fw[], true)
|
||||
for _ in 0..<misses: recordHit(fw[], false)
|
||||
|
||||
proc seedOldRack(t: var VirtualTracker, targetId: int) =
|
||||
## 14 deterministic rates for the old rack.
|
||||
seedWindow(t, targetId, 0, 0, 60, 40)
|
||||
seedWindow(t, targetId, 1, 0, 40, 60)
|
||||
seedWindow(t, targetId, 2, 0, 80, 20)
|
||||
seedWindow(t, targetId, 3, 0, 55, 45)
|
||||
seedWindow(t, targetId, 4, 0, 20, 80)
|
||||
seedWindow(t, targetId, 5, 0, 70, 30)
|
||||
seedWindow(t, targetId, 6, 0, 45, 55)
|
||||
seedWindow(t, targetId, 7, 0, 65, 35)
|
||||
seedWindow(t, targetId, 8, 0, 30, 70)
|
||||
seedWindow(t, targetId, 9, 0, 50, 50)
|
||||
seedWindow(t, targetId, 10, 0, 35, 65)
|
||||
seedWindow(t, targetId, 11, 0, 75, 25)
|
||||
seedWindow(t, targetId, 12, 0, 25, 75)
|
||||
seedWindow(t, targetId, 13, 0, 52, 48)
|
||||
|
||||
# ── registration table ────────────────────────────────────────────────────────
|
||||
|
||||
proc testTable() =
|
||||
check "rack: RackGunNames has 15 entries", RackGunNames.len == 15
|
||||
check "rack: the new gun is named TMPATTERN at id 14",
|
||||
RackGunNames[TmPatternId] == "TMPATTERN"
|
||||
check "rack: the new gun defaults to `off`",
|
||||
DefaultRackMembership[TmPatternId] == rmOff
|
||||
var oldAllBoth = true
|
||||
for i in 0..<TmPatternId:
|
||||
if DefaultRackMembership[i] != rmBoth: oldAllBoth = false
|
||||
check "rack: every pre-existing gun still defaults to `both`", oldAllBoth
|
||||
|
||||
proc testDefaultAdmitsOldRack() =
|
||||
check "default membership admits exactly the old 14-gun rack (1v1)",
|
||||
admittedGuns(15, rm1v1, DefaultRackMembership) ==
|
||||
toSeq(0..13)
|
||||
check "default membership admits exactly the old 14-gun rack (melee)",
|
||||
admittedGuns(15, rmMelee, DefaultRackMembership) ==
|
||||
toSeq(0..13)
|
||||
|
||||
proc testEnvOverride() =
|
||||
for name in RackGunNames: delEnv("TR_RACK_" & name)
|
||||
putEnv("TR_RACK_TMPATTERN", "both")
|
||||
check "env: TR_RACK_TMPATTERN=both admits gun 14",
|
||||
loadRackMembership()[TmPatternId] == rmBoth
|
||||
putEnv("TR_RACK_TMPATTERN", "1v1")
|
||||
check "env: TR_RACK_TMPATTERN=1v1 parses",
|
||||
loadRackMembership()[TmPatternId] == rmOnly1v1
|
||||
putEnv("TR_RACK_TMPATTERN", "melee")
|
||||
check "env: TR_RACK_TMPATTERN=melee parses",
|
||||
loadRackMembership()[TmPatternId] == rmOnlyMelee
|
||||
putEnv("TR_RACK_TMPATTERN", "off")
|
||||
check "env: TR_RACK_TMPATTERN=off parses",
|
||||
loadRackMembership()[TmPatternId] == rmOff
|
||||
delEnv("TR_RACK_TMPATTERN")
|
||||
|
||||
# ── default-path parity (RNG draw for RNG draw) ───────────────────────────────
|
||||
|
||||
proc testDefaultSelectionParity() =
|
||||
## The OLD rack: 14 guns, all-`both`, empty membership (pre-change call).
|
||||
## The NEW rack: 15 guns, DEFAULT membership (gun 14 off). Selection must be
|
||||
## identical draw for draw, because gun 14 is filtered out before any RNG use.
|
||||
var oldT = initTracker(14)
|
||||
seedOldRack(oldT, 7)
|
||||
var newT = initTracker(15)
|
||||
seedOldRack(newT, 7)
|
||||
# Give gun 14 data too: it must still be excluded by the default membership.
|
||||
seedWindow(newT, 7, TmPatternId, 0, 100, 0)
|
||||
|
||||
randomize(20250922)
|
||||
var oldSeq: seq[int]
|
||||
for _ in 0..<400: oldSeq.add oldT.bestGun(7)
|
||||
randomize(20250922)
|
||||
var newSeq: seq[int]
|
||||
for _ in 0..<400:
|
||||
newSeq.add newT.bestGun(7, rackMode = rm1v1, membership = DefaultRackMembership)
|
||||
check "parity: default 15-gun bestGun == old 14-gun rack, RNG draw for draw",
|
||||
oldSeq == newSeq
|
||||
check "parity: gun 14 is never selected under the default membership",
|
||||
TmPatternId notin newSeq
|
||||
|
||||
# Same through the live hysteresis path.
|
||||
var oldH = initTracker(14); seedOldRack(oldH, 7)
|
||||
var newH = initTracker(15); seedOldRack(newH, 7)
|
||||
seedWindow(newH, 7, TmPatternId, 0, 100, 0)
|
||||
randomize(4242)
|
||||
var oldHSeq: seq[int]
|
||||
for tick in 0..<400: oldHSeq.add oldH.selectGun(7, tick = tick)
|
||||
randomize(4242)
|
||||
var newHSeq: seq[int]
|
||||
for tick in 0..<400:
|
||||
newHSeq.add newH.selectGun(7, tick = tick, rackMode = rm1v1,
|
||||
membership = DefaultRackMembership)
|
||||
check "parity: default 15-gun selectGun == old 14-gun rack, RNG draw for draw",
|
||||
oldHSeq == newHSeq
|
||||
|
||||
# Forcing TMPATTERN alone DOES change selection (it is forceable).
|
||||
var forced = initTracker(15)
|
||||
seedOldRack(forced, 7)
|
||||
seedWindow(forced, 7, TmPatternId, 0, 100, 0)
|
||||
var onlyTmp: array[15, RackMembership]
|
||||
for i in 0..<15: onlyTmp[i] = rmOff
|
||||
onlyTmp[TmPatternId] = rmBoth
|
||||
check "force: with every other gun off, TMPATTERN is the only candidate",
|
||||
admittedGuns(15, rm1v1, onlyTmp) == @[TmPatternId]
|
||||
randomize(9)
|
||||
var forcedSeq: seq[int]
|
||||
for _ in 0..<50:
|
||||
forcedSeq.add forced.bestGun(7, rackMode = rm1v1, membership = onlyTmp)
|
||||
var allTmp = true
|
||||
for g in forcedSeq:
|
||||
if g != TmPatternId: allTmp = false
|
||||
check "force: the forced-alone rack always returns TMPATTERN", allTmp
|
||||
|
||||
# ── radial mode + deferred-label fix ─────────────────────────────────────────
|
||||
|
||||
proc testRadialInit() =
|
||||
let g = initTmRadialGun()
|
||||
check "init: initTmRadialGun() selects the radial target mode",
|
||||
g.targetMode == tmRadial
|
||||
|
||||
proc testDeferredLabel() =
|
||||
## Drive a radial-mode gun over a synthetic fixture and prove the deferred
|
||||
## label resolves every bullet: no label misses, and training happened.
|
||||
let fx = synthesizeOscillator(ticks = 400)
|
||||
var g2 = initTmRadialGun()
|
||||
randomize(3)
|
||||
var tracker = initTracker(1, bmPoint)
|
||||
for state in fx.states:
|
||||
var preds: array[len(PowerBins), GunPrediction]
|
||||
for b in 0..<len(PowerBins):
|
||||
preds[b] = g2.predict(state, bulletSpeed(PowerBins[b]))
|
||||
tracker.spawnBullets(0, preds, state, fx.enemyId)
|
||||
var et: Table[int, tuple[x, y: float, lastSeenTick: int, alive: bool]]
|
||||
et[fx.enemyId] = (x: state.enemyX, y: state.enemyY,
|
||||
lastSeenTick: state.tick, alive: true)
|
||||
tracker.tickBullets(state, et,
|
||||
proc(gunId: GunId, binIdx: int, e: FeedbackEvent) = g2.onResult(e))
|
||||
check "deferred: a radial replay trains (totalObs > 0)", g2.totalObs > 0
|
||||
check "deferred: no label misses remain (labelMisses == 0)", g2.labelMisses == 0
|
||||
check "deferred: the pending queue never overflowed", g2.pendingDropped == 0
|
||||
check "deferred: the radial head is scored above chance (radTotal > 0)",
|
||||
g2.radTotal > 0 and g2.radCorrect > 0
|
||||
|
||||
when isMainModule:
|
||||
testTable()
|
||||
testDefaultAdmitsOldRack()
|
||||
testEnvOverride()
|
||||
testDefaultSelectionParity()
|
||||
testRadialInit()
|
||||
testDeferredLabel()
|
||||
if failures > 0:
|
||||
echo "\n", failures, " check(s) FAILED"
|
||||
quit(1)
|
||||
echo "\nAll TM pattern registration checks passed."
|
||||
Reference in New Issue
Block a user