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
+129
-47
No files matched your search
@@ -23,6 +23,10 @@ QL_MAP=thunderstruck
|
|||||||
# Shown to every player when they join. Empty = no message.
|
# Shown to every player when they join. Empty = no message.
|
||||||
QL_WELCOME=^7Welcome! LG only, vampiric, no knockback. Pair up and duel, there is no match to ready up for.
|
QL_WELCOME=^7Welcome! LG only, vampiric, no knockback. Pair up and duel, there is no match to ready up for.
|
||||||
|
|
||||||
|
# Movement recording for the lgbot (home/lgbot-rec.csv, positions/speeds only, no names or IDs):
|
||||||
|
# 0 = off, 2 = automatically whenever two humans are fighting. See docs/ADMIN.md.
|
||||||
|
QL_RECORD=0
|
||||||
|
|
||||||
# Game port (UDP) and RCON port (TCP, bound to 127.0.0.1 only). Change both to run a second server.
|
# Game port (UDP) and RCON port (TCP, bound to 127.0.0.1 only). Change both to run a second server.
|
||||||
QL_PORT=27960
|
QL_PORT=27960
|
||||||
QL_RCON_PORT=28960
|
QL_RCON_PORT=28960
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ Everything per-server is in `.env` (created from [`.env.example`](.env.example)
|
|||||||
| `QL_FACTORY` | `lgvamp` | `lgvamp`, `lgvamp_nolag` or `lgvamp100`, see below |
|
| `QL_FACTORY` | `lgvamp` | `lgvamp`, `lgvamp_nolag` or `lgvamp100`, see below |
|
||||||
| `QL_MAP` | `thunderstruck` | stock map; workshop maps need extra setup |
|
| `QL_MAP` | `thunderstruck` | stock map; workshop maps need extra setup |
|
||||||
| `QL_WELCOME` | short rules text | sent to each player on join; empty = off |
|
| `QL_WELCOME` | short rules text | sent to each player on join; empty = off |
|
||||||
|
| `QL_RECORD` | `0` | `2` = record movement whenever two humans fight (for the lgbot, see docs/ADMIN.md) |
|
||||||
| `QL_PORT` / `QL_RCON_PORT` | `27960` / `28960` | game UDP / RCON TCP (127.0.0.1 only) |
|
| `QL_PORT` / `QL_RCON_PORT` | `27960` / `28960` | game UDP / RCON TCP (127.0.0.1 only) |
|
||||||
| `QL_NAME` | `ql-lg` | Docker container name |
|
| `QL_NAME` | `ql-lg` | Docker container name |
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ Underneath it's plain console settings, if you'd rather: `./rcon addbot lgbot <a
|
|||||||
|
|
||||||
To compare your movement with the bot's: `./rcon set lgbot_record 1`, fight it for a few minutes, `./rcon set lgbot_record 0`, then `python3 tools/lgbot_rec.py home/lgbot-rec.csv` prints both side by side (strafe hold times, speeds, jumps, distance). The recording has positions and speeds only, no names or Steam IDs, and stops by itself after 15 minutes.
|
To compare your movement with the bot's: `./rcon set lgbot_record 1`, fight it for a few minutes, `./rcon set lgbot_record 0`, then `python3 tools/lgbot_rec.py home/lgbot-rec.csv` prints both side by side (strafe hold times, speeds, jumps, distance). The recording has positions and speeds only, no names or Steam IDs, and stops by itself after 15 minutes.
|
||||||
|
|
||||||
|
`set lgbot_record 2` (or `QL_RECORD=2` in `.env`, which survives restarts) records automatically, but only while two humans are fighting each other (alive, within 1000 units, about the same floor), with no time limit. That's how to collect real duel movement to teach the bot. Recording stops at 200 MB; delete or move `home/lgbot-rec.csv` to start over. The same `tools/lgbot_rec.py` command reads it.
|
||||||
|
|
||||||
The bot fights the nearest human and sticks with them unless someone else is clearly closer. Its own movement takes over when you're within 900 units (a bit beyond LG reach) on about the same floor, and keeps going until you're past 1000. Farther away, the normal bot AI walks it over to you. If a wall blocks its strafing for half a second, it lets the AI walk around for 1.5 s, then longer (up to 6 s) if it keeps getting stuck. It speeds up and turns like a player (a full left-right reversal takes about 0.2 s), not instantly. One lgbot at a time.
|
The bot fights the nearest human and sticks with them unless someone else is clearly closer. Its own movement takes over when you're within 900 units (a bit beyond LG reach) on about the same floor, and keeps going until you're past 1000. Farther away, the normal bot AI walks it over to you. If a wall blocks its strafing for half a second, it lets the AI walk around for 1.5 s, then longer (up to 6 s) if it keeps getting stuck. It speeds up and turns like a player (a full left-right reversal takes about 0.2 s), not instantly. One lgbot at a time.
|
||||||
|
|
||||||
## Admins and bans: `home/baseq3/access.txt`
|
## Admins and bans: `home/baseq3/access.txt`
|
||||||
|
|||||||
@@ -43,9 +43,12 @@ SWITCH_MARGIN = 150
|
|||||||
BOT_STEAM_ID = 90071996842377216 # minqlx gives bots this + their client number
|
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>,
|
# 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.
|
# 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.
|
# Stops by itself after REC_MAX_S. lgbot_record 2 (auto): only frames where two humans are fighting
|
||||||
|
# (alive, within REC_FIGHT_DIST on about the same floor), no time limit. Both stop at REC_MAX_BYTES.
|
||||||
REC_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "lgbot-rec.csv")
|
REC_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "lgbot-rec.csv")
|
||||||
REC_MAX_S = 900
|
REC_MAX_S = 900
|
||||||
|
REC_MAX_BYTES = 200 * 1024 * 1024
|
||||||
|
REC_FIGHT_DIST, REC_FIGHT_DZ = 1000, 250
|
||||||
|
|
||||||
|
|
||||||
def is_bot(steam_id):
|
def is_bot(steam_id):
|
||||||
@@ -160,6 +163,15 @@ def approach(cur, want, max_delta):
|
|||||||
return (cur[0] + dx / d * max_delta, cur[1] + dy / d * max_delta)
|
return (cur[0] + dx / d * max_delta, cur[1] + dy / d * max_delta)
|
||||||
|
|
||||||
|
|
||||||
|
def humans_fighting(positions):
|
||||||
|
"""True when two of these (x, y, z) are within duel range of each other on about the same floor."""
|
||||||
|
for i, a in enumerate(positions):
|
||||||
|
for b in positions[i + 1:]:
|
||||||
|
if math.hypot(a[0] - b[0], a[1] - b[1]) <= REC_FIGHT_DIST and abs(a[2] - b[2]) <= REC_FIGHT_DZ:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def pick_target(bot_pos, candidates, current=None):
|
def pick_target(bot_pos, candidates, current=None):
|
||||||
"""Key of the player to fight, from candidates [((x, y, z), key)]. A new target must be within
|
"""Key of the player to fight, from candidates [((x, y, z), key)]. A new target must be within
|
||||||
MAX_TARGET_DIST / MAX_TARGET_DZ; the current one is kept up to the STOP limits unless another
|
MAX_TARGET_DIST / MAX_TARGET_DZ; the current one is kept up to the STOP limits unless another
|
||||||
@@ -237,7 +249,7 @@ if minqlx:
|
|||||||
self.target_id, self.last_tick = None, None
|
self.target_id, self.last_tick = None, None
|
||||||
self.dbg_cmd, self.slow_frames, self.stuck = None, 0, 0 # debug stats
|
self.dbg_cmd, self.slow_frames, self.stuck = None, 0, 0 # debug stats
|
||||||
self.state, self.frame_players = "", [] # for the recorder
|
self.state, self.frame_players = "", [] # for the recorder
|
||||||
self.record_on, self.rec, self.rec_start = False, None, 0.0
|
self.record_mode, self.rec, self.rec_start, self.rec_bytes = 0, None, 0.0, 0
|
||||||
self.add_hook("frame", self.handle_frame)
|
self.add_hook("frame", self.handle_frame)
|
||||||
|
|
||||||
def handle_frame(self):
|
def handle_frame(self):
|
||||||
@@ -255,7 +267,7 @@ if minqlx:
|
|||||||
self.settings = resolve(to_int(self.get_cvar("lgbot_move")), over)
|
self.settings = resolve(to_int(self.get_cvar("lgbot_move")), over)
|
||||||
self.debug = bool(to_int(self.get_cvar("lgbot_debug")))
|
self.debug = bool(to_int(self.get_cvar("lgbot_debug")))
|
||||||
self.target_bots = bool(to_int(self.get_cvar("lgbot_target_bots")))
|
self.target_bots = bool(to_int(self.get_cvar("lgbot_target_bots")))
|
||||||
self.record_on = bool(to_int(self.get_cvar("lgbot_record")))
|
self.record_mode = to_int(self.get_cvar("lgbot_record")) or 0
|
||||||
self.read_at = now
|
self.read_at = now
|
||||||
return self.settings
|
return self.settings
|
||||||
|
|
||||||
@@ -320,23 +332,16 @@ if minqlx:
|
|||||||
if self.debug:
|
if self.debug:
|
||||||
self.debug_line(now, vel, (vx, vy), tpos, bpos, want, direction)
|
self.debug_line(now, vel, (vx, vy), tpos, bpos, want, direction)
|
||||||
|
|
||||||
def record(self, now):
|
def stop_recording(self):
|
||||||
if not self.record_on:
|
|
||||||
if self.rec:
|
if self.rec:
|
||||||
self.rec.close()
|
self.rec.close()
|
||||||
self.rec = None
|
self.rec = None
|
||||||
return
|
|
||||||
if self.rec is None:
|
def record(self, now):
|
||||||
self.rec = open(REC_FILE, "a", buffering=1)
|
mode = self.record_mode
|
||||||
self.rec_start = now
|
if mode not in (1, 2):
|
||||||
self.rec.write("# start %d map %s\n" % (int(now), self.get_cvar("mapname")))
|
return self.stop_recording()
|
||||||
if now - self.rec_start > REC_MAX_S:
|
rows, humans = [], []
|
||||||
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:
|
for p in self.frame_players:
|
||||||
if p.team == "spectator":
|
if p.team == "spectator":
|
||||||
continue
|
continue
|
||||||
@@ -347,8 +352,25 @@ if minqlx:
|
|||||||
else:
|
else:
|
||||||
who, state = "h%d" % p.id, ""
|
who, state = "h%d" % p.id, ""
|
||||||
pos, vel = p.position(), p.velocity()
|
pos, vel = p.position(), p.velocity()
|
||||||
self.rec.write("%.3f,%s,%d,%d,%.0f,%.0f,%.0f,%.0f,%.0f,%.0f,%s\n" % (
|
if who != "bot" and p.is_alive:
|
||||||
t, who, int(bool(p.is_alive)), p.health, pos.x, pos.y, pos.z, vel.x, vel.y, vel.z, state))
|
humans.append((pos.x, pos.y, pos.z))
|
||||||
|
rows.append((who, int(bool(p.is_alive)), p.health, pos.x, pos.y, pos.z, vel.x, vel.y, vel.z, state))
|
||||||
|
if mode == 2 and not humans_fighting(humans):
|
||||||
|
return
|
||||||
|
if self.rec is None:
|
||||||
|
self.rec = open(REC_FILE, "a", buffering=1)
|
||||||
|
self.rec_bytes = os.path.getsize(REC_FILE)
|
||||||
|
self.rec_start = now
|
||||||
|
self.rec.write("# start %d map %s\n" % (int(now), self.get_cvar("mapname")))
|
||||||
|
if (mode == 1 and now - self.rec_start > REC_MAX_S) or self.rec_bytes > REC_MAX_BYTES:
|
||||||
|
self.set_cvar("lgbot_record", "0")
|
||||||
|
self.record_mode = 0
|
||||||
|
return self.stop_recording()
|
||||||
|
t = now - self.rec_start
|
||||||
|
for r in rows:
|
||||||
|
line = "%.3f,%s,%d,%d,%.0f,%.0f,%.0f,%.0f,%.0f,%.0f,%s\n" % ((t,) + r)
|
||||||
|
self.rec.write(line)
|
||||||
|
self.rec_bytes += len(line)
|
||||||
|
|
||||||
def debug_line(self, now, vel, cmd, tpos, bpos, want, direction):
|
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);
|
# cos = how well last frame's command survived the frame (1 = exactly our direction);
|
||||||
|
|||||||
+28
-21
@@ -2,7 +2,8 @@
|
|||||||
"""Compare how you and lgbot move, from a recording (set lgbot_record 1, play, set lgbot_record 0).
|
"""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]
|
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
|
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
|
import math, sys
|
||||||
|
|
||||||
@@ -40,25 +41,31 @@ def parse(lines):
|
|||||||
|
|
||||||
|
|
||||||
def fight_frames(session):
|
def fight_frames(session):
|
||||||
"""-> {"human": [...], "bot": [...]}, frames (t, x, y, z, vx, vy, vz, ox, oy, state) in time order."""
|
"""-> {who: [...]} ("bot", "h1", ...), frames (t, x, y, z, vx, vy, vz, ox, oy, state) in time order.
|
||||||
out = {"human": [], "bot": []}
|
Each alive player is measured against the nearest alive player in fight range (human or bot)."""
|
||||||
|
out = {}
|
||||||
for t in sorted(session):
|
for t in sorted(session):
|
||||||
rows = session[t]
|
alive = [(who, r) for who, r in session[t].items() if r[0]]
|
||||||
bot = rows.get("bot")
|
for who, me in alive:
|
||||||
if not bot or not bot[0]:
|
|
||||||
continue
|
|
||||||
best = None
|
best = None
|
||||||
for who, r in rows.items():
|
for other, r in alive:
|
||||||
if who == "bot" or not r[0]:
|
if other == who or abs(r[4] - me[4]) > FIGHT_DZ:
|
||||||
continue
|
continue
|
||||||
d = math.hypot(r[2] - bot[2], r[3] - bot[3])
|
d = math.hypot(r[2] - me[2], r[3] - me[3])
|
||||||
if best is None or d < best[0]:
|
if d <= FIGHT_DIST and (best is None or d < best[0]):
|
||||||
best = (d, r)
|
best = (d, r)
|
||||||
if best is None or best[0] > FIGHT_DIST or abs(best[1][4] - bot[4]) > FIGHT_DZ:
|
if best is not None:
|
||||||
continue
|
out.setdefault(who, []).append(
|
||||||
h = best[1]
|
(t,) + me[2:8] + (best[1][2], best[1][3], me[8]))
|
||||||
out["human"].append((t,) + h[2:8] + (bot[2], bot[3], ""))
|
return out
|
||||||
out["bot"].append((t,) + bot[2:8] + (h[2], h[3], bot[8]))
|
|
||||||
|
|
||||||
|
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
|
return out
|
||||||
|
|
||||||
|
|
||||||
@@ -143,7 +150,7 @@ ROWS = [("seconds", "fight time s"), ("hold_ms", "strafe hold ms p10/p50/p90"),
|
|||||||
|
|
||||||
|
|
||||||
def report(human, bot):
|
def report(human, bot):
|
||||||
lines = ["%-28s %-22s %s" % ("", "you", "lgbot")]
|
lines = ["%-28s %-22s %s" % ("", "humans", "lgbot")]
|
||||||
for k, label in ROWS:
|
for k, label in ROWS:
|
||||||
lines.append("%-28s %-22s %s" % (label, fmt(human.get(k)), fmt(bot.get(k))))
|
lines.append("%-28s %-22s %s" % (label, fmt(human.get(k)), fmt(bot.get(k))))
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
@@ -153,11 +160,11 @@ def main(argv):
|
|||||||
path = argv[1] if len(argv) > 1 else "home/lgbot-rec.csv"
|
path = argv[1] if len(argv) > 1 else "home/lgbot-rec.csv"
|
||||||
with open(path) as f:
|
with open(path) as f:
|
||||||
sessions = parse(f)
|
sessions = parse(f)
|
||||||
both = {"human": [], "bot": []}
|
per_player = {}
|
||||||
for s in sessions:
|
for s in sessions:
|
||||||
fr = fight_frames(s)
|
for who, frames in fight_frames(s).items():
|
||||||
for k in both:
|
per_player.setdefault(who, []).extend(frames) # t restarts each session: a gap
|
||||||
both[k].extend(fr[k]) # t restarts at 0 each session: the gap rule keeps them apart
|
both = by_kind(per_player)
|
||||||
print("%d session(s)" % len(sessions))
|
print("%d session(s)" % len(sessions))
|
||||||
print(report(analyze(both["human"]), analyze(both["bot"])))
|
print(report(analyze(both["human"]), analyze(both["bot"])))
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ set g_password "$(clean "$QL_PASSWORD")"
|
|||||||
set sv_maxClients "$(clean "${QL_MAXCLIENTS:-16}")"
|
set sv_maxClients "$(clean "${QL_MAXCLIENTS:-16}")"
|
||||||
set zmq_rcon_port "$(clean "${QL_RCON_PORT:-28960}")"
|
set zmq_rcon_port "$(clean "${QL_RCON_PORT:-28960}")"
|
||||||
set qlx_welcome "$(clean "$QL_WELCOME")"
|
set qlx_welcome "$(clean "$QL_WELCOME")"
|
||||||
|
set lgbot_record "$(clean "${QL_RECORD:-0}")"
|
||||||
map $MAP $FACTORY
|
map $MAP $FACTORY
|
||||||
EOF
|
EOF
|
||||||
echo "$MAP|$FACTORY" > $B/mappool_lg.txt
|
echo "$MAP|$FACTORY" > $B/mappool_lg.txt
|
||||||
|
|||||||
@@ -169,6 +169,35 @@ class Tick(unittest.TestCase):
|
|||||||
self.assertEqual(self.w.cvars["lgbot_record"], "0")
|
self.assertEqual(self.w.cvars["lgbot_record"], "0")
|
||||||
self.assertIsNone(self.plugin.rec)
|
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):
|
def test_no_target_means_no_steering(self):
|
||||||
self.human.team = "spectator"
|
self.human.team = "spectator"
|
||||||
self.run_frames(40)
|
self.run_frames(40)
|
||||||
|
|||||||
+23
-7
@@ -32,7 +32,7 @@ def still_at(x):
|
|||||||
class Stats(unittest.TestCase):
|
class Stats(unittest.TestCase):
|
||||||
def test_strafe_holds_and_sideways(self):
|
def test_strafe_holds_and_sideways(self):
|
||||||
s = r.parse(rec(10, adad(0.5), still_at(450)))
|
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.assertAlmostEqual(st["hold_ms"][1], 500, delta=30)
|
||||||
self.assertGreater(st["sideways_share"], 0.95)
|
self.assertGreater(st["sideways_share"], 0.95)
|
||||||
self.assertEqual(st["air_share"], 0)
|
self.assertEqual(st["air_share"], 0)
|
||||||
@@ -41,7 +41,7 @@ class Stats(unittest.TestCase):
|
|||||||
|
|
||||||
def test_bot_steer_share_and_stopped(self):
|
def test_bot_steer_share_and_stopped(self):
|
||||||
s = r.parse(rec(5, adad(0.5), still_at(450)))
|
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["steer_share"], 1)
|
||||||
self.assertEqual(st["stopped_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
|
def hop(t): # one jump a second: 0.7 s in the air, then ground
|
||||||
ph = t % 1.0
|
ph = t % 1.0
|
||||||
return (0, 0, 0, 0, 320, 270 - 800 * ph if ph < 0.7 else 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["jumps_per_s"], 1.0, delta=0.15)
|
||||||
self.assertAlmostEqual(st["air_share"], 0.7, delta=0.05)
|
self.assertAlmostEqual(st["air_share"], 0.7, delta=0.05)
|
||||||
|
|
||||||
def test_far_or_other_floor_is_not_fight_time(self):
|
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])
|
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])
|
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(far, {})
|
||||||
self.assertEqual(up["human"], [])
|
self.assertEqual(up, {})
|
||||||
|
|
||||||
def test_sessions_do_not_join(self):
|
def test_sessions_do_not_join(self):
|
||||||
lines = rec(2, adad(0.5), still_at(450)) + rec(2, adad(0.5), still_at(450))
|
lines = rec(2, adad(0.5), still_at(450)) + rec(2, adad(0.5), still_at(450))
|
||||||
sessions = r.parse(lines)
|
sessions = r.parse(lines)
|
||||||
self.assertEqual(len(sessions), 2)
|
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)
|
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):
|
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([])))
|
self.assertIn("strafe hold", r.report(st, r.analyze([])))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user