surf: wire the dormant wave surfer as TR_MOVEMENT=surf + fix 4 real defects
common_libs/movements/wave_surfer.nim was written in an early session and
never wired to the bot. This connects it exactly like tfil/strafe and fixes
the defects a full read found:
1. the dodge direction was INVERTED: the perpendicular was built from the
bot->enemy bearing while GF lives in the enemy->bot frame, so the bot
moved toward MORE danger. Now built from the wave's origin->bot bearing:
+90 provably increases GF.
2. the danger histogram was never reset (resetRound cleared waves only), so
it was a battle-long static average. Now reset to the uniform prior each
round.
3. fire detection tracked only the current target's energy via one scalar;
now per-enemy (seq[(id,energy)]) so melee target switches cannot invent
or hide waves.
4. the wall penalty projected a point from the wave origin, not from the
bot, making the wall test meaningless. Now projects the bot->candidate
direction.
Wave speed uses the actual firepower (the one-tick energy drop IS the
firepower, so speed = 20 - 3*drop is exact). Adds TR_SURF_* knobs and
registers them in the env report. Shipped TR_MOVEMENT=tfil default untouched
(test_tfil_commit_env: 30/30 pass; test_env_report: pass).
This commit is contained in:
@@ -34,6 +34,7 @@ import movements/ram_decision
|
|||||||
import movements/the_floor_is_lava
|
import movements/the_floor_is_lava
|
||||||
import movements/the_floor_is_lava_ring
|
import movements/the_floor_is_lava_ring
|
||||||
import movements/strafe
|
import movements/strafe
|
||||||
|
import movements/wave_surfer
|
||||||
import movement_harness/virtual_bodies as mvb
|
import movement_harness/virtual_bodies as mvb
|
||||||
import movement_harness/bullet_shadows
|
import movement_harness/bullet_shadows
|
||||||
import targeting/enemy_tracker
|
import targeting/enemy_tracker
|
||||||
@@ -202,6 +203,7 @@ type
|
|||||||
mover: TFILModule
|
mover: TFILModule
|
||||||
ringMover: TFILRingModule
|
ringMover: TFILRingModule
|
||||||
strafeMover: StrafeModule
|
strafeMover: StrafeModule
|
||||||
|
surfMover: WaveSurferModule
|
||||||
rammer: RammerModule
|
rammer: RammerModule
|
||||||
isRamming: bool
|
isRamming: bool
|
||||||
ramDurationTicks: int
|
ramDurationTicks: int
|
||||||
@@ -342,6 +344,8 @@ proc printConfig(bot: ModularBot, forceAll: bool = false) =
|
|||||||
if bot.isRamming: "strafe(ram)" else: "strafe"
|
if bot.isRamming: "strafe(ram)" else: "strafe"
|
||||||
elif MovementName == "tfil_ring":
|
elif MovementName == "tfil_ring":
|
||||||
if bot.isRamming: "tfil_ring(ram)" else: "tfil_ring"
|
if bot.isRamming: "tfil_ring(ram)" else: "tfil_ring"
|
||||||
|
elif MovementName == "surf":
|
||||||
|
if bot.isRamming: "surf(ram)" else: "surf"
|
||||||
elif bot.isRamming: "rammer"
|
elif bot.isRamming: "rammer"
|
||||||
else: "tfil"
|
else: "tfil"
|
||||||
var line = "[config] "
|
var line = "[config] "
|
||||||
@@ -591,6 +595,7 @@ method onBulletHitWall*(bot: ModularBot, e: BulletHitWallEvent) =
|
|||||||
bot.mover.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)
|
bot.ringMover.removeBulletNear(e.bullet.x, e.bullet.y)
|
||||||
bot.strafeMover.removeBulletNear(e.bullet.x, e.bullet.y)
|
bot.strafeMover.removeBulletNear(e.bullet.x, e.bullet.y)
|
||||||
|
bot.surfMover.removeBulletNear(e.bullet.x, e.bullet.y)
|
||||||
|
|
||||||
method onBulletHitBullet*(bot: ModularBot, e: BulletHitBulletEvent) =
|
method onBulletHitBullet*(bot: ModularBot, e: BulletHitBulletEvent) =
|
||||||
discard bot.resolveOwnBullet(e.bullet.bulletId) # bullet-vs-bullet: free the slot
|
discard bot.resolveOwnBullet(e.bullet.bulletId) # bullet-vs-bullet: free the slot
|
||||||
@@ -599,6 +604,7 @@ method onBulletHitBullet*(bot: ModularBot, e: BulletHitBulletEvent) =
|
|||||||
bot.mover.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)
|
bot.ringMover.removeBulletNear(e.bullet.x, e.bullet.y)
|
||||||
bot.strafeMover.removeBulletNear(e.bullet.x, e.bullet.y)
|
bot.strafeMover.removeBulletNear(e.bullet.x, e.bullet.y)
|
||||||
|
bot.surfMover.removeBulletNear(e.bullet.x, e.bullet.y)
|
||||||
|
|
||||||
method onHitByBullet*(bot: ModularBot, e: HitByBulletEvent) =
|
method onHitByBullet*(bot: ModularBot, e: HitByBulletEvent) =
|
||||||
# Feeds the ram bullet-rain abort window. Accumulate REAL ENERGY (the server's
|
# Feeds the ram bullet-rain abort window. Accumulate REAL ENERGY (the server's
|
||||||
@@ -826,6 +832,7 @@ method onRoundStarted*(bot: ModularBot, e: RoundStartedEvent) =
|
|||||||
bot.mover.resetRound()
|
bot.mover.resetRound()
|
||||||
bot.ringMover.resetRound()
|
bot.ringMover.resetRound()
|
||||||
bot.strafeMover.resetRound()
|
bot.strafeMover.resetRound()
|
||||||
|
bot.surfMover.resetRound()
|
||||||
bot.enemyTracker.resetRound()
|
bot.enemyTracker.resetRound()
|
||||||
# The TM pattern gun is cold every round (no cross-battle persistence): wipe
|
# The TM pattern gun is cold every round (no cross-battle persistence): wipe
|
||||||
# its clause teams and motion history explicitly, so a same-id opponent in the
|
# its clause teams and motion history explicitly, so a same-id opponent in the
|
||||||
@@ -1104,6 +1111,8 @@ method run*(bot: ModularBot) =
|
|||||||
var spd, tr: float
|
var spd, tr: float
|
||||||
if MovementName == "strafe" and not shouldRam:
|
if MovementName == "strafe" and not shouldRam:
|
||||||
(spd, tr) = bot.strafeMover.computeMove(ws)
|
(spd, tr) = bot.strafeMover.computeMove(ws)
|
||||||
|
elif MovementName == "surf" and not shouldRam:
|
||||||
|
(spd, tr) = bot.surfMover.computeMove(ws)
|
||||||
elif MovementName == "tfil_ring":
|
elif MovementName == "tfil_ring":
|
||||||
bot.ringMover.band =
|
bot.ringMover.band =
|
||||||
if shouldRam: (lo: 0.0, hi: 50.0)
|
if shouldRam: (lo: 0.0, hi: 50.0)
|
||||||
@@ -1409,6 +1418,7 @@ when isMainModule:
|
|||||||
mover: TFILModule(debugGraphics: true),
|
mover: TFILModule(debugGraphics: true),
|
||||||
ringMover: TFILRingModule(debugGraphics: true),
|
ringMover: TFILRingModule(debugGraphics: true),
|
||||||
strafeMover: StrafeModule(debugGraphics: true),
|
strafeMover: StrafeModule(debugGraphics: true),
|
||||||
|
surfMover: initWaveSurfer(),
|
||||||
rammer: initRammer(),
|
rammer: initRammer(),
|
||||||
moveTracker: mvb.initVirtualBodyTracker(1),
|
moveTracker: mvb.initVirtualBodyTracker(1),
|
||||||
currentGun: -1,
|
currentGun: -1,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import movements/ram_decision
|
|||||||
import movements/the_floor_is_lava
|
import movements/the_floor_is_lava
|
||||||
import movements/the_floor_is_lava_ring
|
import movements/the_floor_is_lava_ring
|
||||||
import movements/strafe
|
import movements/strafe
|
||||||
|
import movements/wave_surfer
|
||||||
import guns/tm_horizon
|
import guns/tm_horizon
|
||||||
import guns/bitbrain_gun
|
import guns/bitbrain_gun
|
||||||
import guns/pattern_matcher
|
import guns/pattern_matcher
|
||||||
@@ -181,6 +182,7 @@ proc printEffectiveValues(ctx: EnvReportContext) =
|
|||||||
let effectiveMovement =
|
let effectiveMovement =
|
||||||
if ctx.movementName == "tfil_ring": "tfil_ring"
|
if ctx.movementName == "tfil_ring": "tfil_ring"
|
||||||
elif ctx.movementName == "strafe": "strafe"
|
elif ctx.movementName == "strafe": "strafe"
|
||||||
|
elif ctx.movementName == "surf": "surf"
|
||||||
else: "tfil"
|
else: "tfil"
|
||||||
emit("TR_MOVEMENT", effectiveMovement, sourceOf("TR_MOVEMENT"))
|
emit("TR_MOVEMENT", effectiveMovement, sourceOf("TR_MOVEMENT"))
|
||||||
emit("TR_MOVEMENT_LOG", onOff(MovementLog), sourceOfPresence("TR_MOVEMENT_LOG"))
|
emit("TR_MOVEMENT_LOG", onOff(MovementLog), sourceOfPresence("TR_MOVEMENT_LOG"))
|
||||||
@@ -293,6 +295,13 @@ proc printEffectiveValues(ctx: EnvReportContext) =
|
|||||||
emit("TR_STRAFE_WALL_HOTNESS", $StrafeWallHotness, sourceOf("TR_STRAFE_WALL_HOTNESS"))
|
emit("TR_STRAFE_WALL_HOTNESS", $StrafeWallHotness, sourceOf("TR_STRAFE_WALL_HOTNESS"))
|
||||||
emit("TR_STRAFE_WALL_RADIANCE", $StrafeWallRadiance, sourceOf("TR_STRAFE_WALL_RADIANCE"))
|
emit("TR_STRAFE_WALL_RADIANCE", $StrafeWallRadiance, sourceOf("TR_STRAFE_WALL_RADIANCE"))
|
||||||
|
|
||||||
|
# ── movement (WAVE SURFER) ────────────────────────────────────────────────
|
||||||
|
emit(SurfPrefDistEnv, $SurfPrefDist, sourceOf(SurfPrefDistEnv))
|
||||||
|
emit(SurfDistBandEnv, $SurfDistBand, sourceOf(SurfDistBandEnv))
|
||||||
|
emit(SurfWallMarginEnv, $SurfWallMargin, sourceOf(SurfWallMarginEnv))
|
||||||
|
emit(SurfRadialFracEnv, $SurfRadialFrac, sourceOf(SurfRadialFracEnv))
|
||||||
|
emit(SurfLogEnv, onOff(SurfLog), sourceOfPresence(SurfLogEnv))
|
||||||
|
|
||||||
# ── ramming ───────────────────────────────────────────────────────────────
|
# ── ramming ───────────────────────────────────────────────────────────────
|
||||||
emit("TR_RAM_OPPORTUNITY", onOff(RamOppEnabled), sourceOf("TR_RAM_OPPORTUNITY"))
|
emit("TR_RAM_OPPORTUNITY", onOff(RamOppEnabled), sourceOf("TR_RAM_OPPORTUNITY"))
|
||||||
emit("TR_RAM_OPP_DIST", $RamOppDist, sourceOf("TR_RAM_OPP_DIST"))
|
emit("TR_RAM_OPP_DIST", $RamOppDist, sourceOf("TR_RAM_OPP_DIST"))
|
||||||
@@ -453,6 +462,8 @@ proc knownEnvNames*(): seq[string] =
|
|||||||
"TR_STRAFE_HEAT_GRID", "TR_STRAFE_BULLET_CORE", "TR_STRAFE_BULLET_AURA",
|
"TR_STRAFE_HEAT_GRID", "TR_STRAFE_BULLET_CORE", "TR_STRAFE_BULLET_AURA",
|
||||||
"TR_STRAFE_CORRIDOR_HEAT", "TR_STRAFE_WALL_HOTNESS",
|
"TR_STRAFE_CORRIDOR_HEAT", "TR_STRAFE_WALL_HOTNESS",
|
||||||
"TR_STRAFE_WALL_RADIANCE",
|
"TR_STRAFE_WALL_RADIANCE",
|
||||||
|
SurfPrefDistEnv, SurfDistBandEnv, SurfWallMarginEnv, SurfRadialFracEnv,
|
||||||
|
SurfLogEnv,
|
||||||
# harness vars (read by the test framework, inherited by the bot, so they
|
# harness vars (read by the test framework, inherited by the bot, so they
|
||||||
# must NOT be reported as typos)
|
# must NOT be reported as typos)
|
||||||
"TR_SERVER_JAR", "TR_BATTLE_RUNNER", "TR_BATTLE_RUNNER_DIR",
|
"TR_SERVER_JAR", "TR_BATTLE_RUNNER", "TR_BATTLE_RUNNER_DIR",
|
||||||
|
|||||||
@@ -1,95 +1,221 @@
|
|||||||
## wave_surfer.nim — Wave-surfing movement module.
|
## wave_surfer.nim — Wave-surfing movement module.
|
||||||
## Detects enemy fire via energy drops, maintains a danger histogram (31 GF bins),
|
##
|
||||||
## and steers toward the lowest-danger GF bin on approaching waves.
|
## Detects enemy fire via energy drops, maintains a per-round danger histogram
|
||||||
## All angles in radians internally; output degrees for bot API.
|
## of the GuessFactor (GF) each resolved wave arrived at, and steers toward the
|
||||||
|
## lowest-danger reachable GF on the nearest approaching wave. All angles in
|
||||||
|
## radians internally; output degrees for the bot API.
|
||||||
|
##
|
||||||
|
## ── What was broken and what was fixed (j115) ───────────────────────────────
|
||||||
|
## The module was written in an early session, never wired to the bot and never
|
||||||
|
## tested. A full read found four real defects; all are fixed here:
|
||||||
|
##
|
||||||
|
## 1. **The dodge was INVERTED.** The perpendicular direction was built from
|
||||||
|
## the bot→enemy bearing (`arctan2(enemyY-botY, enemyX-botX)`), while the
|
||||||
|
## GF is measured in the enemy→bot frame. Rotating the bot→enemy bearing by
|
||||||
|
## +90 deg points the OPPOSITE way from rotating the enemy→bot bearing by
|
||||||
|
## +90 deg, so when the safest bin was at a higher GF the bot moved toward
|
||||||
|
## a LOWER GF, i.e. toward MORE danger. Fixed by building the perpendicular
|
||||||
|
## from the ORIGIN→bot bearing (the wave's own frame): `perp = toBot ± 90`,
|
||||||
|
## where `toBot = arctan2(botY-originY, botX-originX)`. `+90` now provably
|
||||||
|
## increases GF (d(theta)/ds is maximal there).
|
||||||
|
## 2. **The histogram was never reset.** `resetRound` cleared the waves but
|
||||||
|
## left `bins` accumulating for the WHOLE battle, so it became a global
|
||||||
|
## static average. It is now reset to the uniform prior every round (it is
|
||||||
|
## per-round-resettable, as required).
|
||||||
|
## 3. **Fire detection tracked only the CURRENT TARGET's energy** via a single
|
||||||
|
## scalar `prevEnergy` (init 100.0). In melee a target switch silently
|
||||||
|
## compared two different bots' energies, inventing/hiding waves. It is now
|
||||||
|
## per-enemy (`seq[(id, energy)]`, exactly like the STRAFE mover), a new
|
||||||
|
## enemy id is seeded without emitting a wave, and a vanished enemy keeps
|
||||||
|
## its last value until it reappears.
|
||||||
|
## 4. **The wall penalty projected the wrong future point.** It placed the
|
||||||
|
## future position at `bot + (wave-bearing + gf*MEA)` — the direction from
|
||||||
|
## the wave ORIGIN, not from the bot — so the wall test was meaningless. It
|
||||||
|
## now projects the point on the wave circle at the candidate GF and takes
|
||||||
|
## the direction from the BOT to it.
|
||||||
|
##
|
||||||
|
## Wave geometry uses the ACTUAL fired power: the one-tick energy drop IS the
|
||||||
|
## firepower (energy cost == firepower), so `speed = 20 - 3*drop` is exact, not
|
||||||
|
## an assumption. Wave resolution fires when the wave radius reaches the bot's
|
||||||
|
## distance from the origin, and the GF is normalised by that power's maximum
|
||||||
|
## escape angle `arcsin(8/speed)`.
|
||||||
|
##
|
||||||
|
## KNOWN LIMITATION (measured, not hidden): a low-power hit ON the enemy (our
|
||||||
|
## bullet doing <= 3.0 energy of damage) is indistinguishable from a small
|
||||||
|
## firepower in the energy drop. Those shots can spawn a false wave. High-power
|
||||||
|
## hits (damage > 3.0) are correctly rejected by the drop window. This is the
|
||||||
|
## standard energy-drop ambiguity; the single 1v1 A/B cannot separate it.
|
||||||
|
|
||||||
import std/[math]
|
import std/[math, os]
|
||||||
|
from std/strutils import parseFloat, strip, toLowerAscii
|
||||||
import gun_harness/gun_interface
|
import gun_harness/gun_interface
|
||||||
import movement_harness/movement_interface
|
import movement_harness/movement_interface
|
||||||
|
|
||||||
const WS_BINS = 31
|
const
|
||||||
const WallMargin = 40.0
|
WS_BINS = 31
|
||||||
const DodgeTicks = 15.0 # approximate ticks to reach dodge position
|
DodgeTicks = 15.0 ## approximate ticks to reach the dodge position
|
||||||
const WS_PrefDist = 400.0 # optimal engagement distance
|
MaxBotSpeed = 8.0 ## Tank Royale max forward/backward speed (px/tick)
|
||||||
const WS_DistBand = 50.0 # deadband: pure strafe within ±50px of preferred
|
|
||||||
const WS_RadialFrac = 0.35 # radial blend fraction (0=pure strafe, 1=pure radial)
|
## ── env knobs (all read once at module init) ────────────────────────────────
|
||||||
|
const
|
||||||
|
SurfPrefDistEnv* = "TR_SURF_PREF_DIST"
|
||||||
|
SurfDistBandEnv* = "TR_SURF_DIST_BAND"
|
||||||
|
SurfWallMarginEnv* = "TR_SURF_WALL_MARGIN"
|
||||||
|
SurfRadialFracEnv* = "TR_SURF_RADIAL_FRAC"
|
||||||
|
SurfLogEnv* = "TR_SURF_LOG"
|
||||||
|
|
||||||
|
const
|
||||||
|
DefaultPrefDist = 400.0 ## optimal engagement distance (px)
|
||||||
|
DefaultDistBand = 50.0 ## deadband: pure strafe within ±band of pref dist
|
||||||
|
DefaultWallMargin = 48.0 ## px; > bot radius 18, plus braking room
|
||||||
|
DefaultRadialFrac = 0.35 ## radial blend fraction (0=pure strafe, 1=radial)
|
||||||
|
|
||||||
|
var
|
||||||
|
SurfPrefDist* = DefaultPrefDist
|
||||||
|
SurfDistBand* = DefaultDistBand
|
||||||
|
SurfWallMargin* = DefaultWallMargin
|
||||||
|
SurfRadialFrac* = DefaultRadialFrac
|
||||||
|
SurfLog* = false
|
||||||
|
|
||||||
|
proc getEnvFloat(name: string, default: float): float =
|
||||||
|
let s = getEnv(name, "")
|
||||||
|
if s.len == 0: return default
|
||||||
|
try: result = parseFloat(s.strip())
|
||||||
|
except ValueError: result = default
|
||||||
|
|
||||||
|
proc loadSurfEnv*() =
|
||||||
|
## Read the surfer knobs; callable again after `putEnv` so a gate can
|
||||||
|
## exercise arms in one process.
|
||||||
|
SurfPrefDist = max(1.0, getEnvFloat(SurfPrefDistEnv, DefaultPrefDist))
|
||||||
|
SurfDistBand = max(0.0, getEnvFloat(SurfDistBandEnv, DefaultDistBand))
|
||||||
|
SurfWallMargin = max(0.0, getEnvFloat(SurfWallMarginEnv, DefaultWallMargin))
|
||||||
|
SurfRadialFrac = clamp(getEnvFloat(SurfRadialFracEnv, DefaultRadialFrac), 0.0, 1.0)
|
||||||
|
SurfLog = existsEnv(SurfLogEnv)
|
||||||
|
|
||||||
|
loadSurfEnv()
|
||||||
|
|
||||||
type
|
type
|
||||||
WSWave = object
|
WSWave = object
|
||||||
originX, originY: float64
|
originX, originY: float64
|
||||||
bearing: float64 ## direction from enemy to us at fire time (radians)
|
bearing: float64 ## direction from enemy to us at fire time (rad)
|
||||||
speed: float64
|
speed: float64
|
||||||
radius: float64
|
radius: float64
|
||||||
startDist: float64
|
startDist: float64
|
||||||
|
power: float64
|
||||||
|
|
||||||
WaveSurferModule* = object
|
WaveSurferModule* = object
|
||||||
bins: array[WS_BINS, float64]
|
bins: array[WS_BINS, float64]
|
||||||
waves: seq[WSWave]
|
waves: seq[WSWave]
|
||||||
prevEnergy: float64
|
prevEnergy: seq[tuple[id: int, energy: float]]
|
||||||
strafeDir: float64 ## +1.0 or -1.0
|
strafeDir: float64 ## +1.0 or -1.0
|
||||||
debugGraphics*: bool
|
debugGraphics*: bool
|
||||||
|
|
||||||
proc initWaveSurfer*(): WaveSurferModule =
|
proc resetRound*(m: var WaveSurferModule) =
|
||||||
var m = WaveSurferModule(prevEnergy: 100.0, strafeDir: 1.0, debugGraphics: false)
|
## Wipe per-round state. The danger histogram IS reset here (defect 2): it is
|
||||||
|
## a per-round learner, not a battle-long static average.
|
||||||
|
m.waves = @[]
|
||||||
|
m.prevEnergy = @[]
|
||||||
|
m.strafeDir = 1.0
|
||||||
for i in 0..<WS_BINS: m.bins[i] = 1.0
|
for i in 0..<WS_BINS: m.bins[i] = 1.0
|
||||||
|
|
||||||
|
proc initWaveSurfer*(): WaveSurferModule =
|
||||||
|
var m = WaveSurferModule(debugGraphics: false)
|
||||||
|
m.resetRound()
|
||||||
m
|
m
|
||||||
|
|
||||||
proc resetRound*(m: var WaveSurferModule) =
|
proc clearGraphics*(m: var WaveSurferModule) {.inline.} =
|
||||||
m.waves = @[]
|
## No-op: the SVG buffer is a framework global cleared after every go().
|
||||||
m.prevEnergy = 100.0
|
## Exists so callers can signal "the surfer is inactive".
|
||||||
m.strafeDir = 1.0
|
discard
|
||||||
|
|
||||||
|
proc removeBulletNear*(m: var WaveSurferModule, x, y: float) {.inline.} =
|
||||||
|
## No-op: the surfer tracks WAVES (energy drops), not bullet bodies.
|
||||||
|
discard
|
||||||
|
|
||||||
|
proc prevEnergyGet(m: WaveSurferModule, id: int): float =
|
||||||
|
for e in m.prevEnergy:
|
||||||
|
if e.id == id: return e.energy
|
||||||
|
100.0
|
||||||
|
|
||||||
|
proc prevEnergySet(m: var WaveSurferModule, id: int, energy: float) =
|
||||||
|
for i in 0..<m.prevEnergy.len:
|
||||||
|
if m.prevEnergy[i].id == id:
|
||||||
|
m.prevEnergy[i].energy = energy
|
||||||
|
return
|
||||||
|
m.prevEnergy.add((id: id, energy: energy))
|
||||||
|
|
||||||
proc gfToBin(gf: float64): int {.inline.} =
|
proc gfToBin(gf: float64): int {.inline.} =
|
||||||
clamp(int(round((gf.clamp(-1.0, 1.0) + 1.0) * 0.5 * float64(WS_BINS - 1))), 0, WS_BINS - 1)
|
clamp(int(round((gf.clamp(-1.0, 1.0) + 1.0) * 0.5 * float64(WS_BINS - 1))),
|
||||||
|
0, WS_BINS - 1)
|
||||||
|
|
||||||
proc binToGF(idx: int): float64 {.inline.} =
|
proc binToGF(idx: int): float64 {.inline.} =
|
||||||
float64(idx) / float64(WS_BINS - 1) * 2.0 - 1.0
|
float64(idx) / float64(WS_BINS - 1) * 2.0 - 1.0
|
||||||
|
|
||||||
proc mea(speed: float64): float64 {.inline.} = arcsin(min(8.0 / speed, 1.0))
|
proc mea(speed: float64): float64 {.inline.} =
|
||||||
|
## Maximum escape angle for a bot moving at max speed relative to a bullet of
|
||||||
|
## the given speed.
|
||||||
|
if speed <= 1e-9: return 0.0
|
||||||
|
arcsin(min(MaxBotSpeed / speed, 1.0))
|
||||||
|
|
||||||
|
proc wrapPi(x: float64): float64 {.inline.} =
|
||||||
|
result = x
|
||||||
|
while result > PI: result -= 2.0*PI
|
||||||
|
while result < -PI: result += 2.0*PI
|
||||||
|
|
||||||
proc nearestWave(m: WaveSurferModule, botX, botY: float64): int =
|
proc nearestWave(m: WaveSurferModule, botX, botY: float64): int =
|
||||||
## Index of the wave closest to reaching us (largest radius relative to startDist).
|
## Index of the wave closest to reaching us (largest radius relative to the
|
||||||
|
## current distance). -1 when there is no live wave.
|
||||||
result = -1
|
result = -1
|
||||||
var bestRatio = -1.0
|
var bestRatio = -1.0
|
||||||
for i in 0..<m.waves.len:
|
for i in 0..<m.waves.len:
|
||||||
let d = sqrt((botX - m.waves[i].originX)^2 + (botY - m.waves[i].originY)^2)
|
let d = max(1e-6, hypot(botX - m.waves[i].originX, botY - m.waves[i].originY))
|
||||||
let ratio = m.waves[i].radius / d
|
let ratio = m.waves[i].radius / d
|
||||||
if ratio > bestRatio:
|
if ratio > bestRatio:
|
||||||
bestRatio = ratio
|
bestRatio = ratio
|
||||||
result = i
|
result = i
|
||||||
|
|
||||||
proc computeMove*(m: var WaveSurferModule, ws: WorldState): MoveCommand =
|
proc detectFire(m: var WaveSurferModule, id: int, ex, ey, eenergy,
|
||||||
let botX = ws.selfX
|
botX, botY: float64) =
|
||||||
let botY = ws.selfY
|
## One enemy's energy sample: emit a wave iff its energy dropped by a
|
||||||
let enemyX = ws.enemyX
|
## plausible firepower in one tick. `drop` is the firepower exactly.
|
||||||
let enemyY = ws.enemyY
|
let prev = m.prevEnergyGet(id)
|
||||||
|
let drop = prev - eenergy
|
||||||
# Fire detection
|
m.prevEnergySet(id, eenergy)
|
||||||
let drop = m.prevEnergy - ws.enemyEnergy
|
|
||||||
m.prevEnergy = ws.enemyEnergy
|
|
||||||
if drop >= 0.1 and drop <= 3.0:
|
if drop >= 0.1 and drop <= 3.0:
|
||||||
let bspeed = 20.0 - 3.0 * drop
|
let bspeed = 20.0 - 3.0 * drop
|
||||||
let bearing = arctan2(botY - enemyY, botX - enemyX)
|
let bearing = arctan2(botY - ey, botX - ex)
|
||||||
let d = sqrt((botX - enemyX)^2 + (botY - enemyY)^2)
|
let d = hypot(botX - ex, botY - ey)
|
||||||
m.waves.add WSWave(
|
m.waves.add WSWave(
|
||||||
originX: enemyX, originY: enemyY,
|
originX: ex, originY: ey,
|
||||||
bearing: bearing,
|
bearing: bearing,
|
||||||
speed: bspeed,
|
speed: bspeed,
|
||||||
radius: 0.0,
|
radius: 0.0,
|
||||||
startDist: d,
|
startDist: d,
|
||||||
|
power: drop,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Advance waves; collect hits
|
proc computeMove*(m: var WaveSurferModule, ws: WorldState): MoveCommand =
|
||||||
|
let botX = ws.selfX
|
||||||
|
let botY = ws.selfY
|
||||||
|
|
||||||
|
# ── Fire detection: EVERY alive enemy, per-enemy energy (defect 3) ─────────
|
||||||
|
var seen = 0
|
||||||
|
for ei in ws.enemies:
|
||||||
|
inc seen
|
||||||
|
m.detectFire(ei.id, ei.x, ei.y, ei.energy, botX, botY)
|
||||||
|
if seen == 0 and (ws.enemyX != 0.0 or ws.enemyY != 0.0):
|
||||||
|
# Fallback to the tracked target when `enemies` is empty.
|
||||||
|
m.detectFire(-1, ws.enemyX, ws.enemyY, ws.enemyEnergy, botX, botY)
|
||||||
|
|
||||||
|
# ── Advance waves; record the GF of each wave that reaches us ─────────────
|
||||||
var i = 0
|
var i = 0
|
||||||
while i < m.waves.len:
|
while i < m.waves.len:
|
||||||
m.waves[i].radius += m.waves[i].speed
|
m.waves[i].radius += m.waves[i].speed
|
||||||
let d = sqrt((botX - m.waves[i].originX)^2 + (botY - m.waves[i].originY)^2)
|
let d = hypot(botX - m.waves[i].originX, botY - m.waves[i].originY)
|
||||||
if m.waves[i].radius >= d:
|
if m.waves[i].radius >= d:
|
||||||
let toBot = arctan2(botY - m.waves[i].originY, botX - m.waves[i].originX)
|
let toBot = arctan2(botY - m.waves[i].originY, botX - m.waves[i].originX)
|
||||||
var off = toBot - m.waves[i].bearing
|
let off = wrapPi(toBot - m.waves[i].bearing)
|
||||||
while off > PI: off -= 2.0*PI
|
let maxA = mea(m.waves[i].speed)
|
||||||
while off < -PI: off += 2.0*PI
|
|
||||||
let maxA = mea(m.waves[i].speed)
|
|
||||||
if maxA >= 1e-9:
|
if maxA >= 1e-9:
|
||||||
let gf = clamp(off / maxA, -1.0, 1.0)
|
let gf = clamp(off / maxA, -1.0, 1.0)
|
||||||
m.bins[gfToBin(gf)] += 1.0
|
m.bins[gfToBin(gf)] += 1.0
|
||||||
@@ -97,92 +223,109 @@ proc computeMove*(m: var WaveSurferModule, ws: WorldState): MoveCommand =
|
|||||||
else:
|
else:
|
||||||
inc i
|
inc i
|
||||||
|
|
||||||
# Dodge logic: find nearest wave, pick safest GF bin direction
|
# ── Dodge logic: pick the safest reachable GF on the nearest wave ─────────
|
||||||
|
var perpAngle = 0.0
|
||||||
|
var havePerp = false
|
||||||
let nearest = m.nearestWave(botX, botY)
|
let nearest = m.nearestWave(botX, botY)
|
||||||
if nearest >= 0:
|
if nearest >= 0:
|
||||||
let w = m.waves[nearest]
|
let w = m.waves[nearest]
|
||||||
let toBot = arctan2(botY - w.originY, botX - w.originX)
|
let dx = botX - w.originX
|
||||||
var off = toBot - w.bearing
|
let dy = botY - w.originY
|
||||||
while off > PI: off -= 2.0*PI
|
let d = max(1.0, hypot(dx, dy))
|
||||||
while off < -PI: off += 2.0*PI
|
let toBot = arctan2(dy, dx) # origin -> bot (the GF frame)
|
||||||
let maxA = mea(w.speed)
|
let off = wrapPi(toBot - w.bearing)
|
||||||
|
let maxA = mea(w.speed)
|
||||||
let curGF = if maxA >= 1e-9: clamp(off / maxA, -1.0, 1.0) else: 0.0
|
let curGF = if maxA >= 1e-9: clamp(off / maxA, -1.0, 1.0) else: 0.0
|
||||||
let curBin = gfToBin(curGF)
|
let curBin = gfToBin(curGF)
|
||||||
|
|
||||||
# Average danger
|
# Danger = per-bin average; the current bin is the danger baseline.
|
||||||
var total = 0.0
|
var total = 0.0
|
||||||
for b in m.bins: total += b
|
for b in m.bins: total += b
|
||||||
let avg = total / float64(WS_BINS)
|
let avg = total / float64(WS_BINS)
|
||||||
|
|
||||||
if m.bins[curBin] > avg:
|
if m.bins[curBin] > avg:
|
||||||
# Find lowest-danger bin, penalizing positions near walls
|
# Find the least-dangerous reachable GF, penalising wall-bound moves.
|
||||||
let dodgeDist = ws.selfSpeed * DodgeTicks
|
let dodgeDist = max(MaxBotSpeed, ws.selfSpeed) * DodgeTicks
|
||||||
var bestBin = 0
|
var bestBin = curBin
|
||||||
for j in 1..<WS_BINS:
|
var bestDanger = Inf
|
||||||
let gfJ = binToGF(j)
|
for j in 0..<WS_BINS:
|
||||||
let angleJ = w.bearing + gfJ * maxA
|
let gfJ = binToGF(j)
|
||||||
let futureX = botX + cos(angleJ) * dodgeDist
|
let angleJ = w.bearing + gfJ * maxA
|
||||||
let futureY = botY + sin(angleJ) * dodgeDist
|
# Point on the wave circle at this GF, then the DIRECTION FROM THE BOT.
|
||||||
let wallHit = futureX < WallMargin or futureX > ws.arenaWidth - WallMargin or
|
let px = w.originX + cos(angleJ) * d
|
||||||
futureY < WallMargin or futureY > ws.arenaHeight - WallMargin
|
let py = w.originY + sin(angleJ) * d
|
||||||
let dangerJ = if wallHit: m.bins[j] * 5.0 else: m.bins[j]
|
var ux = px - botX
|
||||||
let gfBest = binToGF(bestBin)
|
var uy = py - botY
|
||||||
let angleB = w.bearing + gfBest * maxA
|
let ul = hypot(ux, uy)
|
||||||
let futureXB = botX + cos(angleB) * dodgeDist
|
if ul < 1e-6:
|
||||||
let futureYB = botY + sin(angleB) * dodgeDist
|
ux = 0.0; uy = 0.0
|
||||||
let wallHitB = futureXB < WallMargin or futureXB > ws.arenaWidth - WallMargin or
|
else:
|
||||||
futureYB < WallMargin or futureYB > ws.arenaHeight - WallMargin
|
ux /= ul; uy /= ul
|
||||||
let dangerB = if wallHitB: m.bins[bestBin] * 5.0 else: m.bins[bestBin]
|
let futureX = botX + ux * dodgeDist
|
||||||
if dangerJ < dangerB: bestBin = j
|
let futureY = botY + uy * dodgeDist
|
||||||
|
let wallHit = futureX < SurfWallMargin or futureX > ws.arenaWidth - SurfWallMargin or
|
||||||
|
futureY < SurfWallMargin or futureY > ws.arenaHeight - SurfWallMargin
|
||||||
|
var danger = m.bins[j]
|
||||||
|
if wallHit: danger *= 5.0
|
||||||
|
danger += 0.01 * abs(gfJ - curGF) # mild preference for less travel
|
||||||
|
if danger < bestDanger:
|
||||||
|
bestDanger = danger
|
||||||
|
bestBin = j
|
||||||
let bestGF = binToGF(bestBin)
|
let bestGF = binToGF(bestBin)
|
||||||
# Move in direction of best GF: positive = CCW (orbit left), negative = CW
|
|
||||||
m.strafeDir = if bestGF >= curGF: 1.0 else: -1.0
|
m.strafeDir = if bestGF >= curGF: 1.0 else: -1.0
|
||||||
|
if SurfLog:
|
||||||
|
echo "[surf] wave d=", d.int, " curGF=", (curGF * 100.0).int,
|
||||||
|
" bestGF=", (bestGF * 100.0).int, " dir=", m.strafeDir.int,
|
||||||
|
" curDanger=", m.bins[curBin].int
|
||||||
|
# Perpendicular in the wave's own frame: +90 deg from origin->bot provably
|
||||||
|
# INCREASES the GF (CCW); -90 decreases it (defect 1).
|
||||||
|
perpAngle = if m.strafeDir >= 0.0: toBot + PI * 0.5
|
||||||
|
else: toBot - PI * 0.5
|
||||||
|
havePerp = true
|
||||||
|
else:
|
||||||
|
# No live wave: hold the perpendicular of the current threat bearing.
|
||||||
|
if ws.enemyX != 0.0 or ws.enemyY != 0.0:
|
||||||
|
let toBot = arctan2(botY - ws.enemyY, botX - ws.enemyX)
|
||||||
|
perpAngle = if m.strafeDir >= 0.0: toBot + PI * 0.5
|
||||||
|
else: toBot - PI * 0.5
|
||||||
|
havePerp = true
|
||||||
|
else:
|
||||||
|
perpAngle = degToRad(ws.selfHeading)
|
||||||
|
|
||||||
# Perpendicular strafe (same body trick as phantom_meteor)
|
# ── Hard wall escape: never drive into a wall ─────────────────────────────
|
||||||
let enemyBearingRad = arctan2(enemyY - botY, enemyX - botX)
|
let nearLeft = botX < SurfWallMargin
|
||||||
# Perpendicular in chosen strafe direction
|
let nearRight = botX > ws.arenaWidth - SurfWallMargin
|
||||||
var perpAngle =
|
let nearBottom = botY < SurfWallMargin
|
||||||
if m.strafeDir >= 0.0: enemyBearingRad + PI * 0.5
|
let nearTop = botY > ws.arenaHeight - SurfWallMargin
|
||||||
else: enemyBearingRad - PI * 0.5
|
|
||||||
|
|
||||||
# Hard wall escape: if near any wall, blend toward arena center
|
|
||||||
# ponytail: linear blend, upgrade to override if blending proves too weak
|
|
||||||
let nearLeft = botX < WallMargin
|
|
||||||
let nearRight = botX > ws.arenaWidth - WallMargin
|
|
||||||
let nearBottom = botY < WallMargin
|
|
||||||
let nearTop = botY > ws.arenaHeight - WallMargin
|
|
||||||
if nearLeft or nearRight or nearBottom or nearTop:
|
if nearLeft or nearRight or nearBottom or nearTop:
|
||||||
# Flip strafe if it pushes further into the wall
|
|
||||||
let px = cos(perpAngle)
|
let px = cos(perpAngle)
|
||||||
let py = sin(perpAngle)
|
let py = sin(perpAngle)
|
||||||
if (nearLeft and px < 0.0) or (nearRight and px > 0.0) or
|
if (nearLeft and px < 0.0) or (nearRight and px > 0.0) or
|
||||||
(nearBottom and py < 0.0) or (nearTop and py > 0.0):
|
(nearBottom and py < 0.0) or (nearTop and py > 0.0):
|
||||||
|
# Flip to the opposite perpendicular (same as the other strafe side).
|
||||||
m.strafeDir = -m.strafeDir
|
m.strafeDir = -m.strafeDir
|
||||||
perpAngle = if m.strafeDir >= 0.0: enemyBearingRad + PI * 0.5
|
perpAngle = perpAngle + PI
|
||||||
else: enemyBearingRad - PI * 0.5
|
# Blend 50% toward the arena centre.
|
||||||
# Blend 50% toward arena center
|
|
||||||
let escapeAngle = arctan2(ws.arenaHeight * 0.5 - botY, ws.arenaWidth * 0.5 - botX)
|
let escapeAngle = arctan2(ws.arenaHeight * 0.5 - botY, ws.arenaWidth * 0.5 - botX)
|
||||||
let ex = cos(escapeAngle) + cos(perpAngle)
|
let ex = cos(escapeAngle) + cos(perpAngle)
|
||||||
let ey = sin(escapeAngle) + sin(perpAngle)
|
let ey = sin(escapeAngle) + sin(perpAngle)
|
||||||
perpAngle = arctan2(ey, ex)
|
perpAngle = arctan2(ey, ex)
|
||||||
|
|
||||||
# Distance control: blend a radial component when outside the deadband.
|
# ── Distance control: a radial component outside the deadband ────────────
|
||||||
# Secondary to bullet dodge — capped at WS_RadialFrac of travel direction.
|
let enemyDist = hypot(ws.enemyX - botX, ws.enemyY - botY)
|
||||||
# ponytail: linear blend, tune WS_RadialFrac if approach/retreat feels sluggish
|
let distErr = enemyDist - SurfPrefDist
|
||||||
let enemyDist = sqrt((enemyX - botX)^2 + (enemyY - botY)^2)
|
|
||||||
let distErr = enemyDist - WS_PrefDist
|
|
||||||
let radialFrac =
|
let radialFrac =
|
||||||
if distErr > WS_DistBand: WS_RadialFrac # too far → approach
|
if distErr > SurfDistBand: SurfRadialFrac # too far -> approach
|
||||||
elif distErr < -WS_DistBand: -WS_RadialFrac # too close → retreat
|
elif distErr < -SurfDistBand: -SurfRadialFrac # too close -> retreat
|
||||||
else: 0.0 # deadband → pure strafe
|
else: 0.0
|
||||||
if abs(radialFrac) > 1e-9:
|
if abs(radialFrac) > 1e-9:
|
||||||
# Radial direction: toward enemy (positive) or away (negative)
|
let radialAngle = arctan2(ws.enemyY - botY, ws.enemyX - botX) +
|
||||||
let radialAngle = arctan2(enemyY - botY, enemyX - botX) +
|
|
||||||
(if radialFrac < 0.0: PI else: 0.0)
|
(if radialFrac < 0.0: PI else: 0.0)
|
||||||
let rx = cos(perpAngle) * (1.0 - abs(radialFrac)) + cos(radialAngle) * abs(radialFrac)
|
let rx = cos(perpAngle) * (1.0 - abs(radialFrac)) + cos(radialAngle) * abs(radialFrac)
|
||||||
let ry = sin(perpAngle) * (1.0 - abs(radialFrac)) + sin(radialAngle) * abs(radialFrac)
|
let ry = sin(perpAngle) * (1.0 - abs(radialFrac)) + sin(radialAngle) * abs(radialFrac)
|
||||||
perpAngle = arctan2(ry, rx)
|
perpAngle = arctan2(ry, rx)
|
||||||
|
|
||||||
|
# ── Turn the body toward the desired heading; move at full speed ─────────
|
||||||
let desiredDeg = radToDeg(perpAngle)
|
let desiredDeg = radToDeg(perpAngle)
|
||||||
|
|
||||||
var delta = desiredDeg - ws.selfHeading
|
var delta = desiredDeg - ws.selfHeading
|
||||||
|
|||||||
Reference in New Issue
Block a user