j131 learned movement: real bullet-endpoint resolution (TR_LEARNED_REAL_EVENTS, default off) + exact-geometry Gate A/B (inversion NOT fixed; state still the constraint)
This commit is contained in:
@@ -71,6 +71,13 @@
|
||||
## = the bot radius as an angle at the wave's
|
||||
## distance). See docs/movement_campaign.md,
|
||||
## "outcome label".
|
||||
## TR_LEARNED_REAL_EVENTS =1: resolve a wave on the REAL bullet event
|
||||
## (onHitByBullet / an enemy bullet intercepted by
|
||||
## ours) using the exact origin->endpoint line and
|
||||
## the real flight time, dropping the wave at once.
|
||||
## Default off (arrival-deadline proxy). Enemy WALL
|
||||
## hits are owner-private on server 0.35.5 and stay
|
||||
## deadline misses - see the const-block note.
|
||||
## TR_LEARNED_LOG per-decision log line
|
||||
|
||||
import std/[math, os]
|
||||
@@ -108,8 +115,30 @@ const
|
||||
LearnedWallMarginEnv* = "TR_LEARNED_WALL_MARGIN"
|
||||
LearnedGlobalEnv* = "TR_LEARNED_GLOBAL"
|
||||
LearnedLabelEnv* = "TR_LEARNED_LABEL"
|
||||
LearnedRealEventsEnv* = "TR_LEARNED_REAL_EVENTS"
|
||||
LearnedLogEnv* = "TR_LEARNED_LOG"
|
||||
|
||||
## Real-event matching (job j131).
|
||||
## A wave is normally resolved on the nominal arrival tick
|
||||
## `ceil(startDist/speed)`. With `TR_LEARNED_REAL_EVENTS=1` a wave is instead
|
||||
## resolved by the REAL server event that carries the bullet's endpoint:
|
||||
## * `onHitByBullet` -> the bullet HIT us; endpoint = our impact point;
|
||||
## * a bullet-vs-bullet intercept of an ENEMY bullet (our bullet hit theirs)
|
||||
## -> the enemy bullet's endpoint/heading are in the event.
|
||||
## The bullet's raw straight line (origin at fire -> endpoint) then gives the
|
||||
## EXACT GF bin and the real flight time (a cross-check on the energy-drop
|
||||
## speed inference), and the wave is dropped immediately (no ghost build-up).
|
||||
##
|
||||
## ON THE RUNNING SERVER (0.35.5, verified from the server bytecode +
|
||||
## `TurnToTickEventForBotMapper`) an ENEMY bullet that hits a WALL produces a
|
||||
## `BulletHitWallEvent` only for the bullet's OWNER (`addPrivateBotEvent(
|
||||
## bullet.botId, ...)`), and `bulletStates` is filtered to the bot's own
|
||||
## bullets. So a wall HIT is NOT observable by the dodger; those waves fall
|
||||
## back to the arrival deadline and are labelled a MISS. `resolveEnemyBullet`
|
||||
## accepts a wall endpoint anyway so a future/other server can feed it.
|
||||
RealEventsMatchTol = 12.0 ## max |real flight - nominal| to accept a match, ticks
|
||||
RealEventsGrace = 8 ## ticks past nominal before an unmatched wave resolves
|
||||
|
||||
## Number of joint (vlat,dist,room,turn) state codes = 4^4.
|
||||
LS_STATES = LS_Q * LS_Q * LS_Q * LS_Q
|
||||
## Prior-mix weight for the 2-class outcome readout.
|
||||
@@ -132,6 +161,7 @@ var
|
||||
LearnedWallMargin* = 48.0
|
||||
LearnedGlobal* = false
|
||||
LearnedLabel* = llHistogram
|
||||
LearnedRealEvents* = false
|
||||
LearnedLog* = false
|
||||
|
||||
proc getEnvFloat(name: string, default: float): float =
|
||||
@@ -164,6 +194,7 @@ proc loadLearnedEnv*() =
|
||||
LearnedRadialFrac = clamp(getEnvFloat(LearnedRadialFracEnv, 0.35), 0.0, 1.0)
|
||||
LearnedWallMargin = max(0.0, getEnvFloat(LearnedWallMarginEnv, 48.0))
|
||||
LearnedGlobal = envOn(LearnedGlobalEnv)
|
||||
LearnedRealEvents = envOn(LearnedRealEventsEnv)
|
||||
LearnedLog = envOn(LearnedLogEnv)
|
||||
LearnedLabel =
|
||||
case getEnv(LearnedLabelEnv, "").strip().toLowerAscii()
|
||||
@@ -219,6 +250,8 @@ proc roomToWall(px, py, dx, dy, arenaW, arenaH: float64): float64 =
|
||||
|
||||
type
|
||||
LSWave = object
|
||||
ownerId: int ## enemy that fired (energies are per-enemy)
|
||||
fireTick: int ## `ws.tick` at the fire tick (real flight time)
|
||||
originX, originY: float64
|
||||
bearing: float64 ## enemy -> us at the fire tick (centre line)
|
||||
speed: float64
|
||||
@@ -246,6 +279,9 @@ type
|
||||
prevHeading: float64
|
||||
debugGraphics*: bool
|
||||
decisions*: int ## decisions taken (diagnostic)
|
||||
resolvedReal*: int ## waves resolved by a real bullet event
|
||||
resolvedDead*: int ## waves resolved on the arrival deadline
|
||||
lastFlightErr*: float ## real flight - nominal flight, last resolution
|
||||
|
||||
proc resetRound*(m: var LearnedSurferModule) =
|
||||
## Per-ROUND reset: the waves and the smoothed global prior are per round, but
|
||||
@@ -261,6 +297,9 @@ proc resetRound*(m: var LearnedSurferModule) =
|
||||
m.prevY = 0.0
|
||||
m.prevHeading = 0.0
|
||||
m.decisions = 0
|
||||
m.resolvedReal = 0
|
||||
m.resolvedDead = 0
|
||||
m.lastFlightErr = 0.0
|
||||
for i in 0..<LS_BINS: m.glob[i] = 0
|
||||
m.glc = 0
|
||||
m.hitGlobal = 0
|
||||
@@ -292,6 +331,7 @@ proc resetBattle*(m: var LearnedSurferModule) =
|
||||
|
||||
proc clearGraphics*(m: var LearnedSurferModule) {.inline.} = discard
|
||||
proc removeBulletNear*(m: var LearnedSurferModule, x, y: float) {.inline.} = discard
|
||||
proc liveWaves*(m: LearnedSurferModule): int {.inline.} = m.waves.len
|
||||
|
||||
# ── the learner ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -339,6 +379,79 @@ proc learnWave(m: var LearnedSurferModule, w: LSWave, bin: int, hit: bool) =
|
||||
m.missGlobal = m.missGlobal - (m.missGlobal shr LearnedDecayShift)
|
||||
m.glc = 0
|
||||
|
||||
proc resolveWaveIdx(m: var LearnedSurferModule, idx, bin: int, hit: bool,
|
||||
currentTick: int) =
|
||||
## One live wave -> one training sample + removal. Shared by the arrival
|
||||
## deadline (unobserved wall misses) and the real-event path, so a wave is
|
||||
## ALWAYS trained and dropped exactly once - no ghost accumulation.
|
||||
let w = m.waves[idx]
|
||||
let nom = w.startDist / max(w.speed, 1e-9)
|
||||
let realFlight = float(currentTick - w.fireTick)
|
||||
m.lastFlightErr = realFlight - nom
|
||||
if LearnedLog:
|
||||
echo "[learned] resolve bin=", bin, " state=", w.stateRow, "/",
|
||||
w.stateCol, " d=", w.startDist.int, " e=", w.originX.int, ",",
|
||||
w.originY.int, " flight=", realFlight.int, " nominal=", nom.int,
|
||||
" hit=", hit
|
||||
m.learnWave(w, bin, hit)
|
||||
m.waves.del(idx)
|
||||
|
||||
proc missileLineBin(w: LSWave, x, y, headingRad: float): int =
|
||||
## GF bin of the bullet's real straight line through `(x,y)` (the endpoint),
|
||||
## falling back to the real heading when the endpoint is degenerate. This is
|
||||
## the EXACT geometry: origin at the fire tick + real endpoint, no timing
|
||||
## guess.
|
||||
let maxA = mea(w.speed)
|
||||
if maxA < 1e-9: return gfToBin(0.0)
|
||||
let ex = x - w.originX
|
||||
let ey = y - w.originY
|
||||
let lineDir =
|
||||
if hypot(ex, ey) > 1.0: arctan2(ey, ex)
|
||||
else: headingRad
|
||||
gfToBin(clamp(wrapPi(lineDir - w.bearing) / maxA, -1.0, 1.0))
|
||||
|
||||
proc resolveEnemyBullet*(m: var LearnedSurferModule, x, y, headingRad: float,
|
||||
ownerId, currentTick: int, hit: bool): bool =
|
||||
## Resolve (and DROP) the live wave matching a REAL enemy-bullet event.
|
||||
##
|
||||
## `x,y` the bullet's real endpoint (our impact point for a HIT, the
|
||||
## wall point for a wall hit, the intercept point for a
|
||||
## bullet-vs-bullet hit),
|
||||
## `headingRad` the bullet's real heading (fallback when the endpoint is
|
||||
## degenerate),
|
||||
## `hit` true only for a HIT on us.
|
||||
## The exact straight line origin->endpoint sets the label's GF bin and the
|
||||
## real flight time `currentTick - fireTick` is recorded, which cross-checks
|
||||
## the energy-drop speed inference. No-op unless `TR_LEARNED_REAL_EVENTS=1`.
|
||||
result = false
|
||||
if not LearnedRealEvents or m.waves.len == 0: return
|
||||
# The wave whose nominal arrival is closest to now is the one this bullet
|
||||
# belongs to; ownerId disambiguates when several enemies are firing.
|
||||
var best = -1
|
||||
var bestKey = Inf
|
||||
for i in 0..<m.waves.len:
|
||||
let w = m.waves[i]
|
||||
if ownerId >= 0 and w.ownerId != ownerId: continue
|
||||
let key = abs(float(currentTick - w.fireTick) -
|
||||
w.startDist / max(w.speed, 1e-9))
|
||||
if key < bestKey:
|
||||
bestKey = key
|
||||
best = i
|
||||
if best < 0: # no wave from that enemy: fall back to time-only matching
|
||||
for i in 0..<m.waves.len:
|
||||
let w = m.waves[i]
|
||||
let key = abs(float(currentTick - w.fireTick) -
|
||||
w.startDist / max(w.speed, 1e-9))
|
||||
if key < bestKey:
|
||||
bestKey = key
|
||||
best = i
|
||||
if best < 0 or bestKey > RealEventsMatchTol: return
|
||||
let w = m.waves[best]
|
||||
let bin = missileLineBin(w, x, y, headingRad)
|
||||
m.resolveWaveIdx(best, bin, hit, currentTick)
|
||||
inc m.resolvedReal
|
||||
result = true
|
||||
|
||||
proc predictHit*(m: LearnedSurferModule, row, col, g: int): float =
|
||||
## P(hit | state, candidate bin g) — the `outcome` danger (lower = safer),
|
||||
## from the 2-class counted SBC read out with the per-cell posterior and
|
||||
@@ -416,6 +529,7 @@ proc detectFire(m: var LearnedSurferModule, id: int, ex, ey, eenergy: float,
|
||||
let col = code(room, RoomEdges) * LS_Q + code(turn, TurnEdges)
|
||||
|
||||
m.waves.add LSWave(
|
||||
ownerId: id, fireTick: ws.tick,
|
||||
originX: ex, originY: ey, bearing: bearing, speed: bspeed,
|
||||
startDist: d, power: drop,
|
||||
ticksLeft: max(1, int(ceil(d / max(bspeed, 1e-9)))),
|
||||
@@ -445,24 +559,27 @@ proc computeMove*(m: var LearnedSurferModule, ws: WorldState): MoveCommand =
|
||||
m.waves[i].fresh = false # created this tick: not one tick old yet
|
||||
else:
|
||||
dec m.waves[i].ticksLeft
|
||||
if m.waves[i].ticksLeft <= 0:
|
||||
# With real events on, wait `RealEventsGrace` ticks past the nominal arrival
|
||||
# so a late HitByBullet can still claim the wave; a wave no event claims is
|
||||
# a WALL MISS (the wall event is owner-private - see the const block).
|
||||
let deadline = if LearnedRealEvents: -RealEventsGrace else: 0
|
||||
if m.waves[i].ticksLeft <= deadline:
|
||||
let w = m.waves[i]
|
||||
let maxA = mea(w.speed)
|
||||
if maxA >= 1e-9:
|
||||
let off = wrapPi(arctan2(botY - w.originY, botX - w.originX) - w.bearing)
|
||||
let bin = gfToBin(clamp(off / maxA, -1.0, 1.0))
|
||||
# llOutcome label: did THIS wave hit us? Our own energy dropped since
|
||||
# the fire tick. (One wave is live at a time in 1v1; ramming also drops
|
||||
# energy, so this is a proxy, not an oracle.)
|
||||
let hit = ws.selfEnergy < w.selfEnergyAtFire - 0.01
|
||||
m.learnWave(w, bin, hit)
|
||||
if LearnedLog:
|
||||
echo "[learned] resolve bin=", bin, " state=", w.stateRow, "/",
|
||||
w.stateCol, " d=", w.startDist.int, " e=", w.originX.int, ",",
|
||||
w.originY.int
|
||||
m.waves.del(i)
|
||||
else:
|
||||
inc i
|
||||
# llOutcome label: did THIS wave hit us? The energy drop is the proxy;
|
||||
# with real events on the HIT is taken from `onHitByBullet` instead, so
|
||||
# an unmatched wave is a wall MISS.
|
||||
let hit = (not LearnedRealEvents) and
|
||||
ws.selfEnergy < w.selfEnergyAtFire - 0.01
|
||||
m.resolveWaveIdx(i, bin, hit, ws.tick)
|
||||
inc m.resolvedDead
|
||||
else:
|
||||
m.waves.del(i)
|
||||
continue
|
||||
inc i
|
||||
|
||||
# ── 3. danger of every candidate bin, summed over every live wave ────────
|
||||
# llHistogram: precompute the predicted arrival-bin distribution per wave.
|
||||
|
||||
Reference in New Issue
Block a user