Files
SplitBit-Emulator/Programs/CosmOS/Apps/Lander.asm
T
AnachronautandClaude Opus 5 7257ad369c Landing pads, carved rather than looked for
A random walk does not leave flat ground and a lander wants some. Four
pads are cut into the moon after it is generated, each four columns
levelled to whatever height its first column happened to have - so they
sit in the landscape rather than on a shelf above it. The moon decides
where they are; this only decides that they are flat.

Searching for flat spots was the alternative and it can fail, which means
a fallback that carves anyway - the carving, plus a search nobody needed.

They are marked by an ATTRIBUTE and not a tile of their own, which costs
no art at all: a nibble is added to every index in a tile, so one solid
block is grey moon or a cyan pad depending on the byte beside it.

Which columns are pads is an array, because asking has to be one lookup.
Four comparisons per column per row is 12,800 of them for one screen, and
the landing verdict asks the same question again.

THE LANDER STARTS ABOVE ONE, because that is where a porter's day begins.
Starting in the middle of nowhere meant a straight descent landed in the
middle of nowhere, which is a fine thing to be able to do and a poor thing
to have to.

That change cost the crash test its teeth, and the way it did is worth
keeping. It held nothing at all and let the lander fall - and a short drop
onto the high ground of the base you started above is survivable, which is
correct, and left the test saying nothing. It holds Right now: lateral
speed has no limit and nothing slows it, so a slide always ends badly
however the rest is tuned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-03 11:45:30 -04:00

1321 lines
34 KiB
NASM

; Lunar Porter. Rung one: it flies.
;
; A lander over a moon that wraps. Thrust, gravity, and a surface that comes back round if you
; keep going one way - there is no edge to fall off and no wall to hit, because the map's
; column origin is a ring in hardware and 128 cells of it is 1024 pixels of moon.
;
; ---- What is not here yet ----
;
; Landing, crashing, fuel, cargo, bases. This rung exists to answer whether it FEELS right,
; because everything after it is bookkeeping by comparison and none of it is worth building
; on a lander that is no fun to fly.
;
; ---- Sixteenths of a pixel ----
;
; Position and velocity are sixteen bit, in sixteenths of a pixel. That is the unit that makes
; the whole thing work with adds alone: gravity is a small number added to a velocity, and a
; velocity is a number added to a position, and there is no multiply or divide anywhere.
;
; The moon is 1024 pixels round, which is 16,384 sixteenths, which is 2^14 - so GOING ALL THE
; WAY ROUND IS AN AND WITH 0x3FFF. Not a comparison, not a subtraction, and never wrong at the
; seam. Picking the units so the wrap is a mask is most of the reason this is short.
;
; Written by Anachronaut
#Include services.asm
#Program
#Base 0x5000
start:
; The atlas holds the tiles and the sprite table; the screen holds the map.
INIA 0d4
OUTA 0xE3
INIA 0x30
OUTA 0xE2
INIA 0x03
OUTA 0xE8
INIA 0d5
OUTA 0xE3
INIA 0x3A
OUTA 0xE2
INIA 0x03
OUTA 0xE8
; The screen back afterwards. This one really does need it: it redefines tiles, fills every
; cell of the map and changes the mode, and screenGive is what puts the MODE back - the
; floor the system gives every program does not.
SWI osTakeScreen
RSTA
OUTA 0x31 ; Forty columns. A moon 320 pixels across reads better than 640.
; ---- And the view put back to the top of the map ----
;
; THE SHELL SCROLLS. Its row origin is wherever the last command left it, and the map is a
; ring 128 rows tall that the screen shows 25 of - so a moon drawn into rows nought to 24
; while the screen is looking at row forty is a moon nobody can see. It came out as terrain
; that was missing, or half there, depending on how far down the prompt had got.
;
; Nothing here is being tidy: this is the difference between the rows this program WRITES
; and the rows the screen READS, and only one of them is under its control.
;
; The column origin and the fine offsets are set every frame by follow, so they need no
; help. The row origin is set once, here, because nothing scrolls vertically after this.
OUTA 0x34
OUTA 0x38 ; No fraction of a cell downwards either.
CALL putTiles
CALL makeMoon
CALL carvePads
CALL drawMoon
CALL putLander
CALL putGauge
INIA 0x01
OUTA 0x02 ; Key mode.
; ---- Is there a controller ----
;
; Asked once, and about all four. If there is one anywhere, 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
SETD.1 HasPad
STA.1 ; ANY bit, so any of the four counts as having one.
everyFrame:
CALL waitFrame
CALL readPad
CALL readControls
CALL fall
CALL move
CALL follow
CALL touchdown
CALL showLander
CALL showDrift
CALL showFuel
SETD.0 Flying
LDA.0
BNA everyFrame
RSTA
OUTA 0x02
SWI osExit
; ---- Tiles ----
;
; The ground is a solid block, which is a Fill and needs no art. The lander has a shape and
; comes out of the Data Segment. Both are well above the 135 glyphs the character generator
; copies back, so neither costs the shell a letter.
putTiles:
INIA 0d4
OUTA 0xE3
INIA 0x32
OUTA 0xE4
RSTA
OUTA 0xE5 ; Tile 200 begins at 0x3200.
INIA 0x01
OUTA 0xE2
RSTA
OUTA 0xE6
INIA 0x40
OUTA 0xE7
INIA 0x02
OUTA 0xE8 ; Fill: 64 pixels of index one.
INIA 0x01
OUTA 0xE0
SETD.1 LanderArtAt
SETD.0 LanderArt
STD.0.1
LDA.1
OUTA 0xE1
INCD.1
LDA.1
OUTA 0xE2
INIA 0d4
OUTA 0xE3
INIA 0x32
OUTA 0xE4
INIA 0x40
OUTA 0xE5 ; Tile 201, one tile on from 200.
RSTA
OUTA 0xE6
INIA 0x40
OUTA 0xE7
INIA 0x01
OUTA 0xE8 ; Blit.
RET
; ---- A moon, one column at a time ----
;
; A random walk over 128 columns, clamped so there is always sky above and ground below. The
; numbers come from an eight bit shift register with feedback, which is the cheapest thing
; that is not obviously a pattern: a byte, shifted right, exclusive-ored with a constant when
; the bit that fell off was set.
;
; The SEED IS FIXED, so the moon is the same moon every time. That is worth more than variety
; while the physics is being tuned - a landing that was too hard yesterday should be too hard
; today - and a seed is one byte to change later.
makeMoon:
INIA 0d18
SETD.1 Height
STA.1
SETD.2 Terrain
RSTA
SETD.1 Column
STA.1
makeColumn:
SETD.1 Height
LDA.1
STA.2 ; This column's height, as it stands.
INCD.2
CALL nextRandom
MVQA ; The answer is in Q, because Q is what survives a RET. Without
; this the AND below tests whatever A happened to hold - which
; was the height itself, so the moon oscillated by one row and
; looked flat.
INIB 0x01
AND
BNQ makeUp
; Down a row, unless that is already as low as the ground goes.
SETD.1 Height
LDA.1
INIB 0d22
CCF
SUB
BRQ makeNext ; Equal, so it is already at the floor.
LDA.1
INCA
STA.1
BRI makeNext
makeUp:
; And up a row, unless that is as high as it goes.
SETD.1 Height
LDA.1
INIB 0d10
CCF
SUB
BRQ makeNext
LDA.1
DECA
STA.1
makeNext:
; ---- The counter is in memory, and has to be ----
;
; B is the obvious place for a loop counter and it is the wrong one here: every comparison
; below is an INIB, so the count was overwritten by whichever bound was last tested and the
; loop reset itself for ever. This machine has two registers and a dozen uses for them; a
; counter that has to survive arithmetic lives in memory.
SETD.1 Column
LDA.1
INCA
STA.1
INIB 0d128
CCF
SUB
BNQ makeColumn
RET
; ---- Somewhere to put it down ----
;
; A random walk does not leave flat ground, and a lander wants some. So four pads are CARVED
; after the moon is made rather than looked for in it: searching can fail, and a fallback that
; carves anyway is the carving plus a search nobody needed.
;
; Each is four columns levelled to the height the first of them happened to have, so the pads
; sit in the landscape rather than on a shelf above it - the moon decides where they are, and
; this only decides that they are flat.
;
; The columns are marked in an array of their own. Asking "is this a pad" while drawing has to
; be one lookup: four comparisons per column per row is 12,800 of them for a screen.
carvePads:
SETD.3 PadTable
INIA 0d4
SETD.1 PadsLeft
STA.1
carveOne:
; The height this pad is levelled to, which is whatever its first column already was.
LDA.3
SETD.1 Terrain
DPUA.1
LDA.1
SETD.1 PadHeight
STA.1
RSTA
SETD.1 PadWide
STA.1
carveColumn:
; This column of the pad: levelled, and marked as somewhere to land.
LDA.3
SETD.1 PadWide
LDB.1
CCF
ADD
MVQA ; The pad's first column plus how far along this is.
PSHA
SETD.1 Terrain
DPUA.1
SETD.0 PadHeight
LDA.0
STA.1
POPA
SETD.1 IsPad
DPUA.1
INIA 0x01
STA.1
SETD.1 PadWide
LDA.1
INCA
STA.1
INIB 0d4
CCF
SUB
BNQ carveColumn
INCD.3
SETD.1 PadsLeft
LDA.1
DECA
STA.1
BNA carveOne
RET
; The shift register. Q comes back the new value, because Q is what survives a RET.
nextRandom:
SETD.1 Seed
LDA.1
INIB 0x01
AND
PSHQ ; The bit that is about to fall off.
LDA.1
RSTB
SHR ; A:B right one, and B was nought, so A is the byte halved.
POPB
BNB randomTap
STA.1 ; DP1 is still Seed, from reading it above.
RSTB
CCF
ADD ; Into Q, which is where a subroutine answers.
RET
randomTap:
INIB 0xB8
XOR
SETD.1 Seed
STQ.1
RET
; ---- The moon, drawn ----
;
; Rows outside and columns inside, which is the only affordable way round: cells along a row
; are next to each other, so the address is named once and the controller's Data port steps
; through 128 of them. Cells down a COLUMN are a page apart, and doing it that way would mean
; naming an address for every one of the 3,200 cells.
drawMoon:
RSTA
SETD.1 Row
STA.1
drawRow:
INIA 0d5
OUTA 0xE3
SETD.1 Row
LDA.1
INIB 0x40
CCF
ADD
OUTQ 0xE4 ; The map starts at 0x4000 and a row is a page.
RSTA ; A held the row; DestLow has to be nought.
OUTA 0xE5
SETD.2 Terrain
SETD.1 Column
STA.1 ; A is still the nought that went to DestLow.
drawCell:
; ---- Ground at the surface row and everything under it ----
;
; ROW MINUS HEIGHT and not the other way round, which matters at the surface itself: the
; two are equal there, equal does not borrow, and the row a column's surface is on has to
; be ground rather than the last of the sky.
SETD.1 Row
LDA.1
LDB.2
CCF
SUB
BRC drawSky ; Borrowed: this row is above the surface, so it is sky.
BRQ drawSurface ; Equal: this row IS the surface, which may be somewhere to land.
drawGround:
INIA 0xC8
OUTA 0xE9 ; Tile 200, the ground block.
RSTA
OUTA 0xE9 ; Attribute nought: grey on black, which is a fine moon.
BRI drawNextCell
; ---- The top of a pad, in a colour that says so ----
;
; The same solid tile in a different scheme, which costs no art at all: an attribute is a
; nibble added to every index in the tile, so one block is a grey moon and a cyan landing pad
; depending on the byte beside it.
drawSurface:
SETD.1 IsPad
SETD.0 Column
LDA.0
DPUA.1
LDA.1
BRA drawGround ; Not a pad, so it is ordinary moon.
INIA 0xC8
OUTA 0xE9
INIA 0x06
OUTA 0xE9 ; Scheme six, which is cyan against everything else out there.
BRI drawNextCell
drawSky:
RSTA ; A is still this column's height, from the comparison above.
OUTA 0xE9
OUTA 0xE9 ; Tile nought is the space, and no colour is needed for nothing.
drawNextCell:
INCD.2
SETD.1 Column
LDA.1
INCA
STA.1
INIB 0d128
CCF
SUB
BNQ drawCell
SETD.1 Row
LDA.1
INCA
STA.1
INIB 0d25
CCF
SUB
BNQ drawRow
RET
; ---- The lander, as a sprite ----
;
; Sprite nought, and it never moves horizontally: the world scrolls under it and it stays at
; the middle of the screen. That is one byte a frame instead of two and it is also what makes
; a moon that wraps invisible to the player - there is no moment where the lander jumps.
putLander:
INIA 0d4
OUTA 0xE3
INIA 0xC0
OUTA 0xE4
RSTA
OUTA 0xE5
INIA 0xC9
OUTA 0xE9 ; Tile 201.
INIA 0x03
OUTA 0xE9 ; Attribute three, so it is yellow against a grey moon.
INIA 0d156
OUTA 0xE9
RSTA
OUTA 0xE9 ; X: the middle of a 320 pixel screen, less half a tile.
OUTA 0xE9
OUTA 0xE9 ; Y, which every frame overwrites.
INIA 0x11
OUTA 0xE9 ; One tile by one.
RSTA
OUTA 0xE9 ; No flags, natural size, no depth.
RET
; ---- The gauge, which lives where the moon cannot scroll it away ----
;
; The window is a layer at a SCREEN position rather than a position in the world, so a bar
; drawn in it stays where it is put however far the moon turns underneath. One row, at the
; top, and the label written once because it never changes.
;
; The letters are ordinary tiles. The character generator starts at the space, so glyph n is
; character n plus thirty two - which makes F, U, E and L into 38, 53, 37 and 44.
putGauge:
INIA 0x01
OUTA 0x3D ; One row tall.
RSTA
OUTA 0x3E ; At the top of the screen.
INIA 0d5
OUTA 0xE3
INIA 0xC0
OUTA 0xE4
RSTA
OUTA 0xE5 ; The window begins at 0xC000 of the screen bank.
INIA 0d38
OUTA 0xE9
RSTA
OUTA 0xE9 ; F
INIA 0d53
OUTA 0xE9
RSTA
OUTA 0xE9 ; U
INIA 0d37
OUTA 0xE9
RSTA
OUTA 0xE9 ; E
INIA 0d44
OUTA 0xE9
RSTA
OUTA 0xE9 ; L
RET
; ---- And how much of it is left ----
;
; A byte of fuel makes a bar of up to 31 cells, which is the byte over eight: a shift, because
; there is no divide. Thirty five cells of room after the label, so a full tank does not quite
; fill the row and there is somewhere for it to be seen to stop.
;
; Redrawn whole every frame. Thirty five cells is seventy bytes out of one port with the
; address named once, which is cheaper than working out which of them changed.
showFuel:
RSTA
SETD.1 Fuel
LDB.1
CALL eighth ; Q is the fuel over eight, in the low half where it belongs.
MVQA
SETD.1 GaugeLeft
STA.1
INIA 0d5
OUTA 0xE3
INIA 0xC0
OUTA 0xE4
INIA 0x0A
OUTA 0xE5 ; Cell five of the window row, just past the label.
RSTA
SETD.1 GaugeAt
STA.1
gaugeCell:
; Solid while there is bar left to draw, blank after it.
SETD.1 GaugeLeft
LDA.1
BRA gaugeEmpty
DECA
STA.1
INIA 0xC8
OUTA 0xE9 ; The solid block, which is the ground's tile.
INIA 0x02
OUTA 0xE9 ; Scheme two, so the bar is not the colour of the moon.
BRI gaugeNext
gaugeEmpty:
OUTA 0xE9
OUTA 0xE9 ; A is nought here: no tile and no colour.
gaugeNext:
SETD.1 GaugeAt
LDA.1
INCA
STA.1
INIB 0d35
CCF
SUB
BNQ gaugeCell
RET
; ---- What the pad is holding ----
;
; 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.
; ---- ANY of the four, not pad nought ----
;
; One person flies this, and which socket they plugged into is not a thing they should have
; to know. A controller does not always land on nought - the front end hands out the numbers
; the host gave it - so a game that reads only the first one works on some machines and
; silently does nothing on others, which is the worst of both.
;
; Four reads and three ORs. A port is an immediate byte inside the instruction that names it,
; so it cannot be computed and the four are written out.
readPad:
INA 0x60
SETD.1 Held
STA.1
INA 0x61
LDB.1
OR
STQ.1
INA 0x62
LDB.1
OR
STQ.1
INA 0x63
LDB.1
OR
STQ.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
AND
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
LDA.1
INIB 0x82 ; Left
CCF
SUB
BRQ burnLeft
LDA.1
INIB 0x83 ; Right
CCF
SUB
BRQ burnRight
noKey:
RET
quit:
RSTA
SETD.1 Flying
STA.1
RET
burnUp:
SETD.0 SpeedDown
SETD.1 ThrustUp
CALL addWord
RET
burnLeft:
SETD.0 SpeedAcross
SETD.1 ThrustLeft
CALL addWord
RET
burnRight:
SETD.0 SpeedAcross
SETD.1 ThrustRight
CALL addWord
RET
; ---- Gravity, which is the reason this is a game ----
;
; NOT EVERY FRAME. One sixteenth of a pixel per frame per frame is the smallest step this
; arithmetic can take and it is still far too much - it crossed the screen in a second and
; flew like a gas giant. So it is applied one frame in FallEvery, which divides the pull by
; that much and costs a byte and a compare.
;
; The alternative was a finer unit for velocity than for position, which means a shift every
; time one is added to the other, twice a frame, for ever. A counter is cheaper and it is one
; byte to change while the feel is being found.
fall:
SETD.1 FallTick
LDA.1
DECA
STA.1
BNA fallDone
SETD.1 FallEvery
LDA.1
SETD.1 FallTick
STA.1
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
CALL takeFuel
BRQ fallNotUp ; Nothing in the tank, so nothing out of the engine.
SETD.0 SpeedDown
SETD.1 PadUp
CALL addWord
fallNotUp:
SETD.0 Held
LDA.0
INIB 0x02 ; Left
AND
BRQ fallNotLeft
CALL takeFuel
BRQ fallNotLeft
SETD.0 SpeedAcross
SETD.1 PadLeft
CALL addWord
fallNotLeft:
SETD.0 Held
LDA.0
INIB 0x01 ; Right
AND
BRQ fallDone
CALL takeFuel
BRQ fallDone
SETD.0 SpeedAcross
SETD.1 PadRight
CALL addWord
fallDone:
RET
; ---- One unit, if there is one ----
;
; Q comes back nought when the tank is empty, and the thruster that asked does not fire. There
; is no message and no ending: a lander with no fuel is still flying, it just cannot do
; anything about where. What happens next is gravity, and gravity is patient.
;
; Charged PER THRUSTER PER TICK, so holding two at once costs two - which is the honest price
; and makes a drift you corrected expensive in a way a drift you avoided is not.
takeFuel:
SETD.1 Fuel
LDA.1
BRA takeFuelNone
DECA
STA.1
INIA 0x01
RSTB
CCF
ADD ; Q is one, which the caller reads as yes.
RET
takeFuelNone:
RSTB
CCF
ADD ; A is nought here, so Q is too, which is no.
RET
; ---- Where it is now ----
;
; Across first, and the wrap is an AND rather than a comparison: the moon is 2^14 sixteenths
; round, so falling off one side is the top bits going away.
move:
SETD.0 AcrossLow
SETD.1 SpeedAcross
CALL addWord
SETD.0 AcrossHigh
LDA.0
INIB 0x3F
AND
STQ.0
SETD.0 DownLow
SETD.1 SpeedDown
CALL addWord
RET
; ---- The view, which follows it ----
;
; The lander is at the middle of the screen, so the view starts half a screen behind it. In
; PIXELS: the position is sixteenths, so it is shifted down four first.
follow:
SETD.0 AcrossLow
CALL toPixels
; Half a screen back, and round the moon if that went below nought.
SETD.0 PixelLow
SETD.1 HalfScreen
CALL subWord
SETD.0 PixelHigh
LDA.0
INIB 0x03
AND
STQ.0 ; 1024 pixels round, so three bits of high byte.
; The whole cells are the column origin and the remainder is the fine offset.
SETD.0 PixelLow
LDA.0
INIB 0x07
AND
OUTQ 0x37 ; Fine X.
SETD.0 PixelHigh
LDA.0
SETD.0 PixelLow
LDB.0
CALL eighth
OUTQ 0x36 ; The column origin. IN Q, because RET puts A back.
RET
; ---- The lander, put where it now is ----
showLander:
SETD.0 DownLow
CALL toPixels
INIA 0d4
OUTA 0xE3
INIA 0xC0
OUTA 0xE4
INIA 0x04
OUTA 0xE5 ; Y is bytes four and five of the entry.
SETD.0 PixelLow
LDA.0
OUTA 0xE9
SETD.0 PixelHigh
LDA.0
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.
; ---- 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
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 magnitudeDone
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
magnitudeDone:
RET
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
; 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
; ---- 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 LandColumn
STA.1 ; Kept, because the verdict below wants it too.
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
; ---- Down safely, and whether it was anywhere useful ----
;
; A pad is where a base is, and the rest of the moon is somewhere a lander can survive
; arriving at and do nothing about. The difference is one lookup, because the carving wrote
; an array rather than leaving four comparisons to be done again here.
SETD.1 IsPad
SETD.0 LandColumn
LDA.0
DPUA.1
LDA.1
BRA touchdownNowhere
SETD.0 AtBaseText
BRI touchdownStop
touchdownNowhere:
SETD.0 LandedText
BRI touchdownStop
touchdownCrash:
SETD.0 CrashedText
touchdownStop:
; ---- The view settled before anything is said ----
;
; The console draws into the map, and the map is what is being scrolled - so a message
; printed while the view is three pixels into a cell comes out three pixels off the top,
; with as much of its first row missing as the cell above it has lost.
;
; Nothing is being aligned for tidiness: the flying is over, so the fractional part of the
; view has no more work to do, and putting it back is what makes the whole message visible.
; A HUD that had to stay readable WHILE the map moved is a different problem - see the
; CosmOS README - and this is not it.
RSTA
OUTA 0x37
OUTA 0x38
SWI osPrintString
INIA 0x0A
OUTA 0x00
; ---- And a look at what happened ----
;
; The verdict was printed and the program then left immediately, taking the screen with it -
; so the one thing anybody wanted to see, the lander sitting on the ground it had just
; arrived at, was replaced by a shell prompt before it could be looked at.
touchdownWait:
INA 0x01
INIB 0x01 ; READY
AND
BRQ touchdownWait
INA 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
; it. The carry runs from one half to the other, which is what ADD taking the carry flag is
; for, and CCF at the top is what stops the last sum leaking into this one.
addWord:
CCF
LDA.0
LDB.1
ADD
STQ.0
INCD.0
INCD.1
LDA.0
LDB.1
ADD
STQ.0
RET
subWord:
CCF
LDA.0
LDB.1
SUB
STQ.0
INCD.0
INCD.1
LDA.0
LDB.1
SUB
STQ.0
RET
; ---- Sixteenths into pixels ----
;
; DP0 names a two byte value, low half first. A and B are one sixteen bit register and it
; ROTATES rather than shifts, so four turns to the right bring the bottom four bits round into
; the top - which is why the high byte is masked afterwards and the low byte is not.
toPixels:
LDB.0
INCD.0
LDA.0
SHR
SHR
SHR
SHR
SETD.1 PixelHigh
STA.1 ; Kept whole for a moment, because B has to come out of B first.
RSTA
CCF
ADD ; Nothing plus B is B, in Q, which can be moved.
MVQA
SETD.1 PixelLow
STA.1
SETD.1 PixelHigh
LDA.1
INIB 0x0F
AND
STQ.1
RET
; ---- A pixel column, from a pixel ----
;
; A IS THE HIGH HALF and B the low, which is the way round the shift register is - it was
; written the other way first and the view scrolled by 256 cells for every one it should have.
; Three turns right is a divide by eight, and a moon 1024 pixels round is 128 columns, so the
; whole answer is in the low half and the high half is the bits that rotated out of it.
;
; ---- And the answer comes back in Q ----
;
; It came back in A first, which is a lie a subroutine cannot tell: RET puts A back the way it
; found it, so the caller wrote out the high byte of the position it had passed in. The fine
; register was computed inline and was right, the coarse register was not, and the picture
; scrolled smoothly within a cell and never advanced one. Q is the only register that crosses
; a RET, which is why every answer in this program comes back in it.
;
; B cannot be read at all. Adding nothing to it puts it in Q, which is where this wanted to
; be anyway.
eighth:
SHR
SHR
SHR
RSTA
CCF
ADD
RET
waitFrame:
INA 0x30
INIB 0x01
AND
BRQ waitFrame
RET
#Data
#Base 0x3000
; Sixteenths of a pixel, low half first, because that is the order the sums above walk in.
; ---- Which is over a base, because that is where a day starts ----
;
; 0x14A0 sixteenths is 330 pixels, which is column 41 - inside the pad carved at 40. Starting
; in the middle of nowhere meant a straight descent landed in the middle of nowhere, which is
; a fine thing to be able to do and a poor thing to have to.
AcrossLow:
0xA0
AcrossHigh:
0x14
DownLow:
0x00
DownHigh:
0x02 ; A little way down from the top.
SpeedAcross:
0x00 0x00
SpeedDown:
0x00 0x00
; ---- The four numbers the feel lives in ----
;
; Adjacent on purpose, because tuning them is what the first rung is for.
Gravity:
0x01 0x00
FallEvery:
0d10 ; ---- Gravity one frame in ten ----
;
; Every frame was Jupiter and one in six was still touchy: the
; ratio between thrust and gravity is the FEEL, and how often the
; tick comes round is how fast that feel arrives. Slower ticks are
; the same lander with more time to think in.
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.
; ---- 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:
0xFF 0xFF ; Minus one.
PadRight:
0x01 0x00
Held:
0x00
HasPad:
0x00
DriftLow:
0x00
DriftHigh:
0x00
DriftLeftward:
0x00
DriftAt:
0x00
DriftSize:
0x00
; ---- The tank ----
;
; One byte, and a byte is enough: over eight it is a bar of up to 31 cells, and at one unit a
; thruster a tick it is about forty seconds of holding the engine open. Sixteen bits would be
; more arithmetic for a number nobody reads to the unit.
Fuel:
0xFF
GaugeLeft:
0x00
GaugeAt:
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.
PixelLow:
0x00
PixelHigh:
0x00
Height:
0x00
Row:
0x00
Column:
0x00
Seed:
0x5D
KeyHeld:
0x00
Flying:
0x01
Terrain:
#Reserve 0d128
; One byte a column: whether a lander touching down there is at a base. Written by the carving
; and read while drawing and while landing, which is the whole reason it is an array rather
; than four comparisons done again every time somebody asks.
IsPad:
#Reserve 0d128
PadTable:
0d8 0d40 0d72 0d104 ; Four bases, evenly round a moon 128 columns about.
PadsLeft:
0x00
PadWide:
0x00
PadHeight:
0x00
LandColumn:
0x00
LandedText:
"Down safely, in the middle of nowhere."
AtBaseText:
"Down safely at a base."
CrashedText:
"Crashed."
LanderArt:
0x00 0x00 0x01 0x01 0x01 0x01 0x00 0x00
0x00 0x01 0x01 0x01 0x01 0x01 0x01 0x00
0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01
0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01
0x00 0x01 0x01 0x01 0x01 0x01 0x01 0x00
0x00 0x01 0x00 0x00 0x00 0x00 0x01 0x00
0x01 0x01 0x00 0x00 0x00 0x00 0x01 0x01
0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x01
LanderArtAt:
#Reserve 0d2