Files

175 lines
6.3 KiB
Python

#!/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 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
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):
"""-> {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):
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
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" % ("", "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)
def main(argv):
path = argv[1] if len(argv) > 1 else "home/lgbot-rec.csv"
with open(path) as f:
sessions = parse(f)
per_player = {}
for s in sessions:
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
if __name__ == "__main__":
sys.exit(main(sys.argv))