Lunar Porter, rung one: it flies

A lander over a moon that wraps. Landing, crashing, fuel, cargo and bases
are not here - this rung exists to answer whether it FEELS right, because
everything after it is bookkeeping and none of it is worth building on a
lander that is no fun to fly.

The moon comes for free. The map's column origin is a ring in hardware, so
128 cells is 1024 pixels of surface with no edge and no seam to cross.

Position and velocity are sixteen bit in SIXTEENTHS OF A PIXEL, and the
unit is the design: gravity is a small number added to a velocity and a
velocity is a number added to a position, with no multiply or divide
anywhere. 1024 pixels is 16,384 sixteenths, which is 2^14 - so going all
the way round is an AND with 0x3FFF rather than a comparison, and it is
never wrong at the seam.

The lander never moves sideways. The world scrolls under it and it sits at
the middle of the screen, which is a byte a frame instead of two and is
also what makes the wrap invisible: there is no moment where it jumps.

One key is one burn. The console says which key went down and there is no
such thing as a key coming up, so a thruster cannot be held - a press adds
to the velocity once. That is a property of the machine rather than a
choice this program made, and it reads as pumping the engine.

Four bugs found by running it, all worth keeping written down:

  B CANNOT BE A LOOP COUNTER here. Every comparison is an INIB, so the
  count was overwritten by whichever bound was last tested and the loop
  reset itself for ever. Counters that outlive arithmetic live in memory.

  A subroutine answers in Q, and the AND after it read A. The moon came
  out flat because it was testing the height against 1 instead of the
  random number.

  Row minus height, not height minus row: they are equal at the surface,
  equal does not borrow, and the surface row has to be ground.

  And the shift register has A as its HIGH half. Written the other way,
  the view scrolled by 256 cells for every one it should have.

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 16:52:01 -04:00
co-authored by Claude Opus 5
parent 5222c85100
commit d6c81fa32c
14 changed files with 664 additions and 10 deletions
+2 -1
View File
@@ -25,6 +25,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 984
Crash.sbx 632
vars.script 50
blocks.script 343
@@ -41,7 +42,7 @@ outer.script 376
inner.script 44
loop.script 35
crossed.txt 560
28 files, 1 directory
29 files, 1 directory
> exit
halted
Execution halted.
+2 -1
View File
@@ -15,6 +15,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 984
Crash.sbx 632
vars.script 50
blocks.script 343
@@ -30,7 +31,7 @@ nonl.script 38
outer.script 376
inner.script 44
loop.script 35
27 files, 1 directory
28 files, 1 directory
> drive 1
> dir
other.txt 28
+2 -1
View File
@@ -32,6 +32,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 984
Crash.sbx 632
vars.script 50
blocks.script 343
@@ -47,7 +48,7 @@ nonl.script 38
outer.script 376
inner.script 44
loop.script 35
27 files, 1 directory
28 files, 1 directory
> exit
halted
Execution halted.
+2 -1
View File
@@ -22,6 +22,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 984
Crash.sbx 632
vars.script 50
blocks.script 343
@@ -37,7 +38,7 @@ nonl.script 38
outer.script 376
inner.script 44
loop.script 35
27 files, 1 directory
28 files, 1 directory
> exit
halted
Execution halted.
+2 -1
View File
@@ -22,6 +22,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 984
Crash.sbx 632
vars.script 50
blocks.script 343
@@ -37,7 +38,7 @@ nonl.script 38
outer.script 376
inner.script 44
loop.script 35
27 files, 1 directory
28 files, 1 directory
> exit
halted
Execution halted.
+2 -1
View File
@@ -108,6 +108,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 984
Crash.sbx 632
vars.script 50
blocks.script 343
@@ -123,7 +124,7 @@ nonl.script 38
outer.script 376
inner.script 44
loop.script 35
27 files, 1 directory
28 files, 1 directory
> exit
halted
Execution halted.
+2 -1
View File
@@ -27,6 +27,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 984
Crash.sbx 632
vars.script 50
blocks.script 343
@@ -42,7 +43,7 @@ nonl.script 38
outer.script 376
inner.script 44
loop.script 35
27 files, 1 directory
28 files, 1 directory
> exit
halted
Execution halted.
+2 -1
View File
@@ -15,6 +15,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 984
Crash.sbx 632
vars.script 50
blocks.script 343
@@ -30,7 +31,7 @@ nonl.script 38
outer.script 376
inner.script 44
loop.script 35
27 files, 1 directory
28 files, 1 directory
> load
load what?
> load nosuch.sbx
+2 -1
View File
@@ -13,6 +13,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 984
Crash.sbx 632
vars.script 50
blocks.script 343
@@ -28,7 +29,7 @@ nonl.script 38
outer.script 376
inner.script 44
loop.script 35
27 files, 1 directory
28 files, 1 directory
> load Say.sbx
loaded, starting at 5000
> run the disk took its time
+2 -1
View File
@@ -22,6 +22,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 984
Crash.sbx 632
vars.script 50
blocks.script 343
@@ -37,7 +38,7 @@ nonl.script 38
outer.script 376
inner.script 44
loop.script 35
27 files, 1 directory
28 files, 1 directory
> exit
halted
Execution halted.
+5
View File
@@ -144,6 +144,11 @@ for i in 1 2 3 4 5 6 7 8; do "$TOOL" put "$DISKS/sbfs.img" "filler$i.txt" >/dev/
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
"$ROOT/Programs/CosmOS/Apps/Depth.asm" -o "$WORK/Depth.sbx" >/dev/null
"$TOOL" put "$DISKS/cosmos.img" "$WORK/Depth.sbx" >/dev/null
# Lander.sbx is Lunar Porter, and the biggest program on the disk. It takes the screen, the
# mode and the map, so it is also the one that says a program can have all three back.
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
"$ROOT/Programs/CosmOS/Apps/Lander.asm" -o "$WORK/Lander.sbx" >/dev/null
"$TOOL" put "$DISKS/cosmos.img" "$WORK/Lander.sbx" >/dev/null
# Crash.sbx breaks in each of the four ways the system now catches, which is the only way to
# reach a fault screen from a recorded test.
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
+43
View File
@@ -1519,6 +1519,49 @@ EOT
&& result ok "and the near pillar hides the middle" "sprite nought, behind sprite one" \
|| result no "and the near pillar hides the middle" "$BALLOVER ball pixels across a pillar of $PILLAR"
# ---- Lunar Porter, flying ----
#
# The biggest program on the disk, and the one that uses the most of the machine at once: it
# takes the screen, changes the mode, redefines tiles, fills all 3,200 cells of the map from a
# terrain it generated, and moves a sprite over it every frame.
#
# The keys are a burn of the lifting thruster every sixteenth frame, which is more than the
# gravity and so keeps the lander up long enough to be looked at. The cycle count is tuned to
# catch it before it climbs off the top; if CosmOS's size shifts the boot far enough, the
# lander check fails saying it is not on screen, which is a request to re-tune.
python3 -c "
keys = b'Lander\n'
for i in range(400):
keys += b'\x80' + b'\x00' * 15
open('$BUILD/lander.keys','wb').write(keys + b'\x00' * 4000)
"
timeout 30 "$EMU" --fast --cycles 12000000 --keyboard "$BUILD/lander.keys" \
--screen "$BUILD/lander.ppm" --disk "$ROOT/Tests/build/disks/cosmos.img" \
--ram-disk 2048 "$BUILD/cosmos.bin" > "$BUILD/lander.out" 2>&1 || true
read -r MOON SKY SHIP SHIPLEFT SHIPRIGHT <<EOT
$(python3 -c "
d = open('$BUILD/lander.ppm', 'rb').read()
px = d[d.index(b'255\n') + 4:]
w = 320
moon, ship = bytes.fromhex('d8d8d8'), bytes.fromhex('d8c048')
at = [px[o:o + 3] for o in range(0, len(px), 3)]
shipAt = [i % w for i, c in enumerate(at) if c == ship]
print(sum(1 for c in at if c == moon), sum(1 for c in at if c == bytes(3)), len(shipAt),
min(shipAt) if shipAt else -1, max(shipAt) if shipAt else -1)
" 2>/dev/null || echo "0 0 0 -1 -1")
EOT
# A moon that filled the screen or left it empty would be a terrain generator that had run
# off one end of its clamp, which is the way that kind of loop usually fails.
[ "$MOON" -gt 10000 ] && [ "$SKY" -gt 10000 ] \
&& result ok "a moon with a sky over it" "$MOON pixels of ground and $SKY of sky" \
|| result no "a moon with a sky over it" "$MOON ground, $SKY sky"
# It never moves sideways: the world scrolls under it, so it is at the middle of a 320 pixel
# screen every frame of its life. Forty pixels is the whole of the shape, so none of it has
# been clipped or drawn twice.
[ "$SHIP" = "40" ] && [ "$SHIPLEFT" = "156" ] && [ "$SHIPRIGHT" = "163" ] \
&& 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"
# ---- 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