lgbot: steer only within LG reach on the same floor; let the AI walk around walls

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
esefandClaude Opus 5.5 committed 2026-10-15 14:59:56 +03:00
1 parent f494223305
commit a998614224
4 files changed
+89 -22

No files matched your search

+58 -15
View File
@@ -27,7 +27,10 @@ PRESETS = {
}
PUSH_RANGE, BACKOFF_RANGE = 250, 700
BACKOFF_HEALTH, PUSH_LEAD = 60, 30
MAX_TARGET_DIST = 1500
# Steering is straight-line (no pathfinding), so only steer when the target is within LG reach and
# on about the same floor; otherwise the bot's own navigation walks it over.
MAX_TARGET_DIST = 900
MAX_TARGET_DZ = 200
BOT_STEAM_ID = 90071996842377216 # minqlx gives bots this + their client number
@@ -129,15 +132,43 @@ def fix_z(vz):
return 0.0 if 200 <= vz <= 320 else vz
def pick_target(bot_xy, candidates, max_dist=MAX_TARGET_DIST):
def pick_target(bot_pos, candidates, max_dist=MAX_TARGET_DIST, max_dz=MAX_TARGET_DZ):
"""Nearest candidate (pos = x, y, z) within max_dist horizontally and max_dz vertically."""
best, best_d = None, max_dist
for xy, obj in candidates:
d = math.hypot(xy[0] - bot_xy[0], xy[1] - bot_xy[1])
for pos, obj in candidates:
if abs(pos[2] - bot_pos[2]) > max_dz:
continue
d = math.hypot(pos[0] - bot_pos[0], pos[1] - bot_pos[1])
if d <= best_d:
best, best_d = obj, d
return best
class StuckGuard:
"""Lets go of the bot when steering keeps getting blocked (a wall, a ledge): if the speed it
makes along the commanded direction stays under `ratio` of the command for `after_ms`, steering
is off for `release_ms` so the bot AI can walk around the obstacle."""
def __init__(self, ratio=0.6, after_ms=500, release_ms=1500):
self.ratio, self.after_ms, self.release_ms = ratio, after_ms, release_ms
self.slow_since = None
self.off_until = 0
def update(self, now_ms, cmd_speed, along_speed):
if now_ms < self.off_until:
return False
if cmd_speed > 1 and along_speed < cmd_speed * self.ratio:
if self.slow_since is None:
self.slow_since = now_ms
elif now_ms - self.slow_since >= self.after_ms:
self.slow_since = None
self.off_until = now_ms + self.release_ms
return False
else:
self.slow_since = None
return True
try:
import minqlx
except ImportError: # imported by tests / the setup script outside the server
@@ -156,7 +187,9 @@ if minqlx:
self.settings, self.read_at, self.log_at = None, 0.0, 0.0
self.debug = self.target_bots = False
self.failed = False
self.last_cmd, self.slow_frames, self.stuck = None, 0, 0 # debug stats
self.guard = StuckGuard()
self.last_cmd = None # last command (for the stuck guard)
self.dbg_cmd, self.slow_frames, self.stuck = None, 0, 0 # debug stats
self.add_hook("frame", self.handle_frame)
def handle_frame(self):
@@ -177,6 +210,7 @@ if minqlx:
return self.settings
def idle(self, now, why):
self.last_cmd = None # not steering this frame: nothing for the stuck guard to judge
if self.debug and now - self.log_at >= 1.0:
self.log_at = now
self.logger.info("lgbot idle: %s", why)
@@ -197,34 +231,43 @@ if minqlx:
if is_bot(p.steam_id) and not self.target_bots:
continue
pos = p.position()
cands.append(((pos.x, pos.y), p))
cands.append(((pos.x, pos.y, pos.z), p))
bpos = bot.position()
target = pick_target((bpos.x, bpos.y), cands)
if target is None: # nobody to duel: stock AI roams
return self.idle(now, "no target within %d (%d candidates)" % (MAX_TARGET_DIST, len(cands)))
target = pick_target((bpos.x, bpos.y, bpos.z), cands)
if target is None: # nobody to duel close by: the bot AI walks over
return self.idle(now, "no target within %d on this floor (%d candidates)" % (MAX_TARGET_DIST, len(cands)))
vel = bot.velocity()
cmd_speed, along = 0.0, 0.0
if self.last_cmd:
cmd_speed = math.hypot(*self.last_cmd)
if cmd_speed > 1:
along = (vel.x * self.last_cmd[0] + vel.y * self.last_cmd[1]) / cmd_speed
if not self.guard.update(int(now * 1000), cmd_speed, along):
return self.idle(now, "blocked, letting the bot AI walk around it")
tpos = target.position()
direction = self.strafer.step(int(now * 1000), s)
want = wanted_range(s, bot.health, target.health)
vx, vy = steer((bpos.x, bpos.y), (tpos.x, tpos.y), direction, s, want)
vel = bot.velocity()
comp = FRICTION_COMP if abs(vel.z) < 1 else 1.0
bot.velocity(x=vx * comp, y=vy * comp, z=fix_z(vel.z))
self.last_cmd = (vx, vy)
if self.debug:
self.debug_line(now, vel, (vx, vy), tpos, bpos, want, direction)
def debug_line(self, now, vel, cmd, tpos, bpos, want, direction):
# cos = how well last frame's command survived the frame (1 = exactly our direction);
# stuck = times actual speed stayed under half the command for > 0.5 s (walls)
# stuck = times actual speed stayed under half the command for > 0.5 s (walls; the
# StuckGuard is what acts on it, this is only the count)
actual = math.hypot(vel.x, vel.y)
cos = 0.0
if self.last_cmd:
lc = math.hypot(*self.last_cmd)
if self.dbg_cmd:
lc = math.hypot(*self.dbg_cmd)
if lc > 1 and actual > 1:
cos = (vel.x * self.last_cmd[0] + vel.y * self.last_cmd[1]) / (lc * actual)
cos = (vel.x * self.dbg_cmd[0] + vel.y * self.dbg_cmd[1]) / (lc * actual)
self.slow_frames = self.slow_frames + 1 if actual < lc / 2 else 0
if self.slow_frames == 20:
self.stuck += 1
self.last_cmd = cmd
self.dbg_cmd = cmd
if now - self.log_at >= 1.0:
self.log_at = now
self.logger.info("lgbot dist=%d want=%d dir=%d cmd=%d actual=%d cos=%.2f stuck=%d z=%d",