From 8eb4e4d67e90873c1f4bce023885a137b9057ba8 Mon Sep 17 00:00:00 2001 From: Anachronaut Date: Wed, 2 Sep 2026 22:33:32 -0400 Subject: [PATCH] Lunar Porter, rung two: it lands, or it does not The terrain is an array in Data Memory rather than something read back out of the map, and that is the whole reason this is cheap: the ground under the lander is one index into 128 bytes, where asking the screen would be a transfer through the controller every frame. The column is the world position over eight, masked to the moon's 128. The surface is that column's row times eight - three turns left of the shift register, since a row is at most 24 and 192 fits in the low half. The feet are the lander's top plus its eight pixels. WHAT DECIDES IS THE SPEED AT THE MOMENT IT ARRIVES. Both of them, and both have to be gentle: three quarters of a pixel a frame downwards and half of one sideways. Sideways is the tighter on purpose, because a landing that was soft downwards and sliding is a lander on its side - which is the interesting half of the difficulty, and the half the drift bar was blind about until it existed. Two fixtures say it works, and they differ only in what was held: one holds nothing and falls the whole way, the other pulses the thruster six frames in sixteen and survives. Same terrain, same seed, same keys. Also: the gamepad did nothing, and the reason is that the four direction buttons are the D-PAD. A lot of controllers made this century have one nobody uses - the thumb goes on the stick, which reports as an axis rather than a button - so a pad that was plugged in and working correctly did nothing at all. The stick counts as held past halfway now. Untested here, because there is no controller in this environment and the suite runs headless; Voyager also says at startup which controllers it can see, so a pad that still does nothing can be told apart from one nothing noticed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW --- Programs/CosmOS/Apps/Lander.asm | 144 +++++++++++++++++++++++++-- Programs/CosmOS/README.md | 2 +- Source/Emulator/voyager.c | 30 ++++++ SplitBit Test Manual.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/cosmosLanderCrash.out | 8 ++ Tests/expected/cosmosLanderSoft.out | 8 ++ 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/input/lander.keys | Bin 0 -> 1512 bytes Tests/input/landerFall.pad | Bin 0 -> 3000 bytes Tests/input/landerSoft.pad | Bin 0 -> 3008 bytes Tests/manifest | 11 ++ 20 files changed, 207 insertions(+), 18 deletions(-) create mode 100644 Tests/expected/cosmosLanderCrash.out create mode 100644 Tests/expected/cosmosLanderSoft.out create mode 100644 Tests/input/lander.keys create mode 100644 Tests/input/landerFall.pad create mode 100644 Tests/input/landerSoft.pad diff --git a/Programs/CosmOS/Apps/Lander.asm b/Programs/CosmOS/Apps/Lander.asm index a1c4646..f0b9b52 100644 --- a/Programs/CosmOS/Apps/Lander.asm +++ b/Programs/CosmOS/Apps/Lander.asm @@ -77,6 +77,7 @@ everyFrame: CALL fall CALL move CALL follow + CALL touchdown CALL showLander CALL showDrift SETD.0 Flying @@ -542,10 +543,15 @@ showLander: ; ; 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 +; ---- How big a signed pair is, without its sign ---- +; +; DP0 names a two byte value, low half first. Leaves the size in DriftLow and DriftHigh and +; the sign in DriftLeftward, which is nought for a positive one. +; +; A two's complement pair is inverted and stepped, and the step's carry is what runs into the +; high half - the same carry that makes addWord work, used one instruction at a time because +; there is nothing to add. +magnitude: LDA.0 SETD.1 DriftLow STA.1 @@ -557,7 +563,7 @@ showDrift: AND SETD.1 DriftLeftward STQ.1 ; The top bit of the high half, which is the sign. - BRQ driftSized + BRQ magnitudeDone SETD.0 DriftLow LDA.0 @@ -572,8 +578,12 @@ showDrift: RSTB ADD ; Plus the carry the step above left, which is the other half. STQ.0 +magnitudeDone: + RET -driftSized: +showDrift: + SETD.0 SpeedAcross + CALL magnitude ; ---- 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 @@ -661,6 +671,110 @@ driftAt: OUTA 0xE9 ; And no depth. RET +; ---- Has it arrived, and how hard ---- +; +; The terrain is an array in Data Memory and not something read back out of the map, which is +; the whole reason this is cheap: the ground under the lander is one index into 128 bytes, +; where asking the screen would mean a transfer through the controller every frame. +; +; The column is the world position divided by eight, masked to the moon's 128. The surface is +; that column's row times eight, and the lander's feet are its top plus its eight pixels. +touchdown: + SETD.0 AcrossLow + CALL toPixels + SETD.0 PixelHigh + LDA.0 + SETD.0 PixelLow + LDB.0 + CALL eighth + MVQA + INIB 0x7F + AND ; The moon is 128 columns, so seven bits of it. + MVQA + SETD.1 Terrain + DPUA.1 ; Terrain plus the column, which is the row its surface is on. + LDA.1 + + ; Times eight, into pixels. A is the high half of the shift register and B the low, so the + ; row goes in B and three turns LEFT multiply it - and a row is at most 24, so 192 fits in + ; the low half and the high one stays empty. + RSTB + CCF + ADD + MVQB ; The row, in the low half. + RSTA + SHL + SHL + SHL + RSTA + CCF + ADD + SETD.1 SurfaceAt + STQ.1 + + ; The feet: where the lander's top is, plus the eight pixels of it. + SETD.0 DownLow + CALL toPixels + SETD.0 PixelHigh + LDA.0 + BNA touchdownDone ; Above the screen entirely, so nowhere near the ground. + SETD.0 PixelLow + LDA.0 + INIB 0d8 + CCF + ADD + MVQA + SETD.1 SurfaceAt + LDB.1 + CCF + SUB + BRC touchdownDone ; Borrowed: the feet are still above the surface. + + ; ---- Arrived. Now, how ---- + ; + ; Both speeds, and both have to be gentle. A landing that was soft downwards and sliding + ; sideways is a lander on its side, which is the interesting half of the difficulty: the + ; drift bar is the thing that was blind about it until this rung. + SETD.0 SpeedDown + CALL magnitude + SETD.0 DriftHigh + LDA.0 + BNA touchdownCrash ; Anything in the high half is far too fast to argue about. + SETD.0 DriftLow + LDA.0 + SETD.1 GentleDown + LDB.1 + CCF + SUB + BNC touchdownCrash ; No borrow, so it is at or past the limit. + + SETD.0 SpeedAcross + CALL magnitude + SETD.0 DriftHigh + LDA.0 + BNA touchdownCrash + SETD.0 DriftLow + LDA.0 + SETD.1 GentleAcross + LDB.1 + CCF + SUB + BNC touchdownCrash + + SETD.0 LandedText + BRI touchdownStop +touchdownCrash: + SETD.0 CrashedText +touchdownStop: + SWI osPrintString + INIA 0x0A + OUTA 0x00 + RSTA + SETD.1 Flying + STA.1 ; Which ends the loop, and the program tidies up as it always did. +touchdownDone: + RET + ; ---- Sums ---- ; ; DP0 names the two byte value being changed, low half first, and DP1 the one being added to @@ -821,6 +935,19 @@ DriftAt: 0x00 DriftSize: 0x00 +SurfaceAt: + 0x00 + +; ---- What counts as gentle ---- +; +; In sixteenths of a pixel a frame, so twelve is three quarters of a pixel a frame and eight +; is half of one. Sideways is the tighter of the two on purpose: a lander that arrives +; straight down at three quarters of a pixel is a landing, and one sliding at the same speed +; is a lander on its side. +GentleDown: + 0d12 +GentleAcross: + 0d8 HalfScreen: 0xA0 0x00 ; 160 pixels, which is half of a forty column screen. @@ -844,6 +971,11 @@ Flying: Terrain: #Reserve 0d128 +LandedText: +"Down safely." +CrashedText: +"Crashed." + LanderArt: 0x00 0x00 0x01 0x01 0x01 0x01 0x00 0x00 0x00 0x01 0x01 0x01 0x01 0x01 0x01 0x00 diff --git a/Programs/CosmOS/README.md b/Programs/CosmOS/README.md index ed0db49..82a4e9b 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. 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. | +| 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. It lands or crashes on arrival, and what decides is the speed at the moment it touches: gentler than three quarters of a pixel a frame downwards and half of one sideways, or it is a lander on its side. | | 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/Source/Emulator/voyager.c b/Source/Emulator/voyager.c index 2f93a19..47038d8 100644 --- a/Source/Emulator/voyager.c +++ b/Source/Emulator/voyager.c @@ -129,6 +129,24 @@ static void readPads(void) { if (IsGamepadButtonDown(n, GAMEPAD_BUTTON_RIGHT_FACE_RIGHT)){ held |= PAD_B; } if (IsGamepadButtonDown(n, GAMEPAD_BUTTON_MIDDLE_RIGHT)) { held |= PAD_START; } if (IsGamepadButtonDown(n, GAMEPAD_BUTTON_MIDDLE_LEFT)) { held |= PAD_SELECT; } + + // ---- And the stick, which is what most people actually push ---- + // + // The four buttons above are the D-PAD, and a lot of controllers made this + // century have one that nobody uses: the thumb goes on the stick, which reports + // as an axis and not as a button, so a pad that was plugged in and working did + // nothing at all. + // + // Past halfway counts as held. That is a blunt line and the right kind of blunt + // for a device that reports what is DOWN - a machine with eight bits a pad has + // nothing to say about three-fifths of a push, and picking the threshold here + // rather than in every program is the point of the pad being a device. + const float across = GetGamepadAxisMovement(n, GAMEPAD_AXIS_LEFT_X); + const float down = GetGamepadAxisMovement(n, GAMEPAD_AXIS_LEFT_Y); + if (across > 0.5f) { held |= PAD_RIGHT; } + if (across < -0.5f) { held |= PAD_LEFT; } + if (down > 0.5f) { held |= PAD_DOWN; } + if (down < -0.5f) { held |= PAD_UP; } } if (n == 0) { // ---- And the keyboard, on top of it ---- @@ -431,6 +449,18 @@ int main(int argc, char *argv[]) { // to find out that a default was left as it was found. SetExitKey(KEY_NULL); + // ---- What controllers the host can see, said out loud ---- + // + // A pad that is plugged in and does nothing is indistinguishable from a pad the front + // end never noticed, and the difference is the whole of what to do about it: one is a + // mapping to fix and the other is a driver. Saying which at startup costs a line and + // answers it without anybody having to guess. + for (int n = 0; n < PAD_COUNT; n++) { + if (IsGamepadAvailable(n)) { + printf("Controller %d: %s\n", n, GetGamepadName(n)); + } + } + // ---- And a speaker, if the host has one ---- // // Asked for rather than assumed: a machine with no audio device is a perfectly good diff --git a/SplitBit Test Manual.md b/SplitBit Test Manual.md index a3e9d7f..15c74f8 100644 --- a/SplitBit Test Manual.md +++ b/SplitBit Test Manual.md @@ -101,7 +101,7 @@ from `make`, not from here. ### 1. Recorded output `Tests/run.sh` assembles each program named in `Tests/manifest`, runs it, and compares -everything it printed against a file in `Tests/expected`. 207 tests, of which 145 run, 35 +everything it printed against a file in `Tests/expected`. 209 tests, of which 147 run, 35 only assemble, 16 are expected to fail to assemble, and 11 boot from ROM with no image given at all. diff --git a/Tests/expected/cosmosCrossDisk.out b/Tests/expected/cosmosCrossDisk.out index 499a7f5..ce058d0 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 1330 +Lander.sbx 1543 Crash.sbx 632 vars.script 50 blocks.script 343 diff --git a/Tests/expected/cosmosDrives.out b/Tests/expected/cosmosDrives.out index 37fb3cf..fb89d7d 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 1330 +Lander.sbx 1543 Crash.sbx 632 vars.script 50 blocks.script 343 diff --git a/Tests/expected/cosmosFault.out b/Tests/expected/cosmosFault.out index f8ee47a..452554e 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 1330 +Lander.sbx 1543 Crash.sbx 632 vars.script 50 blocks.script 343 diff --git a/Tests/expected/cosmosFlip.out b/Tests/expected/cosmosFlip.out index 99b12da..6c866ab 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 1330 +Lander.sbx 1543 Crash.sbx 632 vars.script 50 blocks.script 343 diff --git a/Tests/expected/cosmosGrid.out b/Tests/expected/cosmosGrid.out index 94c9159..0d3a4b9 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 1330 +Lander.sbx 1543 Crash.sbx 632 vars.script 50 blocks.script 343 diff --git a/Tests/expected/cosmosLanderCrash.out b/Tests/expected/cosmosLanderCrash.out new file mode 100644 index 0000000..343b429 --- /dev/null +++ b/Tests/expected/cosmosLanderCrash.out @@ -0,0 +1,8 @@ +CosmOS +> Lander +Crashed. +finished +> exit +halted +Execution halted. +[exit 0] diff --git a/Tests/expected/cosmosLanderSoft.out b/Tests/expected/cosmosLanderSoft.out new file mode 100644 index 0000000..6cf07b0 --- /dev/null +++ b/Tests/expected/cosmosLanderSoft.out @@ -0,0 +1,8 @@ +CosmOS +> Lander +Down safely. +finished +> exit +halted +Execution halted. +[exit 0] diff --git a/Tests/expected/cosmosMonitor.out b/Tests/expected/cosmosMonitor.out index 671f94e..4a1c36a 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 1330 +Lander.sbx 1543 Crash.sbx 632 vars.script 50 blocks.script 343 diff --git a/Tests/expected/cosmosMonitorRun.out b/Tests/expected/cosmosMonitorRun.out index a3dc975..c690323 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 1330 +Lander.sbx 1543 Crash.sbx 632 vars.script 50 blocks.script 343 diff --git a/Tests/expected/cosmosRun.out b/Tests/expected/cosmosRun.out index e5b98d9..da06e91 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 1330 +Lander.sbx 1543 Crash.sbx 632 vars.script 50 blocks.script 343 diff --git a/Tests/expected/cosmosSlowDisk.out b/Tests/expected/cosmosSlowDisk.out index 433229a..9ee4a6e 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 1330 +Lander.sbx 1543 Crash.sbx 632 vars.script 50 blocks.script 343 diff --git a/Tests/expected/cosmosSprite.out b/Tests/expected/cosmosSprite.out index 8679a9b..549b8fb 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 1330 +Lander.sbx 1543 Crash.sbx 632 vars.script 50 blocks.script 343 diff --git a/Tests/input/lander.keys b/Tests/input/lander.keys new file mode 100644 index 0000000000000000000000000000000000000000..2c6609d221449c378d6d7a3610b9df173aa70712 GIT binary patch literal 1512 icmeZC%u7iv;$j#DqaiRF0;3@?8Un*E1X3$9OSk|HO#~$X literal 0 HcmV?d00001 diff --git a/Tests/input/landerFall.pad b/Tests/input/landerFall.pad new file mode 100644 index 0000000000000000000000000000000000000000..b9412b146cc2cb1eea284e31f4db599835377ac8 GIT binary patch literal 3000 lcmZP=1*0J_8UmvsFd71*Aut*OqaiRF0;3@?8Ulkp1ON-T00961 literal 0 HcmV?d00001 diff --git a/Tests/input/landerSoft.pad b/Tests/input/landerSoft.pad new file mode 100644 index 0000000000000000000000000000000000000000..7d42694aa0ec4d8ff38753d5e2855dbe66378eb3 GIT binary patch literal 3008 zcmd-80|qz%b2;E#hEYB!1V+OH76PEq;22FGu;3WYAK(N55026B7!41O(ehz5Kfpo& YoE=8%3s`WBmXF{B0S}JR@Srd}0P(>iK>z>% literal 0 HcmV?d00001 diff --git a/Tests/manifest b/Tests/manifest index 6662bb4..5b403ef 100644 --- a/Tests/manifest +++ b/Tests/manifest @@ -1012,6 +1012,17 @@ cosmosSprite | CosmOS/Source/cosmos.asm | run | - # presses A and B together. The program reads each frame twice to show that looking does not # take it away. padTest | testPrograms/padTest.asm | run | - | - | - | - | padTest.pad +# ---- Lunar Porter, arriving ---- +# +# The same program, the same keys, and two different controllers. One holds nothing at all and +# the lander falls the whole way; the other pulses the thruster six frames in sixteen and +# brings it down slowly enough to survive. +# +# WHAT SAYS WHICH IS THE SPEED AT THE MOMENT IT ARRIVES, and nothing else - the terrain is the +# same terrain, from the same fixed seed, so the two runs differ only in what was held. That +# is the whole of the game's difficulty in two fixtures. +cosmosLanderCrash | CosmOS/Source/cosmos.asm | run | - | 60000000 | disks/cosmos.img | lander.keys | landerFall.pad +cosmosLanderSoft | CosmOS/Source/cosmos.asm | run | - | 60000000 | disks/cosmos.img | lander.keys | landerSoft.pad # Which disk the registers mean. Several disks are one controller with a drive register # rather than several devices, because a port is an immediate byte inside the instruction # that names it and a program cannot compute one. Run with a single disk, so drive 1 is a