import math, os, random, sys, unittest sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "home", "minqlx-plugins")) from lgbot import (PRESETS, KEYS, resolve, Strafer, wanted_range, steer, fix_z, pick_target, to_int, is_bot, is_lgbot, StuckGuard) def preset(n, **kw): s = dict(PRESETS[n]); s.update(kw); return s class Resolve(unittest.TestCase): def test_preset_only(self): self.assertEqual(resolve(3, {}), PRESETS[3]) def test_minus_one_and_none_keep_preset(self): self.assertEqual(resolve(2, {k: -1 for k in KEYS}), PRESETS[2]) self.assertEqual(resolve(2, {"fake": None}), PRESETS[2]) def test_override_and_clamp(self): s = resolve(1, {"fake": 50, "range": 5000, "speed": 10}) self.assertEqual((s["fake"], s["range"], s["speed"]), (50, 750, 50)) def test_bad_move_level(self): self.assertEqual(resolve(9, {}), PRESETS[5]) self.assertEqual(resolve(0, {}), PRESETS[1]) self.assertEqual(resolve(None, {}), PRESETS[3]) def test_max_below_min(self): s = resolve(3, {"strafe_min": 800, "strafe_max": 200}) self.assertEqual((s["strafe_min"], s["strafe_max"]), (800, 800)) class StraferTest(unittest.TestCase): def changes(self, s, n=2000, seed=1): st, now, out = Strafer(random.Random(seed)), 0, [] d = st.step(now, s) while len(out) < n: now += 5 nd = st.step(now, s) if nd != d: out.append((now, nd)); d = nd return out def test_holds_within_range_without_fakes(self): s = preset(3, fake=0, strafe_min=250, strafe_max=600, lane=0) ch = self.changes(s, 300) gaps = [b[0] - a[0] for a, b in zip(ch, ch[1:])] self.assertTrue(all(245 <= g <= 610 for g in gaps), (min(gaps), max(gaps))) def test_fake_taps_are_short_and_return(self): s = preset(5, fake=100, strafe_min=300, strafe_max=300, lane=0) ch = self.changes(s, 200) gaps = [b[0] - a[0] for a, b in zip(ch, ch[1:])] # always faking: alternating short tap (60-120) and full hold (300), direction never really changes short = [g for g in gaps if g <= 125] self.assertTrue(len(short) >= 90) self.assertTrue(all(60 <= g <= 125 for g in short)) def test_fake_fraction_matches_setting(self): s = preset(3, fake=30, strafe_min=300, strafe_max=300, lane=0) st, now, fakes, decisions = Strafer(random.Random(7)), 0, 0, 0 st.step(0, s) while decisions < 2000: now += 5 was = st.faking before = st.until st.step(now, s) if st.until != before and not was: decisions += 1; fakes += st.faking self.assertAlmostEqual(fakes / decisions, 0.30, delta=0.04) def test_lane_stops_circling(self): # always faking = always drifting the same way; the lane must turn it back s = preset(5, fake=100, strafe_min=300, strafe_max=300, lane=300, speed=100) lane_ms = 300 / 320.0 * 1000 st, now, drift = Strafer(random.Random(3)), 0, 0 d = st.step(0, s) for _ in range(20000): now += 25 drift += d * 25 self.assertLessEqual(abs(drift), lane_ms + 50) d = st.step(now, s) class Range(unittest.TestCase): def test_plain(self): self.assertEqual(wanted_range(preset(1), 100, 100), 450) def test_back_off_when_low(self): self.assertEqual(wanted_range(preset(5), 50, 20), 700) def test_push_when_ahead(self): self.assertEqual(wanted_range(preset(5), 150, 100), 250) def test_even_fight(self): self.assertEqual(wanted_range(preset(5), 100, 90), 350) def test_range_play_off(self): self.assertEqual(wanted_range(preset(5, range_play=0), 50, 20), 350) class Steer(unittest.TestCase): def test_perpendicular_at_wanted_range(self): vx, vy = steer((0, 0), (400, 0), 1, preset(3), 400) self.assertAlmostEqual(vx, 0, places=3) self.assertAlmostEqual(abs(vy), 320, places=3) def test_direction_flips_side(self): a = steer((0, 0), (400, 0), 1, preset(3), 400) b = steer((0, 0), (400, 0), -1, preset(3), 400) self.assertAlmostEqual(a[1], -b[1], places=3) def test_speed_cap(self): for d in (50, 400, 1200): vx, vy = steer((0, 0), (d, 0), 1, preset(1), 450) self.assertLessEqual(math.hypot(vx, vy), 320 * 0.8 + 1e-6) def test_closes_in_when_far(self): vx, _ = steer((0, 0), (1000, 0), 1, preset(3), 400) self.assertGreater(vx, 100) def test_backs_off_when_close(self): vx, _ = steer((0, 0), (100, 0), 1, preset(3), 400) self.assertLess(vx, -100) def test_same_spot(self): vx, vy = steer((5, 5), (5, 5), 1, preset(3), 400) self.assertLessEqual(math.hypot(vx, vy), 320 + 1e-6) class Misc(unittest.TestCase): def test_fix_z(self): self.assertEqual(fix_z(275), 0) self.assertEqual(fix_z(-100), -100) self.assertEqual(fix_z(40), 40) # slopes/stairs self.assertEqual(fix_z(800), 800) # jump pads def test_is_bot(self): self.assertTrue(is_bot(90071996842377216)) # minqlx bot ids: base + client number self.assertTrue(is_bot(90071996842377216 + 5)) self.assertTrue(is_bot(0)) self.assertFalse(is_bot(76561198000000000)) # real SteamID64 def test_is_lgbot_needs_bot_id(self): self.assertTrue(is_lgbot("lgbot", 90071996842377216)) self.assertTrue(is_lgbot("LGBot", 90071996842377217)) self.assertFalse(is_lgbot("lgbot", 76561198000000000)) # a human calling themselves lgbot self.assertFalse(is_lgbot("hunter", 90071996842377216)) def test_to_int(self): self.assertEqual(to_int("5"), 5) self.assertEqual(to_int(" -1 "), -1) self.assertEqual(to_int("2.7"), 2) self.assertIsNone(to_int("abc")) self.assertIsNone(to_int(None)) def test_pick_target(self): c = [((300, 0, 0), "near"), ((800, 0, 0), "far")] self.assertEqual(pick_target((0, 0, 0), c), "near") self.assertIsNone(pick_target((0, 0, 0), [((1000, 0, 0), "x")])) # beyond LG reach: AI walks there self.assertIsNone(pick_target((0, 0, 0), [])) def test_pick_target_ignores_other_floors(self): c = [((200, 0, 300), "above"), ((600, 0, 40), "same floor")] self.assertEqual(pick_target((0, 0, 0), c), "same floor") self.assertIsNone(pick_target((0, 0, 0), [((200, 0, -300), "below")])) class Stuck(unittest.TestCase): def test_steers_while_moving(self): g = StuckGuard() self.assertTrue(all(g.update(t, 320, 300) for t in range(0, 3000, 25))) def test_lets_go_when_blocked_then_retries(self): g = StuckGuard() results = [(t, g.update(t, 320, 50)) for t in range(0, 4000, 25)] first_off = next(t for t, ok in results if not ok) self.assertTrue(450 <= first_off <= 550) back_on = next(t for t, ok in results if t > first_off and ok) self.assertTrue(1900 <= back_on - first_off + 500 <= 2100) # off for 1.5 s def test_short_bump_is_fine(self): g = StuckGuard() ok = [g.update(t, 320, 50 if 1000 <= t < 1300 else 300) for t in range(0, 3000, 25)] self.assertTrue(all(ok)) import json, tempfile import lgbot_setup as setup STATUS_WITH_BOT = """map: thunderstruck num score ping name lastmsg address qport rate steamid --- ----- ---- --------------- ------- --------------------- ----- ----- ----------------- 0 3 0 lgbot 0 bot 0 16384 0 1 7 48 player 0 100.64.1.2:27960 354 25000 76561198000000000 """ def feeder(*answers): it = iter(answers) def inp(prompt=""): a = next(it) if isinstance(a, BaseException): raise a return a return inp class State(unittest.TestCase): def setUp(self): self.dir = tempfile.mkdtemp() self.path = os.path.join(self.dir, "lgbot.json") def test_missing_file_gives_defaults(self): self.assertEqual(setup.load_state(self.path), {"aim": 3, "move": 3, "details": {}}) def test_corrupt_file_gives_defaults(self): with open(self.path, "w") as f: f.write("{not json") self.assertEqual(setup.load_state(self.path), {"aim": 3, "move": 3, "details": {}}) def test_bad_values_are_dropped(self): with open(self.path, "w") as f: json.dump({"aim": 9, "move": "x", "details": {"fake": 50, "range": 99999, "bogus": 1}}, f) self.assertEqual(setup.load_state(self.path), {"aim": 3, "move": 3, "details": {"fake": 50}}) def test_round_trip(self): st = {"aim": 1, "move": 5, "details": {"fake": 60, "range_play": 0}} setup.save_state(self.path, st) self.assertEqual(setup.load_state(self.path), st) class Parsing(unittest.TestCase): def test_parse_int(self): self.assertEqual(setup.parse_int("", 1, 5, 3), 3) self.assertEqual(setup.parse_int(" 4 ", 1, 5, 3), 4) self.assertIsNone(setup.parse_int("6", 1, 5, 3)) self.assertIsNone(setup.parse_int("abc", 1, 5, 3)) def test_parse_yn(self): self.assertTrue(setup.parse_yn("", True)) self.assertTrue(setup.parse_yn("Y", False)) self.assertFalse(setup.parse_yn("no", True)) self.assertIsNone(setup.parse_yn("maybe", True)) def test_ask_reasks_until_valid(self): said = [] v = setup.ask("Aim: ", lambda t: setup.parse_int(t, 1, 5, 3), feeder("x", "7", "2"), said.append) self.assertEqual(v, 2) self.assertEqual(len(said), 2) def test_human_named_lgbot_is_not_the_bot(self): status = STATUS_WITH_BOT.replace("lgbot 0 bot ", "lgbot 0 1.2.3.4:5") self.assertFalse(setup.bot_present(status)) self.assertTrue(setup.impostor(status)) self.assertFalse(setup.impostor(STATUS_WITH_BOT)) def test_cvar_value(self): reply = 'zmq RCON command from x: lgbot_fake' + chr(10) + '"lgbot_fake" is:"60" default:"-1"' self.assertEqual(setup.cvar_value(reply, "lgbot_fake"), "60") self.assertIsNone(setup.cvar_value("(no output: ok, or server down)", "lgbot_fake")) def test_bot_present(self): self.assertTrue(setup.bot_present(STATUS_WITH_BOT)) self.assertFalse(setup.bot_present(STATUS_WITH_BOT.replace("lgbot ", "Hunter"))) self.assertFalse(setup.bot_present("(no output)")) class Commands(unittest.TestCase): def test_no_details_resets_to_minus_one(self): cmds = setup.build_commands({"aim": 2, "move": 4, "details": {}}, readd=False, bot_in=True) self.assertEqual(cmds[0], "set lgbot_move 4") self.assertIn("set lgbot_fake -1", cmds) self.assertEqual(len(cmds), 1 + len(KEYS)) def test_details_and_readd(self): cmds = setup.build_commands({"aim": 1, "move": 5, "details": {"fake": 50}}, readd=True, bot_in=True) self.assertIn("set lgbot_fake 50", cmds) self.assertEqual(cmds[-2:], ["kick lgbot", "addbot lgbot 1"]) def test_readd_without_bot_does_not_kick(self): cmds = setup.build_commands({"aim": 4, "move": 3, "details": {}}, readd=True, bot_in=False) self.assertNotIn("kick lgbot", cmds) self.assertEqual(cmds[-1], "addbot lgbot 4") class Prompts(unittest.TestCase): def test_enter_keeps_everything(self): old = {"aim": 2, "move": 4, "details": {"fake": 70}} new = setup.prompt_setup(old, feeder("", "", "", "", "", "", "", "", "", ""), lambda *_: None) self.assertEqual(new, old) def test_poor_aim_best_movement_custom(self): new = setup.prompt_setup({"aim": 3, "move": 3, "details": {}}, feeder("1", "5", "y", "", "300", "60", "", "n", "90", "250"), lambda *_: None) self.assertEqual(new, {"aim": 1, "move": 5, "details": {"strafe_max": 300, "fake": 60, "range_play": 0, "speed": 90, "lane": 250}}) # only values that differ from the movement preset are stored as overrides def test_no_details_clears_them(self): new = setup.prompt_setup({"aim": 3, "move": 3, "details": {"fake": 70}}, feeder("", "", "n"), lambda *_: None) self.assertEqual(new["details"], {}) def test_ctrl_c_propagates(self): with self.assertRaises(KeyboardInterrupt): setup.prompt_setup({"aim": 3, "move": 3, "details": {}}, feeder("1", KeyboardInterrupt()), lambda *_: None) if __name__ == "__main__": unittest.main()