lgbot: auto recording while two humans fight (lgbot_record 2 / QL_RECORD), analyzer pairs any opponents
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
3535940331
commit
acf625d72b
8 files changed
+133
-51
No files matched your search
+31
-24
@@ -2,7 +2,8 @@
|
||||
"""Compare how you and lgbot move, from a recording (set lgbot_record 1, play, set lgbot_record 0).
|
||||
python3 tools/lgbot_rec.py [home/lgbot-rec.csv]
|
||||
Only fight time counts: both alive, within 1000 units and about the same floor. Sideways/toward are
|
||||
relative to the opponent (you vs the bot, the bot vs the nearest human). Prints numbers only.
|
||||
relative to the nearest opponent (human or bot), so human-vs-human recordings (lgbot_record 2) work
|
||||
too: "humans" is every human together. Prints numbers only.
|
||||
"""
|
||||
import math, sys
|
||||
|
||||
@@ -40,25 +41,31 @@ def parse(lines):
|
||||
|
||||
|
||||
def fight_frames(session):
|
||||
"""-> {"human": [...], "bot": [...]}, frames (t, x, y, z, vx, vy, vz, ox, oy, state) in time order."""
|
||||
out = {"human": [], "bot": []}
|
||||
"""-> {who: [...]} ("bot", "h1", ...), frames (t, x, y, z, vx, vy, vz, ox, oy, state) in time order.
|
||||
Each alive player is measured against the nearest alive player in fight range (human or bot)."""
|
||||
out = {}
|
||||
for t in sorted(session):
|
||||
rows = session[t]
|
||||
bot = rows.get("bot")
|
||||
if not bot or not bot[0]:
|
||||
continue
|
||||
best = None
|
||||
for who, r in rows.items():
|
||||
if who == "bot" or not r[0]:
|
||||
continue
|
||||
d = math.hypot(r[2] - bot[2], r[3] - bot[3])
|
||||
if best is None or d < best[0]:
|
||||
best = (d, r)
|
||||
if best is None or best[0] > FIGHT_DIST or abs(best[1][4] - bot[4]) > FIGHT_DZ:
|
||||
continue
|
||||
h = best[1]
|
||||
out["human"].append((t,) + h[2:8] + (bot[2], bot[3], ""))
|
||||
out["bot"].append((t,) + bot[2:8] + (h[2], h[3], bot[8]))
|
||||
alive = [(who, r) for who, r in session[t].items() if r[0]]
|
||||
for who, me in alive:
|
||||
best = None
|
||||
for other, r in alive:
|
||||
if other == who or abs(r[4] - me[4]) > FIGHT_DZ:
|
||||
continue
|
||||
d = math.hypot(r[2] - me[2], r[3] - me[3])
|
||||
if d <= FIGHT_DIST and (best is None or d < best[0]):
|
||||
best = (d, r)
|
||||
if best is not None:
|
||||
out.setdefault(who, []).append(
|
||||
(t,) + me[2:8] + (best[1][2], best[1][3], me[8]))
|
||||
return out
|
||||
|
||||
|
||||
def by_kind(per_player):
|
||||
"""{who: frames} -> {"human": frames, "bot": frames}: one player after another, so time jumps
|
||||
back at each join and the gap rule keeps players (and sessions) apart."""
|
||||
out = {"human": [], "bot": []}
|
||||
for who in sorted(per_player):
|
||||
out["bot" if who == "bot" else "human"].extend(per_player[who])
|
||||
return out
|
||||
|
||||
|
||||
@@ -143,7 +150,7 @@ ROWS = [("seconds", "fight time s"), ("hold_ms", "strafe hold ms p10/p50/p90"),
|
||||
|
||||
|
||||
def report(human, bot):
|
||||
lines = ["%-28s %-22s %s" % ("", "you", "lgbot")]
|
||||
lines = ["%-28s %-22s %s" % ("", "humans", "lgbot")]
|
||||
for k, label in ROWS:
|
||||
lines.append("%-28s %-22s %s" % (label, fmt(human.get(k)), fmt(bot.get(k))))
|
||||
return "\n".join(lines)
|
||||
@@ -153,11 +160,11 @@ def main(argv):
|
||||
path = argv[1] if len(argv) > 1 else "home/lgbot-rec.csv"
|
||||
with open(path) as f:
|
||||
sessions = parse(f)
|
||||
both = {"human": [], "bot": []}
|
||||
per_player = {}
|
||||
for s in sessions:
|
||||
fr = fight_frames(s)
|
||||
for k in both:
|
||||
both[k].extend(fr[k]) # t restarts at 0 each session: the gap rule keeps them apart
|
||||
for who, frames in fight_frames(s).items():
|
||||
per_player.setdefault(who, []).extend(frames) # t restarts each session: a gap
|
||||
both = by_kind(per_player)
|
||||
print("%d session(s)" % len(sessions))
|
||||
print(report(analyze(both["human"]), analyze(both["bot"])))
|
||||
return 0
|
||||
|
||||
@@ -29,6 +29,7 @@ set g_password "$(clean "$QL_PASSWORD")"
|
||||
set sv_maxClients "$(clean "${QL_MAXCLIENTS:-16}")"
|
||||
set zmq_rcon_port "$(clean "${QL_RCON_PORT:-28960}")"
|
||||
set qlx_welcome "$(clean "$QL_WELCOME")"
|
||||
set lgbot_record "$(clean "${QL_RECORD:-0}")"
|
||||
map $MAP $FACTORY
|
||||
EOF
|
||||
echo "$MAP|$FACTORY" > $B/mappool_lg.txt
|
||||
|
||||
@@ -169,6 +169,35 @@ class Tick(unittest.TestCase):
|
||||
self.assertEqual(self.w.cvars["lgbot_record"], "0")
|
||||
self.assertIsNone(self.plugin.rec)
|
||||
|
||||
def test_auto_records_only_two_humans_fighting(self):
|
||||
import tempfile
|
||||
path = os.path.join(tempfile.mkdtemp(), "rec.csv")
|
||||
self.mod.REC_FILE = path
|
||||
self.w.cvars["lgbot_record"] = "2"
|
||||
self.run_frames(40) # one human vs the bot: not recorded
|
||||
self.assertFalse(os.path.exists(path))
|
||||
h2 = FakePlayer(2, "other", HUMAN_ID + 1, (450, 300, 0))
|
||||
self.w.players.append(h2)
|
||||
self.run_frames(40)
|
||||
h2.pos = [5000, 0, 0] # walks away: stops writing
|
||||
self.run_frames(5)
|
||||
with open(path) as f:
|
||||
before = f.read()
|
||||
self.run_frames(40)
|
||||
with open(path) as f:
|
||||
after = f.read()
|
||||
self.assertEqual(before, after)
|
||||
self.assertIn(",h2,", before)
|
||||
self.assertNotIn(str(HUMAN_ID), before)
|
||||
self.plugin.stop_recording()
|
||||
|
||||
def test_humans_fighting(self):
|
||||
f = self.mod.humans_fighting
|
||||
self.assertTrue(f([(0, 0, 0), (900, 0, 0)]))
|
||||
self.assertFalse(f([(0, 0, 0), (1200, 0, 0)]))
|
||||
self.assertFalse(f([(0, 0, 0), (100, 0, 400)]))
|
||||
self.assertFalse(f([(0, 0, 0)]))
|
||||
|
||||
def test_no_target_means_no_steering(self):
|
||||
self.human.team = "spectator"
|
||||
self.run_frames(40)
|
||||
|
||||
+23
-7
@@ -32,7 +32,7 @@ def still_at(x):
|
||||
class Stats(unittest.TestCase):
|
||||
def test_strafe_holds_and_sideways(self):
|
||||
s = r.parse(rec(10, adad(0.5), still_at(450)))
|
||||
st = r.analyze(r.fight_frames(s[0])["human"])
|
||||
st = r.analyze(r.by_kind(r.fight_frames(s[0]))["human"])
|
||||
self.assertAlmostEqual(st["hold_ms"][1], 500, delta=30)
|
||||
self.assertGreater(st["sideways_share"], 0.95)
|
||||
self.assertEqual(st["air_share"], 0)
|
||||
@@ -41,7 +41,7 @@ class Stats(unittest.TestCase):
|
||||
|
||||
def test_bot_steer_share_and_stopped(self):
|
||||
s = r.parse(rec(5, adad(0.5), still_at(450)))
|
||||
st = r.analyze(r.fight_frames(s[0])["bot"])
|
||||
st = r.analyze(r.by_kind(r.fight_frames(s[0]))["bot"])
|
||||
self.assertEqual(st["steer_share"], 1)
|
||||
self.assertEqual(st["stopped_share"], 1)
|
||||
|
||||
@@ -49,25 +49,41 @@ class Stats(unittest.TestCase):
|
||||
def hop(t): # one jump a second: 0.7 s in the air, then ground
|
||||
ph = t % 1.0
|
||||
return (0, 0, 0, 0, 320, 270 - 800 * ph if ph < 0.7 else 0)
|
||||
st = r.analyze(r.fight_frames(r.parse(rec(10, hop, still_at(450)))[0])["human"])
|
||||
st = r.analyze(r.by_kind(r.fight_frames(r.parse(rec(10, hop, still_at(450)))[0]))["human"])
|
||||
self.assertAlmostEqual(st["jumps_per_s"], 1.0, delta=0.15)
|
||||
self.assertAlmostEqual(st["air_share"], 0.7, delta=0.05)
|
||||
|
||||
def test_far_or_other_floor_is_not_fight_time(self):
|
||||
far = r.fight_frames(r.parse(rec(2, adad(0.5), still_at(1500)))[0])
|
||||
up = r.fight_frames(r.parse(rec(2, adad(0.5), lambda t: (450, 0, 400, 0, 0, 0)))[0])
|
||||
self.assertEqual(far["human"], [])
|
||||
self.assertEqual(up["human"], [])
|
||||
self.assertEqual(far, {})
|
||||
self.assertEqual(up, {})
|
||||
|
||||
def test_sessions_do_not_join(self):
|
||||
lines = rec(2, adad(0.5), still_at(450)) + rec(2, adad(0.5), still_at(450))
|
||||
sessions = r.parse(lines)
|
||||
self.assertEqual(len(sessions), 2)
|
||||
frames = r.fight_frames(sessions[0])["human"] + r.fight_frames(sessions[1])["human"]
|
||||
frames = r.fight_frames(sessions[0])["h1"] + r.fight_frames(sessions[1])["h1"]
|
||||
self.assertAlmostEqual(r.analyze(frames)["seconds"], 4, delta=0.1)
|
||||
|
||||
def test_two_humans_kept_apart(self):
|
||||
# h1 strafes with 0.5 s holds, h2 with 0.25 s holds, no bot: each is measured on its own
|
||||
lines = ["# start 0 map test"]
|
||||
for i in range(400):
|
||||
t = i * 0.025
|
||||
for who, hold, x in (("h1", 0.5, 0), ("h2", 0.25, 450)):
|
||||
d = 1 if int(t / hold) % 2 == 0 else -1
|
||||
lines.append("%.3f,%s,1,100,%d,0,0,0,%d,0," % (t, who, x, 320 * d))
|
||||
per = r.fight_frames(r.parse(lines)[0])
|
||||
self.assertEqual(sorted(per), ["h1", "h2"])
|
||||
self.assertAlmostEqual(r.analyze(per["h1"])["hold_ms"][1], 500, delta=30)
|
||||
self.assertAlmostEqual(r.analyze(per["h2"])["hold_ms"][1], 250, delta=30)
|
||||
both = r.by_kind(per)["human"]
|
||||
self.assertAlmostEqual(r.analyze(both)["seconds"], 20, delta=0.2)
|
||||
self.assertEqual(r.analyze(r.by_kind(per)["bot"])["frames"], 0)
|
||||
|
||||
def test_report_prints(self):
|
||||
st = r.analyze(r.fight_frames(r.parse(rec(2, adad(0.5), still_at(450)))[0])["human"])
|
||||
st = r.analyze(r.fight_frames(r.parse(rec(2, adad(0.5), still_at(450)))[0])["h1"])
|
||||
self.assertIn("strafe hold", r.report(st, r.analyze([])))
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user