#!/usr/bin/env python3 """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. """ import math, sys FIGHT_DIST, FIGHT_DZ = 1000, 250 SIDE_DEADBAND = 60 # u/s sideways before it counts as strafing to one side GAP_S = 0.2 # a hole in the recording longer than this breaks a run STOP_SPEED = 100 JUMP_VZ = 150 def parse(lines): """-> list of sessions, each {t: {who: row}}; row = (alive, hp, x, y, z, vx, vy, vz, state).""" sessions, cur = [], None for line in lines: line = line.strip() if not line: continue if line.startswith("#"): cur = {} sessions.append(cur) continue f = line.split(",") if len(f) < 11: continue if cur is None: cur = {} sessions.append(cur) try: t = float(f[0]) row = (int(f[2]), int(f[3])) + tuple(float(v) for v in f[4:10]) + (f[10],) except ValueError: continue cur.setdefault(t, {})[f[1]] = row return sessions def fight_frames(session): """-> {"human": [...], "bot": [...]}, frames (t, x, y, z, vx, vy, vz, ox, oy, state) in time order.""" out = {"human": [], "bot": []} 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])) return out def pct(vals, q): if not vals: return None v = sorted(vals) return v[min(len(v) - 1, int(q / 100.0 * len(v)))] def analyze(frames): """Movement stats for one player's fight frames (see fight_frames).""" n = len(frames) res = {"seconds": 0.0, "frames": n} if n < 2: return res ground_speed, air_speed, dist, side_share = [], [], [], [] holds, side, side_since, prev = [], 0, None, None air = stops = jumps = steer = 0 for fr in frames: t, x, y, z, vx, vy, vz, ox, oy, state = fr dx, dy = ox - x, oy - y d = math.hypot(dx, dy) or 1.0 ux, uy = dx / d, dy / d lat = -uy * vx + ux * vy rad = ux * vx + uy * vy speed = math.hypot(vx, vy) dist.append(d) on_ground = abs(vz) < 1 (ground_speed if on_ground else air_speed).append(speed) if not on_ground: air += 1 if speed < STOP_SPEED: stops += 1 if abs(lat) + abs(rad) > 1: side_share.append(abs(lat) / (abs(lat) + abs(rad))) if state == "steer": steer += 1 gap = prev is not None and not 0 <= t - prev[0] <= GAP_S if prev is not None and not gap and abs(prev[6]) < 1 and vz > JUMP_VZ: jumps += 1 if gap: side, side_since = 0, None s = 1 if lat > SIDE_DEADBAND else -1 if lat < -SIDE_DEADBAND else 0 if s and s != side: if side and side_since is not None: holds.append(int((t - side_since) * 1000)) side, side_since = s, t prev = fr secs = sum(min(max(b[0] - a[0], 0), GAP_S) for a, b in zip(frames, frames[1:])) res.update({ "seconds": secs, "hold_ms": (pct(holds, 10), pct(holds, 50), pct(holds, 90)), "reversals_per_s": len(holds) / secs if secs else 0, "ground_speed": (pct(ground_speed, 50), pct(ground_speed, 90)), "air_speed": (pct(air_speed, 50), pct(air_speed, 90)), "air_share": air / n, "jumps_per_s": jumps / secs if secs else 0, "sideways_share": sum(side_share) / len(side_share) if side_share else 0, "stopped_share": stops / n, "dist": (pct(dist, 10), pct(dist, 50), pct(dist, 90)), "steer_share": steer / n, }) return res def fmt(v): if v is None: return "-" if isinstance(v, tuple): return " / ".join(fmt(x) for x in v) if isinstance(v, float): return "%.2f" % v if abs(v) < 10 else "%.0f" % v return str(v) ROWS = [("seconds", "fight time s"), ("hold_ms", "strafe hold ms p10/p50/p90"), ("reversals_per_s", "direction changes /s"), ("ground_speed", "ground speed p50/p90"), ("air_speed", "air speed p50/p90"), ("air_share", "time in air"), ("jumps_per_s", "jumps /s"), ("sideways_share", "sideways (vs toward/away)"), ("stopped_share", "time nearly stopped"), ("dist", "distance p10/p50/p90"), ("steer_share", "plugin steering (bot)")] def report(human, bot): lines = ["%-28s %-22s %s" % ("", "you", "lgbot")] for k, label in ROWS: lines.append("%-28s %-22s %s" % (label, fmt(human.get(k)), fmt(bot.get(k)))) return "\n".join(lines) 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": []} 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 print("%d session(s)" % len(sessions)) print(report(analyze(both["human"]), analyze(both["bot"]))) return 0 if __name__ == "__main__": sys.exit(main(sys.argv))