89370008da
The gun has never contributed anything: Tsetlin.vHits was byte-for-byte equal to Linear.vHits in every measured round of every run, because its learned correction was always exactly 0. Six diagnosed defects fixed, plus one that was required to make the first one work: 1. Type I now conditions on the clause output. It previously rewarded included true literals unconditionally, omitting Granmo's (c=0, lk=1) -> toward Exclude counter-force, so true literals ratcheted toward Include forever. This was the root cause of the saturation. 2. Type II was unreachable dead code: its guard required cOut==1 AND lits[lit]==0 AND st>0 (included), but cOut==1 guarantees every included literal is 1. Its direction was wrong too - it should increment EXCLUDED false literals when the clause fires. 3. Resource allocation restored: Granmo's (T - clip(v,-T,T))/(2T) target replaces |error|/(2*RESID_MAX); TM_T was only an output normaliser. 4. Label baseline fixed - the factor-2 shrink. predX = linearX + cx, so the label was delta - cx while the learner's output IS cx, giving error = delta - 2cx and a fixed point of cx = delta/2: HALF the needed correction even with perfect feedback. TmTrace now stores linearX/linearY and training uses delta. 5. Hits no longer zero their label (a hit means |miss| < 18px, not 0). 6. The enemy-energy feature was duplicated - tmEncodeFrame passed state.selfEnergy with a stale comment claiming enemyEnergy was absent, while WorldState.enemyEnergy exists. Enemy-energy rules were literally unrepresentable. 7. REQUIRED EXTRA: tmEvalClause now implements Granmo Eq. 6 - an all-Exclude clause outputs 1 during learning and 0 during classification. Without it, fix #1 deadlocks every clause at empty. MEASURED EFFECT (energy-threshold-turner fixture, seed 1): mean included literals per active clause 714.0 -> 13.8 active clauses 100/100 -> 53/100 nonzero corrections 8/764 -> 708/764 Tsetlin virtual hits (Linear = 27/400) 27/400 -> 69/400 Divergence achieved: offline on 7/8 fixtures, and in a live gauntlet (RandomMover: Tsetlin 199/1200 vs Linear 288/1200, vDropped=vStarved=0). Tsetlin now LEARNS but is not yet competitive with Linear - the regression head is untuned, flagged as follow-up rather than claimed as a win. Also ignores compiled test harnesses that have no file extension, which the existing '**/tests/test_*' rule misses.
60 lines
2.2 KiB
Nim
60 lines
2.2 KiB
Nim
## Shared helper: build the 13 ModularBot guns as type-erased offline range
|
|
## drivers, in ModularBot's gun-id order, with the same readiness gate the live
|
|
## loop uses (Tsetlin only spawns once its 10-frame window is full).
|
|
|
|
import std/random
|
|
import gun_harness/offline_range
|
|
import guns/head_on
|
|
import guns/linear
|
|
import guns/circular
|
|
import guns/tsetlin
|
|
import guns/guess_factor
|
|
import guns/pattern_matcher
|
|
import guns/wall_bounce
|
|
import guns/accel_predictor
|
|
import guns/stop_shot
|
|
import guns/displacement
|
|
import guns/averaged_lead
|
|
import guns/decay_gf
|
|
import guns/knn_gun
|
|
|
|
proc buildAllGunDrivers*(seed = -1): seq[GunDriver] =
|
|
## seed >= 0 re-seeds the global RNG after constructing Tsetlin so the
|
|
## stochastic gun's learning is reproducible for offline runs. (Its
|
|
## constructor calls randomize(); we override that seed afterwards.)
|
|
var tsetlin = initTsetlinGun()
|
|
if seed >= 0:
|
|
randomize(seed)
|
|
result = @[
|
|
makeDriver("HeadOn", HeadOnGun()),
|
|
makeDriver("Linear", LinearGun()),
|
|
makeDriver("Tsetlin", tsetlin),
|
|
makeDriver("Circular", CircularGun()),
|
|
makeDriver("GuessFactor", initGFGun()),
|
|
makeDriver("Pattern", PatternMatcherGun()),
|
|
makeDriver("WallBounce", initWallBounceGun()),
|
|
makeDriver("Accel", initAccelGun()),
|
|
makeDriver("StopShot", initStopShotGun()),
|
|
makeDriver("Displace", initDisplacementGun()),
|
|
makeDriver("AvgLead", initAveragedLeadGun()),
|
|
makeDriver("DecayGF", initDecayGFGun()),
|
|
makeDriver("KNN", initKNNGun()),
|
|
]
|
|
|
|
proc makeTsetlinDriver*(seed = -1): tuple[driver: GunDriver, gun: ref TsetlinGun] =
|
|
## Same as makeDriver("Tsetlin", ...) but keeps a handle to the concrete gun,
|
|
## so a test can read its clause-sparsity / correction instrumentation after a
|
|
## replay. `makeDriver` heap-boxes a copy internally and drops the handle.
|
|
let g = new(TsetlinGun)
|
|
g[] = initTsetlinGun()
|
|
if seed >= 0:
|
|
randomize(seed)
|
|
result.gun = g
|
|
result.driver = GunDriver(
|
|
name: "Tsetlin",
|
|
predictCb: proc(state: WorldState, bulletSpeed: float): GunPrediction =
|
|
g[].predict(state, bulletSpeed),
|
|
resultCb: proc(e: FeedbackEvent) = g[].onResult(e),
|
|
readyCb: proc(): bool = g[].isWarmedUp(),
|
|
)
|