Dust on landing, gas on letting go, and a lander that stays let go

One particle system, three uses now: a lander coming apart, dust kicked
up by a landing, and gas out of a docking port on release. Where they
start, how fast they go, what colour they are and how long they last are
arguments; everything else is shared.

Dust goes sideways and UP off the lander's feet, because that is where
kicked dust goes and there is ground in the way of the rest of it. Gas
goes evenly in every direction, because nothing is in the way of a
docking port. Neither happens on docking - a dock is a catch and not a
touchdown, and there is nothing under it to kick.

---- Ticked from the frame loop, not run in place ----

The explosion can afford to stop the world; there is nothing left to fly.
The undocking puff cannot, because it goes off on the frame a thruster is
pressed, and freezing a quarter of a second exactly then is felt as the
controls sticking. So a burst advances one frame at a time from the main
loop, and the two that can afford to wait just pump that same tick until
the air is clear.

---- And letting go did not let go ----

Which the puff is what found. A docked lander sits EXACTLY one tile under
the station, so releasing upwards moved it towards the station and it
docked again on the very next frame: took the fuel again, said so again,
and waited to be told the message had been read - which reads as the
controls locking up the instant they are used.

It has to get clear now before it can take hold again, and the two
distances have to differ: docking wants one tile, re-arming wants two.
A single distance re-armed on the frame it let go, because a tile is
exactly where it was sitting.

Three checks, each seen to fail on its own break. The last of them
measures HOW FAR the lander has got and not merely that it moved: a
sixteen frame pause on release still leaves it climbing, four rows short
of a free run, so "it moved" would pass for a stall that has been slept
through.
This commit is contained in:
Anachronaut
2026-09-04 12:23:31 -04:00
parent 115efa1fa1
commit 0264b19a3d
13 changed files with 310 additions and 53 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 7913
Lander.sbx 8243
Pad.sbx 264
Crash.sbx 632
vars.script 50
+1 -1
View File
@@ -15,7 +15,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 7913
Lander.sbx 8243
Pad.sbx 264
Crash.sbx 632
vars.script 50
+1 -1
View File
@@ -32,7 +32,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 7913
Lander.sbx 8243
Pad.sbx 264
Crash.sbx 632
vars.script 50
+1 -1
View File
@@ -22,7 +22,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 7913
Lander.sbx 8243
Pad.sbx 264
Crash.sbx 632
vars.script 50
+1 -1
View File
@@ -22,7 +22,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 7913
Lander.sbx 8243
Pad.sbx 264
Crash.sbx 632
vars.script 50
+1 -1
View File
@@ -108,7 +108,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 7913
Lander.sbx 8243
Pad.sbx 264
Crash.sbx 632
vars.script 50
+1 -1
View File
@@ -27,7 +27,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 7913
Lander.sbx 8243
Pad.sbx 264
Crash.sbx 632
vars.script 50
+1 -1
View File
@@ -15,7 +15,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 7913
Lander.sbx 8243
Pad.sbx 264
Crash.sbx 632
vars.script 50
+1 -1
View File
@@ -13,7 +13,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 7913
Lander.sbx 8243
Pad.sbx 264
Crash.sbx 632
vars.script 50
+1 -1
View File
@@ -22,7 +22,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 7913
Lander.sbx 8243
Pad.sbx 264
Crash.sbx 632
vars.script 50
+77
View File
@@ -2661,6 +2661,83 @@ EOT
&& result ok "and are gone before the verdict" "nothing left over the words" \
|| result no "and are gone before the verdict" "$AFTERBITS pixels still there"
# ---- Dust on landing, and gas on letting go ----
#
# The same six pieces as the explosion, thrown differently: dust goes sideways and UP off the
# lander's feet, because that is where kicked dust goes and there is ground in the way of the
# rest of it; gas goes evenly in every direction, because nothing is in the way of a docking
# port. Neither happens on DOCKING - a dock is a catch, not a touchdown, and there is nothing
# under it to kick.
#
# ---- The gas is the one that must not stop the world ----
#
# It goes off on the frame a thruster is pressed. The explosion can afford to hold everything
# still, because there is nothing left to fly; freezing a quarter of a second exactly as the
# controls are used would be felt as them sticking. So the check watches the LANDER MOVE while
# the gas has gone, and how FAR it has got by then.
python3 -c "
import struct
open('$BUILD/undock.state','wb').write(struct.pack('<hhhhhhBBBB',
0x34A0 - 64, -1536 + 64, 64, 0, 0x34A0, -1536, 100, 1, 0, 0))
"
cp "$ROOT/Tests/build/disks/cosmos.img" "$BUILD/undock.img"
"$ROOT/SplitDisk" put "$BUILD/undock.img" "$BUILD/undock.state" /lander.state > /dev/null
# Acknowledge the dock at frame 400, then hold up from 600 to let go of it.
python3 -c "open('$BUILD/undock.pad','wb').write(b'\x00' * 400 + b'\x10' * 8 + b'\x00' * 192 + b'\x08' * 4000 + b'\x00' * 20000)"
for when in 9900000 10150000 10800000; do
timeout 60 "$EMU" --fast --cycles $when --keyboard "$BUILD/fly.keys" \
--pad "$BUILD/undock.pad" --screen "$BUILD/gas$when.ppm" \
--disk "$BUILD/undock.img" --ram-disk 2048 \
"$BUILD/cosmos.bin" > "$BUILD/gas.out" 2>&1 || true
done
# And a plain fall onto the pad it starts over, which lands rather than crashes.
for when in 2700000 2820000 3100000; do
timeout 60 "$EMU" --fast --cycles $when --keyboard "$BUILD/fly.keys" \
--pad "$BUILD/none.pad" --screen "$BUILD/dust$when.ppm" \
--disk "$ROOT/Tests/build/disks/cosmos.img" --ram-disk 2048 \
"$BUILD/cosmos.bin" > "$BUILD/dust.out" 2>&1 || true
done
read -r DUSTBEFORE DUSTDURING DUSTAFTER GASBEFORE GASDURING GASAFTER SHIPHELD SHIPFREE <<EOT
$(python3 -c "
def look(path):
d = open(path, 'rb').read()
width = int(d[:40].split()[1])
px = d[d.index(b'255\n') + 4:]
def where(colour):
c = bytes.fromhex(colour)
return [(i % width, i // width) for i in range(len(px) // 3)
if px[i * 3:i * 3 + 3] == c]
# White BELOW THE WINDOW. The window's own text is white and sits in the top two rows.
puff = len([1 for x, y in where('f0f0f0') if y > 20])
ship = where('d8c048')
return puff, (min(y for x, y in ship) if ship else -1)
print(look('$BUILD/dust2700000.ppm')[0], look('$BUILD/dust2820000.ppm')[0],
look('$BUILD/dust3100000.ppm')[0],
look('$BUILD/gas9900000.ppm')[0], look('$BUILD/gas10150000.ppm')[0],
look('$BUILD/gas10800000.ppm')[0],
look('$BUILD/gas10150000.ppm')[1], look('$BUILD/gas10800000.ppm')[1])
" 2>/dev/null || echo "-1 -1 -1 -1 -1 -1 -1 -1")
EOT
[ "$DUSTBEFORE" = "0" ] && [ "$DUSTDURING" -gt 0 ] && [ "$DUSTAFTER" = "0" ] \
&& result ok "a landing kicks up dust" "none, then $DUSTDURING pixels of it, then none" \
|| result no "a landing kicks up dust" "$DUSTBEFORE before, $DUSTDURING during, $DUSTAFTER after"
[ "$GASBEFORE" = "0" ] && [ "$GASDURING" -gt 0 ] && [ "$GASAFTER" = "0" ] \
&& result ok "and letting go of the station vents gas" "none, then $GASDURING pixels, then none" \
|| result no "and letting go of the station vents gas" "$GASBEFORE before, $GASDURING during, $GASAFTER after"
# ---- Which is the half that matters ----
#
# The lander has to be somewhere ELSE by the time the gas has gone. If letting go froze the
# world it would still be at the row it undocked from - and so would it if the lander re-docked
# on the next frame, which is what happened before a lander had to get clear of the station
# before it could take hold of it again: it took the fuel again, said so again, and sat waiting
# to be told the message had been read.
# HOW FAR it has got, not merely that it moved. Twelve rows is what a free run manages by
# then; a sixteen frame pause on letting go still leaves it climbing, just four rows short - so
# "it moved" passes for a stall that has been slept through and this does not.
[ "$SHIPFREE" -gt 0 ] && [ "$(( SHIPHELD - SHIPFREE ))" -ge 8 ] \
&& result ok "and the lander is well away, not merely moving" "row $SHIPHELD became row $SHIPFREE" \
|| result no "and the lander is well away, not merely moving" "row $SHIPHELD, then row $SHIPFREE"
# ---- 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