Files
ql-lg-server/tools/test_lgbot_plugin.py
T

180 lines
6.1 KiB
Python

"""The lgbot plugin's frame logic (tick) against a fake minqlx: deaths, gaps, targets, acceleration."""
import importlib.util, logging, math, os, sys, types, unittest
PLUGIN = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "home", "minqlx-plugins", "lgbot.py")
BOT_ID = 90071996842377216
HUMAN_ID = 76561198000000000
class V:
def __init__(self, x, y, z):
self.x, self.y, self.z = x, y, z
class FakePlayer:
def __init__(self, cid, name, steam_id, pos):
self.id, self.clean_name, self.steam_id = cid, name, steam_id
self.team, self.is_alive, self.health = "free", True, 100
self.pos, self.vel = list(pos), [0.0, 0.0, 0.0]
self.sets = []
def position(self):
return V(*self.pos)
def velocity(self, **kw):
if kw:
self.vel = [kw.get("x", self.vel[0]), kw.get("y", self.vel[1]), kw.get("z", self.vel[2])]
self.sets.append(tuple(self.vel))
return True
return V(*self.vel)
class World:
"""Stands in for the server: players, cvars, a clock; moves players by their velocity."""
def __init__(self):
self.players, self.cvars, self.now = [], {}, 1000.0
def frame(self, plugin, dt=0.025):
self.now += dt
plugin.handle_frame()
for p in self.players:
p.pos[0] += p.vel[0] * 0.85 * dt # friction eats ~15%, like the measured server
p.pos[1] += p.vel[1] * 0.85 * dt
p.vel[0] *= 0.85
p.vel[1] *= 0.85
def load(world):
fake = types.ModuleType("minqlx")
class Plugin:
def __init__(self):
self.logger = logging.getLogger("lgbot-test")
def set_cvar_once(self, name, value):
world.cvars.setdefault(name, value)
def get_cvar(self, name):
return world.cvars.get(name)
def set_cvar(self, name, value):
world.cvars[name] = value
def add_hook(self, event, fn):
pass
def players(self):
return list(world.players)
def log_exception(plugin=None):
raise # surface plugin errors in the test
fake.Plugin, fake.log_exception = Plugin, log_exception
sys.modules["minqlx"] = fake
try:
spec = importlib.util.spec_from_file_location("lgbot_under_test", PLUGIN)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
finally:
del sys.modules["minqlx"]
mod.time = types.SimpleNamespace(time=lambda: world.now)
plugin = mod.lgbot()
plugin.strafer.rng.seed(11) if hasattr(plugin.strafer.rng, "seed") else None
return mod, plugin
def sideways_sign(bot, human):
# sign of the bot's velocity across the bot->human line
dx, dy = human.pos[0] - bot.pos[0], human.pos[1] - bot.pos[1]
return 1 if (-dy * bot.vel[0] + dx * bot.vel[1]) > 0 else -1
class Tick(unittest.TestCase):
def setUp(self):
self.w = World()
self.bot = FakePlayer(0, "lgbot", BOT_ID, (0, 0, 0))
self.human = FakePlayer(1, "player", HUMAN_ID, (450, 0, 0))
self.w.players = [self.bot, self.human]
self.w.cvars["lgbot_move"] = "5"
self.mod, self.plugin = load(self.w)
def run_frames(self, n):
for _ in range(n):
self.w.frame(self.plugin)
def test_steers_when_human_in_range(self):
self.run_frames(40)
self.assertTrue(self.bot.sets)
def test_human_named_lgbot_is_left_alone(self):
self.bot.steam_id = HUMAN_ID
self.run_frames(40)
self.assertEqual(self.bot.sets, [])
def test_strafes_both_ways_after_respawn(self):
self.run_frames(80)
self.bot.is_alive = False
self.run_frames(400) # 10 s dead
self.bot.is_alive = True
self.bot.pos = [0, 0, 0]
signs = []
for _ in range(400):
self.w.frame(self.plugin)
signs.append(sideways_sign(self.bot, self.human))
self.assertLess(max(signs.count(1), signs.count(-1)) / 400.0, 0.75)
def test_strafes_both_ways_after_walking_in(self):
self.human.pos = [3000, 0, 0] # far: AI walks
self.run_frames(400)
self.human.pos = [450, 0, 0]
signs = []
for _ in range(400):
self.w.frame(self.plugin)
signs.append(sideways_sign(self.bot, self.human))
self.assertLess(max(signs.count(1), signs.count(-1)) / 400.0, 0.75)
def test_velocity_changes_like_a_player_not_instantly(self):
self.run_frames(400)
sets = self.bot.sets
comp = self.mod.FRICTION_COMP
biggest = max(math.hypot(b[0] - a[0] * 0.85, b[1] - a[1] * 0.85) for a, b in zip(sets, sets[1:]))
# per frame: at most the acceleration step (plus friction compensation), never a 640 u/s flip
self.assertLess(biggest, self.mod.ACCEL * 0.025 * comp + 0.15 * 320 * comp + 1)
def test_recorder_writes_rows_without_names(self):
import tempfile
path = os.path.join(tempfile.mkdtemp(), "rec.csv")
self.mod.REC_FILE = path
self.w.cvars["lgbot_record"] = "1"
self.run_frames(80)
self.w.cvars["lgbot_record"] = "0"
self.w.now += 1.1 # cvars are reread once a second
self.run_frames(2)
with open(path) as f:
text = f.read()
rows = [l.split(",") for l in text.splitlines() if not l.startswith("#")]
self.assertGreater(len(rows), 100)
self.assertEqual({row[1] for row in rows}, {"bot", "h1"})
self.assertIn("steer", {row[10] for row in rows})
self.assertNotIn(str(HUMAN_ID), text)
self.assertIsNone(self.plugin.rec)
def test_recorder_stops_itself(self):
import tempfile
self.mod.REC_FILE = os.path.join(tempfile.mkdtemp(), "rec.csv")
self.mod.REC_MAX_S = 1
self.w.cvars["lgbot_record"] = "1"
self.run_frames(80)
self.assertEqual(self.w.cvars["lgbot_record"], "0")
self.assertIsNone(self.plugin.rec)
def test_no_target_means_no_steering(self):
self.human.team = "spectator"
self.run_frames(40)
self.assertEqual(self.bot.sets, [])
if __name__ == "__main__":
unittest.main()