#!/usr/bin/env python3 """Fly the Delta Saber toward the tutorial waypoint by chasing the yellow off-screen direction arrow. Detection: no PIL/numpy in the box, so the frame comes from `convert ... txt:-`. Two things make it fast AND correct: * `-sample` (point sampling, not `-resize`/`-scale` averaging) — a 1-px-thin glyph keeps its true colour, so the exact colour predicate still fires while the dump shrinks ~9x (3.5s -> 0.65s). * shape, not just colour — the instruction-frame bars and the throttle marker are the same dim yellow, so the arrow is the largest blob that is roughly as tall as it is wide, with the fixed throttle marker blacklisted by position. A ~1.1 s control loop is fast enough to converge; the earlier ~6 s loop was not. """ import re, subprocess, sys, time X0, Y0, W, H = 80, 60, 820, 640 # flight view (the arrow also rides the bottom edge) K = 3.03 # 1/0.33 sample factor CX, CY = 640, 405 # crosshair THROTTLE = (382, 456) # fixed yellow decoy on the speed gauge PX = re.compile(r"^(\d+),(\d+):.*?srgb\((\d+),(\d+),(\d+)\)") def pad(*a): subprocess.run(["/opt/sylph/vgamepad.py", *a], check=False) def arrow(png="/tmp/ap.png"): subprocess.run(["/opt/sylph/screenshot.sh", png], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) out = subprocess.run(["convert", png, "-crop", f"{W}x{H}+{X0}+{Y0}", "+repage", "-sample", "33%", "txt:-"], capture_output=True, text=True).stdout pts = set() for l in out.splitlines()[1:]: m = PX.match(l) if not m: continue x, y, r, g, b = (int(v) for v in m.groups()) if r >= 60 and g >= 48 and b <= 0.35 * g and (r - g) <= 0.45 * r: pts.add((x, y)) seen, best = set(), None for p in pts: if p in seen: continue st, c = [p], [] seen.add(p) while st: x, y = st.pop(); c.append((x, y)) for dx in (-1, 0, 1): for dy in (-1, 0, 1): q = (x + dx, y + dy) if q in pts and q not in seen: seen.add(q); st.append(q) xs = [q[0] for q in c]; ys = [q[1] for q in c] cx, cy = X0 + sum(xs) / len(c) * K, Y0 + sum(ys) / len(c) * K if abs(cx - THROTTLE[0]) < 30 and abs(cy - THROTTLE[1]) < 30: continue if len(c) < 8: continue if best is None or len(c) > best[2]: best = (cx, cy, len(c)) return best def main(): steps = int(sys.argv[1]) if len(sys.argv) > 1 else 25 centred = 0 for i in range(steps): a = arrow() if a is None: print(f"{i:2d}: no arrow -> boost (target ahead)") pad("trig", "RT", "0.55"); time.sleep(1.2); pad("trig", "RT", "0") centred += 1 if centred >= 6: print(" arrival likely — stopping"); return 0 continue x, y, n = a dx, dy = x - CX, y - CY if abs(dx) < 70 and abs(dy) < 70: centred += 1 print(f"{i:2d}: arrow ({x:.0f},{y:.0f}) centred -> boost") pad("trig", "RT", "0.55"); time.sleep(1.2); pad("trig", "RT", "0") continue centred = 0 lx = max(-1.0, min(1.0, dx / 200)) ly = max(-1.0, min(1.0, dy / 160)) print(f"{i:2d}: arrow ({x:.0f},{y:.0f}) n={n} -> LX {lx:+.2f} LY {ly:+.2f}") pad("axis", "LX", f"{lx:.2f}"); pad("axis", "LY", f"{ly:.2f}") time.sleep(0.45) pad("axis", "LX", "0"); pad("axis", "LY", "0") print("steps exhausted") return 1 sys.exit(main())