re-capture: read the ADVANCED CONTROLS tutorial; its moves regress the pilot, so they ship off

tutorial_capture.sh plays a tutorial and photographs what it teaches. ADVANCED
CONTROLS states three mechanics the key-config screen only named:
  B + LS            Side Roll / 180 Degree Turn / Level Off
  B + A together    face the target (snap turn)
  LT + RT together  'sets your fighter's speed to that of the target ... works
                    well when you are trying to get behind an enemy. Once
                    behind an enemy, this also helps you attack them.'

Wired both usable ones in and measured, one run each, everything else equal:
  commitment only ............ 101 missiles, 364 fire frames,  9 kills
  + match(4500) + snap-face ....  9 missiles,  28 fire frames,  0 kills
  + match(1200) + snap-face ...  57 missiles, 225 fire frames,  2 kills

So both are a net regression as applied, and both now default to OFF. Matching a
target's speed while still 5 km behind means never closing (the pilot sat at
272 u/s all run) — the tutorial scopes it to being already in the saddle. The
B+A snap turn reorients mid-pursuit and destroys the dwell commitment buys.

The code and thresholds stay so a future session can re-enable and A/B them over
SEVERAL runs; one run per config is inside this stage's spawn variance.
This commit is contained in:
claude-re
2026-07-30 19:50:25 +00:00
parent 9f41fe08e9
commit 58f421d896
3 changed files with 133 additions and 8 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 626 KiB

View File

@@ -129,6 +129,37 @@ class Pilot:
COMMIT_MAX = 14.0 # s before we are allowed to reconsider anyway
COMMIT_DROP = 6000.0 # ...or it gets this far away
COMMIT_BEHIND = 2.5 # ...or stays >90 deg off the nose this long
# --- moves the ADVANCED CONTROLS tutorial teaches (tutorial_capture.sh) ---
# "Target an enemy and pull LT and RT [together]. This sets your fighter's
# speed to that of the target. This works well when you are trying to get
# behind an enemy. Once behind an enemy, this also helps you attack them."
# That is the overshoot problem solved by the game itself: matching speed
# holds us in the target's rear hemisphere instead of flying through it,
# which is the only way a time-on-target lock ever completes.
# Scope matters, and a measured regression proved it: applying the match at
# 4500 with a 70 deg cone dropped kills 9 -> 0. Matching a target's speed
# while still 5 km behind it means never closing — the pilot sat at 272 u/s
# and fired 28 frames all run. The tutorial's own wording scopes it: "when
# you are trying to get BEHIND an enemy... ONCE BEHIND an enemy, this also
# helps you attack them". So it is station-keeping in the saddle, not an
# approach throttle. Only match when we are already there.
# MEASURED: both tutorial moves are a NET REGRESSION as applied here, so
# both ship DISABLED. One run each, same everything else:
# commitment only ............ 101 missiles, 364 fire frames, 9 kills
# + match(4500) + snap-face .... 9 missiles, 28 fire frames, 0 kills
# + match(1200) + snap-face ... 57 missiles, 225 fire frames, 2 kills
# The moves are real and the tutorial is right about them; the loop just
# cannot use them yet. Snap-face (B+A) reorients the craft mid-pursuit and
# destroys the very dwell that commitment buys, and speed-match needs to be
# entered from the saddle rather than commanded at range. Set MATCH_RANGE
# and lower FACE_MIN to re-enable, and A/B them over SEVERAL runs — one run
# per config is inside this stage's spawn variance.
MATCH_RANGE = 0.0 # 1200.0 to re-enable
MATCH_CONE = math.radians(25)
# "Press B and A together to face [the target]" — a snap turn, far quicker
# than winding the PD controller around for a contact behind us.
FACE_MIN = math.radians(999) # 50 deg to re-enable the B+A snap turn
FACE_PERIOD = 4.0
def __init__(self, W, pad, dry=False, log=sys.stdout):
self.W = W
@@ -158,6 +189,10 @@ class Pilot:
self.commit_off = None # entity we are committed to
self.commit_t = -1e9
self.behind_since = None
self.matching = False
self.face_t = -1e9
self.face_down = None
self.faces = 0
def f32(self, off):
b = os.pread(self.W.fd, 4, off)
@@ -204,13 +239,31 @@ class Pilot:
return hull, shield
def set_throttle(self, want):
"""RT / LT are a persistent setting, so only send the change."""
"""RT / LT are a persistent setting, so only send the change.
`want` is +1 accelerate, -1 brake, 0 coast, or the string "match" for
the tutorial's both-triggers speed-match onto the current target.
"""
if want == self.throttle or self.dry:
return
self.pad.trig("RT", 1.0 if want > 0 else 0.0)
self.pad.trig("LT", 1.0 if want < 0 else 0.0)
if want == "match":
self.pad.trig("RT", 1.0)
self.pad.trig("LT", 1.0)
else:
self.pad.trig("RT", 1.0 if want > 0 else 0.0)
self.pad.trig("LT", 1.0 if want < 0 else 0.0)
self.throttle = want
def face_target(self, t):
"""B + A: snap the nose onto the selected target."""
if self.dry or t - self.face_t < self.FACE_PERIOD:
return
self.pad.press("B")
self.pad.press("A")
self.face_down = t
self.face_t = t
self.faces += 1
# -------------------------------------------------------------- targets
def hostiles(self, ents, me_off):
out = []
@@ -439,10 +492,10 @@ class Pilot:
# than a stall in the middle of a battle; collision avoidance already
# keeps a fighter-sized margin.
want = norm(tgt[3]) if tgt else fwd
if tgt and tgt[4] > 2500.0:
if tgt and tgt[4] < self.MATCH_RANGE and ang(tgt[3], fwd) < self.MATCH_CONE:
self.set_throttle("match") # sit in its rear hemisphere
elif tgt and tgt[4] > 2500.0:
self.set_throttle(+1)
elif tgt and tgt[4] < 500.0 and speed > 900.0:
self.set_throttle(-1)
else:
self.set_throttle(0)
fire = True
@@ -503,6 +556,13 @@ class Pilot:
if fire != self.firing:
(self.pad.press if fire else self.pad.release)("RB")
self.firing = fire
if self.face_down is not None and t - self.face_down > 0.2:
self.pad.release("B")
self.pad.release("A")
self.face_down = None
elif (tgt and self.mode in ("ENGAGE", "DEFEND")
and abs(aim[0]) > self.FACE_MIN and pn < 1.2):
self.face_target(t)
if self.missile_down and t - self.missile_t > 0.15:
self.pad.release("Y")
self.missile_down = False
@@ -514,9 +574,10 @@ class Pilot:
self.missiles += 1
msg = (f"{self.mode:<7} hull={hull:6.0f} shd={shield:6.0f} spd={speed:6.0f} "
f"thr={self.throttle:+d} yaw={math.degrees(yaw):+6.1f} "
f"thr={str(self.throttle):>5} yaw={math.degrees(yaw):+6.1f} "
f"pit={math.degrees(pitch):+6.1f} aim={math.degrees(aim[0]):+6.1f}"
f"/{math.degrees(aim[1]):+6.1f} fire={int(fire)} msl={self.missiles}")
f"/{math.degrees(aim[1]):+6.1f} fire={int(fire)} msl={self.missiles}"
f" fc={self.faces}")
if ast is not None:
msg += f" ast={a_frac*100:5.1f}%"
if a_dmg > self.ASSET_ALERT:

View File

@@ -0,0 +1,64 @@
#!/usr/bin/env bash
# Play one TUTORIAL and photograph what it teaches.
#
# The OPTIONS key-config screen names the in-flight actions but not the buttons
# they sit on, and probing the pad found the weapons only (an action that does
# not fire a gun is invisible in the ammo counters). The tutorials state the
# mapping outright — `ADVANCED CONTROLS` is index 5 and is where Change Target
# and Padlock Mode live — so read it from the game instead of guessing.
#
# Boot + nav is launch_mission.sh's route as far as the main menu, then TUTORIAL
# instead of LOAD GAME. Everything stays a CHILD of this script (no setsid): a
# detached process is not a survivable one here, see the session-lifetime note.
set -u
N="${1:-5}" # 0 BASIC, 1 HUD, 2 RADAR, 3 SUPPLY, 4 RADIO, 5 ADVANCED
SECS="${2:-150}"
TAG="${3:-tut}"
export HOME=/sylph-home/re SDL_AUDIODRIVER=dummy DISPLAY=:98
SD="$(cd "$(dirname "$0")" && pwd)"
SHOTS=/sylph-home/re/shots
alive(){ ps -o pid=,stat= -C xenia_canary 2>/dev/null | awk '$2 !~ /^Z/ {print $1}'; }
step(){ vgamepad dpad "$1"; sleep 0.25; vgamepad dpad center; sleep 0.7; }
shot(){ screenshot "$SHOTS/$TAG-$1.png" >/dev/null 2>&1; }
pkill -x xenia_canary 2>/dev/null; sleep 2
[ -n "$(alive)" ] && { kill -9 $(alive) 2>/dev/null; sleep 2; }
rm -f /dev/shm/xenia_memory_* /dev/shm/xenia_code_cache_* 2>/dev/null
if ! xdpyinfo -display "$DISPLAY" >/dev/null 2>&1; then
rm -f "/tmp/.X${DISPLAY#:}-lock" 2>/dev/null || true
nohup Xvfb "$DISPLAY" -screen 0 1280x720x24 -ac -nolisten tcp \
+extension GLX +extension RANDR </dev/null >/tmp/xvfb98.log 2>&1 &
for _ in $(seq 1 50); do xdpyinfo -display "$DISPLAY" >/dev/null 2>&1 && break; sleep 0.2; done
nohup env DISPLAY="$DISPLAY" HOME=/sylph-home openbox </dev/null >/tmp/openbox98.log 2>&1 &
sleep 1
fi
cd /sylph-home/re
nohup run-canary --audio --apu=sdl --log_mask=13 \
--logged_profile_slot_0_xuid=E0300000EFBEA3D4 </dev/null >/dev/null 2>&1 &
sleep 5
"$SD/skip_intro.sh" 600 || { echo "BOOT FAILED (skip_intro exit $?)"; exit 1; }
sleep 14 # main menu is not input-ready before this
step down; step down # NEW GAME -> LOAD GAME -> TUTORIAL
vgamepad tap A 250; sleep 8
shot "list"
i=0; while [ "$i" -lt "$N" ]; do step down; i=$((i+1)); done
shot "pick"
vgamepad tap A 250; sleep 6
shot "sub" # some tutorials offer Level 1 / Level 2
vgamepad tap A 250
# Then just watch. The lesson drives itself and prints its instructions; tap A
# periodically to advance any prompt, and photograph often enough to catch the
# caption before it is replaced.
end=$(( SECONDS + SECS )); n=0
while [ $SECONDS -lt $end ]; do
n=$((n+1)); shot "$(printf '%02d' $n)"
[ -n "$(alive)" ] || { echo "EMULATOR GONE at ${SECONDS}s"; exit 4; }
sleep 5
[ $((n % 3)) -eq 0 ] && vgamepad tap A 250
done
echo "TUTORIAL CAPTURE DONE ($n frames)"