209 lines
7.3 KiB
Python
209 lines
7.3 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_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):
|
|
self.human.team = "spectator"
|
|
self.run_frames(40)
|
|
self.assertEqual(self.bot.sets, [])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|