movement: range-weighted TFIL variant + tamed heat field (opt-in, default unchanged)
New mover `the_floor_is_lava_ring.nim`, a COPY of `the_floor_is_lava.nim` (which stays byte-identical - the user explicitly wants the current TFIL preserved). Selected only via `TR_MOVEMENT=tfil_ring`; the default stays `tfil`. WHY: our measured real hit rate vs DrussGT is strongly range-dependent - 21.6% at 0-100px, 27.1% at 100-200px, 19.3% at 200-300, 10.9% at 300-400, 6.8% at 400-600, 5.4% at 600-800 - but we shoot from ~450px on average. Plain TFIL has no range preference at all. THE ONE CHANGE: the final tile draw is re-weighted toward a target band. rangeW(d) = 1.0 if lo<=d<=hi; exp(-((lo-d)/K)^2) if d<lo; exp(-((d-hi)/K)^2) if d>hi w_i = rangeW(d_i)^(1/T); chosen ~ Categorical(w) FLAT TOP on purpose: a Gaussian centred on the band midpoint would collapse the band to a point and destroy the within-band hedge. `T` is the only knob; `TR_TFIL_RANGE_TEMP=0` gives plain `rand(candidates.high)` - the exact control arm. Safety stays a HARD constraint: the weighting only reorders the draw among the pool the old code already accepted, so it can never pick a tile the old code rejected (monotone refinement). Small pools (<4) stay uniform. Randomness is deliberately KEPT: a measured A/B showed committing to the "best" tile made real hit rate WORSE (7.02% -> 5.10%), so the distribution is tilted, never removed. HEAT TAMING (ring copy only; env-overridable): TR_TFIL_CORRIDOR_HEAT 20.0 -> 5.0 TR_TFIL_WALL_HOTNESS 30.0 -> 10.0 Rationale, measured: `CorridorHeat=20` is TWICE `PathDangerThreshold=10`, so a single corridor could poison a path by itself; `WallHotness=30` with `WallRadiance=10` put the outer two tile rings over threshold on their own. Per-source shares of total lava: wall 60.6%, corridor 25.4%, pillar 7.4%, everything else <3%. MEASURED EFFECT (primary fixture, 20,026 ticks / 15 rounds, field identity verified max diff 0.000e+00): metric original(20/30) ring(5/10) band-weightable ticks 10.79% 26.45% mean safeTiles/tick 15.19 85.04 ticks with 0 safe (pre-fallback) 58.5% 7.5% safePool >= 4 39.05% 92.47% >=1 safe tile in 100-200px 11.84% 26.64% MEAN CLOSEST-SAFE-TILE DISTANCE 397.78px 284.84px tiles > 10 threshold 0.61 0.15 The 397.78px figure is why the bot stayed far away: the safety filter left nothing safe near the target, and 397px is our WORST range. Control: setting corridor=20 wall=30 reproduces the original baseline exactly. CEILING, honestly: even at corridor 0 / wall 0 only ~40% of ticks are band-weightable, so no constant tweak fully unlocks the range weighting. Ram unification: the ring mover takes a `band` field; ramming becomes just `band=(0,50)`, so there is one movement engine. The `tfil` path is unchanged. Observability: magenta annulus at the band edges, candidates tinted by weight, chosen tile marked; one `[tfil_ring]` log line on change (now including corridorHeat/wallHotness). Guards: test_tfil_ring_weights 24/24 (new, pure, no battle), test_gun_harness 39, test_vbullet_metric 11, test_power_selection 3, test_adaptive_radar 41. UNVERIFIED: the mover's live effect. It has not been run in a battle yet.
This commit is contained in:
@@ -28,6 +28,7 @@ import guns/tm_selector
|
||||
import movements/phantom_meteor
|
||||
import movements/rammer
|
||||
import movements/the_floor_is_lava
|
||||
import movements/the_floor_is_lava_ring
|
||||
import movement_harness/virtual_bodies as mvb
|
||||
import movement_harness/bullet_shadows
|
||||
import targeting/enemy_tracker
|
||||
@@ -87,6 +88,15 @@ let DisabledGuns =
|
||||
s
|
||||
|
||||
proc gunDisabled(id: int): bool {.inline.} = id in DisabledGuns
|
||||
## ── Movement rack (runtime switch) ─────────────────────────────────────────
|
||||
## `TR_MOVEMENT` selects the movement engine, mirroring the gun rack's
|
||||
## env-driven construction. Both engines are always constructed, so the switch
|
||||
## needs no rebuild:
|
||||
## tfil (DEFAULT) — today's behaviour, byte-for-byte, including the
|
||||
## separate rammer dispatch.
|
||||
## tfil_ring — the new RANGE-WEIGHTED mover (movements/the_floor_is_lava_ring).
|
||||
## UNPROVEN: it must never become the default silently.
|
||||
let MovementName* = getEnv("TR_MOVEMENT", "tfil").strip().toLowerAscii()
|
||||
## Per-process output paths so concurrent A/B runs do not clobber each other.
|
||||
let GunStatsPath = getEnv("GUN_STATS_PATH", "/tmp/gun_stats.jsonl")
|
||||
let ShotLogPath = getEnv("GUN_SHOTLOG_PATH", "/tmp/shot_log.jsonl")
|
||||
@@ -137,6 +147,7 @@ type
|
||||
knnGun: KNNGun
|
||||
tmSelector: TmSelectorGun
|
||||
mover: TFILModule
|
||||
ringMover: TFILRingModule
|
||||
rammer: RammerModule
|
||||
isRamming: bool
|
||||
ramStuckTicks: int
|
||||
@@ -241,7 +252,11 @@ proc printConfig(bot: ModularBot, forceAll: bool = false) =
|
||||
let rc = if bot.radarMode != bot.prevRadar or forceAll: CLR_CHANGE else: ""
|
||||
let tc = if bot.currentTargetId != bot.prevTarget or forceAll: CLR_CHANGE else: ""
|
||||
let radarName = if bot.radarMode == 0: "radar_lock" else: "adaptive_melee"
|
||||
let moveName = if bot.isRamming: "rammer" else: "tfil"
|
||||
let moveName =
|
||||
if MovementName == "tfil_ring":
|
||||
if bot.isRamming: "tfil_ring(ram)" else: "tfil_ring"
|
||||
elif bot.isRamming: "rammer"
|
||||
else: "tfil"
|
||||
var line = "[config] "
|
||||
if bot.currentGun >= 0 and bot.currentGun < GunNames.len:
|
||||
line &= gc & "gun=" & GunNames[bot.currentGun] & (if gc != "": rst else: "") & " | "
|
||||
@@ -471,12 +486,14 @@ method onBulletHitWall*(bot: ModularBot, e: BulletHitWallEvent) =
|
||||
bot.resolveShotLog(e.bullet.bulletId, false)
|
||||
bot.moveTracker.shadows.removeBulletNear(e.bullet.x, e.bullet.y)
|
||||
bot.mover.removeBulletNear(e.bullet.x, e.bullet.y)
|
||||
bot.ringMover.removeBulletNear(e.bullet.x, e.bullet.y)
|
||||
|
||||
method onBulletHitBullet*(bot: ModularBot, e: BulletHitBulletEvent) =
|
||||
discard bot.resolveOwnBullet(e.bullet.bulletId) # bullet-vs-bullet: free the slot
|
||||
bot.resolveShotLog(e.bullet.bulletId, false)
|
||||
bot.moveTracker.shadows.removeBulletNear(e.bullet.x, e.bullet.y)
|
||||
bot.mover.removeBulletNear(e.bullet.x, e.bullet.y)
|
||||
bot.ringMover.removeBulletNear(e.bullet.x, e.bullet.y)
|
||||
|
||||
method onHitByBullet*(bot: ModularBot, e: HitByBulletEvent) =
|
||||
bot.moveTracker.registerHit(e.bullet.power, e.bullet.direction, getX(), getY())
|
||||
@@ -611,6 +628,7 @@ method onRoundStarted*(bot: ModularBot, e: RoundStartedEvent) =
|
||||
bot.radar.init()
|
||||
bot.meleeRadar.init()
|
||||
bot.mover.resetRound()
|
||||
bot.ringMover.resetRound()
|
||||
bot.enemyTracker.resetRound()
|
||||
bot.isRamming = false
|
||||
bot.ramStuckTicks = 0
|
||||
@@ -767,11 +785,21 @@ method run*(bot: ModularBot) =
|
||||
bot.ramDurationTicks = 0
|
||||
bot.ramCooldownTicks = 30
|
||||
|
||||
let (spd, tr) = if shouldRam:
|
||||
# Movement dispatch. `TR_MOVEMENT=tfil_ring` routes BOTH holding and
|
||||
# ramming through the single ring engine (ram is just band [0, 50]); the ram
|
||||
# DECISION above is unchanged. The default `tfil` keeps the exact old
|
||||
# two-engine behaviour (tfil when holding, rammer when ramming).
|
||||
var spd, tr: float
|
||||
if MovementName == "tfil_ring":
|
||||
bot.ringMover.band =
|
||||
if shouldRam: (lo: 0.0, hi: 50.0)
|
||||
else: (lo: RangeLo, hi: RangeHi)
|
||||
(spd, tr) = bot.ringMover.computeMove(ws)
|
||||
elif shouldRam:
|
||||
bot.mover.clearGraphics()
|
||||
bot.rammer.computeMove(ws)
|
||||
(spd, tr) = bot.rammer.computeMove(ws)
|
||||
else:
|
||||
bot.mover.computeMove(ws)
|
||||
(spd, tr) = bot.mover.computeMove(ws)
|
||||
setTargetSpeed(spd)
|
||||
setTurnRate(tr)
|
||||
|
||||
@@ -999,6 +1027,7 @@ when isMainModule:
|
||||
radar: RadarLockModule(),
|
||||
meleeRadar: initAdaptiveMeleeRadar(),
|
||||
mover: TFILModule(debugGraphics: true),
|
||||
ringMover: TFILRingModule(debugGraphics: true),
|
||||
rammer: initRammer(),
|
||||
moveTracker: mvb.initVirtualBodyTracker(1),
|
||||
currentGun: -1,
|
||||
|
||||
Reference in New Issue
Block a user