; 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. CALL putTiles CALL makeMoon CALL drawMoon CALL putLander INIA 0x01 OUTA 0x02 ; Key mode. everyFrame: CALL waitFrame CALL readControls CALL fall CALL move CALL follow CALL showLander 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 ; 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. INIA 0xC8 OUTA 0xE9 ; Tile 200, the ground block. RSTA OUTA 0xE9 ; Attribute nought: grey on black, which is a fine moon. 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 controls ---- ; ; ONE KEY IS ONE BURN. The console says which key was pressed and there is no such thing as a ; key being released, so a thruster cannot be held down - what a press does is add to the ; velocity once. It reads as pumping the engine rather than leaning on it, which is a choice ; this machine makes rather than one this program did. readControls: INA 0x01 INIB 0x01 ; READY AND BRQ noKey INA 0x00 INIB 0x80 ; Up CCF SUB BRQ burnUp SETD.1 KeyHeld STA.1 LDA.1 INIB 0x82 ; Left CCF SUB BRQ burnLeft LDA.1 INIB 0x83 ; Right CCF SUB BRQ burnRight LDA.1 INIB 0x71 ; q CCF SUB BRQ quit 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 fallDone: 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 ; ---- 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. AcrossLow: 0x00 AcrossHigh: 0x0F ; Somewhere near the middle of the moon to start. 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: 0d6 ; Gravity one frame in six. Every frame was Jupiter. FallTick: 0d1 ThrustUp: 0xF8 0xFF ; Eight sixteenths upwards, which is minus eight. ThrustLeft: 0xFC 0xFF ; Four to the left. ThrustRight: 0x04 0x00 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 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