lgbot: movement plugin (strafe pattern, range control, no jumps)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
bc5bd09c15
commit
59be15ca2b
3 files changed
+320
-1
No files matched your search
@@ -0,0 +1,141 @@
|
||||
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)
|
||||
|
||||
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
|
||||
|
||||
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_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), "near"), ((900, 0), "far")]
|
||||
self.assertEqual(pick_target((0, 0), c), "near")
|
||||
self.assertIsNone(pick_target((0, 0), [((2000, 0), "x")]))
|
||||
self.assertIsNone(pick_target((0, 0), []))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user