Commit Graph

94 Commits

Author SHA1 Message Date
SirStone 7f706e5b14 fix(guns): GF family aimed at the wrong RADIUS, not the wrong angle
The entire GuessFactor family scored 0% on clean circular and wall-bounce
trajectories. Two hypotheses were on the table and BOTH were wrong:

- MEA range too narrow / edge clamping: REFUTED. Measured 0 clamped shots
  out of 837/849/957, required offsets peak at ~33 deg against MEA
  28.1-46.7 deg, and the 8 in arcsin(8/bulletSpeed) is correct (it is the max
  robot SPEED, not the hit radius). Changing it to BotRadius=18 would have
  coarsened resolution for nothing.
- Peak selection: REFUTED. A sweep of every constant GF value showed the
  ORACLE-BEST constant offset on the original gun was only 6% circular,
  4% wall-bounce, 7.5% random-walk. No peak choice could have done better.
  The learning path was fine too: ~850-960 observations per fixture, 0
  starved waves, well-populated histograms.

REAL CAUSE: the GF family aimed at the FIRE-TIME distance. The virtual-bullet
metric resolves a bullet at the AIM-POINT distance and scores that single
point against the enemy's position on that tick, so with any radial target
motion the bullet stops at the wrong radius and misses even with a perfect
angle. Angle-only prediction is structurally unscoreable under this metric.

FIX: give the GF family a self-consistent constant-velocity forecast as its
base reference (new common_libs/guns/lead_forecast.nim, which iterates the
flight time to the same fixed point circular.nim uses), so the histogram
learns the RESIDUAL against that forecast and the aim point lands at the
right radius. Applied to guess_factor, decay_gf and knn_gun.

Same defect fixed in Linear: it did a one-shot dist/bulletSpeed extrapolation
and never iterated its flight time.

The oracle sweep proves the structural fix, independently of tuning: the best
achievable constant GF moved 6% -> 20% (circular), 4% -> 57% (wall-bounce),
7.5% -> 49% (random-walk).

MEASURED, all 15 fixtures: total 39.0% -> 44.4% (30399 -> 34654 hits).
  circular       GF 6 -> 23,   DecayGF 6 -> 21
  wall-bounce    GF 0 -> 60.2, DecayGF 0 -> 60.2
  constant-vel   GF 26 -> 100, DecayGF 26 -> 100, KNN 26 -> 100, Linear 87 -> 100
  random-walk    GF 0 -> 53,   DecayGF 0 -> 52,  Linear 24 -> 53
  StraightLine   GF 8 -> 77,   DecayGF 8 -> 77
Non-regression: 33 guard checks pass, the range's 12/12 offline==online
acceptance still PASSES, tsetlin tests green, live gauntlet 5/5.

HONEST TRADE-OFF, recorded rather than hidden: on the 5 real DrussGT
wave-surfing captures the GF family REGRESSES - GuessFactor 108 -> 55,
DecayGF 108 -> 76, KNN 101 -> 74 hits per 2000. The linear base is a poor
model for a surfer, so the residual histogram is noisier than the old
total-lead histogram. Linear itself improved there (95 -> 105). The synthetic
range and the live gauntlet both improved, and the structural bug is provably
fixed, so this was judged worth the cost - but recovering the DrussGT
regression is the next job, not something to wave away.
2026-09-21 00:56:02 +02:00
SirStone d5061ee215 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%.
2026-09-21 00:14:46 +02:00
SirStone 89370008da fix(tsetlin): make the TM actually learn - saturation 714 -> 13.8 literals/clause
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.
2026-09-20 23:59:32 +02:00
SirStone 974528d5cf feat(gun_harness): offline gun range, proven equivalent to live play
Gun evaluation previously required a full end-to-end battle (Java server +
battle runner + websocket IPC to 2 bot processes, 50 rounds, ~3.4 min) and
yielded only ~300-900 REAL shots across 13 guns -- far too few to rank
guns, which is why tuning needed many repetitions.

VirtualTracker is already a pure function of (WorldState stream, gun list);
the only reason it needed Java was where WorldState came from. So the range
replays a seq[WorldState] through the SAME tracker: offline and online
scores are the same metric by construction, not an approximation.

ACCEPTANCE TEST (the point of the whole thing): record one live round, replay
it offline, compare per-gun virtual hit rates. 12/12 deterministic guns match
EXACTLY, reproduced twice. Tsetlin is compared separately because tmLearnOne
calls rand(). Getting to 12/12 exposed two real ordering quirks in the live
loop: run() calls go() before the aim/fire block, so tickBullets resolves
against the NEXT tick's scan while the prediction used the previous one; and
if the target dies during that go() the final tick's spawn+resolution is
skipped entirely. The recorder emits an end marker for the second case.
The 5th (selected-gun) predict call was verified to be a no-op.

Measured cost: 8 fixtures (1770 ticks, ~92k virtual bullets, 13 guns) replay
in 2.9 s, ~32k virtual bullets/s -- roughly 70x faster and 100x more samples
than a live gauntlet.

Also adds a per-tick WorldState recorder behind const RecordWorldState
(default off, mirrors the ShotLog idiom) which records the state the bot
ACTUALLY builds, staleness included, rather than true positions -- recording
the latter would hand the guns perfect information and produce flattering
scores.

9 new guard checks (33 total, all passing), including fixture round-trip,
replay determinism, stationary->HeadOn 100%, constant-velocity->Linear>HeadOn,
and the energy-threshold turner crossing at t=41.
2026-09-20 23:44:42 +02:00
SirStone 3c90a5941d feat(selector): range-aware firing gate fitted to 2611 measured shots
Measured, not assumed. With the gate temporarily opened to 20 deg, every
real shot was logged (tick, angle error at fire time, distance, power,
hit) across 3 gauntlets: 2611 shots, 57.3% aggregate. Findings:

- The geometric cone atan(BotRadius/d) is directionally confirmed but a
  WEAK lever: even at 0.0-0.1 deg error the hit rate at 400-600px is only
  ~53-57%, because PREDICTION error dominates alignment error.
- Real effect of tightening the gate: 57.9% -> 68.0% aggregate hit rate
  (fixed 0.1 deg), not the 76.9% previously reported -- that was a
  high-variance draw (per-rep 62.8/66.4/77.2%).
- The shipped range-aware gate (SafetyFactor 0.6) does NOT beat the fixed
  2.0 deg gate on hit rate (55.8% vs 57.9%, ~1.5 sigma, inside noise). It
  fires 22-28% more shots and therefore lands more total hits (~509 vs
  ~434 per rep). No per-adversary score delta exceeded the 300-point
  run-to-run noise band, so no config is demonstrably better on score.

Shipped anyway because it is strictly more expressive (a fixed threshold is
the special case), tunable from one const, and physically motivated, but
the honest verdict is recorded in-code: the gate is not the bottleneck.

AimThresholdDeg is removed; shouldFire now takes distPx. Degenerate or NaN
distance falls back to the ceiling rather than dividing by zero.

Also adds a per-shot logger to ModularBot behind 'const ShotLog' so the
measurement above is reproducible, and 10 new guard checks (24 total, all
passing) covering monotonicity, clamping, formula, perfect alignment,
gross misalignment and degenerate distance.

Cross-checked against the server source: the gun fires BEFORE the turn is
applied, so the logged angle error is the true departure error, and
fireAssist auto-aim is off (unset by the Nim API and forced false by
setAdjustRadarForGunTurn).
2026-09-20 23:28:31 +02:00
SirStone e53690036b fix(guns): speed-sensitive caches, dead stop-shot branch, exact TM trace pairing
Four guns cached a whole prediction per tick while predict() is called once
per power bin, so every bin after the first (and the real fired shot, which
shares lastState) reused the power-1.0 lead. Fixed by caching only the
speed-INDEPENDENT derived state and recomputing the lead per requested speed:
- stop_shot: also fixes prevSpeed being written before it was read, which
  made abs(speed) < abs(prev) permanently false and the entire
  stop-prediction branch unreachable (it was just Linear).
- displacement: the cache key included bulletSpeed, so the guard missed on
  all four bins and the 15-tick window advanced ~4x/tick, making the
  inferred velocity ~4x too small.
- averaged_lead: tick cache removed outright. pattern_matcher: split into
  speed-independent match+path and per-call lead.

FeedbackEvent gains fireTick/powerBin (additive; only virtual_bullets
constructs one) so guns can pair feedback to the exact shot instead of
guessing by coordinates. tsetlin uses it: traces are now keyed exactly by
(fireTick, powerBin) with a 1024-slot ring, and the 10-frame window shifts
at most once per tick (it was shifting ~4-5x/tick, so isWarmedUp tripped
after ~2 ticks).

KNOWN INCOMPLETE: tsetlin still does not diverge from Linear in battle. The
two named bugs are fixed (a 600-tick sim shows trainedShots=2141,
traceMisses=0, and a fixed-input probe converges to a 9.6px correction), but
the TM's clause feedback itself is broken: ~131 of 1740 literals end up
included per clause, so its conjunction never fires. Sweeping TM_S,
TM_N_CLAUSES and a two-branch Type-I update did not change the correction
from 0. Needs a real TM fix or removal, not another bug fix.

First-ever guard tests for the gun selector: common_libs/tests/
test_gun_harness.nim (14 checks, headless, no Java). There were none before,
which is how six broken guns survived a full analysis cycle. Against the
previous HEAD, 5 of these checks FAIL - that is the regression guard.
2026-09-20 22:47:26 +02:00
SirStone 0cc682152d fix(guns): per-bin wave queues unbreak GF/DecayGF/KNN learning; fix vbullet drops
Wave queues (guess_factor, decay_gf, knn_gun): predict() stored ONE wave
per tick while onResult() popped one per resolved bullet (~4/tick), so the
queue drained to empty within a few dozen ticks, ~3 of every 4 resolutions
returned without learning, and the survivor paired with a same-tick wave
(bearingDelta ~= 0) pinning the histogram at centre. PROOF: GF.vHits ==
HeadOn.vHits and DecayGF.vHits == HeadOn.vHits byte-for-byte in every one
of 50 rounds — the guns had degenerated to HeadOn.

Now each gun keeps a per-bin FIFO with an O(1) head cursor. At most one
push per (tick, bin) so the fire site's 5th predict() call is a no-op, and
onResult pops the oldest wave of its OWN bin via e.bulletPower. Aiming
math untouched (it was already correct: 0 deg = East, CCW+).

maxBullets 2048 -> 8192: the rack spawns 52 bullets/tick so the ring wrapped
every ~39 ticks while a long power-3 shot needs ~90, silently discarding
unresolved bullets and biasing every measured hit rate by range. Added a
droppedBullets counter so a future overflow is measurable, and wavePushes/
waveStarved counters on the three guns. After the fix: vDropped = 0 and
vStarved = 0 across all 48 recorded rounds.

fitnessFor is now exported, deterministic (enemies iterated in ascending id
order) and shared by the selector and the stats dump, replacing a hand-rolled
merge in ModularBot that never advanced its window head.

Round lines gain additive keys: vDropped, vStarved.
2026-09-20 22:27:52 +02:00
SirStone 26b66cbb24 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.
2026-09-20 22:15:42 +02:00
SirStone 343e631633 fix(gun_harness): random tiebreak + drop AntiSurfer + raise MinObsBeforeCompete
- bestGun: replace first-index-wins argmax with random pick among guns
  within TieMargin (2%) of best rate. HeadOn at index 0 was silently
  winning every tie, starving Tsetlin/Linear/etc.
- MinObsBeforeCompete 15 -> 50 (Pattern entered competition on noise)
- add MinHitRateFloor 0.10: if no gun clears it, fall back to HeadOn
  instead of selecting the best of a bad field
- ModularBot: remove AntiSurfer gun (0% virtual hit rate everywhere),
  14 -> 13 guns, renumber ids and selection counters
2026-09-20 22:07:49 +02:00
SirStone c034eb9d25 feat(testing): gun rack gauntlet + analysis reports
- fix(ModularBot): onBulletHitBot → onBulletHit (real hits were never tracked)
- feat(ModularBot): per-round gun stats dump to /tmp/gun_stats.jsonl
- feat(ModularBot): gun selection counter per round
- fix(tests): adversary paths _garage suffix removed from 7 test files
- feat(tests): test_gauntlet_5bots.nim — 10-round gauntlet vs all 5 adversaries
- feat(tests): analyze_gun_stats.nim — JSONL parser for gun performance tables
- docs: gun_rack_analysis.md — full per-gun performance report
- docs: gun_rack_summary.md — TL;DR verdict table (keep/drop/tune)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-09-20 21:14:12 +02:00
SirStone 2cc2a3bd87 fix(ModularBot): ram loop prevention, dead-target guards, cleaner logging
- 30-tick cooldown after ghost-stuck/timeout ram exit prevents re-entry loop
- enemy_tracker.update() skips dead bots to prevent same-tick scan resurrection
- TFIL graphics cleared when ramming is active movement
- [config] logs: white base with green-highlighted changes only
- [ram:enter] logs trigger reason and key values on false→true transition
- [death] and [target-invalid] logs retained for diagnostics
2026-09-20 20:44:45 +02:00
SirStone 1f8574db1d fix(rammer): rewrite heading logic — correct forward/backward steering toward enemy 2026-09-20 13:43:35 +02:00
SirStone c90874affd refactor(movement): extract ram to harness — phantom meteor dodge-only, rammer module via harness decision
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-09-20 12:36:09 +02:00
SirStone ae9a5fec6d feat(movement): multi-enemy awareness — phantom meteor + minimum risk track all enemies
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-09-20 12:28:27 +02:00
SirStone f06b263ae5 feat(phantom_meteor): aggressive ram — wider thresholds, desperation mode
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-09-20 12:25:12 +02:00
SirStone ab473c6b68 feat(ModularBot): melee targeting — multi-enemy tracker, per-enemy gun fitness, radar auto-switch
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-09-20 12:25:10 +02:00
SirStone 30d00ba163 feat(movement): dodge timing, wall avoidance, distance control, ram finisher
PhantomMeteor:
- Ram finisher: charge at enemy when <200px and their energy <10
- Ram opportunity: charge when <60px and we have >20 energy advantage
- Integrated gunheat tracker for 1-2 tick earlier wave detection
- Distance control: smooth linear ramp toward preferred engagement distance
- Phantom range expanded 150→250px to catch closer threats

WaveSurfer:
- Wall-aware dodge bin selection: penalize bins leading off-arena
- Dodge timing: predict future position 15 ticks ahead for safety
- Distance control: radial blend when outside deadband (350±50px)
- Wall escape: invert strafe if pushing further into wall, blend toward center

ModularBot:
- Wired KNN gun (purple/magenta)
- Shadows tracked for movement (safer GF prediction)
- Bullet lifecycle management (onBulletFired/onBulletHitBot/onBulletHitWall)
- Unified phantom_meteor movement (wave_surfer unplugged)
- Config logging on round start + gun switch

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-09-20 11:59:05 +02:00
SirStone 5bbc8cbda7 fix(gun_harness): min 15 obs before competing, window 50→100, KNN min k=5
Gun selector now gates competition: guns with <15 total observations across
all power bins sit out until at least one gun qualifies. Falls back to ungated
selection if no gun reaches threshold, preventing cold-start stalls.

Sliding window increased from 50 to 100 ticks to reduce switching noise.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-09-20 11:59:00 +02:00
SirStone 651ce80620 feat(ModularBot): KNN gun, gunheat tracker, bullet shadows — inspired by DrussGT
New DrussGT-inspired modules:
- KNNGun: K-nearest-neighbor statistical targeting using GF density peaks
- GunheatTracker: dual-heat system (predicted + confirmed) for 1-2 tick lead
- ShadowTracker: computes GF regions safe from in-flight bullets (enemy wave dodge)

VirtualBodyTracker now integrates gunheat for earlier fire detection and shadows
for safe-zone multiplier (90% reduction in danger zones).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-09-20 11:58:57 +02:00
SirStone 63db0a4439 refactor: strip _garage suffix from adversarial test bots
Renames PatternMover_garage → PatternMover, RandomMover_garage → RandomMover,
WaveSurfer_garage → WaveSurfer. Updates all .json, .sh, .nimble, and config.nims
files to match TR Booter naming convention (directory name = bot name).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-09-20 10:50:32 +02:00
SirStone 1eaaadf627 refactor: move adversarial test bots to common_libs/test_framework/adversaries/ 2026-09-20 10:49:31 +02:00
SirStone c0abd4c960 feat(ModularBot): virtual body movement selector — wave-based scoring replaces EMA damage
Implements VirtualBodyTracker (wave-based hit/miss scoring) instead of EMA damage accumulation. Movement switching now happens every tick, not every 3 rounds. Also refactors radar colors to dark teal (#004444/#0D4D4D) for faint visibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-09-20 10:44:49 +02:00
SirStone c94ba1f2fe feat(ModularBot): decay-GF gun (recency-weighted), 13 guns total
- Decay-GF: exponential decay on GF histogram (0.998/tick, ~350-tick half-life)
- Adapts faster to mid-battle strategy changes than standard GF
- Battle-tested vs WaveSurfer, Crazy, RandomMover
2026-09-20 01:52:22 +02:00
SirStone 3a1149359d fix(gun_harness): bump virtual bullet ring buffer 512→2048
With 12 guns × 4 power bins = 48 bullets/tick, 512 slots overflow
before slow bullets resolve (~36 ticks travel). 2048 gives headroom.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-09-20 01:49:21 +02:00
SirStone fbf9414375 feat(ModularBot): rammer movement + averaged-lead gun, 12 guns
- Ram movement: drive straight at enemy (not wired, needs selector)
- Averaged-lead gun: mean of linear+circular+wall-bounce predictions
- 12 guns total, battle-tested

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-20 01:46:52 +02:00
SirStone b373d3d505 feat(ModularBot): displacement-vector gun, 11 guns total
- Displacement gun: average velocity over sliding window, captures drift
- Complements instantaneous-velocity linear gun
- Battle-tested vs Crazy, WaveSurfer, PatternMover
2026-09-20 01:43:25 +02:00
SirStone d27efd3f9a feat(ModularBot): random-oscillator movement module + battle tests
- Random-oscillator: perpendicular strafe with randomized reversal timing (15-45 ticks)
- Not wired (needs movement selector)
- Battle-tested 10-gun bot vs RamFire, Corners, VelociBot

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-20 01:39:49 +02:00
SirStone a87be5f99f feat(ModularBot): stop-shot gun, 10 guns total
- Stop-shot gun: predicts enemy deceleration stop point
- Catches bots at direction reversal pauses
- Battle-tested vs WaveSurfer, Crazy, RandomMover

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-20 01:37:45 +02:00
SirStone 18ff2d45b3 feat(ModularBot): acceleration predictor gun, 9 guns total
- Accel gun: tracks velocity+heading changes, extrapolates with acceleration
- Handles speed-up, slow-down, and combined accel+turn
- Battle-tested vs Crazy, RandomMover, Walls
2026-09-20 01:34:27 +02:00
SirStone a3b4f26b69 feat(ModularBot): wall-bounce gun, 8 guns total
- Wall-bounce gun: linear extrapolation with arena wall reflection
- Simulates enemy tick-by-tick, bouncing off walls (billiard-ball reflection)
- 4-iteration convergence on ticksToArrive like circular gun
- 8 guns: head-on, linear, circular, tsetlin, guess-factor, pattern, anti-surfer, wall-bounce
- Turret: #CCCCCC (silver), bullet: #EEEEEE (white)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-20 01:30:21 +02:00
SirStone ebe5b1b7f1 feat(ModularBot): anti-surfer gun + wave-surf movement module
- Anti-surfer gun: inverse GF targeting for wave-surfing enemies
- Wave-surf movement: danger histogram dodge (not wired yet, needs movement selector)
- 7 guns total, battle-tested

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-20 01:28:29 +02:00
SirStone 78723fce13 fix(GFGun): head-on prior + per-bin mea in onResult
- Cold-start bug: uniform bins[0..30]=0.1 made peakBin() always return
  0 (first-wins tie), giving GF=-1 (max CW escape) before any learning.
  Fixed with a triangular head-on bump at bin 15 (GF=0) as the prior.
- onResult now recomputes mea from FeedbackEvent.bulletPower instead of
  the stale first-bin mea cached by predict; correct per-power-bin GF.
- Add DebugGF const (default false) with [gf-dbg] echoes in predict/onResult.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-20 01:05:22 +02:00
SirStone 1ed7797cb6 feat(ModularBot): 6 guns, pattern matcher, melee modules, adversarial bots
- New guns: guess-factor (GF histogram), pattern-matcher (movement tape replay)
- New modules: minimum-risk melee movement, spinning melee radar
- New test bots: PatternMover, RandomMover, WaveSurfer
- Fixed: FeedbackEvent now carries actualX/actualY for proper GF learning
- Fixed: TM gun warmup gating + directional residuals
- Fixed: circular gun integrated formula + multi-bin omega cache
- Fixed: oscillator wall-bounce lockout
- Fixed: phantom meteor perpendicular body orientation
- 6/6 battle wins across all enemy types
2026-09-20 00:59:53 +02:00
SirStone 254c7dc997 feat(ModularBot): pluggable bot with 4 guns, phantom meteor movement, radar harness
- Gun harness: virtual bullet tracker, rolling fitness, auto-selector
- Guns: head-on, linear (extrapolation), circular (integrated formula), tsetlin machine (learning)
- Movement: phantom meteor gravity engine (danger histograms, phantom bullets, fire detection)
- Radar: harness + radar_lock adapter
- Color-coded modules: turret/bullet color per gun, body per movement, scan per radar
- Beats Target, SpinBot, Crazy, TrackFire in 10-round battles
2026-09-20 00:37:10 +02:00
SirStone c9191b7afb fix(circular-gun): correct turn-rate calculation for multi-scan delays
Two bugs fixed:
1. Multi-tick gaps: turn rate assumed 1 tick between observations, but scans can be 5+ ticks apart. Now divides by actual tickDelta.
2. Per-power-bin state corruption: predict() called 4x per tick (per power bin). After first call, prevHeading was already updated, causing subsequent calls to compute 0° delta. Now captures oldHeading/oldTick before updating.

Verified: OscillatorBot at 4°/tick captured correctly; normalization [-180°,180°] works; tickDelta=1 typical; first bin gets delta, subsequent bins see 0 (expected).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-09-19 19:43:40 +02:00
SirStone be4e9549ef fix(test_framework): pass --tps -1 to server for uncapped headless speed
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-09-16 22:57:28 +02:00
SirStone 8f454285aa fix(test_framework): pass --max-speed to Java runner and drain stdout to prevent pipe stall
- Add maxSpeed param (default true) to runBattleRunner/runBattle
- Drain stdout in poll loop — Java blocked on full pipe buffer causing timeout
- TestBattleRunner.java already had --max-speed; .class was stale and needed recompile

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-16 09:02:20 +02:00
SirStone c671e0b0e3 feat(test_framework): use TR_SAMPLE_BOTS env var for external bot directory
Adds TR_SAMPLE_BOTS env var support to point to external sample bots (Walls, Fire, SpinBot, etc). Updates test_bullet_economy.nim to use Walls from the sample-bots directory instead of custom WallsBot, removing the hardcoded path dependency.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-09-16 08:43:46 +02:00
SirStone 67bba96da1 remove(test_framework): delete custom WallsBot, will use original bots 2026-09-16 08:43:16 +02:00
SirStone 94eb805d04 feat(test_framework): add WallsBot adversary for lead prediction testing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-16 08:40:26 +02:00
SirStone 7e1f1483a4 docs(test_framework): add comprehensive integration test guide and fix runner blocking issue
- Document exception handling, zero-value BotResult trap, shared adversary bots
- Add offline parsing example using parseServerOutput
- Skip tests gracefully when JARs missing (guard before suite blocks)
- Fix blocking readLine in runner_process.nim: poll with 50ms sleep + atEnd check
  (was preventing timeout enforcement, now blocks correctly during battle)
- Add test task to QBot.nimble and config.nims setup docs to AGENTS.md
- Add debug logging to TestBattleRunner for bot identity tracking

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-09-13 10:54:56 +02:00
SirStone 57a1915b10 feat: add shared integration test framework for garages (#126)
Implements:
- BattleResult type and JSON-lines parser (#127)
- TR server lifecycle manager (#128)
- Bot compiler using nim c (#129)
- runBattle() orchestrator (#130)
- Example test in OscillatorBot_garage (#131)
- Framework usage guide (#132)
- TestBattleRunner.java for external server (#133)
- BattleRunner process lifecycle (#134)

Fix: runner_process.nim was redefining TimeoutError locally; now
uses std/net.TimeoutError consistently with server_manager.nim.
2026-08-30 12:03:35 +02:00
SirStone 06e68522cf Remove deprecated components from Tank Royale Bot API
- Deleted constants.nim, event_queue.nim, graphics.nim, json_parse.nim, schemas.nim, utils.nim, and ws_client.nim files.
- This cleanup removes unused code and simplifies the codebase, focusing on essential functionalities.
2026-08-27 18:45:56 +02:00
SirStone b509195ee9 chore: rename libs→common_libs, all bot dirs to _garage suffix, fix all path refs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-27 18:18:41 +02:00