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:
esefandClaude Opus 5.5 committed 2026-10-15 15:16:53 +03:00
1 parent a998614224
commit 86754715f5
6 files changed
+321 -30

No files matched your search

+89 -2
View File
@@ -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)