From db0c26e13fb28b0bbd9493af2300c63cff374756 Mon Sep 17 00:00:00 2001 From: Anachronaut Date: Wed, 2 Sep 2026 22:14:36 -0400 Subject: [PATCH] A bar for the drift, and a lighter touch sideways A moon has no air, so a sideways drift never stops by itself and stopping one means cancelling the velocity EXACTLY. That is not hard to do; it is hard to do blind, which is what it was - a number nothing on the screen said anything about. So sprite one is a bar whose width is the drift. It runs right from the middle of the screen for a rightward one and left for a leftward one, so which way is as plain as how fast, and stopped is the one state with nothing drawn at all. The whole of it is a target width written once a frame; the device stretches one tile into it and the program draws nothing. Sideways thrust is one a tick rather than two. At two, the smallest correction available was twice the size it needed to be and overshooting was the normal outcome. WHICH ZERO MEANS NOTHING TURNED OUT TO MATTER. A target width of nought is the NATURAL width, not an empty sprite - so a bar with no drift in it came out eight pixels wide, sitting at the middle of the screen, saying "stopped" in the same shape it says "drifting slightly". What draws nothing is a SIZE of nought, which is the other zero in the other byte. Both meanings are deliberate and documented and it still caught me out inside a week of writing them down. The check for it earned its place by failing on that before it was found, which is the best evidence a check can offer. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW --- Programs/CosmOS/Apps/Lander.asm | 151 +++++++++++++++++++++++++++- Programs/CosmOS/README.md | 2 +- Tests/expected/cosmosCrossDisk.out | 2 +- Tests/expected/cosmosDrives.out | 2 +- Tests/expected/cosmosFault.out | 2 +- Tests/expected/cosmosFlip.out | 2 +- Tests/expected/cosmosGrid.out | 2 +- Tests/expected/cosmosMonitor.out | 2 +- Tests/expected/cosmosMonitorRun.out | 2 +- Tests/expected/cosmosRun.out | 2 +- Tests/expected/cosmosSlowDisk.out | 2 +- Tests/expected/cosmosSprite.out | 2 +- Tests/video.sh | 37 +++++++ 13 files changed, 197 insertions(+), 13 deletions(-) diff --git a/Programs/CosmOS/Apps/Lander.asm b/Programs/CosmOS/Apps/Lander.asm index abc47ac..a1c4646 100644 --- a/Programs/CosmOS/Apps/Lander.asm +++ b/Programs/CosmOS/Apps/Lander.asm @@ -78,6 +78,7 @@ everyFrame: CALL move CALL follow CALL showLander + CALL showDrift SETD.0 Flying LDA.0 BNA everyFrame @@ -529,6 +530,137 @@ showLander: OUTA 0xE9 RET +; ---- How fast sideways, as a bar ---- +; +; A moon has no air, so a drift never stops by itself and stopping one means cancelling the +; velocity EXACTLY. That is not hard to do; it is hard to do BLIND, which is what it was - a +; number nothing on the screen said anything about. +; +; So sprite one is a bar whose width is the drift and which disappears at nought. It grows +; right from the middle of the screen for a rightward drift and left for a leftward one, so +; which way is as plain as how fast, and "stopped" is the one state with nothing drawn. +; +; The whole of it is the sprite's target width, which the device stretches one tile into. The +; program does no drawing at all: it works out one number a frame and writes it. +showDrift: + ; The magnitude, and which way. A two's complement pair is inverted and stepped, and the + ; step's carry is what runs into the high half. + SETD.0 SpeedAcross + LDA.0 + SETD.1 DriftLow + STA.1 + INCD.0 + LDA.0 + SETD.1 DriftHigh + STA.1 + INIB 0x80 + AND + SETD.1 DriftLeftward + STQ.1 ; The top bit of the high half, which is the sign. + BRQ driftSized + + SETD.0 DriftLow + LDA.0 + NOTA + MVQA + INCA + STA.0 + SETD.0 DriftHigh + LDA.0 + NOTA + MVQA + RSTB + ADD ; Plus the carry the step above left, which is the other half. + STQ.0 + +driftSized: + ; ---- Clamped, because a bar wider than the screen says nothing a full one does not ---- + ; + ; And because a target width is sixteen bits: an unclamped one would be asking the device to + ; draw a bar sixty thousand pixels wide, which it clips, but only after being asked. + SETD.0 DriftHigh + LDA.0 + BNA driftWide ; Anything in the high half is already past the limit. + SETD.0 DriftLow + LDA.0 + INIB 0d120 + CCF + SUB + BRC driftReady ; Borrowed, so it is under the limit and stands. +driftWide: + INIA 0d120 + SETD.0 DriftLow + STA.0 + +driftReady: + ; ---- Two zeros, meaning two different things ---- + ; + ; A TARGET WIDTH OF NOUGHT IS THE NATURAL WIDTH, not an empty sprite. So a bar with no drift + ; in it came out eight pixels wide - one whole tile, sitting at the middle of the screen, + ; saying "stopped" in the same shape it says "drifting slightly". What draws nothing is a + ; SIZE of nought, which is the other zero and the other byte. + SETD.0 DriftLow + LDA.0 + BRA driftEmpty + INIA 0x11 ; One tile by one, however wide it ends up drawn. + BRI driftSized2 +driftEmpty: + RSTA ; No tiles at all, which is the off switch. +driftSized2: + SETD.1 DriftSize + STA.1 + + ; Where it starts: the middle for a rightward drift, and that much back for a leftward one. + INIA 0d160 + SETD.0 DriftLeftward + LDB.0 + BRB driftAt ; Nought is rightward, so the bar starts at the middle. + SETD.0 DriftLow + LDB.0 + CCF + SUB + MVQA + +driftAt: + SETD.1 DriftAt + STA.1 + + INIA 0d4 + OUTA 0xE3 + INIA 0xC0 + OUTA 0xE4 + INIA 0x10 + OUTA 0xE5 ; Sprite one begins sixteen bytes in. + INIA 0xC8 + OUTA 0xE9 ; The ground block, which is a solid tile. + INIA 0x01 + OUTA 0xE9 ; Attribute one, so the bar is not the colour of the moon. + SETD.0 DriftAt + LDA.0 + OUTA 0xE9 + RSTA + OUTA 0xE9 ; X. + INIA 0d188 + OUTA 0xE9 + RSTA + OUTA 0xE9 ; Y, near the bottom and clear of the ground most places. + SETD.0 DriftSize + LDA.0 + OUTA 0xE9 ; One tile by one, or nothing at all when there is no drift. + RSTA + OUTA 0xE9 ; No flags. + SETD.0 DriftLow + LDA.0 + OUTA 0xE9 + RSTA + OUTA 0xE9 ; The width, which is the drift. NOUGHT DRAWS NOTHING. + INIA 0d3 + OUTA 0xE9 + RSTA + OUTA 0xE9 ; Three pixels tall. + OUTA 0xE9 ; And no depth. + RET + ; ---- Sums ---- ; ; DP0 names the two byte value being changed, low half first, and DP1 the one being added to @@ -666,14 +798,29 @@ ThrustRight: ; it is one byte each. PadUp: 0xFE 0xFF ; Minus two. +; ---- Sideways, at half the vertical ---- +; +; ONE, not two. There is no air on a moon, so nothing slows a drift but the opposite thruster +; and stopping means cancelling the velocity exactly. At two a tick the smallest correction +; was twice as big as it needed to be and overshooting was the normal outcome. PadLeft: - 0xFE 0xFF ; Minus two. + 0xFF 0xFF ; Minus one. PadRight: - 0x02 0x00 + 0x01 0x00 Held: 0x00 HasPad: 0x00 +DriftLow: + 0x00 +DriftHigh: + 0x00 +DriftLeftward: + 0x00 +DriftAt: + 0x00 +DriftSize: + 0x00 HalfScreen: 0xA0 0x00 ; 160 pixels, which is half of a forty column screen. diff --git a/Programs/CosmOS/README.md b/Programs/CosmOS/README.md index 5c49bb0..ed0db49 100644 --- a/Programs/CosmOS/README.md +++ b/Programs/CosmOS/README.md @@ -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. 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. | +| 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. A bar at the bottom is the sideways drift, drawn as a sprite stretched to the speed - a moon has no air, so a drift never stops by itself and stopping one means cancelling it exactly, which is hard to do blind. | | 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. | diff --git a/Tests/expected/cosmosCrossDisk.out b/Tests/expected/cosmosCrossDisk.out index 46cf473..499a7f5 100644 --- a/Tests/expected/cosmosCrossDisk.out +++ b/Tests/expected/cosmosCrossDisk.out @@ -25,7 +25,7 @@ Mode.sbx 48 Flip.sbx 173 Sprite.sbx 442 Depth.sbx 672 -Lander.sbx 1118 +Lander.sbx 1330 Crash.sbx 632 vars.script 50 blocks.script 343 diff --git a/Tests/expected/cosmosDrives.out b/Tests/expected/cosmosDrives.out index e5f62a6..37fb3cf 100644 --- a/Tests/expected/cosmosDrives.out +++ b/Tests/expected/cosmosDrives.out @@ -15,7 +15,7 @@ Mode.sbx 48 Flip.sbx 173 Sprite.sbx 442 Depth.sbx 672 -Lander.sbx 1118 +Lander.sbx 1330 Crash.sbx 632 vars.script 50 blocks.script 343 diff --git a/Tests/expected/cosmosFault.out b/Tests/expected/cosmosFault.out index 721e1a6..f8ee47a 100644 --- a/Tests/expected/cosmosFault.out +++ b/Tests/expected/cosmosFault.out @@ -32,7 +32,7 @@ Mode.sbx 48 Flip.sbx 173 Sprite.sbx 442 Depth.sbx 672 -Lander.sbx 1118 +Lander.sbx 1330 Crash.sbx 632 vars.script 50 blocks.script 343 diff --git a/Tests/expected/cosmosFlip.out b/Tests/expected/cosmosFlip.out index 56b0dc5..99b12da 100644 --- a/Tests/expected/cosmosFlip.out +++ b/Tests/expected/cosmosFlip.out @@ -22,7 +22,7 @@ Mode.sbx 48 Flip.sbx 173 Sprite.sbx 442 Depth.sbx 672 -Lander.sbx 1118 +Lander.sbx 1330 Crash.sbx 632 vars.script 50 blocks.script 343 diff --git a/Tests/expected/cosmosGrid.out b/Tests/expected/cosmosGrid.out index 2ae30ba..94c9159 100644 --- a/Tests/expected/cosmosGrid.out +++ b/Tests/expected/cosmosGrid.out @@ -22,7 +22,7 @@ Mode.sbx 48 Flip.sbx 173 Sprite.sbx 442 Depth.sbx 672 -Lander.sbx 1118 +Lander.sbx 1330 Crash.sbx 632 vars.script 50 blocks.script 343 diff --git a/Tests/expected/cosmosMonitor.out b/Tests/expected/cosmosMonitor.out index f0a605e..671f94e 100644 --- a/Tests/expected/cosmosMonitor.out +++ b/Tests/expected/cosmosMonitor.out @@ -108,7 +108,7 @@ Mode.sbx 48 Flip.sbx 173 Sprite.sbx 442 Depth.sbx 672 -Lander.sbx 1118 +Lander.sbx 1330 Crash.sbx 632 vars.script 50 blocks.script 343 diff --git a/Tests/expected/cosmosMonitorRun.out b/Tests/expected/cosmosMonitorRun.out index 921cfb7..a3dc975 100644 --- a/Tests/expected/cosmosMonitorRun.out +++ b/Tests/expected/cosmosMonitorRun.out @@ -27,7 +27,7 @@ Mode.sbx 48 Flip.sbx 173 Sprite.sbx 442 Depth.sbx 672 -Lander.sbx 1118 +Lander.sbx 1330 Crash.sbx 632 vars.script 50 blocks.script 343 diff --git a/Tests/expected/cosmosRun.out b/Tests/expected/cosmosRun.out index 0133fc0..e5b98d9 100644 --- a/Tests/expected/cosmosRun.out +++ b/Tests/expected/cosmosRun.out @@ -15,7 +15,7 @@ Mode.sbx 48 Flip.sbx 173 Sprite.sbx 442 Depth.sbx 672 -Lander.sbx 1118 +Lander.sbx 1330 Crash.sbx 632 vars.script 50 blocks.script 343 diff --git a/Tests/expected/cosmosSlowDisk.out b/Tests/expected/cosmosSlowDisk.out index 7bae29e..433229a 100644 --- a/Tests/expected/cosmosSlowDisk.out +++ b/Tests/expected/cosmosSlowDisk.out @@ -13,7 +13,7 @@ Mode.sbx 48 Flip.sbx 173 Sprite.sbx 442 Depth.sbx 672 -Lander.sbx 1118 +Lander.sbx 1330 Crash.sbx 632 vars.script 50 blocks.script 343 diff --git a/Tests/expected/cosmosSprite.out b/Tests/expected/cosmosSprite.out index 3a3a21a..8679a9b 100644 --- a/Tests/expected/cosmosSprite.out +++ b/Tests/expected/cosmosSprite.out @@ -22,7 +22,7 @@ Mode.sbx 48 Flip.sbx 173 Sprite.sbx 442 Depth.sbx 672 -Lander.sbx 1118 +Lander.sbx 1330 Crash.sbx 632 vars.script 50 blocks.script 343 diff --git a/Tests/video.sh b/Tests/video.sh index f633340..139fcc4 100755 --- a/Tests/video.sh +++ b/Tests/video.sh @@ -1599,6 +1599,43 @@ EOT && 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" +# ---- The drift bar, which is a scaled sprite doing a job ---- +# +# A moon has no air, so a sideways drift never stops by itself and stopping one means +# cancelling the velocity exactly. That is not hard; it is hard BLIND. So one sprite's width +# is the drift: it grows right from the middle of the screen for a rightward one and left for +# a leftward one, and NOUGHT DRAWS NOTHING, so "stopped" is the state with no bar at all. +# +# The recording holds Right for eighty frames and then nothing. Checked in two places: with a +# drift, where the bar starts at the middle and runs right; and with none, where a sprite of +# no width is a sprite that is not drawn. +python3 -c "open('$BUILD/drift.pad','wb').write(b'\x00' * 20 + b'\x01' * 80 + b'\x00' * 400)" +timeout 30 "$EMU" --fast --cycles 1600000 --keyboard "$BUILD/lander.keys" \ + --pad "$BUILD/drift.pad" --screen "$BUILD/drift.ppm" \ + --disk "$ROOT/Tests/build/disks/cosmos.img" --ram-disk 2048 \ + "$BUILD/cosmos.bin" > "$BUILD/drift.out" 2>&1 || true +read -r BARLEFT BARWIDE STILLWIDE </dev/null || echo "-1 0 0") +EOT +[ "$BARLEFT" = "160" ] && [ "$BARWIDE" -gt 4 ] \ + && result ok "a drift draws a bar" "$BARWIDE pixels of it, running right from the middle" \ + || result no "a drift draws a bar" "starts at $BARLEFT, $BARWIDE wide" +# The lander recording holds only Up, so there is no sideways drift and nothing to draw. A +# sprite of no width is not drawn at all, which is what makes "stopped" visible as emptiness. +[ "$STILLWIDE" = "0" ] \ + && result ok "and no drift draws none" "a width of nought is a sprite that is not there" \ + || result no "and no drift draws none" "$STILLWIDE pixels of bar with nothing moving" + # ---- 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