Commit Graph

31 Commits

Author SHA1 Message Date
SirStone bd57db6d63 j133 strafe: correct the comment's measured fire count (67065, catch 98.888%->100.000%) 2026-09-26 12:25:06 +02:00
SirStone 799438bbe9 j133 strafe fire detection: correct the enemy energy delta for the server's +3*power hit bonus and our own damage, never drop a too-large drop (TR_STRAFE_FIRE_FIX, default on); catch 98.89%->100% of enemy fires on the 70-battle corpus 2026-09-26 12:18:21 +02:00
SirStone cc332138b3 j131 learned movement: real bullet-endpoint resolution (TR_LEARNED_REAL_EVENTS, default off) + exact-geometry Gate A/B (inversion NOT fixed; state still the constraint) 2026-09-26 11:54:11 +02:00
SirStone 4db46f02dd j132 strafe pick quality: chosen tile's heat in the [strafe] log line (heat=h/10.0 alt=best-other ok=0|1) and ON the tile (green/yellow/red ramp + value), legible with TR_STRAFE_HEAT_GRID=0; deterministic display check vs pathMaxHeat 2026-09-26 11:51:33 +02:00
SirStone 61def1c3e9 j130 learned movement: outcome label (P(hit|state,g)) mode + Gate A; pre-registered outcome arms 2026-09-26 11:27:03 +02:00
SirStone a436e9f16a j128 learned movement: state-conditional counted-SBC wave danger (TR_MOVEMENT=learned, default-off), offline gate + pre-registered panel arms 2026-09-26 10:42:10 +02:00
SirStone 7311aaef5a movement j119 Task A: make the shipped TFIL heat shape env-overridable (default path byte-identical) 2026-09-26 01:38:51 +02:00
SirStone 0f5cfe37b2 surf: wire the dormant wave surfer as TR_MOVEMENT=surf + fix 4 real defects
common_libs/movements/wave_surfer.nim was written in an early session and
never wired to the bot. This connects it exactly like tfil/strafe and fixes
the defects a full read found:

  1. the dodge direction was INVERTED: the perpendicular was built from the
     bot->enemy bearing while GF lives in the enemy->bot frame, so the bot
     moved toward MORE danger. Now built from the wave's origin->bot bearing:
     +90 provably increases GF.
  2. the danger histogram was never reset (resetRound cleared waves only), so
     it was a battle-long static average. Now reset to the uniform prior each
     round.
  3. fire detection tracked only the current target's energy via one scalar;
     now per-enemy (seq[(id,energy)]) so melee target switches cannot invent
     or hide waves.
  4. the wall penalty projected a point from the wave origin, not from the
     bot, making the wall test meaningless. Now projects the bot->candidate
     direction.

Wave speed uses the actual firepower (the one-tick energy drop IS the
firepower, so speed = 20 - 3*drop is exact). Adds TR_SURF_* knobs and
registers them in the env report. Shipped TR_MOVEMENT=tfil default untouched
(test_tfil_commit_env: 30/30 pass; test_env_report: pass).
2026-09-26 00:10:24 +02:00
SirStone 10723ab5f3 strafe: default range 200 -> 325 (mid of the owner's 300-350 band), with the j107 evidence noted 2026-09-25 23:56:19 +02:00
SirStone ccff7e3e4a STRAFE: curved wings + guaranteed wall/corner escape; shipped TFIL default untouched
Task j112. Two changes to the TR_MOVEMENT=strafe engine, both OFF the shipped
tfil path; the binary default is still tfil.

CURVED WINGS: the candidate set was the straight 1-D line through the bot, which
a bounded segment always terminates at a wall. It is now an adaptive parabola
with the vertex on the bot:
  point(y) = bot + yhat*y + xhat*kappa(y)*y^2
xhat is the unit vector away from the nearest wall(s) (summed inward normals, so
a corner yields the diagonal). kappa grows as the wall approaches and saturates
at TR_STRAFE_KAPPA; every wing point is clamped inside TR_STRAFE_WALL_SAFE, so
the wing FLATTENS and runs parallel to the wall instead of touching it. In open
space kappa == 0 and the wing is exactly the old straight line. The wing chord
at the reach tilts the heading band toward the interior by atan(kappa*reach)
(capped by TR_STRAFE_WING_MAX); the body still only turns slowly to follow that
tangent, never to face the target, and reversals are still setForward sign flips.

GUARANTEED ESCAPE: with every candidate over threshold the old fallback minimised
pathMaxHeat, whose gradient points AT the wall (the shortest path has the least
wall exposure), so the least-hot tile was the adjacent one and led further along
the wall. Near a wall the picker now ranks by the DESTINATION (farthest from the
wall, then coolest tile) and commands the sign whose velocity has a positive
component along the wall-away normal. That sign is re-asserted EVERY tick, so
speed*heading . away >= 0 while escape is active: the clearance cannot fall.
mode=escape reaches the [strafe] log. A mild wall-margin bias
(TR_STRAFE_WALL_BIAS) prefers higher-clearance tiles when near a wall.

GUI/log: the curved wings are drawn as an orange polyline (candidates follow the
curve), a white ray + ESCAPE label marks the escape, and the [strafe] line now
carries wall=<dist> kappa=<..> mode=<pick|fallback|escape|radial>.

Gates (offline, kinematic replay of the DrussGT fixtures; see
common_libs/tests/measure_strafe_wings.nim, plus the reused j108/j111 gates):
wall occupancy (within 54 px) falls 25.6->4.3 / 18.3->4.1 / 23.8->4.3 / 27.8->4.7
percent and the longest continuous wall run 191->31 / 49->25 / 208->47 / 246->27
ticks; corner-region occupancy 4.7->0.0 percent with the longest corner run
71->7. The escape sweep (3520 start x heading x enemy runs, 110k escape ticks)
shows ZERO per-tick guarantee violations and a worst corner run of 21 ticks.
Open-space parity is bit-identical (kappa == 0), reversals are still sign flips
(0 non-sign commands), and mean turn/speed are unchanged (OFF 4.42 deg/tick,
31.3 percent no-turn vs ON 4.45 / 30.7; reversal-interval entropy 5.309 -> 5.311
bits). The fixtures are OPEN-LOOP, so these are veto-capable checks, not a live
win claim.
2026-09-25 23:51:12 +02:00
SirStone ed25ce29ad STRAFE: range control (tilt) + corner-stall escape; shipped TFIL default untouched
Task j111. Two changes to the TR_MOVEMENT=strafe engine, both OFF the shipped
tfil path; the binary default is still tfil.

RANGE CONTROL (a hypothesis under test, no default changed elsewhere):
the body is still pinned ~perpendicular to the threat, but the line is tilted
by the range error: lineAngle = threat + 90 + appliedTilt, with the tilt zero
inside +/-TR_STRAFE_RANGE_TOL around TR_STRAFE_RANGE (200 px, chosen because it
is exactly TR_POWER_FAR_DIST) and clamped to +/-TR_STRAFE_TILT_MAX. A tilt alone
cannot change range (the picker chooses both ends at random), so the picker also
PREFERS the end that reduces |distance - target| with a probability that grows
with |tilt|; both ends stay possible. The tilt sign is aligned to the ENEMY
bearing, since  is the bullet direction (roughly its opposite) when a
bullet is in flight. Knobs: TR_STRAFE_RANGE (200), TR_STRAFE_RANGE_TOL (25),
TR_STRAFE_TILT_MAX (15), TR_STRAFE_TILT_GAIN (0.10), all registered in
env_report.nim (emit + knownEnvNames). NOT claimed to be better: j107 measured
that drifting 25-30 px closer made damage/run and wins WORSE.

CORNER STALL (a real defect): a line whose in-arena candidate set was empty set
targetValid=false and kept driving on the last sign, so the bot could oscillate
inside a corner tile forever. Three defenses: (1) a deterministic corner guard
projects the outward component off the line whenever BOTH ends are outside, so
the line becomes wall-parallel and a candidate always exists; (2) a degenerate
line (<=1 candidate) falls back to a radial search for the coolest in-arena
tile and commits the sign; (3) a commanded move with no displacement for
StuckFlipTicks (5) ticks flips the sign. Both warnings now reach the [strafe]
log.

GUI/log: the tilt is drawn as the existing strafe line (it is lineForward), plus
a green/red ray toward the enemy (length = |distance-target|) and white text
d=.. tgt=.. tilt=..; a red disc marks a stuck tick. The existing overlays and
the j110 heat grid are unchanged.

Gates (offline, kinematic replay of the DrussGT fixtures; see
common_libs/tests/measure_strafe_range_stall.nim): on the j110 field all four
corners that were 100% confined inside 72 px / 22.6 px max before now escape
(<=2.6% confined, 209-741 px); the achieved |distance-200| falls on 3 of 4
fixtures (mean -16% to -30%); mean |turnRate| and the 8.00 px/tick speed are
essentially unchanged (no-turn property survives). Reversal-interval entropy
falls 5.85 -> 5.31 bits (still above TFIL's 5.09): the range bias costs some
reversal randomness while closing.
2026-09-25 23:39:28 +02:00
SirStone 1ea84f5b6e STRAFE: draw the heat field, real bullet danger, corrected ring comment
Three defects the owner hit as "no heat tiles anymore" under TR_MOVEMENT=strafe.

1. The strafe overlay drew ONLY the tiles on its strafe line, so the computed
   heat field was essentially invisible. It now draws the WHOLE field exactly as
   TFIL does (every non-zero tile, yellow->orange->red ramp by field max, integer
   value label) behind the same debugGraphics flag, with TR_STRAFE_HEAT_GRID=0 to
   hide it. The strafe overlays draw on top, unchanged.

2. STRAFE carried the SHIPPED bullet constants (core 10 / aura 5), so a bullet's
   own heat sat exactly ON PathDangerThreshold (10.0) and a bullet was never
   dangerous on its own in this mover; it only ever bit through its corridor.
   Defaults are now the retune's 20/10, exposed as TR_STRAFE_BULLET_CORE /
   TR_STRAFE_BULLET_AURA.

3. The ring mover's header documented CorridorHeat 5.0 / WallHotness 10.0 while
   the code has always been 10.0 / 15.0. A job read the comment and handed out
   sub-threshold heat values, which emptied the field. The comment now states the
   real values and their actual behaviour; no code values changed.

Also sets strafe's heat defaults to the retune shape (bullet 20/10, corridor 10,
wall 15/5, pillar 0), documented with the reason.

Gate A re-run (j110, offline DrussGT fixture, measure_strafe_gates.nim):
  corrected DEFAULT : 24.6% of picks with ZERO safe tile, mean 11.17 safe
  j108 shipped field: 63.4% / 3.70   (reproduced exactly)
  j108 ring retune  :  8.1% / 18.41  (reproduced exactly)
  bullet isolated   : 11.4% / 17.07
The corrected default beats the shipped field but is WORSE than j108's retune
row: the bullet retune alone costs 8.1 -> 11.4, the corridor/wall retune accounts
for the rest. That is the deliberate price of making a bullet dangerous.

Guards green: test_env_report 24 PASS, test_tfil_commit_env 30 PASS (shipped TFIL
default untouched, byte-for-byte), test_tfil_ring_weights 24 PASS. The three new
knobs are registered in the boot env report so the tree-scan guard stays clean.
2026-09-25 23:25:34 +02:00
SirStone a50c0125d5 STRAFE movement: body pinned perpendicular to the threat, reversals by sign flip
New engine movements/strafe.nim, selected by TR_MOVEMENT=strafe (default stays
tfil, byte-identical — test_tfil_commit_env.nim's 30 checks still pass).

Design (the owner's):
- AXIS = incoming bullet's direction when a bullet is in flight, else the
  perpendicular of the enemy bearing. The body heading is kept inside a band
  (TR_STRAFE_BAND, default 20 deg) around the perpendicular LINE; it turns only
  when outside the band, and never turns to face a movement target.
- Candidate tiles on the perpendicular line through our position, both forward
  and backward, within TR_STRAFE_REACH px, with a perpendicular jitter of
  +/- TR_STRAFE_SPREAD tiles. A tile is acceptable when its path max heat is
  <= PathDangerThreshold, the SAME safety rule TFIL uses.
- Move by SIGN only: setForward(+/-MaxSpeed>). Dwell is re-picked after a random
  number of ticks in [TR_STRAFE_DWELL_MIN, TR_STRAFE_DWELL_MAX], on arrival, or
  on a serious threat spike.
- Heat machinery is REUSED from the shipped mover, not re-implemented: the
  exported heatDecay()/bulletMagScale() (j105 time-indexed model) and the
  PillarHotness/PillarRadiance globals (j106 pillar-free default). The heat
  shape is overridable via TR_STRAFE_CORRIDOR_HEAT/WALL_HOTNESS/WALL_RADIANCE
  (defaults = the shipped TFIL field).
- GUI overlay: strafe line, threat axis, candidate tiles (safe/unsafe), chosen
  target, sign-coloured movement ray, and the heading band.

Gates (offline, recorded DrussGT fixture, 20026 ticks):
- A TILE AVAILABILITY: shipped heat field -> a safe tile exists on only 36.6%
  of picks (63.4% fall back to the least-hot tile); the ring retune
  (corridor 5, wall 10/5) raises it to 91.9%.
- B PREDICTABILITY: reversal-interval entropy 5.84 bits vs TFIL 5.09; direction
  entropy 1.00 both; long-lag autocorrelation ~0 for both (no periodic
  component). Fewer reversals (710 vs 1453) and more full-speed ticks.
  measurements: common_libs/tests/measure_strafe_gates.nim

Also registers TR_STRAFE_* in the boot env report (ModularBot_garage/src/
env_report.nim) and wires the engine into ModularBot.nim (hold -> strafe,
ram trigger -> rammer).
2026-09-25 22:38:52 +02:00
SirStone d0750ab020 TFIL: remove the virtual centre pillar from the shipped default; register j102 env reads
The default mover painted a 30/10 radiance blob on the arena centre even
though the arena has NO physical pillar there, creating a 4x4 tile
(144x144 px) exclusion zone over open centre floor. Set
PillarHotness/PillarRadiance to 0/0 in the shipped default (matching the
ring variant) and add TR_TFIL_PILLAR_ON=1 to restore the old 30/10 field
for A/B without a rebuild; registered in env_report.

Because the shipped default legitimately changed, the default-path parity
golden (fixtures/tfil_commit_default.golden) was regenerated from the NEW
default, with an explicit 'deliberate default change' note in the test so
a future failure is treated as a real regression.

Also register the three env reads job j102 added in common_libs/bitbrain
(TR_BITBRAIN_MODE / _DECAY_EVERY / _DECAY_SHIFT), which the env-report
guard was failing on.

Verification: test_env_report all green; test_tfil_commit_env 30/30.
2026-09-25 22:02:00 +02:00
SirStone fca899376e TFIL: time-indexed bullet heat (TR_TFIL_HEAT_TIME, DEFAULT OFF)
Make danger a function of time-to-arrival instead of flat distance. Bullet
core/aura/corridor heat becomes magnitude(power) * decay(dt), dt = along/speed:

  * decay(dt) = exp(-dt/tau) is a function of TIME; a fixed tau projects a
    pixel reach of speed*tau, so fast/weak bullets get a longer slope and slow
    ones a shorter one — derived from speed = 20 - 3*power, not hand-tuned.
    tau = TR_TFIL_HEAT_TAU.
  * magnitude(power) scales the near-end heat with power from DAMAGE
    (calcBulletDamage = 4p, linear in p; SCORE_PER_BULLET_DAMAGE = 1.0). Hit
    probability is FLAT across power (docs/env_reference.md), so risk does not
    justify power scaling — the cost of the hit does. Floored at 1.0 so a weak
    bullet's near end is never less dangerous than the flat model.
    Gain = TR_TFIL_HEAT_POWER_GAIN.

Every source is already f(dt), so the time-indexed planner (evaluate a cell at
the tick the bot would ARRIVE, i.e. heatDecay(dt - arrivalDelay)) is a one-line
change. It is intentionally NOT implemented here.

Default path is byte-identical: with TR_TFIL_HEAT_TIME unset both factors are
exactly 1.0 (IEEE x*1.0 is exact), and the committed golden replay in
common_libs/tests/test_tfil_commit_env.nim (20,026 ticks) still passes
byte-for-byte against the pre-change mover. The debug corridor outline is also
drawn only to the model's reach when enabled, so the GUI shows the shortening.

Offline field measurement (common_libs/tests/measure_tfil_heat_time.nim,
46,054 fixture ticks, tau=9/gain=1): corridor reach drops from 443px
wall-to-wall to 143px mean (32% retained); fraction of tiles > 10 goes
0.61 -> 0.57; largest contiguous safe region 118 -> 140 tiles; mean
distance-to-nearest-safe-tile 49 -> 42px. Saturation stays high because wall
radiance + pillar alone are 44% of tiles over threshold and are untouched.

Registers the three knobs in env_report (report + known-name set).
2026-09-25 21:47:54 +02:00
SirStone 19bf4610e4 TFIL: env-gated commitment arms (tile-replan cancel is a bug)
The tile-change replan cancels the 15-tick movement commitment whenever OUR
tile changes. With GridSize=36 and speed up to 8 px/tick that is every ~5
ticks, so the commitment is cancelled by the motion it commands (measured:
96.9% of picks were tile-change replans, 33.8% of picks reversed direction).

Adds four env knobs, every default reproducing the shipped mover
byte-for-byte:
  TR_TFIL_TILE_REPLAN  self (default) | off | enemy
  TR_TFIL_COMMIT_TICKS 15 (default)
  TR_TFIL_NO_REV       0 (default)
  TR_TFIL_COMMIT_LOG   off (default, JSONL per-tick diagnostics)

- `off` honours the commitment; the danger replan stays the safety valve.
- `enemy` keys the cancel to the TARGET's tile displacement (the intent the
  original comment claimed).
- `TR_TFIL_NO_REV` down-weights (never filters) tiles >90 deg from the travel
  direction; the pool can never be emptied.

Default-path parity is guarded by test_tfil_commit_env.nim, which replays
tools/fixtures/tr_drussgt_vs_modularbot.jsonl and diffs every move command
against a golden generated from the pre-change build (git archive f842ac0).
env_report known-name list updated for the four new names.
2026-09-23 23:28:45 +02:00
SirStone 7f6ccfb015 Ring mover heat retune (author: the user) - dodge bullets, ignore walls/centre,
re-plan 3x more often

Uncommitted working-tree change in `the_floor_is_lava_ring.nim`, confirmed by the
user as theirs. Committing it so it stops appearing in every job's `git status`.

WHAT IT DOES - a coherent "be far more afraid of bullets" strategy:
- BULLETS 2x hotter: BulletCore 10 -> 20, BulletAura 5 -> 10.
- Walls weaker and thinner: WallRadiance 10 -> 5; the `WallHotness` default
  10 -> 15 (so the heat falls off over ~3 tiles instead of 1, with a higher peak).
- CENTRE PILLAR DISABLED: PillarHotness 30 -> 0, PillarRadiance 10 -> 0, so the
  bot may now use the middle of the arena instead of treating it as a no-go zone.
- MUCH more reactive: CommitTicks 15 -> 5 and MinCommitTicks 5 -> 0, i.e. the
  dodge target is re-chosen 3x more often and a replan is allowed immediately.
- `CorridorHeat` default 5 -> 10.

This applies ONLY to the ring mover, which is NOT the shipped movement (the default
is `tfil`), so the shipped bot is unaffected.

TWO FACTS RECORDED, not objections - it is the user's call:
1. `CorridorHeat = 10` now EQUALS `PathDangerThreshold = 10`. The earlier measured
   taming set it to 5 precisely so a single corridor could no longer poison a path
   on its own (a corridor at 10 puts every tile along it at/over the threshold).
   That is part of what lifted the "band-weightable" fraction from 10.8% to 26.5%.
   At 10 the range weighting gets less to work with.
2. UNMEASURED: this retune has not been A/B'd. The ring mover it modifies was
   itself measured as a glass cannon (best offline hit rate, but round wins
   16/49 -> 6/49, p=0.012). The two changes push in the same direction - hotter
   bullets and more frequent replanning mean MORE dodging and LESS time spent
   closing to the 100-200px band where our hit rate peaks (27% vs 5% at 450px).
   Worth an A/B before drawing conclusions, but it is an experiment, not a
   shipped change.
2026-09-23 00:30:52 +02:00
SirStone eb74f9b2e3 Ram: finisher-only by default, and the bullet-rain abort now measures real energy
Follows the diagnosis that proactive straight-line ramming CANNOT work: both bots
have MAX_SPEED=8, so a pursuit cannot catch an evading equal-speed opponent.
Measured over 49 rounds per arm, opportunity -> contact was **0/6** (base), 0/40
(ring), 0/12 (ringhot). The only proactive conversion in the whole corpus came
from a FINISHER, and only because a <20-energy DrussGT stops fleeing (that episode
closed at 6-8 px/tick). Opportunity episodes never got below ~80px; one ran the
full 60-tick duration cap and closed only 198->171px; a perfectly aligned
full-speed one closed 195->114px then plateaued.

CHANGES
- **Finisher-only default.** `finisher` (<20 energy, dist<300, we are healthier)
  and the rare `desperation` (both <5, dist<150) are kept; `opportunity` and the
  speculative `plan` are OFF. Both are env-reenableable with no rebuild:
  `TR_RAM_OPPORTUNITY=1` (tune via TR_RAM_OPP_DIST/MARGIN) and `TR_RAM_PLAN=1`.
  Justification: it removes 100+ non-converting episodes per fixture at zero
  measured loss (oldram vs base was p=0.69, damage 279 vs 284, survival 17/49 vs
  16/49) - and each of those episodes spent up to 60 ticks driving STRAIGHT at
  the enemy, abandoning the mover's dodging and disrupting aim.
- **`desperation` KEPT** deliberately: it is cheap and rare, fires only when both
  bots are nearly dead at short range (a coin-flip where 0.6 contact can decide
  it), and it is not the refuted straight-line pursuit.
- **THE BULLET-RAIN ABORT WAS DEAD CODE AND IS NOW FIXED.** `onHitByBullet`
  accumulated raw bullet FIREPOWER while `TR_RAM_ABORT_DMG = 0.5` was documented
  as a DAMAGE rate - so the bar was implicitly "sum of power > 7.5 over 15 turns"
  and the maximum rate ever observed was 0.27. It now accumulates REAL ENERGY via
  a `bulletDamage(power)` helper matching the server's `4p` / `6p-2` formula, and
  `TR_RAM_ABORT_DMG` defaults to **2.0 energy/turn** (~30 HP over 15 turns):
  "abort an in-progress ram if we take > 2.0 energy per turn". Same effective bar
  for normal firepower, and it can now actually fire - the live run reports
  `dmgRate=1.07/turn` where the old units said 0.27.
- **`ramStuckTicks` REMOVED.** It required `dist < 5px`; contact occurs at ~36px
  (two 18px radii) and position rewind prevents getting closer, so it could never
  increment. Only the 60-tick duration cap can now self-end a ram.

LIVENESS (measured, default config, vs a charging Java RamFire, 3 rounds):
  default              -> `[ram] ON reason=finisher` x3, `reason=opportunity` x0
  TR_RAM_OPPORTUNITY=1 -> `reason=opportunity` x4, `reason=finisher` x2
So the opportunity states DID occur and are suppressed by the new default - the
removal is real, not an arm that never fires. A line also read
`[ram] OFF reason=duration dmgRate=1.07/turn`, confirming the new energy units.

Adds docs/ramming_negative_result.md (70 lines) recording the question, the five
diagnostic answers, the geometric reason, the finisher exception, the two dead
code paths, and an explicit "do not re-attempt a proactive straight-line ram; if
point-blank forcing is ever wanted it is an INTERCEPTION/cornering movement
problem" note - the same pattern that stopped the corpse bug recurring.

Guards: test_ram_decision 40 (was 28), test_gun_harness 39, test_vbullet_metric 11,
test_power_selection 3, test_adaptive_radar 41, test_tfil_ring_weights 24,
test_power_policy 26, test_rack_membership 48, test_selector_tiebreak 19,
test_tm_pattern_registration 20, test_vbullet_admit_gate 12,
acceptance_offline_vs_online 12/12. ModularBot compiles.

Honest note: the abort-threshold fix is a real (tiny) behaviour change, NOT
measured-neutral - it only bites while a finisher ram is under sustained fire,
which is exactly the user's stated wish. The finisher-only removal itself is
measured-neutral per the given A/B.
2026-09-22 08:14:03 +02:00
SirStone 994f88d7a7 ramming: make the decision PROACTIVE, with a bullet-rain abort
The user watched 1v1 and melee runs and saw ram opportunities arise that the bot
declined: "there were moments where the bot could jump over the enemy and shred
it but shot it down instead."

DIAGNOSIS - a chicken-and-egg loop. `ramOpportunity` required dist < 50px, but
an offline measurement over 15 rounds vs DrussGT found the closest approach was
118.7px and the <50px trigger had NEVER fired: the mover has no reason to close,
so the trigger waited for a proximity nothing created. The MECHANISM to close
already existed (the ring mover expresses a ram as band=(0,50)); what was
missing was a decision that fires at a range the bot can actually close from.

Changes:
- opportunity gate relaxed: dist 50 -> TR_RAM_OPP_DIST (200), energy margin
  +30 -> TR_RAM_OPP_MARGIN (15). Both env-tunable, no rebuild needed.
- New pure module `common_libs/movements/ram_decision.nim` holding the trigger
  and abort logic (no battle/API deps), so it is unit-testable.
- BULLET-RAIN ABORT (the user asked for this earlier): `onHitByBullet` now
  accumulates `e.bullet.power` into a 15-turn ring; damageRatePerTurn = sum/15;
  an in-progress ram aborts when rate > TR_RAM_ABORT_DMG (0.5/turn). On abort:
  isRamming=false, cooldown 30, TARGET KEPT, and the mover returns to the normal
  range band. It never stops the bot.
- Opt-in, DEFAULT-OFF `plan` trigger for "change of plan when the gun duel is
  failing" (dist<250, margin+20, selected gun's pooled virtual rate < 0.05).
  Left off because a cold gun reads 0.0 and would qualify - speculative.
- `TR_RAM_LOG=1` change-gated line: `[ram] ON reason=opportunity dist=143
  selfE=78 enemyE=41 cap=3.0 band=[0,50]` / `[ram] OFF reason=bulletRain`.
- Existing cooldown/duration/stuck machinery untouched (stuck>10 or duration>60
  -> abort + cooldown 30). A refactor bug that briefly DROPPED the
  `ramCooldownTicks == 0` gate was caught and fixed.

Trigger set (first match wins): finisher (dist<300, enemy<20, we are healthier);
opportunity (dist<200, we lead by 15+); desperation (both <5, dist<150); plan
(off). All require enemy>0, a valid target, and no cooldown.

Proof the intent now fires at a closable distance (28/28 unit checks):
  PASS: opportunity fires at dist 143 with a 37-energy lead   <- the exact case
  PASS: old gate (dist<50, margin+30) does NOT fire at 143    <- the old bug
  PASS: fires at 199px / does NOT fire at 201px
  + margin, finisher priority, desperation, plan on/off, window mean, abort
    threshold checks.

HONEST FRAMING: ram damage is 0.6 per CONTACT EVENT, one-shot (collision
resolution rewinds positions so contacts do not stream) - small next to a p=3.0
bullet hit (16). The payoff is that point-blank forces hit probability toward 1,
so heavy bullets stop missing and E[dE]=p(3P-1) turns positive above P=1/3; ram
damage also scores 2.0/point (highest in the game) and a ram kill carries a 0.30
bonus vs 0.20. So this is "force the fight to point-blank", not "the ram shreds
them". Base rate is rare (2 collisions in the whole fixture corpus).

Guards: test_ram_decision 28 (new), test_power_policy 26, test_gun_harness 39,
test_vbullet_metric 11, test_power_selection 3, test_adaptive_radar 41,
test_tfil_ring_weights 24. Compiles (release).
UNVERIFIED: the live effect. No A/B has run, and whether the mover actually
reaches contact is unproven.
2026-09-22 00:24:45 +02:00
SirStone 9caf1d3728 movement: range-weighted TFIL variant + tamed heat field (opt-in, default unchanged)
New mover `the_floor_is_lava_ring.nim`, a COPY of `the_floor_is_lava.nim` (which
stays byte-identical - the user explicitly wants the current TFIL preserved).
Selected only via `TR_MOVEMENT=tfil_ring`; the default stays `tfil`.

WHY: our measured real hit rate vs DrussGT is strongly range-dependent - 21.6%
at 0-100px, 27.1% at 100-200px, 19.3% at 200-300, 10.9% at 300-400, 6.8% at
400-600, 5.4% at 600-800 - but we shoot from ~450px on average. Plain TFIL has
no range preference at all.

THE ONE CHANGE: the final tile draw is re-weighted toward a target band.
  rangeW(d) = 1.0 if lo<=d<=hi; exp(-((lo-d)/K)^2) if d<lo; exp(-((d-hi)/K)^2) if d>hi
  w_i = rangeW(d_i)^(1/T);  chosen ~ Categorical(w)
FLAT TOP on purpose: a Gaussian centred on the band midpoint would collapse the
band to a point and destroy the within-band hedge. `T` is the only knob;
`TR_TFIL_RANGE_TEMP=0` gives plain `rand(candidates.high)` - the exact control
arm. Safety stays a HARD constraint: the weighting only reorders the draw among
the pool the old code already accepted, so it can never pick a tile the old code
rejected (monotone refinement). Small pools (<4) stay uniform.
Randomness is deliberately KEPT: a measured A/B showed committing to the "best"
tile made real hit rate WORSE (7.02% -> 5.10%), so the distribution is tilted,
never removed.

HEAT TAMING (ring copy only; env-overridable):
  TR_TFIL_CORRIDOR_HEAT  20.0 -> 5.0
  TR_TFIL_WALL_HOTNESS   30.0 -> 10.0
Rationale, measured: `CorridorHeat=20` is TWICE `PathDangerThreshold=10`, so a
single corridor could poison a path by itself; `WallHotness=30` with
`WallRadiance=10` put the outer two tile rings over threshold on their own.
Per-source shares of total lava: wall 60.6%, corridor 25.4%, pillar 7.4%,
everything else <3%.

MEASURED EFFECT (primary fixture, 20,026 ticks / 15 rounds, field identity
verified max diff 0.000e+00):
  metric                        original(20/30)   ring(5/10)
  band-weightable ticks              10.79%         26.45%
  mean safeTiles/tick                 15.19          85.04
  ticks with 0 safe (pre-fallback)    58.5%           7.5%
  safePool >= 4                       39.05%         92.47%
  >=1 safe tile in 100-200px          11.84%         26.64%
  MEAN CLOSEST-SAFE-TILE DISTANCE    397.78px       284.84px
  tiles > 10 threshold                 0.61           0.15
The 397.78px figure is why the bot stayed far away: the safety filter left
nothing safe near the target, and 397px is our WORST range. Control: setting
corridor=20 wall=30 reproduces the original baseline exactly.
CEILING, honestly: even at corridor 0 / wall 0 only ~40% of ticks are
band-weightable, so no constant tweak fully unlocks the range weighting.

Ram unification: the ring mover takes a `band` field; ramming becomes just
`band=(0,50)`, so there is one movement engine. The `tfil` path is unchanged.

Observability: magenta annulus at the band edges, candidates tinted by weight,
chosen tile marked; one `[tfil_ring]` log line on change (now including
corridorHeat/wallHotness).

Guards: test_tfil_ring_weights 24/24 (new, pure, no battle), test_gun_harness 39,
test_vbullet_metric 11, test_power_selection 3, test_adaptive_radar 41.
UNVERIFIED: the mover's live effect. It has not been run in a battle yet.
2026-09-21 23:46:47 +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 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 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 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 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 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