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:
co-authored by
Claude Opus 5
parent
fed1453e6e
commit
b1dde7908c
@@ -59,8 +59,20 @@ start:
|
||||
INIA 0x01
|
||||
OUTA 0x02 ; Key mode.
|
||||
|
||||
; ---- Is there a controller ----
|
||||
;
|
||||
; Asked once. If there is, the console's arrow keys are ignored: under a window the same
|
||||
; keypress reaches BOTH - the pad as a level and the console as a byte - and a thruster that
|
||||
; fired twice for one press would be a mystery to anybody tuning it.
|
||||
INA 0x64
|
||||
INIB 0x01
|
||||
AND
|
||||
SETD.1 HasPad
|
||||
STQ.1
|
||||
|
||||
everyFrame:
|
||||
CALL waitFrame
|
||||
CALL readPad
|
||||
CALL readControls
|
||||
CALL fall
|
||||
CALL move
|
||||
@@ -312,12 +324,23 @@ putLander:
|
||||
OUTA 0xE9 ; No flags, natural size, no depth.
|
||||
RET
|
||||
|
||||
; ---- The controls ----
|
||||
; ---- What the pad is holding ----
|
||||
;
|
||||
; ONE KEY IS ONE BURN. The console says which key was pressed and there is no such thing as a
|
||||
; key being released, so a thruster cannot be held down - what a press does is add to the
|
||||
; velocity once. It reads as pumping the engine rather than leaning on it, which is a choice
|
||||
; this machine makes rather than one this program did.
|
||||
; One read, every button at once, and it does not go away when it is looked at. THIS IS THE
|
||||
; THING THE CONSOLE CANNOT DO: a key that is down and staying down sends nothing, so a
|
||||
; thruster driven by the console can only be pumped and never leaned on.
|
||||
readPad:
|
||||
INA 0x60
|
||||
SETD.1 Held
|
||||
STA.1
|
||||
RET
|
||||
|
||||
; ---- The console, which is still worth reading ----
|
||||
;
|
||||
; For q, always, because a pad has no letter for it. And for the arrows on a machine with no
|
||||
; controller, where one press is one burn and that is the best that can be done - it reads as
|
||||
; pumping the engine, which is a thing this machine makes true rather than a thing this
|
||||
; program chose.
|
||||
readControls:
|
||||
INA 0x01
|
||||
INIB 0x01 ; READY
|
||||
@@ -325,12 +348,23 @@ readControls:
|
||||
BRQ noKey
|
||||
INA 0x00
|
||||
|
||||
SETD.1 KeyHeld
|
||||
STA.1
|
||||
|
||||
INIB 0x71 ; q, whatever else is plugged in.
|
||||
CCF
|
||||
SUB
|
||||
BRQ quit
|
||||
|
||||
SETD.0 HasPad
|
||||
LDA.0
|
||||
BNA noKey ; There is a pad, so the arrows are its business and not this.
|
||||
|
||||
LDA.1 ; DP1 is still KeyHeld, from storing the key above.
|
||||
INIB 0x80 ; Up
|
||||
CCF
|
||||
SUB
|
||||
BRQ burnUp
|
||||
SETD.1 KeyHeld
|
||||
STA.1
|
||||
LDA.1
|
||||
INIB 0x82 ; Left
|
||||
CCF
|
||||
@@ -341,11 +375,6 @@ readControls:
|
||||
CCF
|
||||
SUB
|
||||
BRQ burnRight
|
||||
LDA.1
|
||||
INIB 0x71 ; q
|
||||
CCF
|
||||
SUB
|
||||
BRQ quit
|
||||
noKey:
|
||||
RET
|
||||
|
||||
@@ -394,6 +423,41 @@ fall:
|
||||
SETD.0 SpeedDown
|
||||
SETD.1 Gravity
|
||||
CALL addWord
|
||||
|
||||
; ---- And whatever is being leaned on, on the same tick ----
|
||||
;
|
||||
; A held thruster fires here rather than every frame, for the reason gravity does: 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, and both are one byte to change.
|
||||
;
|
||||
; Position still moves every frame. Only the acceleration is stepped, which nothing can see.
|
||||
SETD.0 Held
|
||||
LDA.0
|
||||
INIB 0x08 ; Up
|
||||
AND
|
||||
BRQ fallNotUp
|
||||
SETD.0 SpeedDown
|
||||
SETD.1 PadUp
|
||||
CALL addWord
|
||||
fallNotUp:
|
||||
SETD.0 Held
|
||||
LDA.0
|
||||
INIB 0x02 ; Left
|
||||
AND
|
||||
BRQ fallNotLeft
|
||||
SETD.0 SpeedAcross
|
||||
SETD.1 PadLeft
|
||||
CALL addWord
|
||||
fallNotLeft:
|
||||
SETD.0 Held
|
||||
LDA.0
|
||||
INIB 0x01 ; Right
|
||||
AND
|
||||
BRQ fallDone
|
||||
SETD.0 SpeedAcross
|
||||
SETD.1 PadRight
|
||||
CALL addWord
|
||||
fallDone:
|
||||
RET
|
||||
|
||||
@@ -586,12 +650,30 @@ FallEvery:
|
||||
0d6 ; Gravity one frame in six. Every frame was Jupiter.
|
||||
FallTick:
|
||||
0d1
|
||||
; A press on a machine with no pad, which has to be a whole burn because it happens once.
|
||||
ThrustUp:
|
||||
0xF8 0xFF ; Eight sixteenths upwards, which is minus eight.
|
||||
ThrustLeft:
|
||||
0xFC 0xFF ; Four to the left.
|
||||
ThrustRight:
|
||||
0x04 0x00
|
||||
|
||||
; ---- And a held one, which happens on every tick it is held for ----
|
||||
;
|
||||
; TWO AGAINST GRAVITY'S ONE, which makes climbing and falling the same speed: hold it and you
|
||||
; rise as fast as letting go drops you. Three was the first try and it left the moon in about
|
||||
; a second of holding. The ratio between these and Gravity is the whole feel of the thing and
|
||||
; it is one byte each.
|
||||
PadUp:
|
||||
0xFE 0xFF ; Minus two.
|
||||
PadLeft:
|
||||
0xFE 0xFF ; Minus two.
|
||||
PadRight:
|
||||
0x02 0x00
|
||||
Held:
|
||||
0x00
|
||||
HasPad:
|
||||
0x00
|
||||
HalfScreen:
|
||||
0xA0 0x00 ; 160 pixels, which is half of a forty column screen.
|
||||
|
||||
|
||||
@@ -726,7 +726,7 @@ from every assembly file in it. Several are old programs written for the bare ma
|
||||
| Break | Stops itself twice with SWI osBreak, so that the registers can be seen changing between one stop and the next. |
|
||||
| Grid | The first program to use the screen as a screen. Redefines a tile above the font, fills all 128 map rows, and scrolls it diagonally a pixel at a time. |
|
||||
| Sprite | Moves a ball across the shell's own text, writing not one byte of the map to do it. It leaves the sprite in the table on the way out, because clearing them is the system's job - see below. |
|
||||
| Lander | Lunar Porter, rung one: a lander over a moon that wraps. The map's column origin is a ring in hardware, so 128 cells of it is 1024 pixels of surface with no edge and no seam. Position and velocity are sixteen bit in sixteenths of a pixel, which is what makes going all the way round an AND rather than a comparison. |
|
||||
| Lander | Lunar Porter, rung one: a lander over a moon that wraps. Flown with a controller if there is one - a held thruster burns every tick it is held for - and with the arrow keys if there is not, where one press is one burn and that is the most the console can say. |
|
||||
| Depth | Four pillars at four distances and a ball walking past them, behind the near ones and in front of the far ones. The ball is sprite nought and every pillar is numbered after it, so table order puts it in front of all four - what actually decides is the depth buffer, asked a column at a time. |
|
||||
| Flip | Draws a whole screen into the bank that is not being shown, waits, and then shows it in one byte out of one port. It writes nothing else at all - not a tile, not a colour - so it does not ask for the screen to be saved, and the line it printed is still there when it comes back. It deliberately does not put the displayed screen back either, because that is the system's to restore: a program that faulted while flipped could not have. |
|
||||
| Edit | A line editor. |
|
||||
|
||||
+18
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user