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:
1 parent
e1c64bed4c
commit
3535940331
6 files changed
+324
-5
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user