Compare commits
4 Commits
940fa44631
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 7632aaba06 | |||
| 208f4a9092 | |||
| 55e92bc5e0 | |||
| 486e2a69c6 |
+552
-59
@@ -10,19 +10,187 @@
|
||||
# Every value below IS the built-in default, so running the bot with this file
|
||||
# is identical to a clean run with no file at all. Delete a line (or comment it
|
||||
# out with #) and that knob falls back to the built-in default. An inline
|
||||
# `# comment` after a value is fine — the loader strips it.
|
||||
# `# comment` after a value is fine — the loader strips it (a `#` that follows
|
||||
# a space starts the comment; a `#` glued to the value, like `x#y`, is data).
|
||||
#
|
||||
# A few switches are PRESENCE-only (`existsEnv`): for those, OFF means the line
|
||||
# is absent, so they are shown commented out. Writing `=0` would still turn them
|
||||
# ON.
|
||||
# These values mirror the current defaults, not a frozen snapshot of one commit.
|
||||
# Regenerate this file whenever a default changes, or it will start lying.
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
# 1. ONE EXPERIMENT, END TO END
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
#
|
||||
# SNAPSHOT of the code at commit 5e32ec1. Regenerate this file whenever a default
|
||||
# changes, or it will start lying.
|
||||
# Pick ONE knob. Here the example is TR_TFIL_ARRIVE_TICKS, but the shape is the
|
||||
# same for every knob in this file.
|
||||
#
|
||||
# # 1. write the arm. In ModularBot_garage/.env, change ONE line:
|
||||
# # TR_TFIL_ARRIVE_TICKS=15.0
|
||||
# # A per-run file is better than editing .env, because it is how you
|
||||
# # GUARANTEE the arm: whatever else is in .env or in your shell, this
|
||||
# # file is the one that is applied.
|
||||
# cat > /tmp/arm_arrive15.env <<'EOF'
|
||||
# TR_MOVEMENT=tfil
|
||||
# TR_TFIL_ARRIVE_TICKS=15.0
|
||||
# EOF
|
||||
#
|
||||
# # 2. RESTART THE BOT. Env is read ONCE, at boot (module init). Editing
|
||||
# # .env while the bot runs changes nothing. There is no live reload.
|
||||
# # (Two exceptions read lazily on first use: TR_PATTERN_RAD_* and a few
|
||||
# # TR_TMHORIZON_* — do not rely on either.)
|
||||
# cd ModularBot_garage && ./ModularBot.sh # or restart the GUI
|
||||
#
|
||||
# # 3. CONFIRM IT TOOK EFFECT, before you read a single result line.
|
||||
# # `source: .env` = your file was applied. `source: default` = it was not.
|
||||
# grep '^\[env\]' /tmp/modularbot_stdout.log | grep -E 'env file|ARRIVE_TICKS'
|
||||
# # [env] env file: /tmp/arm_arrive15.env (source: TR_ENV_FILE)
|
||||
# # [env] TR_TFIL_ARRIVE_TICKS = 15.0 (source: .env)
|
||||
# # A value showing `(source: default)` means YOUR FILE NEVER REACHED THE BOT.
|
||||
#
|
||||
# # 4. point at the file instead of copying it into .env:
|
||||
# TR_ENV_FILE=/tmp/arm_arrive15.env ./out/ModularBot
|
||||
# ./out/ModularBot --env-file /tmp/arm_arrive15.env
|
||||
# # This is what an A/B run does: one frozen binary, one env file per arm.
|
||||
# # A file you ASKED for and that does not exist stops the bot with an error
|
||||
# # (it never silently falls back); a missing default .env is silent.
|
||||
#
|
||||
# # 5. the module inventory, when you want to know what is on at all:
|
||||
# grep '^\[modules\]' /tmp/modularbot_stdout.log
|
||||
#
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
# 2. SAFE TO EXPERIMENT WITH RIGHT NOW
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
#
|
||||
# The honest list is SHORT. After the recent campaign most experimental knobs
|
||||
# are either never live-tested or already measured null/harmful, and this file
|
||||
# says so on every one of them. These four are safe in the sense that they
|
||||
# either cannot change a decision, or are the ones a measurement actually
|
||||
# supports.
|
||||
#
|
||||
# TR_GEO_DEBUG=on Draw-only: the candidate-tile geometry overlay.
|
||||
# Watch: the circle on the two tanks and each
|
||||
# heading line. Good: you can SEE the tile the
|
||||
# picker chose. Cannot change any decision.
|
||||
# TR_VBULLET_DEBUG=1 Draw-only: each admitted gun's virtual bullets.
|
||||
# TR_VBULLET_DEBUG_GUN=all
|
||||
# Watch: travelled path, aim ring, miss vector.
|
||||
# Good: you can see the signal the selector ranks
|
||||
# on. Also draw-only. Needs a gun in the rack.
|
||||
# TR_TFIL_DIAG=on Observability only, on the tfil mover. Fills the
|
||||
# per-pick LOSS HISTOGRAM. Watch: the tfil pick log
|
||||
# line. Good: the sReach/sCool/sSafe/sCand counts
|
||||
# tell you where tiles are lost. Provably does not
|
||||
# move a single command (guard-tested).
|
||||
# TR_MOVEMENT=tfil The long-shipped mover, as an explicit override.
|
||||
# Watch: nothing to compare against — it is the
|
||||
# same engine you had before j119. Good: you are
|
||||
# reproducing an older, documented behaviour. Only
|
||||
# do this together with the tfil knobs below.
|
||||
#
|
||||
# Anything else on this list is a MEASUREMENT, not a free change: read its
|
||||
# STATUS line before you type it.
|
||||
#
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
# 3. ALREADY REJECTED OR MEASURED NULL — WITH THE NUMBER
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
#
|
||||
# TR_TFIL_GEO_MODE=both-rej REJECTED live, 420 battles, 15-opponent panel.
|
||||
# TR_TFIL_GEO_TAU=60 damage/run -8.83, p(sign-flip) = 0.0061,
|
||||
# Wilcoxon p = 0.011. Round wins null.
|
||||
# Docs: docs/tfil_geo_ab.md. DO NOT re-run it.
|
||||
# TR_RAM_FLOOR_ENERGY=5 CLEAN NULL, 900 battles, 450 runs/arm.
|
||||
# -0.018 wins/run, p(sign-flip) = 0.7676, under
|
||||
# a 0.1420 wins/run MDE. Docs:
|
||||
# docs/ram_floor_exhaustion_ab.md. DO NOT re-run
|
||||
# it — the mechanism fired on 0.04% of ticks, so
|
||||
# more runs buy resolution on an effect that is
|
||||
# not there.
|
||||
# TR_RAM_FLOOR_ENERGY=10/20 MEASURED COSTLY OFFLINE (24.7% of ticks blocked
|
||||
# at 20) and the live zone they guard is almost
|
||||
# empty: only 4.8% of shots are ever taken below
|
||||
# 10 energy. Do not go above 5.
|
||||
# TR_TMHORIZON_WINDOW=150 MEASURED HARMFUL live: 26.5% round wins vs 49.0%
|
||||
# for the shipped rack, p = 0.036. Keep 0.
|
||||
# TR_POWER_POLICY=0 MEASURED HARMFUL live: real hit rate 10.61% ->
|
||||
# 7.88%, p = 0.0012. Keep it on.
|
||||
# TR_TFIL_HEAT_TIME=1 MEASURED HARMFUL live at every tau tried
|
||||
# (3/5/9/15): tau15 alone is -22 damage/run,
|
||||
# p = 0.046. Keep it off.
|
||||
# TR_MOVEMENT=tfil_ring MEASURED: round wins 16/49 -> 6/49, p = 0.012. A
|
||||
# glass cannon — best live hit rate of anything
|
||||
# measured, half the survival. Do not ship.
|
||||
# TR_RACK_* (the full rack) MEASURED NEGATIVE VALUE: Pattern ALONE beats
|
||||
# the full 13-gun rack, p = 0.0012. Adding guns
|
||||
# costs rounds.
|
||||
# TR_TFIL_TURN_BIAS=9 LIVE NULL: +0.15 wins/run, p(sign) = 0.244, under
|
||||
# TR_TFIL_TURN_REF_DEG=0 a 0.30 MDE; 300 battles. Docs:
|
||||
# docs/movement_campaign.md (j145).
|
||||
# TR_RAM_OPPORTUNITY=on MEASURED not to convert: 0/59
|
||||
# opportunity -> contact. The finisher ram is the
|
||||
# only path that converts, and it is always on.
|
||||
#
|
||||
# READ THIS BEFORE YOU TRUST ANY "null" ABOVE. A null only excludes an
|
||||
# effect at or above the MDE that run resolved. The 15-opponent panel at
|
||||
# 14 runs/arm resolves ~0.17 wins/run and ~7.65 damage/run; the j163 run
|
||||
# resolved 0.1420 wins/run. So "clean null" here means "no effect >= that
|
||||
# size", NOT "no effect".
|
||||
#
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
# 4. PRESENCE-GATED KNOBS — FOR THESE, `NAME=0` TURNS THE FEATURE ON
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
#
|
||||
# grep -rn 'existsEnv' ModularBot_garage/src common_libs | grep -v /tests/
|
||||
#
|
||||
# The knobs below are read with `existsEnv`, not by value. OFF means THE LINE
|
||||
# IS ABSENT. Writing `TR_POWER_LOG=0` does not disable the power log — it
|
||||
# ENABLES it, because 0 is a perfectly good value for a knob nobody reads.
|
||||
# That is why they are all shown COMMENTED OUT in this file: there is no
|
||||
# "off" spelling for them, only absence. To disable one, DELETE its line.
|
||||
#
|
||||
# TR_POWER_LOG one line per power-decision CHANGE
|
||||
# TR_RAM_LOG one line per ram start/stop, with the reason
|
||||
# TR_MOVEMENT_LOG movement band / range-class changes
|
||||
# TR_STRAFE_LOG one line per strafe tile pick
|
||||
# TR_SURF_LOG one line per wave-surfing decision
|
||||
# TR_FIRE_DIAG per-reading fire-detection tick/raw/correction
|
||||
# TR_RECORD_WORLDSTATE dump every observed world state to JSONL
|
||||
# TR_CAPTURE_AIM aim_scan/aim_fire records (gun id + bot belief).
|
||||
# KNOWN LIMIT: only shots that PASSED setFire are
|
||||
# recorded, so a gap is ambiguous - see docs/env_reference.md
|
||||
# TR_RADAR_SCANLOG log every radar scan tick
|
||||
# TR_RADAR_FORCE_SPIN force the old full-360 spin radar
|
||||
# TR_TRACKER_PROBE dump the enemy-tracker internals
|
||||
#
|
||||
# The VALUE-based switches are the opposite: 0 / false / no / off really
|
||||
# disable them, and anything else enables them. Those are TR_RESULT_LOG,
|
||||
# TR_TMHORIZON_LOG, TR_TMHORIZON_ACCURVE, TR_TMHORIZON_RESET_ON_TARGET,
|
||||
# TR_LEADGAIN_LOG, TR_LEARNED_LOG, TR_LEARNED_GLOBAL, TR_LEARNED_REAL_EVENTS,
|
||||
# TR_POWER_POLICY, TR_POWER_FINISH_KILL, TR_RAM_OPPORTUNITY, TR_RAM_PLAN,
|
||||
# TR_FIRE_FIX, TR_STRAFE_FIRE_FIX, TR_STRAFE_ESCAPE, TR_STRAFE_HEAT_GRID,
|
||||
# TR_TFIL_HEAT_TIME, TR_TFIL_PILLAR_ON, TR_TFIL_DIAG, TR_TFIL_NO_REV,
|
||||
# TR_TFIL_HOLD_WHEN_TRAPPED, TR_TFIL_COMMIT_ARRIVAL,
|
||||
# TR_TFIL_RING_COMMIT_ARRIVAL, GUN_SELECTOR_POOL, GUN_VBULLET_ADMIT_ONLY,
|
||||
# TR_VBULLET_DEBUG, TR_GEO_DEBUG, TR_DEBUG_DRAW, TR_ENV_REPORT.
|
||||
# (TR_TFIL_DIAG / _NO_REV / _HOLD_WHEN_TRAPPED / _COMMIT_ARRIVAL /
|
||||
# _RING_COMMIT_ARRIVAL are read by value in the source; the boot report
|
||||
# labels them by presence, which only affects the "(source: ...)" line, never
|
||||
# the value.)
|
||||
|
||||
# ── movement ─────────────────────────────────────────────────────────────────
|
||||
# Which dodging engine runs: tfil, tfil_ring, strafe, surf or learned.
|
||||
# Which dodging engine runs. VALUE: strafe (default) | tfil | tfil_ring |
|
||||
# surf | learned. Any unrecognised value silently runs `tfil`, with no warning.
|
||||
# WHAT: the engine that picks the dodge tile every tick.
|
||||
# STATUS: DEFAULT = strafe, the measured champion — 300 fresh battles,
|
||||
# +0.30 wins/run over tfil, 95% CI [+0.02, +0.58], sign-flip p = 0.045
|
||||
# (docs/movement_campaign.md, "Fresh-data confirmation (gate v2)").
|
||||
# tfil_ring is measured harmful (round wins 16/49 -> 6/49, p = 0.012).
|
||||
# surf and learned are wired and measured, and neither beats strafe.
|
||||
# GOTCHA: a typo does not warn. `TR_MOVEMENT=straf` runs tfil.
|
||||
# TRY: TR_MOVEMENT=tfil -> the long-shipped engine; the `[env]` block then
|
||||
# reads `move.effective = tfil`. Pairs with the TR_TFIL_* knobs below.
|
||||
TR_MOVEMENT=strafe
|
||||
# Whole-engine on/off switches. 0 removes an engine from the TR_MOVEMENT choices.
|
||||
# Whole-engine on/off switches. VALUE: on | 0/false/no/off. 0 removes an engine
|
||||
# from the TR_MOVEMENT choices; the effective engine then falls back to the
|
||||
# first still-enabled one, and tfil if all are off (there is no "no movement").
|
||||
TR_MODULE_MOVE_TFIL=on # the long-shipped "floor is lava" engine
|
||||
TR_MODULE_MOVE_TFIL_RING=on # the same, re-weighted toward a target range
|
||||
TR_MODULE_MOVE_STRAFE=on # perpendicular strafe with sign-flip reversals
|
||||
@@ -30,7 +198,17 @@ TR_MODULE_MOVE_SURF=on # wave surfing, steered by the GuessFactor
|
||||
TR_MODULE_MOVE_LEARNED=on # learned per-state danger field
|
||||
|
||||
# ── gun rack: which guns the bot may choose (off | 1v1 | melee | both) ───────
|
||||
# The shipped rack is Pattern only. Turn a gun on with `both`.
|
||||
# WHAT: which guns the selector is allowed to fire. The shipped rack is
|
||||
# PATTERN only; every other gun is `off`.
|
||||
# VALUES: off | 1v1 | melee | both. Aliases: any/empty->both, single/lock->1v1,
|
||||
# only1v1->1v1, multi/onlymelee->melee, none/disabled/disable->off.
|
||||
# STATUS: MEASURED — the full 13-gun rack is WORSE than Pattern alone,
|
||||
# p = 0.0012. Do not re-enable guns to "improve" the bot.
|
||||
# GOTCHA: an UNRECOGNISED value warns on stderr and falls back to `both`, i.e.
|
||||
# a typo ADDS the gun back into the rack. It never turns one off.
|
||||
# TRY: TR_RACK_PATTERN=off -> nothing admitted in 1v1; the selector falls
|
||||
# back to the full rack, so do not ship this. Real use is a PAIR:
|
||||
# TR_RACK_PATTERN=off + TR_RACK_HEADON=both -> exactly one gun fires.
|
||||
TR_RACK_PATTERN=both # the only admitted gun; both = usable in 1v1 and melee
|
||||
TR_RACK_HEADON=off # aim straight at the target, no lead
|
||||
TR_RACK_LINEAR=off # constant-angle linear aim
|
||||
@@ -48,16 +226,24 @@ TR_RACK_TMSELECT=off # Tsetlin machine used as the shot selector
|
||||
TR_RACK_TMPATTERN=off # Tsetlin machine used as a pattern matcher
|
||||
TR_RACK_TMHORIZON=off # horizon Tsetlin automata gun
|
||||
TR_RACK_LEADGAIN=off # per-range-band learned lead-gain corrector
|
||||
# Give every admitted gun a fixed share of the turns instead of ranking them,
|
||||
# e.g. TR_RACK_SHARE=PATTERN:60%,HEADON:40%. Empty = the ranking selector.
|
||||
# Give every named gun a fixed share of the turns instead of ranking them.
|
||||
# e.g. TR_RACK_SHARE=PATTERN:60%,HEADON:40% (GUN:weight, comma separated,
|
||||
# the % sign is optional). Empty = the ranking selector. GOTCHA: every gun you
|
||||
# name must ALSO be admitted by its own TR_RACK_<GUN> line, or the share is
|
||||
# refused with a loud `[gun_harness] ERROR` and the ranking selector is used
|
||||
# instead. In the shipped rack that means PATTERN and nothing else.
|
||||
TR_RACK_SHARE=
|
||||
# Drop whole guns by rack id (comma separated, e.g. 16). Empty = keep them all.
|
||||
# Ids: 0 HEADON 1 LINEAR 2 TSETLIN 3 CIRCULAR 4 GUESSFACTOR 5 PATTERN
|
||||
# 6 WALLBOUNCE 7 ACCEL 8 STOPSHOT 9 DISPLACE 10 AVGLEAD 11 DECAYGF 12 KNN
|
||||
# 13 TMSELECT 14 TMPATTERN 15 TMHORIZON 16 LEADGAIN. A disabled gun never even
|
||||
# spawns a virtual bullet, so its fitness stays empty and it cannot be picked.
|
||||
GUN_RACK_DISABLE=
|
||||
|
||||
# ── gun selector (which admitted gun fires this tick) ───────────────────────
|
||||
GUN_VBULLET_METRIC=path # fitness measure: path (time-to-collision) or point
|
||||
GUN_SELECTOR_MODE=relative # rank guns against the incumbent (absolute = vs a fixed bar)
|
||||
GUN_SELECTOR_WINDOW=100 # ticks of virtual-bullet history behind the fitness
|
||||
GUN_SELECTOR_WINDOW=100 # ticks of virtual-bullet history behind the fitness (clamped 1..100)
|
||||
GUN_SELECTOR_MINOBS=50 # observations a gun needs before it may compete
|
||||
GUN_SELECTOR_TIE=0.2 # relative margin two guns must differ by to count as separated
|
||||
GUN_SELECTOR_FLOOR=0.25 # fitness fraction of the peak below which a band is unsafe
|
||||
@@ -73,6 +259,8 @@ GUN_SELECTOR_SEED=
|
||||
|
||||
# ── power / energy policy ────────────────────────────────────────────────────
|
||||
# Every rule below only ever CAPS power; the gun's own preference is the ceiling.
|
||||
# The measured case for keeping it on: TR_POWER_POLICY=0 drops the real hit rate
|
||||
# from 10.61% to 7.88%, p = 0.0012.
|
||||
TR_POWER_POLICY=on # 0 = no cap at all (the control arm)
|
||||
TR_POWER_FAR_DIST=200.0 # px; past this the enemy is in the bad-chances zone
|
||||
TR_POWER_FAR_CAP=1.0 # cap applied past TR_POWER_FAR_DIST
|
||||
@@ -85,7 +273,7 @@ TR_POWER_ENERGY_MAX=3.0 # the cap at/above ENERGY_HI; 3.0 means effectively unc
|
||||
TR_POWER_FINISH_KILL=on # cap to the smallest bullet that still kills a low-energy enemy
|
||||
|
||||
# ── radar ────────────────────────────────────────────────────────────────────
|
||||
#TR_RADAR_FORCE_SPIN=1 # presence-only: force the old full 360 spin instead of 1v1 lock
|
||||
#TR_RADAR_FORCE_SPIN=1 # PRESENCE-only: force the old full 360 spin instead of 1v1 lock
|
||||
TR_RADAR_SCAN_LOG_PATH=/tmp/radar_scan_log.jsonl # where the per-tick scan log is written
|
||||
|
||||
# ── ram ──────────────────────────────────────────────────────────────────────
|
||||
@@ -98,42 +286,274 @@ TR_RAM_PLAN=off # the change-of-plan trigger (enemy outguns us while
|
||||
TR_RAM_PLAN_DIST=250.0 # px; max range at which the plan trigger may fire
|
||||
TR_RAM_PLAN_MARGIN=20.0 # energy advantage the plan trigger needs
|
||||
TR_RAM_PLAN_HITRATE=0.05 # pooled virtual hit rate below which the gun duel counts as failing
|
||||
TR_RAM_FLOOR_ENERGY=0.0 # j160 firing floor: at/below this self energy stop firing (0 = off)
|
||||
TR_RAM_ENEMY_ENERGY=0.0 # j160 exhaustion: last-scanned enemy energy <= this -> ram (0 = off)
|
||||
# WHAT: at or below this much SELF energy we start no new shot, keeping a
|
||||
# reserve for the ram. Units: energy points.
|
||||
# VALUES: energy, 0.0 = off (today's behaviour). Any float parses.
|
||||
# STATUS: =5 is a CLEAN NULL and must not be re-run: -0.018 wins/run,
|
||||
# p(sign-flip) = 0.7676, under a 0.1420 wins/run MDE, 900 battles. The
|
||||
# mechanism fired on 0.04% of ticks, ~200x less than the offline ruler
|
||||
# predicted. Docs: docs/ram_floor_exhaustion_ab.md.
|
||||
# GOTCHA: 0.0 genuinely disables it, but any POSITIVE value arms it, and the
|
||||
# higher it goes the more of the low-energy zone it blocks (20 blocked 24.7%
|
||||
# of all ticks offline).
|
||||
# TRY: TR_RAM_FLOOR_ENERGY=5 -> already measured, do not re-run. If you
|
||||
# want to see it at all, that is the only defensible value; anything
|
||||
# higher is worse by the offline ruler and by the live null.
|
||||
TR_RAM_FLOOR_ENERGY=0.0
|
||||
# WHAT: the last-scanned enemy energy at or below this switches the ram decider
|
||||
# into exhaustion mode (they are out of ammo, we are not). Units: energy.
|
||||
# VALUES: energy, 0.0 = off. Any float parses.
|
||||
# STATUS: DEFAULT-OFF, never live-tested. It is the second half of the j160 pair;
|
||||
# the other half (FLOOR_ENERGY) measured null, so the pair is not a win.
|
||||
# GOTCHA: 0.0 is the only safe "off". The always-on finisher (enemy < 20 energy
|
||||
# and we are healthier, within 300 px) already covers most of this; setting
|
||||
# this to 20 makes the two overlap.
|
||||
# TRY: TR_RAM_ENEMY_ENERGY=10 -> ram once a scan shows them at <= 10.
|
||||
# Watch: the `[ram] ON rrExhausted` line. Good: the reason field says
|
||||
# `exhausted` rather than `finisher`. Needs TR_RAM_LOG=1 to see it.
|
||||
TR_RAM_ENEMY_ENERGY=0.0
|
||||
|
||||
# ── movement internals: tfil (the floor-is-lava field) ──────────────────────
|
||||
TR_TFIL_RANGE_LO=100.0 # px; lower edge of the range band the ring mover prefers
|
||||
TR_TFIL_RANGE_HI=200.0 # px; upper edge of that band
|
||||
TR_TFIL_RANGE_TEMP=0.4 # sharpness of the ring mover's weighted random draw
|
||||
TR_TFIL_RANGE_K=60.0 # px; how fast the weight falls off outside the band
|
||||
TR_TFIL_RING_COMMIT_ARRIVAL=off # tfil_ring only: on = hold the dodge tile until we are ON it (not a fixed dwell)
|
||||
TR_TFIL_RING_NOREV_SPEED=0.0 # tfil_ring only; px/tick; below this, a mid-flight switch may not turn the bot around
|
||||
# WHAT (tfil_ring only): hold the committed dodge tile until we are actually ON
|
||||
# it, instead of the fixed dwell. VALUE: on | 0/off.
|
||||
# STATUS: DEFAULT-OFF, never live-tested. Ported to the ring fork in j165; the
|
||||
# tfil original (TR_TFIL_COMMIT_ARRIVAL) has a real but under-powered live
|
||||
# result — see that line. Neither is a proven win.
|
||||
# GOTCHA: the ring mover is NOT the default and is not shippable (round wins
|
||||
# 16/49 -> 6/49, p = 0.012), so this only matters while you are measuring it.
|
||||
# TRY: TR_MOVEMENT=tfil_ring + TR_TFIL_RING_COMMIT_ARRIVAL=1
|
||||
# -> the `[env]` block reads TR_TFIL_RING_COMMIT_ARRIVAL = on.
|
||||
TR_TFIL_RING_COMMIT_ARRIVAL=off
|
||||
# WHAT (tfil_ring only): below this self speed (px/tick), a mid-flight switch
|
||||
# may not turn the bot around. UNITS: px/tick (top speed is 8).
|
||||
# STATUS: DEFAULT-OFF (0.0), never live-tested. The tfil original is in the
|
||||
# j144 recommendation below.
|
||||
# TRY: TR_TFIL_RING_NOREV_SPEED=4 -> 4 px/tick is half of top speed, the
|
||||
# value j144 used on tfil. Accepts any float >= 0.
|
||||
TR_TFIL_RING_NOREV_SPEED=0.0
|
||||
TR_TFIL_CORRIDOR_HEAT=10.0 # lava painted per corridor-overlapping tile
|
||||
TR_TFIL_CORRIDOR_TICKS=0.0 # corridor length in ticks: 0 = to the wall (shipped); N>0 = min(to wall, bullet speed * N)
|
||||
# WHAT: cap the bullet-danger corridor at `bullet speed x this many ticks`,
|
||||
# instead of running it all the way to the arena wall. UNITS: ticks.
|
||||
# VALUES: ticks, 0.0 = off (corridor reaches the wall, today's behaviour).
|
||||
# Any float parses; negatives are treated as 0.
|
||||
# STATUS: DEFAULT-OFF, never live-tested, and NOT recommended. Commit 5e213df
|
||||
# (j148) shipped it with no measurement at all: no battle, no offline ruler.
|
||||
# The neighbouring j146 field-shape sweep, which is the closest evidence,
|
||||
# says halving the corridor restores a safe tile set offline (filter-broken
|
||||
# 63.5% -> 30.4%) and is a LIVE NULL on outcome.
|
||||
# GOTCHA: value knob - 0.0 genuinely disables it. It is NOT a presence knob.
|
||||
# TRY: TR_TFIL_CORRIDOR_TICKS=20 -> a 20-tick look-ahead. Bullet speed is
|
||||
# 20 - 3*power, so over the shipped power bins 1.0..3.0 that is
|
||||
# 17.0..11.0 px/tick = 340..220 px of corridor, instead of the whole
|
||||
# wall. Watch: in the debug overlay the corridor stops short of the
|
||||
# wall. Only bind it to tfil; strafe has its own knob below.
|
||||
TR_TFIL_CORRIDOR_TICKS=0.0
|
||||
TR_TFIL_WALL_HOTNESS=15.0 # peak heat painted on tiles next to a wall
|
||||
TR_TFIL_WALL_RADIANCE=10.0 # how fast wall heat falls off with distance
|
||||
TR_TFIL_BULLET_CORE=10.0 # tfil only: lava per bullet-overlapping tile (== PathDangerThreshold, so a bullet is never hot on its own)
|
||||
TR_TFIL_BULLET_AURA=5.0 # tfil only: lava for the bullet's aura ring tiles
|
||||
TR_TFIL_TILE_REPLAN=self # self | enemy | off: when a dodge commitment is cancelled
|
||||
TR_TFIL_COMMIT_TICKS=15 # ticks to commit to a dodge point before replanning
|
||||
TR_TFIL_ARRIVE_TICKS=0.0 # hard bound: never pick a tile farther than this (ticks at 8px/tick); 0 = off = today's draw
|
||||
TR_TFIL_HOLD_WHEN_TRAPPED=off # on = when NO safe tile exists, hold position one tick instead of taking the 2 least-hot blocked tiles; off = today's fallback
|
||||
TR_TFIL_HOLD_MAX_TICKS=0 # BOUNDED version of the above: hold at most N ticks per empty-safe-set streak (default 0 = off = today's fallback). The budget is DERIVED from the enemy's rate of fire (2 x 3.0-power shots = 16 ticks); the hold is released the tick a safe tile exists and overridden outright if a tracked bullet reaches us within min(N, 16) ticks
|
||||
TR_TFIL_DANGER_THRESHOLD=10.0 # hard heat filter on the path; lava is quantised to 5, so the effective steps are 10/15/20 and 10-14 admits exactly what 10 does
|
||||
TR_TFIL_DIAG=off # on = fill the per-pick picker loss histogram (TfilLoss*); observability only
|
||||
TR_TFIL_GEO_MODE=off # off | turn | dist | both, each with a `-soft` (default) / `-topk` / `-rej` suffix; shapes the DRAW over the heat-filtered pool
|
||||
TR_TFIL_GEO_TAU=0.0 # deg; the geometric cost scale. 0 = off = today's uniform draw
|
||||
TR_TFIL_NO_REV=off # on = never reverse direction inside a corridor
|
||||
# WHAT: how hot a bullet paints the tile it is sitting on. UNITS: lava points
|
||||
# on the picker's heat scale.
|
||||
# VALUES: any float >= 0. 10.0 is exactly the danger threshold, so a bullet is
|
||||
# never dangerous on its own; 20.0 puts one bullet's own tile over it.
|
||||
# STATUS: live-tested ONLY as part of the j146 five-shape batch, 375 battles:
|
||||
# core 10 -> 20 was a live NULL on both primaries, and the arm that combined
|
||||
# it with no corridor/wall field LOST 13.45 damage/run, p(sign-flip) =
|
||||
# 0.0095. The middle shape (corridor 10 / wall 15/5 / core 20 / aura 10) is
|
||||
# also a null. Do not retune the shape on one axis.
|
||||
# GOTCHA: changing CORE alone with the shipped corridor (10) and wall (15) is
|
||||
# the one combination j146 did NOT isolate.
|
||||
# TRY: TR_TFIL_BULLET_CORE=20 + TR_TFIL_BULLET_AURA=10 -> the j146
|
||||
# "middle" bullet heat. Only meaningful as part of the whole middle
|
||||
# shape; on its own it is a null at best.
|
||||
TR_TFIL_BULLET_CORE=10.0
|
||||
# WHAT: the heat painted on the bullet's AURA ring (the tiles around it), as
|
||||
# opposed to the core tile. UNITS: lava points.
|
||||
# VALUES: any float >= 0; 5.0 is half the core's 10.0.
|
||||
# STATUS: live-tested only inside the j146 batch — null, see BULLET_CORE.
|
||||
# TRY: TR_TFIL_BULLET_AURA=10 -> doubles the aura heat; pairs with
|
||||
# TR_TFIL_BULLET_CORE=20 in the j146 "middle" shape.
|
||||
TR_TFIL_BULLET_AURA=5.0
|
||||
# WHAT: when a dodge commitment is cancelled, replan from whose state?
|
||||
# self = today's behaviour. VALUE: self | enemy | off.
|
||||
# STATUS: `self` is the shipped default; `off` is the pre-j144 behaviour and is
|
||||
# exactly what an earlier A/B (cc11ede arm D) tried and could not measure.
|
||||
# TRY: TR_TFIL_TILE_REPLAN=off -> the tile-crossing cancel stops firing.
|
||||
# Accepts self, enemy, off, none, never, 0, false (case-insensitive);
|
||||
# anything else falls back to self with no warning.
|
||||
TR_TFIL_TILE_REPLAN=self
|
||||
TR_TFIL_COMMIT_TICKS=15 # ticks to commit to a dodge point before replanning (min 1)
|
||||
# WHAT: hold the committed dodge tile until we are actually ON it, instead of
|
||||
# letting our own tile-boundary crossing cancel it. VALUE: on | 0/off.
|
||||
# STATUS: LIVE, 600 battles in two blocks (j144). Mechanism is real and
|
||||
# confirmed: incoming hit rate 18.07% -> 14.92%, sign-flip p = 0.0013, damage
|
||||
# taken -24.57/run. Outcome is NOT distinguishable: +0.28 wins/run,
|
||||
# p(sign) = 0.0574 against a 0.31 MDE. The ledger's answer for your own .env
|
||||
# is YES to this knob, and NO to COMMIT_MARGIN.
|
||||
# GOTCHA: only meaningful with TR_MOVEMENT=tfil; the default strafe mover has
|
||||
# no tfil commitment. With TR_TFIL_COMMIT_ARRIVAL on, COMMIT_TICKS becomes a
|
||||
# MINIMUM dwell, not a maximum.
|
||||
# TRY: TR_MOVEMENT=tfil + TR_TFIL_COMMIT_ARRIVAL=1
|
||||
# -> `[env] TR_TFIL_COMMIT_ARRIVAL = on (source: .env)`.
|
||||
TR_TFIL_COMMIT_ARRIVAL=off
|
||||
# WHAT: leave the committed tile only if the best alternative is at least this
|
||||
# much COOLER on the same pathMaxHeat scale the picker uses. UNITS: lava
|
||||
# points; 10.0 is exactly one PathDangerThreshold level.
|
||||
# VALUES: any float >= 0; 0.0 = off = today's behaviour (any improvement ends
|
||||
# the commitment).
|
||||
# STATUS: LIVE, j144, 600 battles, as the `arrive_hyst` arm. It is the WEAKEST
|
||||
# of the three j144 arms on both primaries (+0.19 wins/run, p = 0.092) and
|
||||
# the ledger's explicit answer is: YES to COMMIT_ARRIVAL, NO to this.
|
||||
# GOTCHA: it is a hysteresis, so it makes the bot commit harder; a large value
|
||||
# with a busy field means it holds a tile that is no longer the best one.
|
||||
# TRY: TR_TFIL_COMMIT_MARGIN=10 -> the j144 value. Already measured, and
|
||||
# the answer was no. Use it only to isolate COMMIT_ARRIVAL, not as an
|
||||
# improvement.
|
||||
TR_TFIL_COMMIT_MARGIN=0.0
|
||||
# WHAT: refuse a candidate tile we cannot REACH inside this many ticks. The
|
||||
# bot's top speed is 8 px/tick, so 1 tick = 8 px. UNITS: ticks.
|
||||
# VALUES: ticks, 0.0 = off = today's uniform draw over every safe tile.
|
||||
# STATUS: DEFAULT-OFF, never live-tested. Found by the j151 offline ruler: 65%
|
||||
# of tfil picks outran the 15-tick commitment and the chosen tile was reached
|
||||
# only 6.5% of the time.
|
||||
# GOTCHA: it is a HARD bound, not a preference. If every safe tile is out of
|
||||
# range the pool empties and the code falls back to today's full pool, so it
|
||||
# can never starve the draw — it can also silently do nothing.
|
||||
# TRY: TR_TFIL_ARRIVE_TICKS=15 -> refuse anything farther than 15*8 = 120
|
||||
# px. That is deliberately the same length as TR_TFIL_COMMIT_TICKS,
|
||||
# i.e. "only pick a tile you can still reach while you hold it".
|
||||
TR_TFIL_ARRIVE_TICKS=0.0
|
||||
# WHAT: when the safe-tile set is EMPTY (no tile under the heat threshold),
|
||||
# hold position for one tick instead of promoting the 2 least-hot blocked
|
||||
# tiles. VALUE: on | 0/off.
|
||||
# STATUS: DEFAULT-OFF, never live-tested. j153 wrote the proposal and the A/B
|
||||
# design; it was NOT run (docs/tfil_hold_when_trapped_ab.md is titled
|
||||
# "NOT RUN"). Four mechanism-positive / outcome-null results preceded it, so
|
||||
# a null was always the likely answer.
|
||||
# GOTCHA: it can never latch — the pick site only runs on a replan tick, so the
|
||||
# next tick re-reads the field from scratch. The gun is untouched: a held
|
||||
# tick still fires exactly like every other tick.
|
||||
# TRY: TR_TFIL_HOLD_WHEN_TRAPPED=on -> the tfil pick log shows `hold`
|
||||
# instead of `promote` on a trapped tick. Needs TR_TFIL_COMMIT_LOG set.
|
||||
TR_TFIL_HOLD_WHEN_TRAPPED=off
|
||||
# WHAT: the BOUNDED version of the knob above: while the safe set stays empty,
|
||||
# hold for at most this many ticks per empty streak. UNITS: ticks (integer).
|
||||
# VALUES: integer >= 0; 0 = off = today's promote-the-2 fallback.
|
||||
# STATUS: DEFAULT-OFF, never live-tested (j154, no battle).
|
||||
# GOTCHA: the budget is DERIVED, not guessed: the enemy fires two 3.0-power
|
||||
# shots 16 ticks apart, so 16 is the first window that admits its second
|
||||
# shot. The hold is released the tick a safe tile exists, the counter resets
|
||||
# when one is taken, and a tracked bullet reaching us within min(N,16) ticks
|
||||
# overrides the hold outright (panic release).
|
||||
# TRY: TR_TFIL_HOLD_MAX_TICKS=16 -> the derived budget. Any integer parses;
|
||||
# a junk value degrades to 0 (off).
|
||||
TR_TFIL_HOLD_MAX_TICKS=0
|
||||
# WHAT: the hard heat filter on a candidate's path. UNITS: lava points.
|
||||
# VALUES: any float >= 0. Lava is QUANTISED to 5, so the only values that
|
||||
# change anything are 10, 15 and 20: 10-14 admits exactly what 10 admits.
|
||||
# STATUS: 10.0 is the shipped const; j150 made it sweepable so the offline
|
||||
# ruler could move it. Never live-tested as a knob.
|
||||
# GOTCHA: raising it does not make the bot braver, it makes the safe set
|
||||
# smaller and more of the picks forced. j146's `nofield` arm is the warning:
|
||||
# -13.45 damage/run, p = 0.0095.
|
||||
# TRY: TR_TFIL_DANGER_THRESHOLD=15 -> one quantisation step stricter. The
|
||||
# picker log then reports fewer safe candidates per pick.
|
||||
TR_TFIL_DANGER_THRESHOLD=10.0
|
||||
# WHAT: fill the per-pick LOSS HISTOGRAM (TfilLoss*): how many tiles die at
|
||||
# each picker stage. VALUE: on | 0/off (read by value, not by presence).
|
||||
# STATUS: DIAGNOSTIC ONLY, j150. Provably does not move a single move command
|
||||
# (byte-for-byte, guard-tested). Never measured on outcome, by design.
|
||||
# GOTCHA: nothing. It is the safest tfil knob in this file.
|
||||
# TRY: TR_TFIL_DIAG=on -> the tfil pick log gains the per-stage counts.
|
||||
TR_TFIL_DIAG=off
|
||||
# WHAT: shape the tile DRAW over the heat-filtered pool by geometry (how far
|
||||
# the tile sits from where we are already going) as well as by heat.
|
||||
# VALUES: off | turn | dist | both, each optionally suffixed -soft (default),
|
||||
# -topk or -rej. `distance`=dist, `rejection`=rej are also accepted.
|
||||
# Anything unrecognised, and `off`, means OFF — today's uniform draw.
|
||||
# The form is NOT printed by the boot report, only the dim.
|
||||
# STATUS: DEFAULT-OFF. THE ONE LIVE-TESTED ARM IS `both-rej` + TAU=60 AND IT
|
||||
# WAS REJECTED: -8.83 damage/run, p(sign-flip) = 0.0061, Wilcoxon p = 0.011,
|
||||
# 420 battles, 15 opponents. Round wins null. Offline it did exactly what was
|
||||
# predicted (arrivals 4.5% -> 29.4%) and that is WHY it is bad: the bot ends
|
||||
# up 26 px further out on 15/15 opponents and deals less. See
|
||||
# docs/tfil_geo_ab.md. DO NOT re-run both-rej.
|
||||
# GOTCHA: this knob ALONE is inert — TR_TFIL_GEO_TAU=0.0 means the weighting is
|
||||
# off whatever the mode says. And it needs TR_MOVEMENT=tfil.
|
||||
# TRY: TR_TFIL_GEO_MODE=both-soft + TR_TFIL_GEO_TAU=45
|
||||
# -> the j152 offline headline (arrivals 4.5% -> 16.9%, top-tile share
|
||||
# only 7.0% -> 8.6%). Still never live-tested, and the family has one
|
||||
# measured loss, so treat it as a hypothesis.
|
||||
TR_TFIL_GEO_MODE=off
|
||||
# WHAT: the geometric cost scale, in degrees. 0.0 = off, i.e. exactly today's
|
||||
# uniform draw. UNITS: degrees. VALUES: any float >= 0.
|
||||
# STATUS: never live-tested with a mode other than off. The one live arm used
|
||||
# TAU=60 and lost (see GEO_MODE).
|
||||
# GOTCHA: TAU is IGNORED unless GEO_MODE is not off. A big TAU with mode=off
|
||||
# looks like it is doing something and is not.
|
||||
# TRY: TR_TFIL_GEO_TAU=45 -> 45 degrees of turn cost. Pairs with
|
||||
# TR_TFIL_GEO_MODE=both-soft; on its own it changes nothing.
|
||||
TR_TFIL_GEO_TAU=0.0
|
||||
# WHAT: inside a corridor, never pick a tile in the reverse direction. VALUE:
|
||||
# on | 0/false/no/off (read by value, so `=off` really disables it).
|
||||
# STATUS: never live-tested as a standalone arm. The j144/j145 arms relied on
|
||||
# the NOREV_SPEED knob instead, which is stricter.
|
||||
# GOTCHA: soft only — every weight is floored, so the pool can never empty; and
|
||||
# it overlaps TR_TFIL_NOREV_SPEED, which is the knob that was actually run.
|
||||
# TRY: TR_TFIL_NO_REV=on -> a 3:1 forward:rearward draw weight inside a
|
||||
# corridor. Accepts on/1/true/yes to enable, 0/false/no/off to disable.
|
||||
TR_TFIL_NO_REV=off
|
||||
TR_TFIL_COMMIT_LOG= # path for the per-commit log; empty = no log
|
||||
TR_TFIL_COMMIT_ARRIVAL=off # on = hold the dodge tile until we are ON it (not a fixed dwell)
|
||||
TR_TFIL_COMMIT_MARGIN=0.0 # lava an alternative tile must be cooler by before it wins the tile
|
||||
TR_TFIL_NOREV_SPEED=0.0 # px/tick; below this, a mid-flight switch may not turn the bot around
|
||||
TR_TFIL_TURN_BIAS=0.0 # turn TIEBREAK odds ratio among SAFE tiles; 0 = uniform draw as today
|
||||
TR_TFIL_TURN_REF_DEG=45.0 # deg; turn below which the tiebreak applies no penalty
|
||||
# WHAT: while our own speed is below this, a mid-flight target switch may not
|
||||
# take a tile more than 90 degrees off the travel direction. UNITS: px/tick
|
||||
# (top speed 8). VALUES: any float >= 0; 0.0 = off = shipped.
|
||||
# STATUS: LIVE, j144/j145, 600 battles. Mechanism confirmed: slow opposite-way
|
||||
# mid-flight switches fell 394 -> 64 offline. Outcome not distinguishable on
|
||||
# its own (+0.12 wins/run, p = 0.39); the ledger recommends 4 in your .env
|
||||
# together with COMMIT_ARRIVAL, not alone.
|
||||
# GOTCHA: the pool can never be emptied — with every candidate behind us it
|
||||
# takes the least-bad turn. 0.0 genuinely disables it.
|
||||
# TRY: TR_TFIL_NOREV_SPEED=4 -> 4 px/tick = half of top speed, the j144
|
||||
# value. Watch: fewer >90 deg switches in the tfil pick log.
|
||||
TR_TFIL_NOREV_SPEED=0.0
|
||||
# WHAT: turn TIEBREAK odds ratio among the SAFE tiles: a straight-ahead safe
|
||||
# tile is drawn `1 + bias` times as often as a 180-degree one.
|
||||
# w = max(1, round(1 + BIAS * (1 - max(0,|turn| - REF)/180)))
|
||||
# UNITS: dimensionless. VALUES: any float >= 0; 0.0 = uniform draw as today.
|
||||
# STATUS: LIVE NULL, j145, 300 battles: +0.15 wins/run, p(sign) = 0.244, under
|
||||
# a 0.30 MDE. Docs: docs/movement_campaign.md. A real but small mechanism
|
||||
# with an under-powered outcome.
|
||||
# GOTCHA: the turn cost is NEVER folded into the heat score — the filter stays
|
||||
# hard, and every weight is floored at 1, so the pool can never empty.
|
||||
# TRY: TR_TFIL_TURN_BIAS=9 + TR_TFIL_TURN_REF_DEG=0
|
||||
# -> the j145 arm value (the knee of the offline bias curve: mean
|
||||
# |turn| -11%, opposite picks -22%). Already measured null.
|
||||
TR_TFIL_TURN_BIAS=0.0
|
||||
# WHAT: the turn below which the tiebreak above applies NO penalty. UNITS:
|
||||
# degrees. VALUES: any float >= 0; 45.0 = today's default.
|
||||
# STATUS: inert unless TR_TFIL_TURN_BIAS > 0. j145 used 0 with bias 9.
|
||||
# GOTCHA: on its own this knob does nothing at all.
|
||||
# TRY: TR_TFIL_TURN_REF_DEG=0 -> penalise every turn, not just the sharp
|
||||
# ones. Pairs with TR_TFIL_TURN_BIAS=9; alone it is a no-op.
|
||||
TR_TFIL_TURN_REF_DEG=45.0
|
||||
# WHAT: paint heat on the virtual centre pillar. VALUE: on | 0/off. The shipped
|
||||
# field has NO pillar: PillarHotness/PillarRadiance are 0.0.
|
||||
# STATUS: live-tested, and the OWNER OVERRULED the recommendation to turn it
|
||||
# back on: `old` (pillar on) was best on damage/run (287) and round wins
|
||||
# (35/70) but the contrast is INSIDE the MDE (33 damage/run, 1.22 wins/run
|
||||
# at n=10) and damage taken was 30.8/run higher with the pillar off
|
||||
# (p = 0.040, not corrected for multiple arms). Decision: pillar stays
|
||||
# removed. Docs: docs/tfil_heat_pillar_ab.md.
|
||||
# GOTCHA: this restores an INVENTED hazard with no physical object behind it.
|
||||
# Reversing that decision needs its own pre-registered A/B.
|
||||
# TRY: TR_TFIL_PILLAR_ON=1 -> the pre-change field (30/10), for a fair
|
||||
# A/B against the shipped one. Needs TR_MOVEMENT=tfil.
|
||||
TR_TFIL_PILLAR_ON=off
|
||||
TR_TFIL_HEAT_TIME=off # on = index bullet heat by time (flat field when off)
|
||||
TR_TFIL_HEAT_TAU=9.0 # ticks a tracked bullet's heat lives for
|
||||
TR_TFIL_HEAT_POWER_GAIN=1.0 # scale of the heat a bullet paints, per firepower
|
||||
TR_TFIL_PILLAR_ON=off # on = paint heat on the arena centre, which has no pillar
|
||||
|
||||
# ── movement internals: strafe ───────────────────────────────────────────────
|
||||
TR_STRAFE_BAND=20.0 # degrees the heading may sit off the perpendicular
|
||||
@@ -152,14 +572,42 @@ TR_STRAFE_WALL_BIAS=0.35 # how strongly a tile farther from the wall is preferr
|
||||
TR_STRAFE_WALL_SAFE=24.0 # px; a wing point never lands nearer than this to a wall
|
||||
TR_STRAFE_ESCAPE=on # the guaranteed wall escape when every candidate is hot
|
||||
TR_STRAFE_FIRE_FIX=on # strafe's share of the shared TR_FIRE_FIX switch
|
||||
TR_FIRE_FIX=on # 0 = the shipped previous-energy bullet detector
|
||||
#TR_FIRE_DIAG=1 # presence-only: per-reading tick/raw/correction trace
|
||||
#TR_FIRE_LAG=0 # ticks to back-date each detected fire at spawn (0=shipped; 1=the measured live detection lag)
|
||||
# WHAT: the shared enemy-fire detector. VALUE: on | 0/off. One switch, read by
|
||||
# every mover; each mover may AND it with its own (TR_STRAFE_FIRE_FIX).
|
||||
# STATUS: j134 propagated it to all five movers; the per-mover catch table went
|
||||
# 98.888% -> 100% of enemy fires on a 70-battle corpus.
|
||||
# GOTCHA: TR_STRAFE_FIRE_FIX is an AND, so turning TR_FIRE_FIX off is enough;
|
||||
# turning only TR_STRAFE_FIRE_FIX off does not disable strafe's detector.
|
||||
# TRY: TR_FIRE_FIX=0 -> the shipped previous-energy detector everywhere
|
||||
# (the control arm for any fire-detector A/B).
|
||||
TR_FIRE_FIX=on
|
||||
#TR_FIRE_DIAG=1 # PRESENCE-only: per-reading tick/raw/correction trace
|
||||
# WHAT: back-date every detected enemy fire by this many ticks when the ghost
|
||||
# is spawned. UNITS: ticks (integer, clamped at 0). 0 = shipped.
|
||||
# STATUS: LIVE, j147, 180 battles. The 1-tick detection lag is OURS and was
|
||||
# measured on 1777 matched ghost spawns (displacement 19.1 px mean on tfil,
|
||||
# 16.1 on strafe; the arrival deadline the mover reads was 0.99 / 0.77 ticks
|
||||
# late). With =1 it falls to 5.4 / 9.0 px and 0.06 ticks. The live OUTCOME is
|
||||
# null on both movers (+2.08 / +3.77 damage/run, every p > 0.6), so it stays 0.
|
||||
# GOTCHA: a junk or negative value degrades to 0, never a negative back-date.
|
||||
# TRY: TR_FIRE_LAG=1 -> one bullet step at power 1.0 is 17 px; the ghost is
|
||||
# born that far downrange. Watch the tfil/strafe pick log's arrival
|
||||
# deadline, which is what the knob actually corrects.
|
||||
TR_FIRE_LAG=0
|
||||
TR_STRAFE_HEAT_GRID=on # draw the whole heat grid; 0 leaves only the chosen tile
|
||||
TR_STRAFE_BULLET_CORE=20.0 # strafe's own retune: lava per bullet-overlapping tile
|
||||
TR_STRAFE_BULLET_AURA=10.0 # strafe's own retune: lava for the bullet aura ring
|
||||
TR_STRAFE_CORRIDOR_HEAT=10.0 # strafe's own retune: lava per corridor tile
|
||||
TR_STRAFE_CORRIDOR_TICKS=0.0 # same length bound for strafe: 0 = to the wall (shipped)
|
||||
# WHAT: the same corridor LENGTH bound as TR_TFIL_CORRIDOR_TICKS, for the
|
||||
# strafe mover. UNITS: ticks. VALUES: ticks, 0.0 = to the wall (shipped).
|
||||
# STATUS: DEFAULT-OFF, never live-tested (commit 5e213df, j148). Same standing
|
||||
# as the tfil one: no battle, no offline ruler, not recommended.
|
||||
# GOTCHA: this is the DEFAULT engine's knob. Setting only the tfil one does
|
||||
# nothing at all, because the shipped movement is strafe.
|
||||
# TRY: TR_STRAFE_CORRIDOR_TICKS=20 -> 20-tick look-ahead, 220..340 px over
|
||||
# the shipped power bins, instead of the whole wall. Watch: the strafe
|
||||
# heat grid's corridor stops before the wall.
|
||||
TR_STRAFE_CORRIDOR_TICKS=0.0
|
||||
TR_STRAFE_WALL_HOTNESS=15.0 # strafe's own retune: peak wall heat
|
||||
TR_STRAFE_WALL_RADIANCE=5.0 # strafe's own retune: wall heat falloff
|
||||
|
||||
@@ -168,7 +616,7 @@ TR_SURF_PREF_DIST=400.0 # px; the wave distance the mover tries to sit at
|
||||
TR_SURF_DIST_BAND=50.0 # px dead band around it
|
||||
TR_SURF_WALL_MARGIN=48.0 # px kept from the wall when picking a wave point
|
||||
TR_SURF_RADIAL_FRAC=0.35 # how much of the remaining weight goes to the radial blend
|
||||
#TR_SURF_LOG=1 # presence-only: one line per wave-surfing decision
|
||||
#TR_SURF_LOG=1 # PRESENCE-only: one line per wave-surfing decision
|
||||
|
||||
# ── movement internals: learned (per-state learned danger) ──────────────────
|
||||
TR_LEARNED_DECAY_EVERY=128 # learns between forgetting passes; 0 never forgets
|
||||
@@ -183,18 +631,20 @@ TR_LEARNED_WALL_MARGIN=48.0 # px kept from the wall
|
||||
TR_LEARNED_GLOBAL=off # on = ignore the learned state (ablation arm)
|
||||
TR_LEARNED_LABEL=histogram # histogram (default) or outcome: what a wave is labelled with
|
||||
TR_LEARNED_REAL_EVENTS=off # on = resolve a wave on the real bullet event, not on energy
|
||||
#TR_LEARNED_LOG=1 # presence-only: one line per learned decision
|
||||
#TR_LEARNED_LOG=1 # VALUE-based (1/on/yes to enable, 0/off to disable)
|
||||
|
||||
# ── guns ─────────────────────────────────────────────────────────────────────
|
||||
# Virtual bullets: the prediction the whole gun selector is built on.
|
||||
TR_MODULE_VBULLETS=on # 0 = no gun predicts or spawns; the selector falls back to its floor gun
|
||||
|
||||
# TMH — the horizon Tsetlin automata gun.
|
||||
# TMH — the horizon Tsetlin automata gun. Inert unless TR_RACK_TMHORIZON=both.
|
||||
TR_TMHORIZON_SHIFT=2.0 # degrees added to the aim; 0 disables the correction arm
|
||||
TR_TMHORIZON_BIG_MULT=1.5 # extra scale applied when the error magnitude is big
|
||||
TR_TMHORIZON_RESET_ON_TARGET=on # wipe the automata when the target changes
|
||||
TR_TMHORIZON_NSTATES=64 # automata state count (the inertia it can hold)
|
||||
TR_TMHORIZON_WINDOW=0 # samples kept in the sliding window; 0 keeps everything
|
||||
# e.g. TR_TMHORIZON_WINDOW=30 -> retrain on the 30 most recent samples only.
|
||||
# MEASURED HARMFUL LIVE at 150: 26.5% round wins vs 49.0%, p = 0.036. Keep 0.
|
||||
TR_TMHORIZON_RESET_DROP=0.0 # rolling accuracy drop, in points, that forces a retrain
|
||||
TR_TMHORIZON_ACCURVE=off # log the accuracy curve even without the thinking log
|
||||
TR_TMHORIZON_RETRAIN_EVERY=50 # samples between full retrains in sliding mode
|
||||
@@ -207,7 +657,7 @@ TR_LEADGAIN_MIN_OBS=8 # samples a band needs before its gain is trusted
|
||||
TR_LEADGAIN_DECAY=250 # samples between count-decay passes
|
||||
TR_LEADGAIN_DECAY_FRAC=0.02 # fraction each decay pass takes off every count
|
||||
TR_LEADGAIN_RESET_ON_TARGET=on # wipe the learned gains when the target changes
|
||||
TR_LEADGAIN_LOG=off # on = one line per gain change
|
||||
TR_LEADGAIN_LOG=off # on = one line per gain change (value-based: 0 disables)
|
||||
# Kept only so a pre-rename .env does not warn. The gun does not read them.
|
||||
TR_LEADGAIN_N=32 # NO-OP: the old SBC geometry, no longer used
|
||||
TR_LEADGAIN_NADE=256 # NO-OP: the old ADE count, no longer used
|
||||
@@ -221,6 +671,10 @@ TR_LEADGAIN_SEED=20240921 # NO-OP: the old seed, no longer used
|
||||
TR_PATTERN_LEN=10 # ticks of movement history used as the search key
|
||||
TR_PATTERN_DEPTH=500 # how far back the history scan may reach
|
||||
TR_PATTERN_RAD_OFFSET=0.0 # px added to the aim distance; negative aims short
|
||||
# Both RAD_* knobs are read LAZILY, inside predict() — the one place a
|
||||
# mid-run change can matter, and it still does not. The live aim is bearing
|
||||
# only, so a purely radial offset is structurally invisible: MEASURED
|
||||
# byte-identical on bmPath. Docs: docs/env_reference.md §7.
|
||||
TR_PATTERN_RAD_SCALE=1.0 # multiplier on the whole aim distance
|
||||
|
||||
# The SBC library (common_libs/bitbrain), not a gun knob. Registered so a config
|
||||
@@ -250,32 +704,71 @@ TR_BITBRAIN_DECAY_SHIFT=1 # forgetting strength, counted mode only; 0 disa
|
||||
#TR_BITBRAIN_RESET_ON_TARGET=on # LEGACY: old name of TR_LEADGAIN_RESET_ON_TARGET
|
||||
|
||||
# ── debug overlays (all on top of the gun; they never change a decision) ─────
|
||||
TR_DEBUG_DRAW=on # master switch for every mover's debugGraphics
|
||||
TR_GEO_DEBUG=off # draw the shared candidate-tile geometry overlay
|
||||
TR_VBULLET_DEBUG=off # draw each admitted gun's virtual-bullet paths
|
||||
TR_VBULLET_DEBUG_GUN= # which gun the overlay draws: empty = the selected one
|
||||
TR_VBULLET_DEBUG_MAX=32 # max virtual bullets drawn per gun
|
||||
# WHAT: master switch for every mover's debugGraphics. VALUE: on | 0/off.
|
||||
TR_DEBUG_DRAW=on
|
||||
# WHAT: draw the shared candidate-tile geometry overlay. VALUE: on | 0/off.
|
||||
# DRAW ONLY — it cannot change a decision, so it is the safest knob here.
|
||||
# GOTCHA: independent of TR_DEBUG_DRAW: the overlay is drawn either way, and
|
||||
# TR_DEBUG_DRAW=0 is what suppresses the movers' own graphics.
|
||||
# TRY: TR_GEO_DEBUG=on -> the geometry circles appear over the arena.
|
||||
TR_GEO_DEBUG=off
|
||||
# WHAT: draw each admitted gun's virtual-bullet paths. VALUE: 1/on/yes to
|
||||
# enable, 0/off/no/false to disable, unset = off. DRAW ONLY.
|
||||
# GOTCHA: needs a gun in the rack to be legible; the shipped rack admits
|
||||
# Pattern only, so set TR_VBULLET_DEBUG_GUN too.
|
||||
# TRY: TR_VBULLET_DEBUG=1 + TR_VBULLET_DEBUG_GUN=all
|
||||
# -> travelled path, aim ring and miss vector for every admitted gun.
|
||||
TR_VBULLET_DEBUG=off
|
||||
# WHICH gun the overlay draws. VALUES: empty = the currently selected gun;
|
||||
# `all` or `*` = every gun; otherwise a gun name, e.g. `Pattern`.
|
||||
# TRY: TR_VBULLET_DEBUG_GUN=all -> every admitted gun at once.
|
||||
TR_VBULLET_DEBUG_GUN=
|
||||
TR_VBULLET_DEBUG_MAX=32 # max virtual bullets drawn per gun (clamped to >= 1)
|
||||
TR_VBULLET_ADMIT_ONLY=on # on = a gun the rack does not admit is not even predicted
|
||||
|
||||
# ── logs (set the value to 1; presence alone turns some of them on) ──────────
|
||||
TR_RESULT_LOG=on # one line per round result
|
||||
#TR_POWER_LOG=1 # presence-only: one line per power decision
|
||||
#TR_RAM_LOG=1 # presence-only: one line per ram start/stop and why
|
||||
#TR_MOVEMENT_LOG=1 # presence-only: movement band/class changes
|
||||
#TR_STRAFE_LOG=1 # presence-only: one line per strafe tile pick
|
||||
#TR_TMHORIZON_LOG=1 # presence-only: the per-shot thinking of the TM horizon gun
|
||||
# ── logs (set the value to 1; PRESENCE alone turns these on) ─────────────────
|
||||
# WHAT: one line per round result. VALUE-based (unlike the block below): 0/off
|
||||
# really disables it, which is why this one is written out uncommented.
|
||||
# TRY: TR_RESULT_LOG=off -> no [result] lines at all.
|
||||
TR_RESULT_LOG=on
|
||||
#TR_POWER_LOG=1 # PRESENCE-only: one line per power decision (0 would ENABLE it)
|
||||
#TR_RAM_LOG=1 # PRESENCE-only: one line per ram start/stop and why
|
||||
#TR_MOVEMENT_LOG=1 # PRESENCE-only: movement band/class changes
|
||||
#TR_STRAFE_LOG=1 # PRESENCE-only: one line per strafe tile pick
|
||||
#TR_TMHORIZON_LOG=1 # VALUE-based: the per-shot thinking of the TM horizon gun
|
||||
GUN_STATS_PATH=/tmp/gun_stats.jsonl # where the per-round gun stats are written
|
||||
GUN_SHOTLOG_PATH=/tmp/shot_log.jsonl # where the per-shot log is written
|
||||
|
||||
# ── measurement helpers (leave off unless you are measuring) ─────────────────
|
||||
#TR_RECORD_WORLDSTATE=1 # presence-only: dump every observed world state
|
||||
#TR_RADAR_SCANLOG=1 # presence-only: log every radar scan tick
|
||||
#TR_TRACKER_PROBE=1 # presence-only: dump the enemy-tracker's internal state
|
||||
# WHAT: aim capture. Adds TWO record kinds to the TR_RECORD_WORLDSTATE file:
|
||||
# aim_scan — one per radar scan: the raw reading, our own state, the gun
|
||||
# that fired, the PREVIOUS tracker belief, and the scan parity
|
||||
# (age = tick - previous lastSeenTick);
|
||||
# aim_fire — one per real shot: gun, power, the aim angle, the turret error,
|
||||
# gun heat, the predicted intercept/TOF, and the exact WorldState the
|
||||
# predictor consumed (with the tick it came from). It also adds the `gun`
|
||||
# id to the per-tick world-state rows.
|
||||
# VALUES: presence-only, like the other keys in this block. Unset = off.
|
||||
# STATUS: default-off, diagnostic only, never live-tested. j176 could not
|
||||
# attribute the 11.9 deg aim error because the corpus had no gun id and no
|
||||
# bot-side belief; this knob makes both a lookup instead of an inverse
|
||||
# problem. No aim model changed with it.
|
||||
# GOTCHA: it only writes when TR_RECORD_WORLDSTATE is on as well, and it makes
|
||||
# the capture file bigger, not different: the extra lines are annotations and
|
||||
# the offline replay skips them.
|
||||
# TRY: TR_RECORD_WORLDSTATE=1 TR_CAPTURE_AIM=1 ./out/ModularBot
|
||||
#TR_CAPTURE_AIM=1 # PRESENCE-only: aim_scan / aim_fire records (needs TR_RECORD_WORLDSTATE)
|
||||
#TR_RECORD_WORLDSTATE=1 # PRESENCE-only: dump every observed world state
|
||||
#TR_RADAR_SCANLOG=1 # PRESENCE-only: log every radar scan tick
|
||||
#TR_TRACKER_PROBE=1 # PRESENCE-only: dump the enemy-tracker's internal state
|
||||
TR_TRACKER_PROBE_PATH=/tmp/tracker_probe.jsonl # where that dump is written
|
||||
|
||||
# ── dotenv / boot report ─────────────────────────────────────────────────────
|
||||
# Name of the env file to load. Must be set in the REAL environment, not in the
|
||||
# file it points at. Empty = use ./.env, else .env next to the binary.
|
||||
# GOTCHA: the line below sets it to the EMPTY string, which is the correct
|
||||
# "use the default" spelling; putting a real path here would make this file
|
||||
# load itself, recursively, at every start.
|
||||
TR_ENV_FILE=
|
||||
# 1 = print the [env] report on startup (default). 0 = do not print it.
|
||||
TR_ENV_REPORT=1
|
||||
|
||||
@@ -44,6 +44,7 @@ import movement_harness/bullet_shadows
|
||||
import targeting/enemy_tracker
|
||||
import targeting/target_selector
|
||||
import env_report
|
||||
import aim_capture
|
||||
import vbullet_draw
|
||||
import geo_overlay
|
||||
|
||||
@@ -69,6 +70,13 @@ const ShotLog = true
|
||||
## can enable recording for just the battle it spawns by exporting the env var.
|
||||
let RecordWorldState* = existsEnv("TR_RECORD_WORLDSTATE")
|
||||
const WorldStateRecordPath = "/tmp/worldstate_record.jsonl"
|
||||
## j177 aim capture: with TR_CAPTURE_AIM set, append two extra record kinds to
|
||||
## the SAME world-state file — `aim_scan` (one per radar scan: the raw reading,
|
||||
## our own state, the GUN, the previous belief and the scan parity) and
|
||||
## `aim_fire` (one per firing decision: the gun, the power, the aim angle, the
|
||||
## turret error, and the exact WorldState the predictor consumed). Off by
|
||||
## default, so a normal run writes byte-for-byte what it wrote before.
|
||||
let CaptureAim* = existsEnv("TR_CAPTURE_AIM")
|
||||
## Radar measurement switches (all RUNTIME, read once at process start):
|
||||
## TR_RADAR_FORCE_SPIN=1 force the melee radar to the old stateless full
|
||||
## spin (always 45 deg/tick). This reproduces the
|
||||
@@ -477,6 +485,9 @@ proc recordWorldState(bot: ModularBot, ws: WorldState) =
|
||||
"eid": tid,
|
||||
}
|
||||
if lst >= 0: row["lst"] = %lst
|
||||
# j177: the gun in force when this state was built. Absent before j177,
|
||||
# which made a per-gun decomposition of the aim error impossible.
|
||||
if CaptureAim: row["gun"] = %bot.currentGun
|
||||
try:
|
||||
let f = open(WorldStateRecordPath, fmAppend)
|
||||
f.writeLine($row)
|
||||
@@ -616,6 +627,23 @@ proc recordRadarStats(bot: ModularBot) =
|
||||
inc bot.arcWidthHist[min(11, int(width / 30.0))]
|
||||
|
||||
method onScannedBot*(bot: ModularBot, e: ScannedBotEvent) =
|
||||
# j177 aim capture: the belief we are about to REPLACE, and the fire site's
|
||||
# state, recorded BEFORE the update. Written first so the record is the
|
||||
# pre-update belief by construction, not by argument.
|
||||
if CaptureAim:
|
||||
var rec = AimScan(tick: bot.tick, eid: e.scannedBotId,
|
||||
ex: e.x, ey: e.y, eh: e.direction, es: e.speed, ee: e.energy,
|
||||
sx: getX(), sy: getY(), sh: getDirection(), ss: getSpeed(),
|
||||
gun: bot.currentGun,
|
||||
rlock: bot.radarMode == 0, rdir: getRadarDirection(),
|
||||
bx: 0.0, by: 0.0, blst: -1)
|
||||
if bot.enemyTracker.enemies.contains(e.scannedBotId):
|
||||
let prev = bot.enemyTracker.enemies[e.scannedBotId]
|
||||
rec.bx = prev.x; rec.by = prev.y; rec.bh = prev.heading
|
||||
rec.bs = prev.speed; rec.blst = prev.lastSeenTick
|
||||
rec.lbear = bearing(rec.bx, rec.by, rec.sx, rec.sy)
|
||||
rec.boff = (bearing(rec.ex, rec.ey, rec.sx, rec.sy) - rec.rdir) mod 360.0
|
||||
appendLine(WorldStateRecordPath, scanRow(rec))
|
||||
bot.enemyTracker.update(e.scannedBotId, e.x, e.y, e.direction, e.speed, e.energy, bot.tick)
|
||||
bot.hasContact = true
|
||||
if RadarScanLog and bot.radarMeleeActive:
|
||||
@@ -1467,6 +1495,22 @@ method run*(bot: ModularBot) =
|
||||
# Enqueue the selected gun so onBulletFired can stamp the server's bulletId.
|
||||
# getEnergy() > power mirrors the server's "bot.energy <= firepower" reject.
|
||||
if not floorBlocks and setFire(power) and getEnergy() > power:
|
||||
# j177 aim capture: one `aim_fire` line per REAL shot, with the gun
|
||||
# that fired and the exact WorldState the predictor consumed. This
|
||||
# is the only place where `lst` is knowable, so it is the only place
|
||||
# the scan parity of a firing decision can be recorded.
|
||||
if CaptureAim:
|
||||
let bspd = bulletSpeed(power)
|
||||
appendLine(WorldStateRecordPath, fireRow(AimFire(
|
||||
tick: bot.tick, eid: tid, gun: selectedGun, power: power,
|
||||
aim: aimTarget, turret: gunDir, terr: normDelta, heat: gunHeat,
|
||||
ax: pred.x, ay: pred.y, tof: (if bspd > 0: distPx / bspd else: 0.0),
|
||||
ex: bot.lastState.enemyX, ey: bot.lastState.enemyY,
|
||||
eh: bot.lastState.enemyHeading, es: bot.lastState.enemySpeed,
|
||||
ee: bot.lastState.enemyEnergy,
|
||||
sx: bot.lastState.selfX, sy: bot.lastState.selfY,
|
||||
lst: (if tid >= 0 and bot.enemyTracker.enemies.contains(tid):
|
||||
bot.enemyTracker.enemies[tid].lastSeenTick else: -1))))
|
||||
bot.pendingFires.add(PendingShot(
|
||||
gunId: selectedGun,
|
||||
angleErr: abs(normDelta),
|
||||
@@ -1558,6 +1602,7 @@ when isMainModule:
|
||||
vBulletDebugGun: getEnv(VBulletDebugGunEnv, ""),
|
||||
vBulletDebugMax: VBulletDebugMax,
|
||||
recordWorldState: RecordWorldState,
|
||||
captureAim: CaptureAim,
|
||||
geoDebug: GeoDebugOn,
|
||||
debugDraw: DebugDrawOn,
|
||||
radarForceSpin: RadarForceSpin,
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
## j177 aim capture — the two records that make the aim error ATTRIBUTABLE.
|
||||
##
|
||||
## j176 could not answer "why is the aim 11.9 deg off at 450+ px" because the
|
||||
## corpus has neither the bot's own belief (staleness was an INVERSE problem,
|
||||
## unidentifiable) nor the gun id (a good gun's contribution was
|
||||
## indistinguishable from a bad one's). Both are cheap to log and impossible
|
||||
## to recover later. This module builds the two JSON records; ModularBot.nim
|
||||
## calls it and the lines go into the EXISTING world-state capture
|
||||
## (`TR_RECORD_WORLDSTATE` file), so the offline tooling sees one stream.
|
||||
##
|
||||
## It is deliberately PURE (no bot API, no env reads): the bot passes plain
|
||||
## floats, the offline guard test passes the recorded fixture, and both go
|
||||
## through the SAME row builders — so a field that the test proves present is
|
||||
## a field the live bot writes.
|
||||
##
|
||||
## Key convention: `e*` = the enemy, `s*` = us, `b*` = the enemy's PREVIOUS
|
||||
## belief in the tracker (before this scan's update), `lst` = the tick that
|
||||
## state came from. `age = tick - blst` is the scan PARITY, recorded rather
|
||||
## than inferred.
|
||||
|
||||
import std/[json, os, math]
|
||||
|
||||
const
|
||||
ScanRecordKey* = "aim_scan" ## wrapper key, sibling of "meta"/"end"
|
||||
FireRecordKey* = "aim_fire"
|
||||
|
||||
type
|
||||
AimScan* = object
|
||||
## One `onScannedBot` event, as the bot saw it BEFORE the update.
|
||||
tick*: int
|
||||
eid*: int
|
||||
ex*, ey*: float ## raw scanned values
|
||||
eh*, es*: float ## scanned heading (= direction) and speed
|
||||
ee*: float
|
||||
sx*, sy*: float ## our state at the scan (the FIRE SITE)
|
||||
sh*, ss*: float
|
||||
gun*: int ## bot.currentGun — the gun that fired, not the rack slot
|
||||
bx*, by*: float ## previous belief, BEFORE this scan's update
|
||||
bh*, bs*: float
|
||||
blst*: int ## previous lastSeenTick; -1 = never scanned before
|
||||
rlock*: bool ## radar lock engaged (false = the melee radar)
|
||||
rdir*: float ## 36 deg scan window centre (radar heading)
|
||||
lbear*: float ## bearing the lock is chasing (believed target)
|
||||
boff*: float ## scanned bearing - rdir: where in the window it landed
|
||||
|
||||
AimFire* = object
|
||||
## One firing decision, as the model computed it.
|
||||
tick*: int
|
||||
eid*: int
|
||||
gun*: int ## bot.currentGun = the gun that fired
|
||||
power*: float
|
||||
aim*: float ## the raw angle handed to setFire/turret
|
||||
turret*: float ## getGunDirection() at the command
|
||||
terr*: float ## signed turret error (aim - turret)
|
||||
heat*: float ## getGunHeat() BEFORE firing
|
||||
ax*, ay*: float ## the intercept the gun predicted
|
||||
tof*: float ## implied time of flight, ticks
|
||||
ex*, ey*: float ## the WorldState the predictor CONSUMED
|
||||
eh*, es*: float
|
||||
ee*: float
|
||||
sx*, sy*: float
|
||||
lst*: int ## which tick that enemy state came from (scan parity)
|
||||
|
||||
proc bearing*(x, y, fx, fy: float): float =
|
||||
arctan2(y - fy, x - fx).radToDeg
|
||||
|
||||
proc scanRow*(s: AimScan): JsonNode =
|
||||
## The `aim_scan` line. Every field is unconditional: a capture that
|
||||
## silently omits a field is worse than no capture.
|
||||
result = newJObject()
|
||||
result[ScanRecordKey] = newJObject()
|
||||
let b = result[ScanRecordKey]
|
||||
b["tick"] = %s.tick
|
||||
b["eid"] = %s.eid
|
||||
b["ex"] = %s.ex
|
||||
b["ey"] = %s.ey
|
||||
b["eh"] = %s.eh
|
||||
b["es"] = %s.es
|
||||
b["ee"] = %s.ee
|
||||
b["sx"] = %s.sx
|
||||
b["sy"] = %s.sy
|
||||
b["sh"] = %s.sh
|
||||
b["ss"] = %s.ss
|
||||
b["gun"] = %s.gun
|
||||
b["bx"] = %s.bx
|
||||
b["by"] = %s.by
|
||||
b["bh"] = %s.bh
|
||||
b["bs"] = %s.bs
|
||||
b["blst"] = %s.blst
|
||||
b["age"] = %(if s.blst >= 0: s.tick - s.blst else: -1)
|
||||
b["rlock"] = %s.rlock
|
||||
b["rdir"] = %s.rdir
|
||||
b["lbear"] = %s.lbear
|
||||
b["boff"] = %s.boff
|
||||
|
||||
proc fireRow*(f: AimFire): JsonNode =
|
||||
## The `aim_fire` line.
|
||||
result = newJObject()
|
||||
result[FireRecordKey] = newJObject()
|
||||
let b = result[FireRecordKey]
|
||||
b["tick"] = %f.tick
|
||||
b["eid"] = %f.eid
|
||||
b["gun"] = %f.gun
|
||||
b["power"] = %f.power
|
||||
b["aim"] = %f.aim
|
||||
b["turret"] = %f.turret
|
||||
b["terr"] = %f.terr
|
||||
b["heat"] = %f.heat
|
||||
b["ax"] = %f.ax
|
||||
b["ay"] = %f.ay
|
||||
b["tof"] = %f.tof
|
||||
b["ex"] = %f.ex
|
||||
b["ey"] = %f.ey
|
||||
b["eh"] = %f.eh
|
||||
b["es"] = %f.es
|
||||
b["ee"] = %f.ee
|
||||
b["sx"] = %f.sx
|
||||
b["sy"] = %f.sy
|
||||
b["lst"] = %f.lst
|
||||
|
||||
proc appendLine*(path: string, row: JsonNode) =
|
||||
## Append one JSONL line, fully guarded: a full disk or a bad path must
|
||||
## never take the bot down (same contract as the existing recorders).
|
||||
try:
|
||||
let f = open(path, fmAppend)
|
||||
f.writeLine($row)
|
||||
f.close()
|
||||
except CatchableError:
|
||||
discard
|
||||
@@ -52,6 +52,7 @@ type
|
||||
vBulletDebugGun*: string
|
||||
vBulletDebugMax*: int
|
||||
recordWorldState*: bool
|
||||
captureAim*: bool ## j177: aim_scan / aim_fire records, default off
|
||||
geoDebug*: bool
|
||||
debugDraw*: bool
|
||||
radarForceSpin*: bool
|
||||
@@ -245,6 +246,8 @@ proc printEffectiveValues(ctx: EnvReportContext) =
|
||||
emit("TR_RESULT_LOG", onOff(ctx.resultLog), sourceOf("TR_RESULT_LOG"))
|
||||
emit("TR_RECORD_WORLDSTATE", onOff(ctx.recordWorldState),
|
||||
sourceOfPresence("TR_RECORD_WORLDSTATE"))
|
||||
emit("TR_CAPTURE_AIM", onOff(ctx.captureAim),
|
||||
sourceOfPresence("TR_CAPTURE_AIM"))
|
||||
emit("TR_RADAR_FORCE_SPIN", onOff(ctx.radarForceSpin),
|
||||
sourceOfPresence("TR_RADAR_FORCE_SPIN"))
|
||||
emit("TR_RADAR_SCANLOG", onOff(ctx.radarScanLog),
|
||||
@@ -657,7 +660,7 @@ proc knownEnvNames*(): seq[string] =
|
||||
"GUN_SELECTOR_SHRINK", "GUN_SELECTOR_DWELL", "GUN_SELECTOR_MARGIN",
|
||||
"GUN_SELECTOR_POINT_TIE", "GUN_SELECTOR_SEED",
|
||||
"GUN_RACK_DISABLE", "GUN_STATS_PATH", "GUN_SHOTLOG_PATH",
|
||||
"TR_MOVEMENT", "TR_MOVEMENT_LOG", "TR_RECORD_WORLDSTATE",
|
||||
"TR_MOVEMENT", "TR_MOVEMENT_LOG", "TR_RECORD_WORLDSTATE", "TR_CAPTURE_AIM",
|
||||
"TR_RADAR_FORCE_SPIN", "TR_RADAR_SCANLOG", "TR_RADAR_SCAN_LOG_PATH",
|
||||
"TR_TRACKER_PROBE", "TR_TRACKER_PROBE_PATH", "TR_VBULLET_ADMIT_ONLY",
|
||||
VBulletDebugEnv, VBulletDebugGunEnv, VBulletDebugMaxEnv,
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
## A trailing live end marker is also optional:
|
||||
## {"end":{"enemy_died":<bool>,"ticks":<int>}}
|
||||
## It lets the replay reproduce the live resolver's final-tick behaviour.
|
||||
## `aim_scan` / `aim_fire` annotation lines (written only when
|
||||
## TR_CAPTURE_AIM is set) carry no `ex` and are skipped.
|
||||
##
|
||||
## The replay never calls the gun selector, so it is RNG-free for every
|
||||
## deterministic gun. Tsetlin is stochastic and is expected to differ.
|
||||
@@ -196,6 +198,10 @@ proc loadFixture*(path: string): Fixture =
|
||||
if node["end"].hasKey("enemy_died"):
|
||||
result.enemyDied = node["end"]["enemy_died"].getBool()
|
||||
continue
|
||||
# j177: the recorder can also write `aim_scan` / `aim_fire` lines into the
|
||||
# same file when TR_CAPTURE_AIM is set. They are annotations on ticks, not
|
||||
# ticks, so they carry no `ex` and are skipped here.
|
||||
if not node.hasKey("ex"): continue
|
||||
result.states.add stateFromJson(node, arenaW, arenaH, enemyId)
|
||||
result.lastSeen.add (if node.hasKey("lst"): node["lst"].getInt() else: -1)
|
||||
result.enemyId = enemyId
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# j177 gun-path default-parity golden.
|
||||
# Generated from the PRE-CHANGE tree (`git archive 55e92bc`) with
|
||||
# TR_CAPTURE_AIM unset, over the whole tr_drussgt_vs_modularbot.jsonl.
|
||||
# Format: <gun> shots=<n> hits=<n>, one line per rack gun
|
||||
HeadOn shots=400 hits=59
|
||||
Linear shots=400 hits=47
|
||||
Tsetlin shots=400 hits=74
|
||||
Circular shots=400 hits=51
|
||||
GuessFactor shots=400 hits=44
|
||||
Pattern shots=400 hits=46
|
||||
WallBounce shots=400 hits=55
|
||||
Accel shots=400 hits=59
|
||||
StopShot shots=400 hits=84
|
||||
Displace shots=400 hits=46
|
||||
AvgLead shots=400 hits=51
|
||||
DecayGF shots=400 hits=47
|
||||
KNN shots=400 hits=29
|
||||
TMSelect shots=0 hits=0
|
||||
@@ -0,0 +1,132 @@
|
||||
## j177 guard: the aim capture actually contains every field it promises, and
|
||||
## the default gun path is byte-for-byte unchanged.
|
||||
##
|
||||
## NO battle, NO Java, NO server, NO GUI.
|
||||
## nim c -r --path:common_libs --path:ModularBot_garage/src \
|
||||
## --nimcache:/tmp/nc_j177 common_libs/tests/test_aim_capture.nim
|
||||
##
|
||||
## Part 1 (the guard): with the capture ON, build one `aim_scan` and one
|
||||
## `aim_fire` row from a real recorded tick of
|
||||
## tools/fixtures/tr_drussgt_vs_modularbot.jsonl, through the SAME
|
||||
## `aim_capture` row builders ModularBot.nim calls, and assert every required
|
||||
## key is present and carries the value that was passed in. A capture that
|
||||
## silently omits a field is worse than none.
|
||||
##
|
||||
## Part 2 (default parity): replay the whole recorded fixture through the real
|
||||
## VirtualTracker + the guns with the capture OFF and compare the per-gun
|
||||
## fitness report against `fixtures/aim_capture_gunpath.golden`. That golden
|
||||
## was generated from the PRE-CHANGE tree (`git archive 55e92bc`) with the
|
||||
## knob unset — regenerating it from this code would defeat the check.
|
||||
## Part 2b: the same replay over a fixture that ALSO carries aim_scan/aim_fire
|
||||
## lines must give the identical report, i.e. the annotations are inert.
|
||||
|
||||
import std/[json, os, strutils, sequtils, strformat]
|
||||
import gun_harness/offline_range
|
||||
import range_guns
|
||||
import ../../ModularBot_garage/src/aim_capture
|
||||
|
||||
const
|
||||
repoRoot = currentSourcePath().parentDir.parentDir.parentDir
|
||||
fixtureRel = "tr_drussgt_vs_modularbot.jsonl"
|
||||
fixture = repoRoot / "tools" / "fixtures" / fixtureRel
|
||||
goldenPath = currentSourcePath().parentDir / "fixtures" / "aim_capture_gunpath.golden"
|
||||
annotated = "/tmp/j177_annotated_fixture.jsonl"
|
||||
ScanKeys = ["tick", "eid", "ex", "ey", "eh", "es", "ee", "sx", "sy", "sh", "ss",
|
||||
"gun", "bx", "by", "bh", "bs", "blst", "age", "rlock", "rdir",
|
||||
"lbear", "boff"]
|
||||
FireKeys = ["tick", "eid", "gun", "power", "aim", "turret", "terr", "heat",
|
||||
"ax", "ay", "tof", "ex", "ey", "eh", "es", "ee", "sx", "sy", "lst"]
|
||||
|
||||
var failures = 0
|
||||
proc check(name: string, ok: bool) =
|
||||
if ok: echo "PASS: ", name
|
||||
else:
|
||||
echo "FAIL: ", name
|
||||
inc failures
|
||||
|
||||
proc replayReport(path: string): string =
|
||||
## Per-gun shots/hits over the whole fixture, through the REAL tracker and
|
||||
## the REAL guns, with the capture OFF.
|
||||
let res = replayFixture(loadFixture(path), buildAllGunDrivers(seed = 1))
|
||||
for r in res:
|
||||
result.add r.name & " shots=" & $r.shots & " hits=" & $r.hits & "\n"
|
||||
|
||||
# ── Part 1: the records carry every field ────────────────────────────────────
|
||||
proc fieldCheck() =
|
||||
let states = loadFixture(fixture).states
|
||||
check("fixture loaded", states.len > 1000)
|
||||
let ws = states[900]
|
||||
let scan = scanRow(AimScan(
|
||||
tick: 900, eid: 7,
|
||||
ex: ws.enemyX, ey: ws.enemyY, eh: ws.enemyHeading, es: ws.enemySpeed,
|
||||
ee: ws.enemyEnergy, sx: ws.selfX, sy: ws.selfY, sh: ws.selfHeading,
|
||||
ss: ws.selfSpeed, gun: 5,
|
||||
bx: ws.enemyX - 8.0, by: ws.enemyY - 8.0, bh: ws.enemyHeading,
|
||||
bs: ws.enemySpeed, blst: 896,
|
||||
rlock: true, rdir: 42.5, lbear: bearing(ws.enemyX - 8, ws.enemyY - 8, ws.selfX, ws.selfY),
|
||||
boff: -12.25))
|
||||
let fire = fireRow(AimFire(
|
||||
tick: 900, eid: 7, gun: 5, power: 1.6, aim: 88.25, turret: 74.0,
|
||||
terr: 14.25, heat: 0.31, ax: ws.enemyX + 40.0, ay: ws.enemyY - 15.0,
|
||||
tof: 12.5, ex: ws.enemyX, ey: ws.enemyY, eh: ws.enemyHeading,
|
||||
es: ws.enemySpeed, ee: ws.enemyEnergy, sx: ws.selfX, sy: ws.selfY, lst: 897))
|
||||
|
||||
for k in ScanKeys:
|
||||
check("aim_scan has " & k, scan[ScanRecordKey].hasKey(k))
|
||||
for k in FireKeys:
|
||||
check("aim_fire has " & k, fire[FireRecordKey].hasKey(k))
|
||||
|
||||
check("aim_scan carries the gun id", scan[ScanRecordKey]["gun"].getInt() == 5)
|
||||
check("aim_fire carries the gun id", fire[FireRecordKey]["gun"].getInt() == 5)
|
||||
check("aim_scan records the scan parity age",
|
||||
scan[ScanRecordKey]["age"].getInt() == 4)
|
||||
check("aim_fire records the source tick (parity)",
|
||||
fire[FireRecordKey]["lst"].getInt() == 897)
|
||||
check("aim_fire carries the aim angle", fire[FireRecordKey]["aim"].getFloat() == 88.25)
|
||||
check("aim_fire carries the turret error", fire[FireRecordKey]["terr"].getFloat() == 14.25)
|
||||
check("aim_fire carries the WorldState the model consumed",
|
||||
fire[FireRecordKey]["ex"].getFloat() == ws.enemyX and
|
||||
fire[FireRecordKey]["ey"].getFloat() == ws.enemyY)
|
||||
|
||||
echo "\n--- aim_scan record ---"
|
||||
echo $scan
|
||||
echo "--- aim_fire record ---"
|
||||
echo $fire
|
||||
echo ""
|
||||
|
||||
# ── Part 2: default gun-path parity ──────────────────────────────────────────
|
||||
proc parityCheck() =
|
||||
let clean = replayReport(fixture)
|
||||
let ticks = loadFixture(fixture).states.len
|
||||
if defined(aimCapGenGolden):
|
||||
var g = "# j177 gun-path default-parity golden.\n"
|
||||
g.add "# Generated from the PRE-CHANGE tree (`git archive 55e92bc`) with\n"
|
||||
g.add "# TR_CAPTURE_AIM unset, over the whole " & fixtureRel & ".\n"
|
||||
g.add "# Format: <gun> shots=<n> hits=<n>, one line per rack gun\n"
|
||||
g.add clean
|
||||
createDir(goldenPath.parentDir)
|
||||
writeFile(goldenPath, g)
|
||||
echo "wrote ", goldenPath, " (", ticks, " ticks)"
|
||||
return
|
||||
check("golden exists", fileExists(goldenPath))
|
||||
if not fileExists(goldenPath): return
|
||||
let g = lines(goldenPath).toSeq().filterIt(not it.startsWith("#")).join("\n").strip()
|
||||
check("gun path byte-for-byte identical over " & $ticks & " ticks", g == clean.strip())
|
||||
|
||||
# 2b: the annotation lines must be inert for the replay.
|
||||
let extra = @[
|
||||
$scanRow(AimScan(tick: 0, eid: 1, ex: 1.0, ey: 2.0, eh: 3.0, es: 4.0, ee: 5.0,
|
||||
sx: 6.0, sy: 7.0, sh: 8.0, ss: 9.0, gun: 3,
|
||||
bx: 0.0, by: 0.0, blst: -1, rlock: true, rdir: 1.0, lbear: 2.0)),
|
||||
$fireRow(AimFire(tick: 1, eid: 1, gun: 3, power: 1.5, aim: 1.0, turret: 2.0,
|
||||
terr: 3.0, heat: 0.0, ax: 1.0, ay: 1.0, tof: 1.0,
|
||||
ex: 1.0, ey: 1.0, eh: 1.0, es: 1.0, ee: 1.0,
|
||||
sx: 1.0, sy: 1.0, lst: 0))]
|
||||
writeFile(annotated, (lines(fixture).toSeq() & extra).join("\n"))
|
||||
check("aim records do not perturb the replay", replayReport(annotated) == clean)
|
||||
removeFile(annotated)
|
||||
|
||||
fieldCheck()
|
||||
parityCheck()
|
||||
echo (if failures == 0: "\nALL PASS" else: "\n" & $failures & " FAILURE(S)")
|
||||
quit(if failures == 0: 0 else: 1)
|
||||
+131
-5
@@ -47,13 +47,123 @@ misled people:**
|
||||
warning at all**.
|
||||
2. **Some flags are presence-based, not value-based.** They are read with
|
||||
`existsEnv`, so **`TR_POWER_LOG=0` turns the log ON** (any value does).
|
||||
Presence-based: `TR_POWER_LOG`, `TR_RAM_LOG`, `TR_MOVEMENT_LOG`,
|
||||
`TR_RECORD_WORLDSTATE`, `TR_RADAR_FORCE_SPIN`, `TR_RADAR_SCANLOG`,
|
||||
`TR_TRACKER_PROBE`. Value-based (`0`/`false`/`off` really disable):
|
||||
`TR_ENV_REPORT`, `TR_TMHORIZON_LOG`, `TR_TMHORIZON_ACCURVE`,
|
||||
Value-based (`0`/`false`/`off` really disable): `TR_ENV_REPORT`,
|
||||
`TR_TMHORIZON_LOG`, `TR_TMHORIZON_ACCURVE`,
|
||||
`TR_TMHORIZON_RESET_ON_TARGET`, `TR_POWER_POLICY`, `TR_POWER_FINISH_KILL`,
|
||||
`TR_RAM_OPPORTUNITY`, `TR_RAM_PLAN`, `GUN_SELECTOR_POOL`,
|
||||
`TR_VBULLET_ADMIT_ONLY`.
|
||||
`TR_VBULLET_ADMIT_ONLY`, `TR_LEADGAIN_LOG`, `TR_LEARNED_LOG`,
|
||||
`TR_LEARNED_GLOBAL`, `TR_LEARNED_REAL_EVENTS`, `TR_FIRE_FIX`,
|
||||
`TR_STRAFE_FIRE_FIX`, `TR_STRAFE_ESCAPE`, `TR_STRAFE_HEAT_GRID`,
|
||||
`TR_TFIL_HEAT_TIME`, `TR_TFIL_PILLAR_ON`, `TR_TFIL_DIAG`, `TR_TFIL_NO_REV`,
|
||||
`TR_TFIL_HOLD_WHEN_TRAPPED`, `TR_TFIL_COMMIT_ARRIVAL`,
|
||||
`TR_TFIL_RING_COMMIT_ARRIVAL`, `TR_VBULLET_DEBUG`, `TR_GEO_DEBUG`,
|
||||
`TR_DEBUG_DRAW`, `TR_RESULT_LOG`.
|
||||
|
||||
### THE FULL PRESENCE-GATED LIST (j172, from `grep -rn existsEnv`)
|
||||
|
||||
The list above was **incomplete**: it was missing four knobs. The complete set,
|
||||
from `grep -rn 'existsEnv' ModularBot_garage/src common_libs | grep -v /tests/`,
|
||||
is:
|
||||
|
||||
| knob | read at | what it logs / does |
|
||||
|---|---|---|
|
||||
| `TR_POWER_LOG` | `ModularBot.nim:145` | one line per power-decision CHANGE |
|
||||
| `TR_RAM_LOG` | `ram_decision.nim:119` | one line per ram start/stop + reason |
|
||||
| `TR_MOVEMENT_LOG` | `the_floor_is_lava_ring.nim:192` | movement band / range-class changes |
|
||||
| `TR_STRAFE_LOG` | `strafe.nim:402` | one line per strafe tile pick |
|
||||
| `TR_SURF_LOG` | `wave_surfer.nim:102` | one line per wave-surfing decision |
|
||||
| `TR_FIRE_DIAG` | `ModularBot.nim:137`, `the_floor_is_lava.nim:299`, `strafe.nim:415` | per-reading fire-detection tick/raw/correction |
|
||||
| `TR_RECORD_WORLDSTATE` | `ModularBot.nim:70` | dump every observed world state |
|
||||
| `TR_RADAR_SCANLOG` | `ModularBot.nim:80` | log every radar scan tick |
|
||||
| `TR_RADAR_FORCE_SPIN` | `ModularBot.nim:79` | force the old full-360 spin radar |
|
||||
| `TR_TRACKER_PROBE` | `ModularBot.nim:86` | dump the enemy-tracker internals |
|
||||
|
||||
**For these ten, `NAME=0` turns the feature ON.** OFF means the line is ABSENT.
|
||||
That is why `.env.example` shows every one of them commented out: there is no
|
||||
"off" spelling, only absence. To disable one, delete its line.
|
||||
|
||||
Two more are read with `existsEnv` but are *not* features — `TR_ENV_FILE` (an
|
||||
empty value is the correct "use the default" spelling) and the loader's own
|
||||
`existsEnv(e.key)` conflict check.
|
||||
|
||||
**And one label is misleading:** `env_report.nim` prints `TR_TFIL_DIAG`,
|
||||
`TR_TFIL_HOLD_WHEN_TRAPPED`, `TR_TFIL_COMMIT_ARRIVAL` and
|
||||
`TR_TFIL_RING_COMMIT_ARRIVAL` through `sourceOfPresence`, but all four are read
|
||||
**by value** (`getEnvBool`) in the source. The value is always right; only the
|
||||
`(source: ...)` label is affected. Do not read that label as "presence-gated".
|
||||
|
||||
---
|
||||
|
||||
## ONE EXPERIMENT, END TO END (mirrored from `.env.example`)
|
||||
|
||||
Pick ONE knob. Here it is `TR_TFIL_ARRIVE_TICKS`; the shape is the same for
|
||||
every knob.
|
||||
|
||||
```sh
|
||||
# 1. write the arm as its own file — that is how you GUARANTEE the arm, because
|
||||
# nothing else can be applied on top of it
|
||||
cat > /tmp/arm_arrive15.env <<'EOF'
|
||||
TR_MOVEMENT=tfil
|
||||
TR_TFIL_ARRIVE_TICKS=15.0
|
||||
EOF
|
||||
|
||||
# 2. RESTART THE BOT. Env is read ONCE, at boot (module init). Editing .env
|
||||
# while the bot runs changes nothing; there is no live reload.
|
||||
cd ModularBot_garage && ./ModularBot.sh # or restart the GUI
|
||||
|
||||
# 3. CONFIRM IT TOOK EFFECT, before reading a single result line.
|
||||
# `source: .env` = your file was applied. `source: default` = it was not.
|
||||
grep '^\[env\]' /tmp/modularbot_stdout.log | grep -E 'env file|ARRIVE_TICKS'
|
||||
# [env] env file: /tmp/arm_arrive15.env (source: TR_ENV_FILE)
|
||||
# [env] TR_TFIL_ARRIVE_TICKS = 15.0 (source: .env)
|
||||
|
||||
# 4. point at the file instead of copying it into .env:
|
||||
TR_ENV_FILE=/tmp/arm_arrive15.env ./out/ModularBot
|
||||
./out/ModularBot --env-file /tmp/arm_arrive15.env
|
||||
# A file you ASKED for and that does not exist stops the bot with an error; a
|
||||
# missing default .env is silent. This is what an A/B run does: one frozen
|
||||
# binary, one env file per arm.
|
||||
|
||||
# 5. what is switched on at all:
|
||||
grep '^\[modules\]' /tmp/modularbot_stdout.log
|
||||
```
|
||||
|
||||
## SAFE TO EXPERIMENT WITH RIGHT NOW
|
||||
|
||||
The honest list is SHORT: after the recent campaign most experimental knobs are
|
||||
either never live-tested or already measured null/harmful, and `.env.example`
|
||||
says so on every one of them. These four are safe in the sense that they either
|
||||
cannot change a decision, or are the ones a measurement actually supports.
|
||||
|
||||
| knob | what changes | what to watch | a good result |
|
||||
|---|---|---|---|
|
||||
| `TR_GEO_DEBUG=on` | draw-only geometry overlay | the circles on the two tanks, each heading line | you can SEE the tile the picker chose; it cannot change a decision |
|
||||
| `TR_VBULLET_DEBUG=1` + `TR_VBULLET_DEBUG_GUN=all` | draw-only: each admitted gun's virtual bullets | travelled path, aim ring, miss vector | you can see the signal the selector ranks on; also draw-only |
|
||||
| `TR_TFIL_DIAG=on` | fills the per-pick loss histogram (tfil only) | the tfil pick log line | `sReach/sCool/sSafe/sCand` tell you where tiles are lost; provably moves no command |
|
||||
| `TR_MOVEMENT=tfil` | runs the long-shipped mover | nothing to compare against | you are reproducing an older, documented behaviour; only do it together with the `TR_TFIL_*` knobs |
|
||||
|
||||
## ALREADY REJECTED OR MEASURED NULL — WITH THE NUMBER
|
||||
|
||||
Do not re-run these by accident.
|
||||
|
||||
| knob / arm | result | where |
|
||||
|---|---|---|
|
||||
| `TR_TFIL_GEO_MODE=both-rej` + `TR_TFIL_GEO_TAU=60` | **REJECTED** live, 420 battles, 15 opponents: damage/run **-8.83**, p(sign-flip) **0.0061**, Wilcoxon p 0.011. Round wins null. Offline it did what was predicted (arrivals 4.5% -> 29.4%) and that is why it is bad: +26 px distance on 15/15, less damage. | `docs/tfil_geo_ab.md` |
|
||||
| `TR_RAM_FLOOR_ENERGY=5` | **CLEAN NULL**: **-0.018 wins/run**, p(sign-flip) **0.7676**, under a **0.1420 wins/run** MDE, 900 battles. Mechanism fired on 0.04% of ticks (~200x less than the offline ruler said). Do not re-test: more runs buy resolution on an effect that is not there. | `docs/ram_floor_exhaustion_ab.md` |
|
||||
| `TR_RAM_FLOOR_ENERGY=10/20` | measured COSTLY offline (20 blocked 24.7% of all ticks) and the zone it guards is nearly empty: only 4.8% of shots are taken below 10 energy. | same |
|
||||
| `TR_TMHORIZON_WINDOW=150` | **MEASURED HARMFUL** live: 26.5% round wins vs 49.0% for the shipped rack, p = 0.036. Keep 0. | env_reference "Measured verdicts" |
|
||||
| `TR_POWER_POLICY=0` | **MEASURED HARMFUL** live: real hit rate 10.61% -> 7.88%, p = 0.0012. | same |
|
||||
| `TR_TFIL_HEAT_TIME=1` | **MEASURED HARMFUL** live at every tau tried (3/5/9/15); tau15 alone is -22 damage/run, p = 0.046. | `docs/tfil_heat_pillar_ab.md` |
|
||||
| `TR_MOVEMENT=tfil_ring` | **MEASURED**: round wins 16/49 -> 6/49, p = 0.012. Best live hit rate of anything measured, half the survival. | same / env_reference |
|
||||
| the full `TR_RACK_*` rack | **MEASURED NEGATIVE VALUE**: Pattern ALONE beats the full 13-gun rack, p = 0.0012. Adding guns costs rounds. | `docs/gun_rack_analysis.md` |
|
||||
| `TR_TFIL_TURN_BIAS=9` + `_TURN_REF_DEG=0` | **LIVE NULL**: +0.15 wins/run, p(sign) 0.244, under a 0.30 MDE, 300 battles. | `docs/movement_campaign.md` (j145) |
|
||||
| `TR_RAM_OPPORTUNITY=on` | **MEASURED not to convert**: 0/59 opportunity -> contact. The finisher ram is the only path that converts, and it is always on. | env_reference |
|
||||
| `TR_TFIL_PILLAR_ON=1` | live-tested, and the recommendation to revert to pillar-on was **OVERRULED by the owner**: the contrast is inside the MDE (33 damage/run, 1.22 wins/run at n=10). Pillar stays removed. | `docs/tfil_heat_pillar_ab.md` |
|
||||
|
||||
> **A null is only a null at the resolution that run reached.** The frozen
|
||||
> 15-opponent panel at 14 runs/arm resolves ~0.17 wins/run and ~7.65 damage/run;
|
||||
> the j163 run resolved 0.1420 wins/run. "Clean null" here means *no effect at or
|
||||
> above that size* — not *no effect*.
|
||||
|
||||
---
|
||||
|
||||
@@ -506,6 +616,7 @@ Measured byte-identical on `bmPath`. Kept for experiments; leave at defaults.
|
||||
| `TR_RADAR_SCAN_LOG_PATH` | `/tmp/radar_scan_log.jsonl` | where that goes |
|
||||
| `TR_TRACKER_PROBE` | off | presence-based; per-tick enemy tracker vs server enemy count |
|
||||
| `TR_TRACKER_PROBE_PATH` | `/tmp/tracker_probe.jsonl` | where that goes |
|
||||
| `TR_CAPTURE_AIM` | off | presence-based; append `aim_scan` / `aim_fire` records — what the lead model BELIEVED (gun id, blst, age, boff, aim, turret, terr, heat, ax/ay, tof) to the same capture file as `TR_RECORD_WORLDSTATE` (needs `TR_RECORD_WORLDSTATE=1`) |
|
||||
| `TR_VBULLET_DEBUG` | off | presence-based; overlay the virtual bullets in the GUI debug graphics (see below) |
|
||||
| `TR_VBULLET_DEBUG_GUN` | selected gun | `all`/`*` for every gun, or a gun name (e.g. `Pattern`); unset = only the currently selected gun |
|
||||
| `TR_VBULLET_DEBUG_MAX` | `32` | cap on bullets drawn per tick |
|
||||
@@ -523,6 +634,21 @@ selector's training signal visible. Turn it on with:
|
||||
- colour per gun is the SAME table as the turret (`vbullet_draw.gunColors`), with
|
||||
a one-line legend in the top-left corner.
|
||||
|
||||
### Known limitations — `TR_CAPTURE_AIM` (j177)
|
||||
|
||||
**`aim_fire` only records shots that PASSED `setFire`.** The capture is written
|
||||
from the bot's own fire call, so a shot the **server rejected or that the bot
|
||||
never issued** produces no `aim_fire` record at all.
|
||||
|
||||
That is a real blind spot: from these records you can never answer *why* a shot
|
||||
did not happen — e.g. the gun was still hot, or the turret was not yet aligned.
|
||||
A missing `aim_fire` is ambiguous between "no target / didn't try" and "tried and
|
||||
was refused". The `aim_scan` records carry the belief state (age, boff, turret,
|
||||
heat) for every scan, so you can often *infer* the cause by looking at the scans
|
||||
that precede the gap, but the capture does not state it. Read the gaps as
|
||||
"no recorded shot", never as "the server blocked it". Closing this needs a
|
||||
pre-`setFire` gate record, which is j177+ work, not present today.
|
||||
|
||||
The **adaptive-melee radar** has no env knobs. Its tuning lives in compile-time
|
||||
constants in `radars/adaptive_melee_radar.nim:36-50`: `MaxRadarTurnRate=45`,
|
||||
`FreshnessTicks=16`, `FreshStreakTicks=3`, `MarginDeg=20`,
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
# Range vs approach: melee is unavailable against DrussGT, so the gun at range is the only lever
|
||||
|
||||
**Date:** 2026-09-27 · **Job:** j167 · **Branch:** `research/lead-targeting`
|
||||
**Evidence base:** j166 (`worktrees/j166-aim` @ `a5a49bd`), j165 (`fe77056`), j159
|
||||
(`4a1f3e1`), j165/j151/j152/j154, j160/j163 (`0df7763` / `51bfa57`), j161
|
||||
(`docs/ram_floor_exhaustion_ab.md:219`).
|
||||
**Instrument for the new numbers below:** `worktrees/j167-ceiling/j167_probe.py`
|
||||
(branch `j167-ceiling`) — pure replay of the recorded corpora
|
||||
(`/tmp/tfil_ab2/out/`, 60 252 of our own scored shots over 140 battles; and
|
||||
`/tmp/firelag_live2/`, 1 700 shots / 1 664 incoming bullets over 4 battles).
|
||||
**No battle, A/B, server or GUI was run for this document.**
|
||||
|
||||
---
|
||||
|
||||
## 1. The ceiling
|
||||
|
||||
**Melee is structurally unavailable against DrussGT, and the exhaust/ram line is a
|
||||
niche rather than a lever.** The evidence is two-sided and independent: (a) *our
|
||||
mover's own ruler* — 94% of forced (no-safe-tile) picks happen at range > 300 u
|
||||
and only 6-7% of picks reach the chosen tile at the estimated arrival time, with
|
||||
the destination hot on arrival 35-42% of the time; and (b) *the j166 pursuit
|
||||
probe* — 35 windows × 250 ticks of open-loop kinematics in which **every**
|
||||
steering law is equal-or-worse than doing nothing clever:
|
||||
|
||||
| steering law (j166) | closing (u/tick) | contact % | TTI (ticks) |
|
||||
|---|---:|---:|---:|
|
||||
| current-position closing | 4.03 | 65.7 | 72.3 |
|
||||
| body/barrel ray | 1.38 | 40.0 | 131.4 |
|
||||
| velocity intercept (degenerate at equal speed) | — | 0 over 2 118 ticks | — |
|
||||
| best case: lag-5 lead | 4.20 | 65.7 | 68.9 |
|
||||
|
||||
The root cause of the historical **0/59 proactive-ram** result
|
||||
(`docs/ramming_negative_result.md`) is not a bad gate: **DrussGT never let the
|
||||
distance drop.** Per-round minimum distance 152-338 u, median ~490 u, and
|
||||
`frac(dist < 50) = 0.000` in all four recorded rounds. A pursuit that never gets
|
||||
below 152 u cannot make contact, whatever the gate says. It is also not a
|
||||
gun-side problem: **the server never transmits the enemy's gun direction**
|
||||
(`ScannedBotEvent` = `energy, x, y, direction` where `direction` is the BODY
|
||||
heading, plus `speed`; `TurnProcessor.kt:313-323`). There is no aim-based lead,
|
||||
no aim-based dodge and no early warning available. Against DrussGT the
|
||||
body-to-bullet angle has median **90.1 deg**, and the body ray passes within
|
||||
10 deg of us on **0.0% of 1 794 ticks** — its gun is always on us, its body
|
||||
never is.
|
||||
|
||||
**Recorded so the idea is not re-proposed:** the j166 lag-5 residue does improve
|
||||
TTI (72.3 → 68.9) and **converts to contact 0% of the time**. A 4% TTI gain with
|
||||
zero contact conversion is noise, not a lead.
|
||||
|
||||
### The honest remaining niches for exhaust/ram
|
||||
|
||||
1. **An opponent that closes on us.** Ram works whenever the other side comes to
|
||||
us. Nothing here generalises away from that.
|
||||
2. **A late-round exhaustion when they are already near.** The one conversion
|
||||
ever recorded came from a *finisher* (enemy 16 → 1 energy), which is already
|
||||
the default gate.
|
||||
3. **Any 2v1+ mode**, where closing dynamics are not symmetric.
|
||||
|
||||
Against DrussGT specifically none of these will move the score, and
|
||||
`TR_RAM_FLOOR_ENERGY` is under test in j163 — do not duplicate it.
|
||||
|
||||
---
|
||||
|
||||
## 2. What the ceiling implies
|
||||
|
||||
**If range is held, the only remaining lever is the gun at range, and the binding
|
||||
numbers are the gun's, not the tile picker's.** The long-range hit rate is
|
||||
**~9-10%** (Pattern live: 12.3% at 300-450 px, 9.2% at 450+; overall 10.5% —
|
||||
`docs/headon_longrange_live.md`), the live hit half-window at 450 px is
|
||||
**`atan(18/450) = 2.29°`** (`docs/gun_campaign.md:59`), and the measured arrival
|
||||
aim error is **16.19° mean-abs at 450+** (`docs/bitbrain_campaign.md:107`,
|
||||
`docs/headon_longrange_live.md:85`). 16.19° is **7× the window**. The tile picker
|
||||
cannot close a 7× gap that sits downstream of the gun.
|
||||
|
||||
### New measurement — arrival aim error decomposed (j167, 23 275 shots at 450+ px)
|
||||
|
||||
Arrival aim error is defined non-circularly: the angle between the fired bearing
|
||||
and the bearing to where the target *actually is* when the bullet arrives
|
||||
(`tof = 20 - 3·power`, so the flight time comes from the power, not from the
|
||||
shot's own geometry). It splits **exactly**, as signed angles, into
|
||||
|
||||
* **B, the model part** = the error the gun's own lead model leaves behind, and
|
||||
* **C, manoeuvre** = the target's path curvature relative to the
|
||||
constant-velocity extrapolation from the true state at fire time.
|
||||
|
||||
| band (px) | n | mean|A| | mean|B| (model) | mean|C| (manoeuvre) | sd(B) | sd(C) | corr(B,C) |
|
||||
|---|---:|---:|---:|---:|---:|---:|---:|
|
||||
| 0-100 | 24 767 | 83.36 | 101.56 | 49.38 | 124.5 | 75.9 | −0.65 |
|
||||
| 300-450 | 11 372 | 13.07 | 21.77 | 11.00 | 26.0 | 13.0 | −0.87 |
|
||||
| **450+** | **23 275** | **11.26** | **17.18** | **7.73** | **20.7** | **9.3** | **−0.85** |
|
||||
|
||||
At 450+ the model part's variance is **2.2× the manoeuvre part's**, and
|
||||
`corr(B,C) = −0.85` means the two largely *cancel* — the net 11.26° is much
|
||||
smaller than either part. **The 16° is a lead-model number, not a dodge number.**
|
||||
Two supporting numbers: a naive constant-velocity extrapolation of a **2-tick-old**
|
||||
position scores 8.45° mean-abs at 450+, and the time-of-flight implied by the
|
||||
shot's own geometry (holding the current velocity) sits a **median 10 ticks short**
|
||||
of the power-derived arrival tick (p10 −18, p90 +31) — i.e. the gun systematically
|
||||
**under-leads in time**, consistent with `docs/lead_capture_by_range.md`
|
||||
(capture 0.135 at 450+).
|
||||
|
||||
> **Do not read "a stale-CV model scores 8.45°" as "simplify the gun".** This is
|
||||
> exactly the offline-ruler trap that killed HeadOn: the ruler said a no-lead gun
|
||||
> was equal-or-better at 300+ and live it hit **20×/23× less**
|
||||
> (`docs/headon_longrange_live.md`). The corpus is closed-loop — the target's
|
||||
> manoeuvre is a *reaction to our own bullet* — so (B) and (C) are not separable
|
||||
> here, and per `docs/offline_harness_trust.md` (j89: 0/6 on closed-loop) this
|
||||
> instrument ranks per-gun single-tick prediction, it does not predict a live A/B.
|
||||
|
||||
### Cross-reference: what is still open in the gun docs
|
||||
|
||||
| doc | finding | status after this ceiling |
|
||||
|---|---|---|
|
||||
| `docs/gun_campaign.md:59` | hit half-window 2.29° at 450 px; measured signal 4.6-7.6° | **STILL OPEN and now the load-bearing number.** The decomposition says the gap is in the *model*, and the model is systematically 10 ticks short in time-of-flight. |
|
||||
| `docs/gun_campaign.md:40-45` | lead amplitude is dead (1.0/1.5/2.0/3.0 all worse); radial knobs are bearing-invariant by construction | **CLOSED.** |
|
||||
| `docs/gun_campaign.md:737-753` | `len6` +0.49 wins/run (p=0.039, n=15) did not replicate on n=33 | **CLOSED.** |
|
||||
| `docs/bitbrain_campaign.md:189` | BitBrain / TMHorizon corrector adds no measurable aim (16.199 vs 16.193) | **CLOSED.** |
|
||||
| `docs/bitbrain_campaign.md:107` | Pattern's own lead correlation with the required lead is 0.165 at 450+ | **STILL OPEN.** It is the same defect the decomposition names. |
|
||||
| `docs/state_window_gate.md` | single wave-relative state at Q=4 predicts the miss bin at 0.4094 vs 0.2348 majority, but bins are 4.58-7.63° wide | **STILL OPEN, and now the best-placed surviving idea** — it is a *model* correction, which is where the error is. |
|
||||
| `docs/gun_rack_analysis.md:423-455` | the 16-candidate rack ranking A/B found no winner; knobs added, all neutral | **CLOSED** (13 guns, `onlyPattern` shipped). |
|
||||
|
||||
---
|
||||
|
||||
## 3. Negative-results ledger — mechanisms closed by measurement
|
||||
|
||||
Do not re-litigate any row. The unit of evidence is the **opponent**.
|
||||
|
||||
| mechanism | knob / job | headline number | verdict |
|
||||
|---|---|---|---|
|
||||
| Geometry-weighted tile draw | `TR_TFIL_GEO_MODE/TAU`, j152 `38fbc6e`, A/B'd j159 `4a1f3e1` | **−8.83 damage/run, p=0.0061**; wins −0.05, p=0.46; +26.3 px mean distance on 15/15 opponents | **REJECTED.** Default off, stays off. |
|
||||
| The bounded hold | `TR_TFIL_HOLD_MAX_TICKS`, j154 `2223ca6` | mechanism-positive, outcome-null (j146/j153) | **Default off.** No live win. |
|
||||
| The proactive ram | `oldram` vs `base` gate `dist<200` | **p=0.69**, damage 279 vs 284, survival 17/49 vs 16/49; **0/59 opportunity→contact** | **CLOSED** (`docs/ramming_negative_result.md`). |
|
||||
| The aim-based ram | j166 `a5a49bd` | body ray within 10° of us on **0.0% of 1 794 ticks**; body/barrel ray contact 40.0% vs 65.7% for doing nothing clever | **IMPOSSIBLE** — the server never sends gun direction (`TurnProcessor.kt:313-323`). |
|
||||
| Arrival commitment (`tfil`) | j144 `d2005ab` | mechanism-positive, outcome-null | Default off. |
|
||||
| Turn-cost tiebreak among safe tiles | j145 `39c90fd` | real but small mechanism, under-powered outcome null (300 battles, 5 arms) | Default off. |
|
||||
| Field shape (safety) | j146 `de5d02b` | safe-set broken 63.5% → 30.4% offline; live null on damage and wins (375 battles, 5 arms) | **Default off.** |
|
||||
| Corridor bound | j148 `5e213df` `TR_{TFIL,STRAFE}_CORRIDOR_TICKS` | never landed in a live A/B | Untested, not a candidate. |
|
||||
| Ring arrival commitment | j165 `fe77056` `TR_TFIL_RING_COMMIT_ARRIVAL` | reach 0.24% → **3.05%**, picks 5 521 → 525, byte-for-byte default parity over 20 026 ticks, 148 guards | **Mechanism-positive, default off.** The strongest surviving movement mechanism. |
|
||||
| Firing floor / enemy-exhaustion ram | j160 `23bce2d`, A/B'd j163 `51bfa57` | **clean negative**; the offline energy corpus missed the live game by 200× | **Under test in j163 — do not duplicate.** |
|
||||
| Fire-detection lag | j147 `d21f7ce` `TR_FIRE_LAG` | displacement 19.06 → 5.37 px, deadline error 0.99 → 0.06 ticks; **live outcome-neutral**; ceiling ~10% of incoming damage (measured below) | **Default off, permanently.** |
|
||||
| Hard arrival bound | j151 `a01141c` `TR_TFIL_ARRIVE_TICKS` | mechanism-positive, outcome-null | Default off. |
|
||||
|
||||
> **Methodological caution (j161), binding on everything above.** Pooled tests
|
||||
> can hide real per-opponent effects: j159's safety signal was **p=0.0008
|
||||
> per-opponent while the pooled test was null** (`docs/ram_floor_exhaustion_ab.md:219`).
|
||||
> **Any future mechanism claim must report per-opponent mechanism metrics, not a
|
||||
> pooled mean.** A pooled null is not evidence of absence; it is evidence that
|
||||
> the heterogeneity was not averaged down.
|
||||
|
||||
---
|
||||
|
||||
## 4. Lead-time lever 1 — what a 2-tick-stale ghost really costs
|
||||
|
||||
`TR_FIRE_LAG` back-dates the bullet ghost (default 0). Energy-drop shot
|
||||
detection lags **1.9 ticks mean**; median bullet flight is **19 ticks**
|
||||
(`onHitByBullet` gives 82 hits / 7 421 ticks, one update per ~90 ticks).
|
||||
|
||||
**Measured on 55 750 incoming bullets** (`/tmp/tfil_ab2/out/`). For each bullet:
|
||||
the time to closest approach of the target's recorded path to the bullet line
|
||||
(**median 9 ticks**, p10 1, p90 39), and the minimum number of ticks of lead time
|
||||
a max-speed hard-turn dodge needs to build 17 px of lateral displacement:
|
||||
|
||||
| minimum dodge lead time (ticks) | 0 | 1 | 2 | 3 | 4 | 5+ |
|
||||
|---|---:|---:|---:|---:|---:|---:|
|
||||
| share of incoming bullets | **57%** | 33% | 4% | 2% | 1% | 2% |
|
||||
|
||||
**57% of incoming bullets are already undodgeable at the instant they are fired**,
|
||||
and only **~10%** (need ≥ 2 ticks) are in a regime where a 2-tick detection lag
|
||||
can change anything. Applying the lag to the open-loop dodge model:
|
||||
|
||||
| ghost lag (ticks) | modelled hits | Δ vs perfect | share of all bullets whose hit/miss verdict flips |
|
||||
|---|---:|---:|---:|
|
||||
| 0 | 22 419 | — | — |
|
||||
| **1.9 / 2** | **25 100** | **+2 681 (+12.0%)** | **10.18%** |
|
||||
| 3 | 26 230 | +14.6% | 14.63% |
|
||||
| 5 | 28 949 | +22.0% | 22.04% |
|
||||
|
||||
**Verdict: the 1.9-tick lag costs on the order of 10% more incoming hits** — at
|
||||
the measured ~200 damage/run, roughly **20 damage/run**, an order of magnitude
|
||||
below the movement A/B damage MDE. This is consistent with `TR_FIRE_LAG`'s already
|
||||
measured live outcome-neutral result. The ghost is *wrong*, but wrongness at
|
||||
10% of incoming damage cannot be turned into wins at this sample size.
|
||||
|
||||
**Recommendation: `TR_FIRE_LAG` stays off permanently.** It is a correctness fix
|
||||
with a measured, bounded, sub-MDE payoff.
|
||||
|
||||
---
|
||||
|
||||
## 5. Lead-time lever 2 — the 16° decomposed, component by component
|
||||
|
||||
At 450+ px (23 275 shots), against the 11.26° net arrival error:
|
||||
|
||||
| component | measured | addressable? |
|
||||
|---|---|---|
|
||||
| **(a) enemy body-gun decoupling** | `\|gun dir − body heading\|` median **89.9°** (p10 25.9, p90 154.0, n=60 928). Extrapolating the target along its **gun** instead of its **body** would put the arrival bearing **79.5° median** wrong. | **Not present, and not addressable.** The intercept model uses the target's *recorded position and velocity*, both of which are the true body quantities and both exactly observed. Body-gun decoupling therefore contributes **exactly 0** to our arrival error. It is fatal for *aim-based* leading and threat warning (j166) and irrelevant to *position-based* leading. |
|
||||
| **(b) our own leading model** | mean|·| **17.18°**, sd **20.7**; implied time-of-flight a **median 10 ticks short** of the power-derived arrival tick | **DOMINANT, and addressable.** This is ~2.2× the manoeuvre variance and it is the whole of the 16°. |
|
||||
| **(c) target manoeuvre between scan and fire** | mean|·| **7.73°**, sd **9.3** | Small relative to (b), and **irreducible** — it is the dodger's own unpredictability, exactly the ~half of the under-lead `docs/lead_capture_by_range.md` attributes to a trivial predictor's own ceiling. |
|
||||
| **(d) gun turn rate / time-to-fire** | the correct solution drifts a **median 0.416°/tick** (p90 5.45). The gun turns at 10°/tick, so a 17° correction takes **1.7 ticks ≈ 0.40°** of drift. | **Not binding.** Contributes ~**0.4°, i.e. ~3% of the 11.26° error.** The gun can always reach the answer; it aims at the wrong answer. |
|
||||
|
||||
**So the 16° is not (a), not (c) and not (d). It is (b) — the lead model's
|
||||
time-of-flight, short by ~10 ticks.** Caveat, stated once and load-bearing: on a
|
||||
closed-loop corpus (B) and (C) are not cleanly separable, since the target's
|
||||
manoeuvre is a reaction to our own shot; the `corr(B,C) = −0.85` is exactly that
|
||||
confound showing up. The *rank order* (b) ≫ (c) ≫ (d) > (a)=0 is robust to it
|
||||
because (b) and (c) differ by 2.2× in variance and (d) is 3%.
|
||||
|
||||
---
|
||||
|
||||
## 6. The proposed lever: pre-multiply before learning — PREMISE DEAD
|
||||
|
||||
The design: aim error is largely a *product* (bearing-rate × time-of-flight), so
|
||||
pre-multiply the two features and feed one small Tsetlin machine. **Measured on
|
||||
the same corpus, the premise does not hold and the experiment should not be
|
||||
built.** `y` = the required lead angle (current bearing → arrival bearing), i.e.
|
||||
exactly the quantity the gun must predict; `b` = the observable 4-tick finite
|
||||
difference of the bearing; `t = 20 − 3·power`.
|
||||
|
||||
| band (px) | n | corr(**b·t**, y) | corr(b+t, y) | R² additive [1,b,t] | R² product [1,b·t] | held-out side acc, additive | held-out side acc, product | held-out residual rms (deg) |
|
||||
|---|---:|---:|---:|---:|---:|---:|---:|---:|
|
||||
| 0-200 | 24 934 | −0.1020 | −0.1022 | 0.0105 | 0.0104 | 0.579 | 0.580 | 75.98 |
|
||||
| 200-300 | 671 | 0.1190 | 0.0766 | 0.0147 | 0.0142 | 0.488 | 0.487 | 13.70 |
|
||||
| 300-450 | 11 372 | **0.1501** | 0.0299 | 0.0256 | 0.0225 | 0.544 | 0.534 | 8.43 |
|
||||
| **450+** | **23 275** | **0.2833** | 0.1052 | **0.0831** | 0.0803 | **0.603** | **0.601** | **6.68** |
|
||||
| pooled | 60 252 | −0.1005 | −0.1009 | 0.0102 | 0.0101 | — | — | — |
|
||||
|
||||
*(side accuracy is 2-fold held-out and balanced; the TM record is ~0.47-0.49)*
|
||||
|
||||
Two things are true and the second kills the idea:
|
||||
|
||||
1. **As a single scalar, the product is much the better feature at range**:
|
||||
`corr(b·t, y) = 0.283` vs `corr(b+t, y) = 0.105` at 450+ — 2.7× better, and
|
||||
5× better at 300-450. So the *premise* ("the error is a product, not a sum")
|
||||
is **confirmed as a statement about correlation**.
|
||||
2. **But it buys nothing a weight-sum cannot already express.** The best linear
|
||||
additive model on the same two features reaches **R² 0.0831 vs the product's
|
||||
0.0803**, and the held-out balanced side accuracy is **0.603 (additive) vs
|
||||
0.601 (product)** — a 0.002 difference, i.e. nothing. Pooled, the two are
|
||||
identical (−0.1005 vs −0.1009; R² 0.0102 vs 0.0101). A TM with two input
|
||||
features **already reconstructs the product term**; the multiplication is what
|
||||
the network was doing anyway.
|
||||
|
||||
**Even the ceiling is out of reach.** The best held-out residual on the required
|
||||
lead at 450+ is **6.68° rms**, against a live hit half-window of **2.29°** — a
|
||||
2.9× shortfall. Pre-multiplying does not get a classifier to 2.29°; nothing in
|
||||
this family does. **Do not build it.** The spec is recorded here so the idea is
|
||||
closed on measurement rather than on taste.
|
||||
|
||||
*(Had it survived, the spec would have been: one TM, ONE input feature `b·t`
|
||||
binarised on sign, plus the 4-bit horizon one-hot as today; offline gate =
|
||||
held-out balanced side accuracy above 0.55 and residual rms below 3° at 450+;
|
||||
live gate = wins/run with CI excluding 0 and sign-flip p<0.05 at 210 runs/arm,
|
||||
damage not detectably down, MDE 0.17 wins/run. Predicted accuracy was 0.60 side
|
||||
accuracy, which is a real signal against the 0.47-0.49 record — and still not
|
||||
close enough to the window to convert.)*
|
||||
|
||||
---
|
||||
|
||||
## 7. The A/B queue, in priority order, with the MDE honestly restated
|
||||
|
||||
Throughput **22.7-23.2 runs/min**; movement gate resolved **0.17 wins/run at 210
|
||||
runs/arm**; the `1/√n` extrapolation to 0.10 wins/run is **607 runs/arm ≈ 1.4 h —
|
||||
a FLOOR on elapsed time, not an estimate**, because opponent heterogeneity does
|
||||
not average down. A null at this sample size **only excludes a LARGE effect.**
|
||||
(j163 additionally measured 14-22 runs/min, not 22.7-23.2, so even the floor is
|
||||
optimistic.)
|
||||
|
||||
| # | experiment | what it tests | cost | a null would license |
|
||||
|---|---|---|---|---|
|
||||
| **1** | **The lead-model time-of-flight correction** (j167's (b)): re-derive the gun's arrival prediction so the implied flight is the power-derived tick, not 10 ticks short. | The one component that carries 2.2× the error variance at 450+, and the only open axis in `docs/gun_campaign.md` (lead *information*, not amplitude). | Offline gate first: arrival aim error at 450+ must fall below 11.26° mean-abs on held-out battles, ideally <8°; only then 2 arms × 15 opponents × 14 runs = 420 battles ≈ **0.3-0.4 h** wall. | Closing the single open gun axis. Nothing left in the gun. |
|
||||
| 2 | `TR_TFIL_RING_COMMIT_ARRIVAL` (j165, default off) | Whether the largest surviving *movement* mechanism (reach 0.24% → 3.05%, picks 5 521 → 525, 148 guards) converts to wins. | 210 runs/arm ≈ **1.4 h floor**. | Retiring the whole ring/approach programme: if even a 12× reach gain is outcome-null, the ceiling argument is confirmed end to end. |
|
||||
| 3 | `TR_FIRE_LAG` (tfil/strafe, default off) | Nothing worth testing — its ceiling is now measured at **~10% of incoming damage ≈ 20 dmg/run**, below the MDE. | Would be 1.4 h to learn nothing. | Nothing. **Skip it**; the measurement has already answered it. |
|
||||
| 4 | `TR_TFIL_ARRIVE_TICKS` (j151, default off) | Whether a hard arrival bound converts now that the ring is rehabilitated. | 1.4 h. | Retiring it with j151's own null attached. |
|
||||
| 5 | `TR_RAM_FLOOR_ENERGY` (j160, j163) | **Under test in j163. DO NOT DUPLICATE.** | — | — |
|
||||
|
||||
**Recommendation.** Run **only experiment 1**, and only after the *offline* gate
|
||||
passes; if the offline gate does not move the 450+ arrival error below ~8°, run
|
||||
nothing at all. Given five consecutive nulls or near-nulls (j144, j145, j146,
|
||||
j147, j159) plus a clean negative in j163, spending 1.4 h of live time on
|
||||
experiments 2-4 is not justified — those are mechanism-positive
|
||||
mechanisms whose outcome nulls are already the standing record, and a null there
|
||||
teaches nothing that the ledger does not already say.
|
||||
|
||||
**"The ceiling is real and we should stop spending on movement" is the answer.**
|
||||
The remaining budget belongs to the gun's lead model, or it is not spent.
|
||||
Reference in New Issue
Block a user