175 lines
6.3 KiB
Python
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))
|