lgbot: review fixes: drift reset after pauses, player-like acceleration, sticky targets with hysteresis, wall backoff; plugin glue tests
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
a998614224
commit
86754715f5
6 files changed
+321
-30
No files matched your search
@@ -14,6 +14,9 @@ RUN_SPEED = 320 # QL ground run speed, units/s
|
||||
# ground we set this much more to end up at about the wanted speed.
|
||||
# In the air there is no friction, so no boost (vertical speed 0 = on the ground, near enough).
|
||||
FRICTION_COMP = 1.2
|
||||
# How fast the bot's velocity may change, units/s per second: QL ground acceleration (10) x run
|
||||
# speed, so a full left-right reversal takes ~200 ms like a player, not one frame.
|
||||
ACCEL = 3200
|
||||
|
||||
KEYS = ["strafe_min", "strafe_max", "fake", "range", "range_play", "speed", "lane"]
|
||||
LIMITS = {"strafe_min": (50, 2000), "strafe_max": (50, 2000), "fake": (0, 100),
|
||||
@@ -27,10 +30,15 @@ PRESETS = {
|
||||
}
|
||||
PUSH_RANGE, BACKOFF_RANGE = 250, 700
|
||||
BACKOFF_HEALTH, PUSH_LEAD = 60, 30
|
||||
# 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.
|
||||
# Steering is straight-line (no pathfinding), so only start steering when the target is at most a
|
||||
# bit beyond LG reach (768) and on about the same floor; otherwise the bot's own navigation walks it
|
||||
# over. Once steering, keep the target up to the STOP limits so it doesn't flip at the edge, and
|
||||
# only switch to another player who is clearly closer.
|
||||
MAX_TARGET_DIST = 900
|
||||
MAX_TARGET_DZ = 200
|
||||
STOP_TARGET_DIST = 1000
|
||||
STOP_TARGET_DZ = 250
|
||||
SWITCH_MARGIN = 150
|
||||
BOT_STEAM_ID = 90071996842377216 # minqlx gives bots this + their client number
|
||||
|
||||
|
||||
@@ -81,9 +89,14 @@ class Strafer:
|
||||
self.drift = 0 # signed ms of strafing, + = direction 1
|
||||
self.last = None
|
||||
|
||||
def reset(self):
|
||||
"""Not steering (dead, no target, walking): drift so far means nothing for the next fight."""
|
||||
self.drift = 0
|
||||
self.last = None
|
||||
|
||||
def step(self, now_ms, s):
|
||||
if self.last is not None:
|
||||
self.drift += self.direction * (now_ms - self.last)
|
||||
self.drift += self.direction * min(now_ms - self.last, 100) # a pause is not drift
|
||||
self.last = now_ms
|
||||
if s["lane"]:
|
||||
lane_ms = s["lane"] * 1000.0 / (RUN_SPEED * s["speed"] / 100.0)
|
||||
@@ -132,40 +145,65 @@ def fix_z(vz):
|
||||
return 0.0 if 200 <= vz <= 320 else vz
|
||||
|
||||
|
||||
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 pos, obj in candidates:
|
||||
if abs(pos[2] - bot_pos[2]) > max_dz:
|
||||
continue
|
||||
def approach(cur, want, max_delta):
|
||||
"""Move velocity `cur` toward `want` by at most `max_delta` (acceleration limit)."""
|
||||
dx, dy = want[0] - cur[0], want[1] - cur[1]
|
||||
d = math.hypot(dx, dy)
|
||||
if d <= max_delta:
|
||||
return (want[0], want[1])
|
||||
return (cur[0] + dx / d * max_delta, cur[1] + dy / d * max_delta)
|
||||
|
||||
|
||||
def pick_target(bot_pos, candidates, current=None):
|
||||
"""Key of the player to fight, from candidates [((x, y, z), key)]. A new target must be within
|
||||
MAX_TARGET_DIST / MAX_TARGET_DZ; the current one is kept up to the STOP limits unless another
|
||||
is SWITCH_MARGIN closer. None = nobody (the bot AI walks)."""
|
||||
near, cur_d = [], None
|
||||
for pos, key in candidates:
|
||||
dz = abs(pos[2] - bot_pos[2])
|
||||
d = math.hypot(pos[0] - bot_pos[0], pos[1] - bot_pos[1])
|
||||
if d <= best_d:
|
||||
best, best_d = obj, d
|
||||
return best
|
||||
if key == current and d <= STOP_TARGET_DIST and dz <= STOP_TARGET_DZ:
|
||||
cur_d = d
|
||||
if d <= MAX_TARGET_DIST and dz <= MAX_TARGET_DZ:
|
||||
near.append((d, key))
|
||||
best = min(near, key=lambda t: t[0]) if near else None
|
||||
if cur_d is not None and (best is None or best[0] > cur_d - SWITCH_MARGIN):
|
||||
return current
|
||||
return best[1] if best else None
|
||||
|
||||
|
||||
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."""
|
||||
is off for `release_ms` so the bot AI can walk around the obstacle. If it keeps failing, each
|
||||
release is twice as long (up to `max_release_ms`); `clean_ms` of good steering resets that."""
|
||||
|
||||
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
|
||||
def __init__(self, ratio=0.6, after_ms=500, release_ms=1500, max_release_ms=6000, clean_ms=3000):
|
||||
self.ratio, self.after_ms = ratio, after_ms
|
||||
self.release_ms, self.max_release_ms, self.clean_ms = release_ms, max_release_ms, clean_ms
|
||||
self.slow_since = None
|
||||
self.off_until = 0
|
||||
self.streak = 0
|
||||
self.clean_since = None
|
||||
|
||||
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:
|
||||
self.clean_since = None
|
||||
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
|
||||
self.off_until = now_ms + min(self.max_release_ms, self.release_ms * 2 ** self.streak)
|
||||
self.streak += 1
|
||||
return False
|
||||
else:
|
||||
self.slow_since = None
|
||||
if self.clean_since is None:
|
||||
self.clean_since = now_ms
|
||||
elif now_ms - self.clean_since >= self.clean_ms:
|
||||
self.streak = 0
|
||||
return True
|
||||
|
||||
|
||||
@@ -188,7 +226,8 @@ if minqlx:
|
||||
self.debug = self.target_bots = False
|
||||
self.failed = False
|
||||
self.guard = StuckGuard()
|
||||
self.last_cmd = None # last command (for the stuck guard)
|
||||
self.last_cmd = None # last command (for the stuck guard and the acceleration limit)
|
||||
self.target_id, self.last_tick = None, None
|
||||
self.dbg_cmd, self.slow_frames, self.stuck = None, 0, 0 # debug stats
|
||||
self.add_hook("frame", self.handle_frame)
|
||||
|
||||
@@ -210,7 +249,10 @@ 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
|
||||
# not steering this frame: nothing for the stuck guard to judge, and the strafe pattern
|
||||
# starts fresh next time (otherwise the pause counts as sideways drift)
|
||||
self.last_cmd = self.dbg_cmd = self.target_id = None
|
||||
self.strafer.reset()
|
||||
if self.debug and now - self.log_at >= 1.0:
|
||||
self.log_at = now
|
||||
self.logger.info("lgbot idle: %s", why)
|
||||
@@ -231,11 +273,15 @@ if minqlx:
|
||||
if is_bot(p.steam_id) and not self.target_bots:
|
||||
continue
|
||||
pos = p.position()
|
||||
cands.append(((pos.x, pos.y, pos.z), p))
|
||||
cands.append(((pos.x, pos.y, pos.z), p.id))
|
||||
bpos = bot.position()
|
||||
target = pick_target((bpos.x, bpos.y, bpos.z), cands)
|
||||
if target is None: # nobody to duel close by: the bot AI walks over
|
||||
tid = pick_target((bpos.x, bpos.y, bpos.z), cands, self.target_id)
|
||||
if tid 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)))
|
||||
if tid != self.target_id:
|
||||
self.strafer.reset() # drift was measured around the previous target
|
||||
self.target_id = tid
|
||||
target = next(p for p in players if p.id == tid)
|
||||
vel = bot.velocity()
|
||||
cmd_speed, along = 0.0, 0.0
|
||||
if self.last_cmd:
|
||||
@@ -248,6 +294,10 @@ if minqlx:
|
||||
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)
|
||||
dt = min(0.1, now - self.last_tick) if self.last_tick else 0.025
|
||||
self.last_tick = now
|
||||
start = self.last_cmd if self.last_cmd else (vel.x, vel.y)
|
||||
vx, vy = approach(start, (vx, vy), ACCEL * dt) # turn like a player, not in one frame
|
||||
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)
|
||||
|
||||
Reference in new issue
Block a user