lgbot: movement recorder (lgbot_record) and tools/lgbot_rec.py to compare player vs bot movement

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
esefandClaude Opus 5.5 committed 2026-10-15 18:32:22 +03:00
1 parent e1c64bed4c
commit 3535940331
6 files changed
+324 -5

No files matched your search

+49 -5
View File
@@ -5,6 +5,7 @@
# override one part of it, -1 = use the preset. The pattern code below has no minqlx dependency so
# tools/test_lgbot.py and tools/lgbot.py can import it.
import math
import os
import random
import time
@@ -40,6 +41,11 @@ STOP_TARGET_DIST = 1000
STOP_TARGET_DZ = 250
SWITCH_MARGIN = 150
BOT_STEAM_ID = 90071996842377216 # minqlx gives bots this + their client number
# lgbot_record 1: every frame, the position/velocity of the bot and each human (as h<client number>,
# no names or Steam IDs) go to home/lgbot-rec.csv, for tools/lgbot_rec.py to compare how they move.
# Stops by itself after REC_MAX_S.
REC_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "lgbot-rec.csv")
REC_MAX_S = 900
def is_bot(steam_id):
@@ -221,6 +227,7 @@ if minqlx:
self.set_cvar_once("lgbot_" + k, "-1")
self.set_cvar_once("lgbot_debug", "0")
self.set_cvar_once("lgbot_target_bots", "0") # test aid: let it fight other bots
self.set_cvar_once("lgbot_record", "0")
self.strafer = Strafer()
self.settings, self.read_at, self.log_at = None, 0.0, 0.0
self.debug = self.target_bots = False
@@ -229,11 +236,14 @@ if minqlx:
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.state, self.frame_players = "", [] # for the recorder
self.record_on, self.rec, self.rec_start = False, None, 0.0
self.add_hook("frame", self.handle_frame)
def handle_frame(self):
try:
self.tick()
self.record(time.time())
except Exception:
if not self.failed: # log once, never spam 40 tracebacks a second
self.failed = True
@@ -245,10 +255,12 @@ if minqlx:
self.settings = resolve(to_int(self.get_cvar("lgbot_move")), over)
self.debug = bool(to_int(self.get_cvar("lgbot_debug")))
self.target_bots = bool(to_int(self.get_cvar("lgbot_target_bots")))
self.record_on = bool(to_int(self.get_cvar("lgbot_record")))
self.read_at = now
return self.settings
def idle(self, now, why):
def idle(self, now, code, why):
self.state = code
# 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
@@ -261,11 +273,12 @@ if minqlx:
now = time.time()
s = self.read_cvars(now)
players = self.players()
self.frame_players = players
bot = next((p for p in players if is_lgbot(p.clean_name, p.steam_id)), None)
if bot is None:
return self.idle(now, "no lgbot on the server")
return self.idle(now, "nobot", "no lgbot on the server")
if not bot.is_alive:
return self.idle(now, "lgbot is dead")
return self.idle(now, "dead", "lgbot is dead")
cands = []
for p in players:
if p.id == bot.id or p.team == "spectator" or not p.is_alive:
@@ -278,7 +291,7 @@ if minqlx:
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
near = min(((math.hypot(c[0][0] - bpos.x, c[0][1] - bpos.y), c[0][2] - bpos.z) for c in cands), default=None)
return self.idle(now, "no target within %d on this floor (%d candidates, nearest %s)" % (
return self.idle(now, "notarget", "no target within %d on this floor (%d candidates, nearest %s)" % (
MAX_TARGET_DIST, len(cands), "dist=%d dz=%d bot=%d,%d" % (near + (bpos.x, bpos.y)) if near else "-"))
if tid != self.target_id:
self.strafer.reset() # drift was measured around the previous target
@@ -291,7 +304,7 @@ if minqlx:
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")
return self.idle(now, "blocked", "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)
@@ -303,9 +316,40 @@ if minqlx:
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)
self.state = "steer"
if self.debug:
self.debug_line(now, vel, (vx, vy), tpos, bpos, want, direction)
def record(self, now):
if not self.record_on:
if self.rec:
self.rec.close()
self.rec = None
return
if self.rec is None:
self.rec = open(REC_FILE, "a", buffering=1)
self.rec_start = now
self.rec.write("# start %d map %s\n" % (int(now), self.get_cvar("mapname")))
if now - self.rec_start > REC_MAX_S:
self.set_cvar("lgbot_record", "0")
self.record_on = False
self.rec.close()
self.rec = None
return
t = now - self.rec_start
for p in self.frame_players:
if p.team == "spectator":
continue
if is_lgbot(p.clean_name, p.steam_id):
who, state = "bot", self.state
elif is_bot(p.steam_id):
continue
else:
who, state = "h%d" % p.id, ""
pos, vel = p.position(), p.velocity()
self.rec.write("%.3f,%s,%d,%d,%.0f,%.0f,%.0f,%.0f,%.0f,%.0f,%s\n" % (
t, who, int(bool(p.is_alive)), p.health, pos.x, pos.y, pos.z, vel.x, vel.y, vel.z, state))
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; the