lgbot: review fixes: drift reset after pauses, player-like acceleration, sticky targets with hysteresis, wall backoff; plugin glue tests
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
a998614224
commit
86754715f5
6 files changed
+321
-30
No files matched your search
+89
-2
@@ -1,7 +1,7 @@
|
||||
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)
|
||||
from lgbot import (PRESETS, KEYS, resolve, Strafer, wanted_range, steer, fix_z, pick_target, to_int, is_bot, is_lgbot, StuckGuard, approach, ACCEL)
|
||||
|
||||
|
||||
def preset(n, **kw):
|
||||
@@ -83,6 +83,40 @@ class StraferTest(unittest.TestCase):
|
||||
d = st.step(now, s)
|
||||
|
||||
|
||||
def test_gap_does_not_count_as_drift(self):
|
||||
# bot dead / out of range for 10 s: afterwards it must still strafe both ways (C1)
|
||||
s = preset(5)
|
||||
st, now = Strafer(random.Random(5)), 0
|
||||
for _ in range(80):
|
||||
now += 25; st.step(now, s)
|
||||
now += 10000
|
||||
dirs = []
|
||||
for _ in range(400):
|
||||
now += 25; dirs.append(st.step(now, s))
|
||||
self.assertLess(max(dirs.count(1), dirs.count(-1)) / 400.0, 0.75)
|
||||
|
||||
def test_reset_clears_drift(self):
|
||||
st = Strafer(random.Random(1))
|
||||
st.drift, st.last = 5000, 123
|
||||
st.reset()
|
||||
self.assertEqual((st.drift, st.last), (0, None))
|
||||
|
||||
|
||||
class Accel(unittest.TestCase):
|
||||
def test_limits_change_per_frame(self):
|
||||
v = approach((320, 0), (-320, 0), ACCEL * 0.025)
|
||||
self.assertAlmostEqual(v[0], 320 - ACCEL * 0.025, places=6)
|
||||
|
||||
def test_reaches_target_when_close(self):
|
||||
self.assertEqual(approach((300, 0), (320, 0), 80), (320, 0))
|
||||
|
||||
def test_reversal_takes_human_time(self):
|
||||
v, frames = (320.0, 0.0), 0
|
||||
while v[0] > -319:
|
||||
v = approach(v, (-320, 0), ACCEL * 0.025); frames += 1
|
||||
self.assertTrue(6 <= frames <= 10, frames) # ~150-250 ms, not one frame
|
||||
|
||||
|
||||
class Range(unittest.TestCase):
|
||||
def test_plain(self):
|
||||
self.assertEqual(wanted_range(preset(1), 100, 100), 450)
|
||||
@@ -158,7 +192,7 @@ class Misc(unittest.TestCase):
|
||||
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), [((950, 0, 0), "x")])) # too far to start steering
|
||||
self.assertIsNone(pick_target((0, 0, 0), []))
|
||||
|
||||
def test_pick_target_ignores_other_floors(self):
|
||||
@@ -166,6 +200,17 @@ class Misc(unittest.TestCase):
|
||||
self.assertEqual(pick_target((0, 0, 0), c), "same floor")
|
||||
self.assertIsNone(pick_target((0, 0, 0), [((200, 0, -300), "below")]))
|
||||
|
||||
def test_keeps_current_target_a_bit_past_the_start_limit(self):
|
||||
self.assertEqual(pick_target((0, 0, 0), [((950, 0, 0), "a")], current="a"), "a") # hysteresis
|
||||
self.assertIsNone(pick_target((0, 0, 0), [((1050, 0, 0), "a")], current="a"))
|
||||
|
||||
def test_sticky_target(self):
|
||||
c = [((500, 0, 0), "a"), ((420, 0, 0), "b")]
|
||||
self.assertEqual(pick_target((0, 0, 0), c, current="a"), "a") # b only 80 closer: stay
|
||||
c = [((500, 0, 0), "a"), ((300, 0, 0), "b")]
|
||||
self.assertEqual(pick_target((0, 0, 0), c, current="a"), "b") # clearly closer: switch
|
||||
self.assertEqual(pick_target((0, 0, 0), [((400, 0, 0), "b")], current="a"), "b") # a gone
|
||||
|
||||
|
||||
class Stuck(unittest.TestCase):
|
||||
def test_steers_while_moving(self):
|
||||
@@ -180,6 +225,29 @@ class Stuck(unittest.TestCase):
|
||||
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_backs_off_longer_when_it_keeps_failing(self):
|
||||
g = StuckGuard()
|
||||
offs, prev = [], True
|
||||
for t in range(0, 20000, 25):
|
||||
ok = g.update(t, 320, 50)
|
||||
if prev and not ok:
|
||||
offs.append(t)
|
||||
prev = ok
|
||||
gaps = [b - a for a, b in zip(offs, offs[1:])]
|
||||
self.assertGreater(gaps[1], gaps[0]) # 1.5 s, 3 s, 6 s ... between retries
|
||||
self.assertLessEqual(max(gaps), 6000 + 525)
|
||||
|
||||
def test_backoff_resets_after_clean_steering(self):
|
||||
g = StuckGuard()
|
||||
for t in range(0, 4000, 25):
|
||||
g.update(t, 320, 50) # two releases: streak up
|
||||
for t in range(4000, 12000, 25):
|
||||
g.update(t, 320, 300) # clean for a while
|
||||
res = [(t, g.update(t, 320, 50)) for t in range(12000, 16000, 25)]
|
||||
off = next(t for t, ok in res if not ok)
|
||||
back = next(t for t, ok in res if t > off and ok)
|
||||
self.assertEqual(back - off, 1500) # back to a 1.5 s release
|
||||
|
||||
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)]
|
||||
@@ -267,6 +335,25 @@ class Parsing(unittest.TestCase):
|
||||
self.assertFalse(setup.bot_present("(no output)"))
|
||||
|
||||
|
||||
class Main(unittest.TestCase):
|
||||
def test_refused_run_keeps_saved_setup(self):
|
||||
path = os.path.join(tempfile.mkdtemp(), "lgbot.json")
|
||||
impostor_status = STATUS_WITH_BOT.replace("lgbot 0 bot ", "lgbot 0 1.2.3.4:5")
|
||||
sent = []
|
||||
def run(cmd):
|
||||
sent.append(cmd)
|
||||
return impostor_status if cmd == "status" else ""
|
||||
old_state, old_server = setup.STATE, setup.server
|
||||
setup.STATE, setup.server = path, (lambda: run)
|
||||
try:
|
||||
rc = setup.main(["lgbot"], feeder("1", "5", "n"))
|
||||
finally:
|
||||
setup.STATE, setup.server = old_state, old_server
|
||||
self.assertNotEqual(rc, 0)
|
||||
self.assertFalse(os.path.exists(path))
|
||||
self.assertEqual(sent, ["status"]) # no kick, no set
|
||||
|
||||
|
||||
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)
|
||||
|
||||
Reference in new issue
Block a user