Going sideways lifts the moon off you. Not because gravity weakened - because at speed the surface falls away underneath as fast as the lander falls towards it, which is what an orbit is. A GRADIENT OUT OF INTEGER ARITHMETIC. Gravity is one sixteenth of a pixel a tick and there is nothing between that and nothing, so it cannot be scaled down. Instead four times the sideways speed goes into a byte every tick and the tick's gravity is skipped whenever that byte carries: the fraction cancelled is the speed over 64, smoothly, with no multiply and no divide. At four pixels a frame it carries every time. That is the linear approximation; the honest one is the square, and wants a table. It did nothing at all for its first two versions. Once because the relief was a 256th a tick, so orbit wanted a speed no lander would reach; and once because A IS THE HIGH HALF of the shift register, so multiplying by four left the answer in A while the code read B, which is nought. The same trap as the scroll register and the pixel conversion before it. And a bar for the vertical speed beside the one for drift, both GREEN WHILE A LANDING WOULD SURVIVE AND RED WHILE IT WOULD NOT. That turns two numbers into one question - can I put down - and answers it at a glance. WHAT THIS COST: the flown delivery check. A recording is a list of buttons and not a flight, so replaying it under different gravity flies somewhere else; the delivery became a crash two columns short. The fixture is still there and is still a faithful record of what somebody did, and is no longer a record of what happens. That is the standing cost of a flown fixture, and it is worse than the transcript tests dropped earlier: those broke when an output moved, and this breaks whenever a NUMBER moves. Making the delivery reachable without flying - a way to start already carrying, or at a chosen base - is what would fix it properly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
1774 lines
47 KiB
NASM
1774 lines
47 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 showFall
|
|
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
|
|
; ---- WHICH base, and not merely that there is one ----
|
|
;
|
|
; The number plus one, so nought still means no pad and the array is still one lookup. A
|
|
; flag would have to be followed by "and which of the four", which is the same walk done
|
|
; twice for an answer that was already in hand.
|
|
INIA 0d4
|
|
SETD.0 PadsLeft
|
|
LDB.0
|
|
CCF
|
|
SUB
|
|
MVQA ; Four less what is left to carve, which counts up from nought.
|
|
INCA
|
|
STA.1
|
|
|
|
SETD.1 PadWide
|
|
LDA.1
|
|
INCA
|
|
STA.1
|
|
INIB 0d8 ; ---- Eight columns, not four ----
|
|
;
|
|
; Four was 32 pixels of pad in a moon 1024 round, which is a
|
|
; target somebody flying by feel misses over and over. Eight is
|
|
; still small and is somewhere a person can aim at.
|
|
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.
|
|
DECA ; Back to the base's own number.
|
|
SETD.1 PadColour
|
|
DPUA.1
|
|
INIA 0xC8
|
|
OUTA 0xE9 ; The solid block again.
|
|
LDA.1
|
|
OUTA 0xE9 ; And this base's scheme, which is the whole of its name.
|
|
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 0x02
|
|
OUTA 0x3D ; Two rows: the gauge, and whatever there is to say.
|
|
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
|
|
; Gravity does not pull on a lander that is already sitting on the ground.
|
|
SETD.0 Landed
|
|
LDA.0
|
|
BNA fallResting
|
|
|
|
; ---- Orbit ----
|
|
;
|
|
; The faster it is going sideways, the less of the moon it feels. Not because gravity got
|
|
; weaker - because at speed the surface is falling away underneath as fast as the lander is
|
|
; falling towards it, which is what an orbit IS.
|
|
;
|
|
; A GRADIENT OUT OF INTEGER ARITHMETIC. Gravity is one sixteenth of a pixel a tick and there
|
|
; is nothing between that and nothing, so it cannot simply be scaled down. Instead the
|
|
; sideways speed is added into a byte every tick and the tick's gravity is skipped whenever
|
|
; that byte carries - so the fraction of ticks cancelled is the speed over 256, smoothly,
|
|
; with no multiply and no divide. At 256 sixteenths a frame it carries every time and the
|
|
; lander is in orbit.
|
|
;
|
|
; That is the linear approximation. The honest one is the square of the speed, which wants a
|
|
; multiply this machine has not got; a table would give the curve if the feel ever asks for
|
|
; it. What is here is one add and one branch.
|
|
SETD.0 SpeedAcross
|
|
CALL magnitude
|
|
SETD.0 DriftHigh
|
|
LDA.0
|
|
BNA fallResting ; A whole high byte of it is far past orbital speed.
|
|
; ---- Four times the speed, and orbit at sixty four ----
|
|
;
|
|
; The speed alone made the relief a 256th a tick, so orbit wanted a lateral speed no lander
|
|
; would ever reach and the whole thing was invisible. Times four puts it at 64 sixteenths -
|
|
; four pixels a frame, which crosses the moon in about three seconds and takes ten seconds
|
|
; of holding a thruster to build. Something worked up to rather than stumbled into.
|
|
SETD.0 DriftLow
|
|
LDA.0
|
|
INIB 0d64
|
|
CCF
|
|
SUB
|
|
BNC fallOrbiting ; No borrow, so it is at orbital speed and gravity is gone.
|
|
LDA.0 ; DP0 is still DriftLow, from the comparison above.
|
|
RSTB
|
|
SHL
|
|
SHL ; ---- Times four, and it comes out in A ----
|
|
;
|
|
; A is the HIGH half of the shift register, so a value in A with
|
|
; nothing in B is already that value times 256 - and rotating left
|
|
; twice makes it times 1024, whose top byte is the value times
|
|
; four. Written the other way first, reading B out of it, which is
|
|
; nought every time: the relief was always none and the whole
|
|
; mechanic did nothing at all.
|
|
SETD.1 OrbitAt
|
|
LDB.1
|
|
CCF
|
|
ADD
|
|
STQ.1
|
|
BRC fallResting ; Carried, so this tick's pull is what the speed cancelled.
|
|
BRI fallPulled
|
|
fallOrbiting:
|
|
BRI fallResting
|
|
|
|
fallPulled:
|
|
SETD.0 SpeedDown
|
|
SETD.1 Gravity
|
|
CALL addWord
|
|
fallResting:
|
|
|
|
; ---- 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 Landed
|
|
LDA.0
|
|
BRA fallAlreadyUp
|
|
RSTA
|
|
STA.0 ; Lifting off, which is the only way to stop being landed.
|
|
SETD.0 EmptyText
|
|
CALL sayInWindow ; And whatever the base said goes with it.
|
|
fallAlreadyUp:
|
|
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
|
|
|
|
; ---- Green if it would survive, red if it would not ----
|
|
;
|
|
; The magnitude is in DriftLow and DriftHigh and DP1 names the limit. Q comes back the
|
|
; attribute to draw with, which turns a bar from a number into an ANSWER: a person aiming at a
|
|
; pad does not want to know their speed, they want to know whether they can put it down.
|
|
barColour:
|
|
SETD.0 DriftHigh
|
|
LDA.0
|
|
BNA barColourFast ; A whole high byte of speed is past any limit worth having.
|
|
SETD.0 DriftLow
|
|
LDA.0
|
|
LDB.1
|
|
CCF
|
|
SUB
|
|
BNC barColourFast ; No borrow, so it is at or over the limit.
|
|
INIA 0x02
|
|
RSTB
|
|
CCF
|
|
ADD ; Scheme two, green, which is within tolerance.
|
|
RET
|
|
barColourFast:
|
|
INIA 0x01
|
|
RSTB
|
|
CCF
|
|
ADD ; Scheme one, red, which is not.
|
|
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
|
|
SETD.1 GentleAcross
|
|
CALL barColour
|
|
SETD.1 DriftColour
|
|
STQ.1
|
|
; ---- 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.
|
|
SETD.0 DriftColour
|
|
LDA.0
|
|
OUTA 0xE9 ; Green while this drift could be landed with, red while not.
|
|
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.
|
|
|
|
; Already sitting on it, so there is nothing new to say. Without this the arrival happens
|
|
; again every frame the lander rests, which is a base handing out cargo sixty times a
|
|
; second.
|
|
SETD.0 Landed
|
|
LDA.0
|
|
BNA touchdownDone
|
|
|
|
; ---- 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, which is not the end of anything ----
|
|
;
|
|
; The view settles, the arrival is dealt with, and the lander sits where it is until
|
|
; somebody opens the throttle again. A landing used to stop the program, which is fine for a
|
|
; game about landing once and wrong for one about carrying things between four places.
|
|
RSTA
|
|
OUTA 0x37
|
|
OUTA 0x38
|
|
CALL arrive
|
|
CALL waitKey
|
|
|
|
; Sitting still, exactly on the surface. Both speeds go, because a lander that kept a
|
|
; hundredth of a pixel a frame would drift off its pad while nobody was looking.
|
|
RSTA
|
|
SETD.0 SpeedDown
|
|
STA.0
|
|
INCD.0
|
|
STA.0
|
|
SETD.0 SpeedAcross
|
|
STA.0
|
|
INCD.0
|
|
STA.0
|
|
CALL restOnSurface
|
|
INIA 0x01
|
|
SETD.0 Landed
|
|
STA.0
|
|
RET
|
|
|
|
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.
|
|
CALL waitKey
|
|
RSTA
|
|
SETD.1 Flying
|
|
STA.1 ; Which ends the loop, and the program tidies up as it always did.
|
|
touchdownDone:
|
|
RET
|
|
|
|
; ---- A key, or a button ----
|
|
;
|
|
; Somebody flying on a controller should not have to reach for the keyboard to say "yes, I
|
|
; read that". A or Start does it, and so does any key - asked for rather than waited on, so
|
|
; one already pressed counts.
|
|
waitKey:
|
|
CALL readPad
|
|
SETD.0 Held
|
|
LDA.0
|
|
INIB 0x50 ; A, or Start.
|
|
AND
|
|
BNQ waitKeyDone
|
|
INA 0x01
|
|
INIB 0x01 ; READY
|
|
AND
|
|
BRQ waitKey
|
|
INA 0x00
|
|
waitKeyDone:
|
|
RET
|
|
|
|
; ---- Put down exactly on the ground ----
|
|
;
|
|
; The feet are the top plus eight, so the top is the surface less eight - times sixteen, which
|
|
; is what the position is measured in. Three turns left of the shift register with the pixels
|
|
; in the low half, and the answer needs both halves because a screen is taller than 255
|
|
; sixteenths.
|
|
restOnSurface:
|
|
SETD.0 SurfaceAt
|
|
LDA.0
|
|
INIB 0d8
|
|
CCF
|
|
SUB
|
|
MVQB
|
|
RSTA
|
|
SHL
|
|
SHL
|
|
SHL
|
|
SHL ; Four turns, which is times sixteen.
|
|
SETD.0 DownHigh
|
|
STA.0
|
|
RSTA
|
|
CCF
|
|
ADD
|
|
MVQA
|
|
SETD.0 DownLow
|
|
STA.0
|
|
RET
|
|
|
|
; ---- A line of text, into the window ----
|
|
;
|
|
; DP0 names a string. It goes into window row one and the rest of the row is BLANKED, so a
|
|
; short message never leaves the tail of a long one behind it.
|
|
;
|
|
; Into the window rather than out of the console, which is the whole of two bugs at once. The
|
|
; console draws into the map, so a message printed while flying was a message the lander then
|
|
; flew over - and printing scrolls, so every one of them moved the whole world up a row.
|
|
; The window is at a screen position and forty cells wide, and neither is true of it.
|
|
;
|
|
; The letters are ordinary tiles: the character generator starts at the space, so glyph n is
|
|
; character n less thirty two.
|
|
sayInWindow:
|
|
INIA 0d5
|
|
OUTA 0xE3
|
|
INIA 0xC1
|
|
OUTA 0xE4
|
|
RSTA
|
|
OUTA 0xE5 ; Window row one begins at 0xC100.
|
|
SETD.1 SayAt
|
|
STA.1
|
|
|
|
sayCell:
|
|
LDA.0
|
|
BRA sayBlank ; The string has ended, so the rest of the row is nothing.
|
|
INIB 0d32
|
|
CCF
|
|
SUB
|
|
OUTQ 0xE9
|
|
INIA 0x07
|
|
OUTA 0xE9 ; Scheme seven, white, which is nothing else on this screen.
|
|
INCD.0
|
|
BRI sayNext
|
|
sayBlank:
|
|
OUTA 0xE9
|
|
OUTA 0xE9 ; A is nought here, and DP0 stays put so it stays nought.
|
|
sayNext:
|
|
SETD.1 SayAt
|
|
LDA.1
|
|
INCA
|
|
STA.1
|
|
INIB 0d40
|
|
CCF
|
|
SUB
|
|
BNQ sayCell
|
|
RET
|
|
|
|
; ---- What a base does when a lander arrives ----
|
|
;
|
|
; Empty and at a base: it hands over cargo for the base ACROSS THE MOON, two along, so the
|
|
; pairs are cyan with red and green with blue. Carrying and at the right base: it takes the
|
|
; cargo and pays in fuel. Carrying and at the wrong one: nothing, which is the whole reason
|
|
; the destination is on the screen.
|
|
arrive:
|
|
SETD.1 IsPad
|
|
SETD.0 LandColumn
|
|
LDA.0
|
|
DPUA.1
|
|
LDA.1
|
|
BRA arriveNowhere
|
|
DECA
|
|
SETD.1 AtBase
|
|
STA.1
|
|
|
|
SETD.0 Carrying
|
|
LDA.0
|
|
BRA arriveLoad
|
|
|
|
; Carrying something. Is this where it goes?
|
|
DECA
|
|
LDB.1 ; DP1 is still AtBase, from storing it above.
|
|
CCF
|
|
SUB
|
|
BNQ arriveWrong
|
|
|
|
RSTA
|
|
STA.0 ; And DP0 is still Carrying, from reading it.
|
|
CALL payFuel
|
|
SETD.0 DeliveredText
|
|
CALL sayInWindow
|
|
RET
|
|
|
|
arriveLoad:
|
|
; Two along, round a moon of four bases, which is the far side of it.
|
|
SETD.1 AtBase
|
|
LDA.1
|
|
INIB 0x02
|
|
CCF
|
|
ADD
|
|
MVQA
|
|
INIB 0x03
|
|
AND
|
|
MVQA
|
|
INCA ; Plus one, so nought can go on meaning nothing.
|
|
SETD.0 Carrying
|
|
STA.0
|
|
SETD.0 LoadedText
|
|
CALL sayInWindow
|
|
RET
|
|
|
|
arriveWrong:
|
|
SETD.0 WrongText
|
|
CALL sayInWindow
|
|
RET
|
|
|
|
arriveNowhere:
|
|
SETD.0 LandedText
|
|
CALL sayInWindow
|
|
RET
|
|
|
|
; ---- Paid ----
|
|
;
|
|
; Eighty units, and a tank that will not take more than it holds. The carry from a byte's
|
|
; worth of adding is what says it went past, which is the same flag a sixteen bit sum uses to
|
|
; run one half into the other.
|
|
payFuel:
|
|
SETD.0 Fuel
|
|
LDA.0
|
|
INIB 0d80
|
|
CCF
|
|
ADD
|
|
BRC payFuelFull
|
|
STQ.0
|
|
RET
|
|
payFuelFull:
|
|
INIA 0xFF
|
|
STA.0
|
|
RET
|
|
|
|
; ---- How fast downwards, as a bar down the side ----
|
|
;
|
|
; The same idea stood on its end. Sprite two, three pixels wide, growing DOWN from the middle
|
|
; of the screen while falling and UP while climbing - so which way is as plain as how fast,
|
|
; and a lander holding its height has no bar at all.
|
|
;
|
|
; Two bars and two limits is the whole instrument panel: when both are green the lander can be
|
|
; put down, and that is a question a person can answer at a glance instead of by counting.
|
|
showFall:
|
|
SETD.0 SpeedDown
|
|
CALL magnitude
|
|
SETD.1 GentleDown
|
|
CALL barColour
|
|
SETD.1 FallColour
|
|
STQ.1
|
|
|
|
; Clamped, and to less than the drift bar because there is half a screen either way.
|
|
SETD.0 DriftHigh
|
|
LDA.0
|
|
BNA fallBarWide
|
|
SETD.0 DriftLow
|
|
LDA.0
|
|
INIB 0d90
|
|
CCF
|
|
SUB
|
|
BRC fallBarReady
|
|
fallBarWide:
|
|
INIA 0d90
|
|
SETD.0 DriftLow
|
|
STA.0
|
|
|
|
fallBarReady:
|
|
; No tiles at all when there is nothing to show, which is the off switch: a target size of
|
|
; nought is the NATURAL size, not an empty sprite.
|
|
SETD.0 DriftLow
|
|
LDA.0
|
|
BRA fallBarEmpty
|
|
INIA 0x11
|
|
BRI fallBarSized
|
|
fallBarEmpty:
|
|
RSTA
|
|
fallBarSized:
|
|
SETD.1 FallSize
|
|
STA.1
|
|
|
|
; Where it starts: the middle going down, and that much back going up.
|
|
INIA 0d100
|
|
SETD.0 DriftLeftward
|
|
LDB.0
|
|
BRB fallBarAt ; Nought is downward, so it starts at the middle.
|
|
SETD.0 DriftLow
|
|
LDB.0
|
|
CCF
|
|
SUB
|
|
MVQA
|
|
fallBarAt:
|
|
SETD.1 FallAt
|
|
STA.1
|
|
|
|
INIA 0d4
|
|
OUTA 0xE3
|
|
INIA 0xC0
|
|
OUTA 0xE4
|
|
INIA 0x20
|
|
OUTA 0xE5 ; Sprite two begins thirty two bytes in.
|
|
INIA 0xC8
|
|
OUTA 0xE9 ; The solid block.
|
|
SETD.0 FallColour
|
|
LDA.0
|
|
OUTA 0xE9
|
|
INIA 0x2C
|
|
OUTA 0xE9
|
|
INIA 0x01
|
|
OUTA 0xE9 ; X is 300, which is two bytes: the screen is wider than one.
|
|
SETD.0 FallAt
|
|
LDA.0
|
|
OUTA 0xE9
|
|
RSTA
|
|
OUTA 0xE9 ; Y.
|
|
SETD.0 FallSize
|
|
LDA.0
|
|
OUTA 0xE9
|
|
RSTA
|
|
OUTA 0xE9 ; No flags.
|
|
INIA 0x03
|
|
OUTA 0xE9
|
|
RSTA
|
|
OUTA 0xE9 ; Three pixels wide.
|
|
SETD.0 DriftLow
|
|
LDA.0
|
|
OUTA 0xE9
|
|
RSTA
|
|
OUTA 0xE9 ; And as tall as the lander is fast.
|
|
OUTA 0xE9 ; No depth.
|
|
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
|
|
DriftColour:
|
|
0x00
|
|
FallColour:
|
|
0x00
|
|
FallSize:
|
|
0x00
|
|
FallAt:
|
|
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.
|
|
|
|
; ---- Their names, which are colours ----
|
|
;
|
|
; A base is told from a base by the scheme its pad is drawn in, so "the cyan one" is a thing a
|
|
; person can say and a thing the machine already knows. Yellow is missing on purpose: it is
|
|
; the lander, and a base the same colour as the thing landing on it would be a poor joke.
|
|
;
|
|
; Cargo goes to the base ACROSS THE MOON, two along, so the pairs are cyan with red and green
|
|
; with blue. That is what makes the wrapping surface a route rather than scenery.
|
|
PadColour:
|
|
0d2 0d6 0d4 0d1 ; Green, cyan, blue, red.
|
|
PadNames:
|
|
0x00 ; Room kept: the messages name colours in words below.
|
|
PadsLeft:
|
|
0x00
|
|
PadWide:
|
|
0x00
|
|
PadHeight:
|
|
0x00
|
|
LandColumn:
|
|
0x00
|
|
AtBase:
|
|
0x00
|
|
|
|
; Where the orbit sum has got to. The sideways speed goes in here every tick and the carry out
|
|
; of it is a tick of gravity that never happened.
|
|
OrbitAt:
|
|
0x00
|
|
SayAt:
|
|
0x00
|
|
|
|
; ---- What is aboard, and whether the feet are down ----
|
|
;
|
|
; Carrying is the destination base plus one, so nought means an empty hold and the number is
|
|
; still one byte. Landed is what stops a base handing out cargo sixty times a second while a
|
|
; lander sits on it.
|
|
Carrying:
|
|
0x00
|
|
Landed:
|
|
0x00
|
|
|
|
; ---- Forty cells is the row, so forty characters is the limit ----
|
|
LandedText:
|
|
"Nowhere in particular. Nothing here."
|
|
LoadedText:
|
|
"Loaded. Take it across the moon."
|
|
DeliveredText:
|
|
"Delivered. Eighty units of fuel."
|
|
WrongText:
|
|
"Not the base this cargo is for."
|
|
EmptyText:
|
|
0x00
|
|
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
|