Lunar Porter flies on a controller

A held thruster burns every tick it is held for, which is the whole reason
the pad exists: the console can only say a key went down, so a thruster
driven by it could be pumped and never leaned on.

The burn happens on the same tick gravity does, and for the same reason -
a sixteenth of a pixel is the smallest step this arithmetic takes, and
applied sixty times a second it is an enormous acceleration. On the tick,
thrust and gravity are two numbers whose RATIO is the whole feel of the
thing. Position still moves every frame; only the acceleration is stepped,
and nothing can see that.

Two against gravity's one, so climbing and falling are the same speed.
Three was the first try and it left the moon after about a second of
holding.

If there is a pad the console's arrows are ignored, because under a window
the same keypress reaches both - the pad as a level, the console as a byte
- and a thruster that fired twice for one press would be a mystery to
anybody tuning it. q still quits, since a pad has no letter for it. With
no pad the arrows still burn once a press, which is the most that can be
done down a wire.

And break.sh now rebuilds the disk images as well as the binaries. Half
the things worth breaking here are SplitBit assembly rather than C, and
those live on the fixture disks - so an edit to a .asm file changed
nothing the suite could see, and the tool reported that nothing caught the
break. That is the exact lie it was written to prevent, turning up in a
new place. With the disks rebuilt it catches this one: the lander falls
between the two captures instead of climbing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
Anachronaut
2026-09-02 21:58:31 -04:00
co-authored by Claude Opus 5
parent fed1453e6e
commit b1dde7908c
14 changed files with 154 additions and 24 deletions
+18 -1
View File
@@ -44,7 +44,10 @@ FILE="$1"; ANCHOR="$2"; REPLACEMENT="$3"; shift 3
# working tree that looks fine is worse than any failing test.
KEEP="$(mktemp)"
cp "$FILE" "$KEEP"
restore() { cp "$KEEP" "$FILE"; rm -f "$KEEP"; (cd "$ROOT" && make >/dev/null 2>&1); }
restore() {
cp "$KEEP" "$FILE"; rm -f "$KEEP"
(cd "$ROOT" && make >/dev/null 2>&1 && ./Tests/makedisks.sh Tests/build >/dev/null 2>&1)
}
trap restore EXIT INT TERM
# ---- The edit, and proof it happened ----
@@ -63,6 +66,12 @@ open(path, "w").write(text.replace(anchor, replacement))
PY
# ---- The build, and proof of that too ----
#
# And the disk images after it, because half the things worth breaking are SplitBit assembly
# rather than C, and those live on the fixture disks rather than in the binaries. Rebuilding
# only the emulator meant an edit to a .asm file changed nothing the suite could see and the
# tool reported that nothing caught the break - which is the exact lie it exists to prevent,
# turning up in a new place.
if ! (cd "$ROOT" && make) > "$KEEP.build" 2>&1; then
printf '%sbreak.sh: the broken version does not build, so nothing was tested%s\n' \
"$RED" "$RESET" >&2
@@ -72,6 +81,14 @@ if ! (cd "$ROOT" && make) > "$KEEP.build" 2>&1; then
fi
rm -f "$KEEP.build"
if ! (cd "$ROOT" && ./Tests/makedisks.sh Tests/build) > "$KEEP.disks" 2>&1; then
printf '%sbreak.sh: the disks did not build, so nothing was tested%s\n' "$RED" "$RESET" >&2
tail -5 "$KEEP.disks" >&2
rm -f "$KEEP.disks"
exit 2
fi
rm -f "$KEEP.disks"
# ---- And then the suites ----
#
# A suite that fails is the GOOD outcome here, so the exit status is inverted: this reports
+1 -1
View File
@@ -25,7 +25,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 1009
Lander.sbx 1118
Crash.sbx 632
vars.script 50
blocks.script 343
+1 -1
View File
@@ -15,7 +15,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 1009
Lander.sbx 1118
Crash.sbx 632
vars.script 50
blocks.script 343
+1 -1
View File
@@ -32,7 +32,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 1009
Lander.sbx 1118
Crash.sbx 632
vars.script 50
blocks.script 343
+1 -1
View File
@@ -22,7 +22,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 1009
Lander.sbx 1118
Crash.sbx 632
vars.script 50
blocks.script 343
+1 -1
View File
@@ -22,7 +22,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 1009
Lander.sbx 1118
Crash.sbx 632
vars.script 50
blocks.script 343
+1 -1
View File
@@ -108,7 +108,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 1009
Lander.sbx 1118
Crash.sbx 632
vars.script 50
blocks.script 343
+1 -1
View File
@@ -27,7 +27,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 1009
Lander.sbx 1118
Crash.sbx 632
vars.script 50
blocks.script 343
+1 -1
View File
@@ -15,7 +15,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 1009
Lander.sbx 1118
Crash.sbx 632
vars.script 50
blocks.script 343
+1 -1
View File
@@ -13,7 +13,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 1009
Lander.sbx 1118
Crash.sbx 632
vars.script 50
blocks.script 343
+1 -1
View File
@@ -22,7 +22,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 1009
Lander.sbx 1118
Crash.sbx 632
vars.script 50
blocks.script 343
+31
View File
@@ -1568,6 +1568,37 @@ EOT
&& result ok "and a lander in the middle of it" "all forty pixels, at the screen's centre" \
|| result no "and a lander in the middle of it" "$SHIP pixels, x $SHIPLEFT to $SHIPRIGHT"
# ---- And the same lander, on a controller ----
#
# A pad reports what is HELD, so the thruster can be leaned on rather than pumped - which is
# the whole reason the device exists and the one thing the console cannot express. The
# recording holds nothing for forty frames and then holds Up.
#
# What is checked is that it is HIGHER LATER: two captures, the second further on, and the
# lander nearer the top in the second. A thruster that only fired once for a held button
# would let it fall between them instead, which is exactly what the console does.
python3 -c "open('$BUILD/lander.pad','wb').write(b'\x00' * 40 + b'\x08' * 400)"
for when in 800000 1600000; do
timeout 30 "$EMU" --fast --cycles $when --keyboard "$BUILD/lander.keys" \
--pad "$BUILD/lander.pad" --screen "$BUILD/held$when.ppm" \
--disk "$ROOT/Tests/build/disks/cosmos.img" --ram-disk 2048 \
"$BUILD/cosmos.bin" > "$BUILD/held.out" 2>&1 || true
done
read -r EARLY LATE <<EOT
$(python3 -c "
def top(path):
d = open(path, 'rb').read()
px = d[d.index(b'255\n') + 4:]
ship = bytes.fromhex('d8c048')
ys = [i // 320 for i in range(len(px) // 3) if px[i * 3:i * 3 + 3] == ship]
return min(ys) if ys else -1
print(top('$BUILD/held800000.ppm'), top('$BUILD/held1600000.ppm'))
" 2>/dev/null || echo "-1 -1")
EOT
[ "$EARLY" -gt 0 ] && [ "$LATE" -gt 0 ] && [ "$LATE" -lt "$EARLY" ] \
&& result ok "a held thruster keeps lifting" "row $EARLY early, row $LATE later" \
|| result no "a held thruster keeps lifting" "row $EARLY early, row $LATE later"
# ---- Clearing puts the cursor back at the top ----
#
# A screen with nothing on it and a cursor half way down it is not a cleared screen. This