Episode 6: Attractive Saucers
Were we're introducing the fierce saucers (here still peaceful) and get, while not as simple as intended, the attract mode for free.
Code – write-up balance is really something, I've to improve on in order to progress with this project. There were some things I wanted to see next to another in a write-up, and, fortunately, we've done this already. But now, as we're coming to the more complex things, we wont be able to keep up with the previous level of detail. This may be sad, as there are lots of things to mention and to discuss, but, yeah….
On the bright side, we arrive at what is already the fully implemented attract mode including all the logic regarding saucer movement and animation. For testing purpose, we're also doing a few things that will be not in attract mode, namely ticking the clock and inverting the screen, as the clock wraps around at 99/00. And this is, what looks like:
▶ Try it in in-browser emulation.
General Obeservations
Before we touch any of the details, there are actually a few things to write home about. As a few of the readers may recall, I implemented the game for RetroChallenge 2016/10 for the early 1960's DEC PDP-1 (announced Nov/Dec. 1959). While the PDP-1 features just 2 usable registers and a rather restricted, RISC-like instruction set, coding the same game in PDP-1 assembler was actually fun and intuitive, thanks to the straight-forward instruction set and universal indirect addressing. While the PDP-1 is far from an orthogonal machine, coding for it was much more orthogonal experience, when it came to approaching specific tasks.
The 6502, while providing 3 principal registers and a stack, proved much more complicated: Frequently, we've to handle 3 things at the same time, like x and y coordinates and a value (e.g., screen-x, screen-y, and a screen code to go there). If there's one more thing to handle, like processing any of this values with regard to yet another one, you're running out of resources. So there's still the stack. But we may push the accumulator only, so we may preserve only 2 out of 3 registers, since we'll have to destruct the contents of one of them. Also, the order of execution matters, since the stack is last-in-first-out (LIFO) and, if there is a subroutine involved, the processor is accessing the stack, too. So you have to observe nesting levels of code, which isn't always an option.
But the biggest hassle is indirect addressing, because — apart from the jmp
instruction — it's only available for zero page addresses. Moreover, there's always either the X or the Y register involved, so, while the zero-page is really more like a set of additional registers, you're losing another register in the process. The 6502 is noticeably designed with higher languages in mind and coding in assembler is often worse than with the humble PDP-8 (the epitome of the mini computer — sharing the notion of zero page memory as a set of additional registers, but actually optimizing on this by auto-indexing registers and a more general indirect addressing scheme). As a result, the path from the idea of an algorithm to implementation is a much more complicated one as on the PDP-1, where a thought translates directly to code, forcing us, while coding for the 6502, into a state of mind which is more and more about schemes and patterns that may be used to arrive at an implementation. Overall, this renders coding for the 6502 a comparatively much more nerdy experience.
Dirty Details
And here are the nasty details:
Asynchrounous Screen Updates
We mentioned before that we were going to have a queue for storing screen addresses and screen codes to be updated during V BLANK by the interrupt routine. Actually, we're going to have two of them, one for resetting background characters and one for fresh content. Looping over a repeating structure of bytes as in address Lo, address Hi, screen code, ...
and copying the value (screen code) into the given address in screen memory should be easy. But, in fact, it isn't. Neither of the indirect address modes is of help, as none of them allows us to use the address bytes already in the queue as a pointer (that is, if the queue isn't in the zero page, which is sadly not an option). Copying the address and reaccessing them by indirect addressing would cost us 12 cycles per address byte and isn't an option for a loop in the interrupt routine, so we've to do it with self-modifying code:
; symbols / constants charQueue = $027A ;start of cassette buffer, used as a drawing buffer resetQueue = charQueue+60 ;buffer for screen resets ; zero-page addresses qPosX = $2B ;temp x coor for display purpose qPosY = $2C ;temp y coor for display purpose qScreenCode = $2D ;temp screen code for display purpose charQueuePtr = $2E ;pointer to top offset of charQueue resetQueuePtr = $2F ;pointer to top offset of charQueue ; draws chars in charQueue of (screenCode, addrLo, addrHi)* ; self-modifying (sets address at .dcqScreen, sta xxxx) drawCharQueue ldx charQueuePtr ;get top-of-queue pointer beq .dcqDone ;exit, if empty dex .dcqLoop lda charQueue, x ;get screen address hi-byte sta .dcqScreen+2 ;fix-up dex lda charQueue, x ;get screen address lo-byte sta .dcqScreen+1 ;fix-up dex lda charQueue, x ;get screen code eor videoMask ;adjust for normal/reverse video .dcqScreen sta $ffff ;store it (dummy address) dex bpl .dcqLoop lda #0 ;reset top-of-queue pointer sta charQueuePtr .dcqDone rts
And there's also a copy of this code (drawResetQueue
) for drawing the characters stored in the reset-queue (resetQueue
).
This is the code for pushing a screen code/character and an address onto the queue, which also eclipses any off-screen characters:
; a single character 'sprite routine' ; pushes a screen code and address onto the charQueue, if on-screen pushScreenCode lda qPosY bmi .pcqDone ;negative cmp #25 ;gte 25 (off-screen to the bottom)? bcs .pcqDone lda qPosX bmi .pcqDone ;negative cmp #40 ;gte 40 (off-screen to the right)? bcs .pcqDone ldx charQueuePtr lda qScreenCode sta charQueue, x inx ldy qPosY lda qPosX clc adc screenLinesLo, y sta charQueue, x inx lda #0 adc screenLinesHi, y sta charQueue, x inx stx charQueuePtr .pcqDone rts
And, yes, there's another copy of this code (pushScreenReset
) for servicing the reset-queue.
Pushing Saucers
On the bright side, we've abstracted much of what is involved in servicing the screen. Therefor, pushing the characters of the saucer outline is rather straight forward. As a saucer is 3 characters wide (see below), there may be a negative offset of two horizontal screen positions with a bit of the saucer still left visible. Thus, we've either to use negative offsets, or we've to use a positive offset in our internal system of coordinates. We decide to go with the latter, adding an offset of 2 both to the x and y coordinates respectively.
;saucer outline, $20 (blank) ignored !byte $20,$64,$20 ; ▁ !byte $73,$20,$6B ; ┤ ├ !byte $20,$63,$20 ; ▔
displaySaucer ;pushes a saucer at saucerX /saucerY onto the charQueue ldx saucerY dex ;2 pos offset dex dex ;-1 for top row stx qPosY ldx saucerX dex ;2 pos offset dex stx qPosX lda #$64 sta qScreenCode jsr pushScreenCode ;$64 at x, y-1 ldx qPosY inx stx qPosY ldx qPosX dex stx qPosX lda #$73 sta qScreenCode jsr pushScreenCode ;$73 at x-1, y ldx qPosX inx stx qPosX ldx saucerPhase lda saucerPhases, x sta qScreenCode jsr pushScreenCode ;center code at x, y ldx qPosX inx stx qPosX lda #$6B sta qScreenCode jsr pushScreenCode ;$6B at x+1, y ldx qPosY inx stx qPosY ldx qPosX dex stx qPosX lda #$63 sta qScreenCode jsr pushScreenCode ; $63 at x, y+1 rts
And there's a similar routine for clearing a saucer at it's current position, using our getStar
routine for the right background screen code. We could optimize our code here for specific movements, but we've to go for the worst case with regard to run time anyway. So we decide to go on — so there's still a chance of us finishing the project — and stick to the worst case approach, resettimng and redrawing the entire outline each time the saucer moves.
The animation of the saucer's center is yet another routine, which is to be called more often.
animateSaucer ;saucer center animation lda saucerPhaseDir ;flag for animation direction: 0 = left, 1 = right beq .asLeft ldx saucerPhase inx cpx #10 bne .asNext ldx #0 beq .asNext .asLeft ldx saucerPhase dex bpl .asNext ldx #9 .asNext stx saucerPhase lda saucerPhases, x sta qScreenCode ldx saucerX dex ;2 pos offset dex stx qPosX ldx saucerY dex ;2 pos offset dex stx qPosY jsr pushScreenCode rts ; variables saucerX !byte 0 saucerY !byte 0 saucerDx !byte 0 saucerDy !byte 0 saucerPhase !byte 0 saucerPhaseDir !byte 0 saucerPhaseMask !byte 0 saucerCnt !byte 0 saucerLegCnt !byte 0 ; data saucerPhases !byte $20,$65,$54,$47,$42,$5D,$48,$59,$67,$20
Finally, regarding display code, we've to service the saucer twice by an offset. For this we introduce a routine flipping the saucer's y coordinate by an offset (assembler symbol saucerOffset
= maxY/2
= 15):
flipSaucer ;flips saucerY by saucerOffset lda saucerY clc adc #saucerOffset cmp #maxY bcc .fpSave sec sbc #maxY .fpSave sta saucerY rts
Roaming the Heavens
This leaves us yet the task of moving the saucer around. We'll do this every 7 frames and we're going to control this by a counter of its own (allowing us to freely configure the speed to our liking). As the saucer(s) move(s) for a certain time and then either sits idle or changes its direction, we have to set up a leg-count and decide on dx and dy values and also for the direction and speed of the center animation for this leg. For this we implement a simple 1-byte random number generator:
ran = $31 ;random number (1 byte) random ; a simple random number generator lda ran ror lda ran ror eor %11011001 sta ran rts
The rest is rather a matter of sitting down and tediously coding our random-based decision tree for setting up all the respective values controlling the saucer and ccalling the routines for clearing, display and animation as needed. (See the listing below for details.)
And here we are. For the purpose of testing, we stick with the counting seconds, which should be really set to "00" in attract mode. (This reveals a tiny glitch, regarding our plane stacking and mixing scheme, still to be addressed.) Also, we move the switch to reverse video to the seconds counter fully wrapping around, from 99 to 00.
And this is what it looks in white:
And in reverse video:
▶ Try it in in-browser emulation.
Code Listing
And here is our code, in its entirety (yes, we more than doubled the byte count since last episode):
△! Caution, the interrupt routine used in this version does not preserve registers! (Not an issue here.)
!to "saucer.prg", cbm ;set output file and format ; symbols / constants ticksPerSecond = 60 ;60: time in game corresponds to NTSC timing charQueue = $027A ;start of cassette buffer, used as a drawing buffer resetQueue = charQueue+60 ;buffer for screen resets maxX = 45 ;x-coors max value maxY = 30 ;y-coors max value saucerSpeed = 7 ;frames saucerOffset = 15 ;screen lines y offset ; zero-page ; BASIC input buffer at $23 .. $5A may be reused safely (cf, PET 2001 manual) gameState = $23 ;0: attract, 1: active fIRQ = $24 ;flag to synchronize irq operations fRepaint = $25 ;flag for video rendering/irq control ticks = $26 ;ticks counter videoMask = $27 ;0: normal, $80: reverse (xor-ed) IRQVector = $28 ;backup of original irq vector (2 bytes) frameCounter = $2A ;counter for animations qPosX = $2B ;temp x coor for display purpose qPosY = $2C ;temp y coor for display purpose qScreenCode = $2D ;temp screen code for display purpose charQueuePtr = $2E ;pointer to top offset of charQueue resetQueuePtr = $2F ;pointer to top offset of charQueue scoreRepaint = $30 ;flag to request a repaint (buffer to fRepaint) ran = $31 ;random number (1 byte) PT1 = $50 ;versatile pointer (2 bytes) PT2 = $52 ;versatile pointer (2 bytes) ; intro ; insert a tiny BASIC program, calling our code at $044C (1100) ; ; 10 REM PERSONAL COMPUTER SPACE ; 20 REM TRANSACTOR 2001 (NL,2017) ; 30 SYS 1100 * = $0401 !byte $1F, $04, $0A, $00, $8F, $20, $50, $45 ; $0401 !byte $52, $53, $4F, $4E, $41, $4C, $20, $43 ; $0409 !byte $4F, $4D, $50, $55, $54, $45, $52, $20 ; $0411 !byte $53, $50, $41, $43, $45, $00, $3F, $04 ; $0419 !byte $14, $00, $8F, $20, $54, $52, $41, $4E ; $0421 !byte $53, $41, $43, $54, $4F, $52, $20, $32 ; $0429 !byte $30, $30, $31, $20, $28, $4E, $4C, $2C ; $0431 !byte $32, $30, $31, $37, $29, $00, $4A, $04 ; $0439 !byte $1E, $00, $9E, $20, $31, $31, $30, $30 ; $0441 !byte $00, $00, $00 ; $0449 .. $044B ; main * = $044C ; reset / setup cld ;reset BCD flag lda #0 sta fRepaint setup ; setup irq vector sei lda $91 and #$F0 cmp #$E0 ;is it ROM 2.0 or higher? bne .rom1 ;no, it's ROM 1.0 .rom2 lda $90 sta IRQVector lda $91 sta IRQVector+1 lda #<irqRoutine sta $90 lda #>irqRoutine sta $91 jmp .setupDone .rom1 lda $219 sta IRQVector lda $21A sta IRQVector+1 lda #<irqRoutine sta $219 lda #>irqRoutine sta $21A .setupDone cli init lda #0 sta gameState sta videoMask sta fRepaint jsr background lda #0 sta score1 sta score2 sta time1 sta time2 sta ticks sta frameCounter sta charQueuePtr sta saucerCnt sta saucerLegCnt lda $E844 ; initialize random number from VIA timer 1 sta ran lda #10 sta saucerY lda #18 sta saucerX jsr displaySaucer jsr animateSaucer lda #1 sta fRepaint sta fIRQ ; main job loop loop lda fIRQ bne loop ;manage a frame lda #0 sta scoreRepaint lda ticks ;manage time sec sbc #ticksPerSecond ;has a second passed? bcc .gameFrame ;no sta ticks inc time1 lda time1 cmp #$0A bne .loopScoresFinal lda #0 sta time1 inc time2 lda time2 cmp #$0A bne .loopScoresFinal lda #0 sta time2 jsr revertVideo .loopScoresFinal lda #1 sta scoreRepaint .gameFrame jsr saucerHandler .loopIter sei lda scoreRepaint ora resetQueuePtr ora charQueuePtr sta fRepaint .loopEnd lda #1 sta fIRQ cli jmp loop ; irq handling irqRoutine inc ticks ;manage time inc frameCounter .checkRepaint lda fRepaint beq .irqDone jsr drawResetQueue jsr drawScores jsr drawCharQueue .irqDone lda #0 sta fRepaint sta fIRQ jmp (IRQVector) ; subroutines background ;fills the screen with stars ldx #24 .row lda screenLinesLo, x sta PT1 lda screenLinesHi, x sta PT1+1 ldy #39 .col jsr getStar sta (PT1), y dey bpl .col dex bpl .row rts getStar ;returns a background screen code (in AC) for row X, col Y lda starMaskY, x beq .blank and starMaskX, y beq .blank lda #$2E ; return a dot rts .blank lda #$20 ; return a blank rts ; score and time display ; screen locations of score and time numerals screenAddressScore1 = $8000 + 4*40 + 36 screenAddressScore2 = $8000 + 10*40 + 36 screenAddressTime1 = $8000 + 16*40 + 36 screenAddressTime2 = $8000 + 16*40 + 33 drawScores ;draws scores and time display ldy score1 lda #<screenAddressScore1 sta PT1 lda #>screenAddressScore1 sta PT1+1 jsr drawDigit ldy score2 lda #<screenAddressScore2 sta PT1 lda #>screenAddressScore2 sta PT1+1 jsr drawDigit ldy time1 lda #<screenAddressTime1 sta PT1 lda #>screenAddressTime1 sta PT1+1 jsr drawDigit ldy time2 lda #<screenAddressTime2 sta PT1 lda #>screenAddressTime2 sta PT1+1 jsr drawDigit rts drawDigit ;draws a digit (screen address in PT1, digit in Y) ldx digitOffsets, y ldy #0 lda #4 sta PT2 .dgRow lda digits, x eor videoMask ;adjust for normal/reverse video sta (PT1), y inx iny lda digits, x eor videoMask sta (PT1), y dec PT2 beq .dgDone inx dey ;reset y to zero and increment PT1 by a screen line clc lda PT1 adc #40 sta PT1 bcc .dgRow inc PT1+1 jmp .dgRow .dgDone rts revertVideo ;reverts the screen video lda videoMask eor #$80 sta videoMask ldx #24 .rvRow lda screenLinesLo, x sta PT1 lda screenLinesHi, x sta PT1+1 ldy #39 .rvCol lda (PT1), y eor #$80 sta (PT1), y dey bpl .rvCol dex bpl .rvRow rts ; draws chars in charQueue of (screenCode, addrLo, addrHi)* ; self-modifying (sets address at .dcqScreen, sta xxxx) drawCharQueue ldx charQueuePtr ;get top-of-queue pointer beq .dcqDone ;exit, if empty dex .dcqLoop lda charQueue, x ;get screen address hi-byte sta .dcqScreen+2 ;fix-up dex lda charQueue, x ;get screen address lo-byte sta .dcqScreen+1 ;fix-up dex lda charQueue, x ;get screen code eor videoMask ;adjust for normal/reverse video .dcqScreen sta $ffff ;store it (dummy address) dex bpl .dcqLoop lda #0 ;reset top-of-queue pointer sta charQueuePtr .dcqDone rts ; same as above, but for resetQueue drawResetQueue ldx resetQueuePtr beq .drqDone dex .drqLoop lda resetQueue, x sta .drqScreen+2 dex lda resetQueue, x sta .drqScreen+1 dex lda resetQueue, x eor videoMask .drqScreen sta $ffff dex bpl .drqLoop lda #0 sta resetQueuePtr .drqDone rts ; a single character 'sprite routine' ; pushes a screen code and address onto the charQueue, if on-screen pushScreenCode lda qPosY bmi .pcqDone ;negative cmp #25 ;gte 25 (off-screen to the bottom)? bcs .pcqDone lda qPosX bmi .pcqDone ;negative cmp #40 ;gte 40 (off-screen to the right)? bcs .pcqDone ldx charQueuePtr lda qScreenCode sta charQueue, x inx ldy qPosY lda qPosX clc adc screenLinesLo, y sta charQueue, x inx lda #0 adc screenLinesHi, y sta charQueue, x inx stx charQueuePtr .pcqDone rts ; same as above,but for resetQueue pushScreenReset lda qPosY bmi .psrDone cmp #25 bcs .psrDone lda qPosX bmi .psrDone cmp #40 bcs .psrDone ldx resetQueuePtr lda qScreenCode sta resetQueue, x inx ldy qPosY lda qPosX clc adc screenLinesLo, y sta resetQueue, x inx lda #0 adc screenLinesHi, y sta resetQueue, x inx stx resetQueuePtr .psrDone rts random ; a simple random number generator lda ran ror lda ran ror eor %11011001 sta ran rts ; saucer(s) saucerHandler dec saucerCnt bmi .shUpdate lda scoreRepaint ;do we have a score/time update? bne .shRedraw ;yes, redraw the saucers jmp .shAnimate ;just check the animation state .shRedraw jmp .shDisplay .shUpdate lda #saucerSpeed sta saucerCnt jsr clearSaucer jsr flipSaucer jsr clearSaucer jsr flipSaucer dec saucerLegCnt bpl .shMoveY jsr random ;new random number in ac and ran and #15 clc adc #7 sta saucerLegCnt ;7..22 lda ran and #1 sta saucerPhaseDir ;0/1 ldx #3 lda ran bpl .shAnimSpeed ldx #7 .shAnimSpeed stx saucerPhaseMask ;3/7 jsr random and #$3F beq .shStop ;another opportunity to stop and #3 cmp #3 bne .shSaveDx ;0,1,2 lda #0 .shSaveDx sta saucerDx jsr random and #3 cmp #3 bne .shSaveDy lda #0 .shSaveDy sta saucerDy ;0,1,2 .shMoveY ldx saucerY lda saucerDy beq .shMoveX cmp #1 beq .shMoveY1 inx cpx #maxY bcc .shSaveY ldx #0 beq .shSaveY .shMoveY1 dex bpl .shSaveY ldx #maxY-1 .shSaveY stx saucerY .shMoveX ldx saucerX lda saucerDx beq .shDisplay cmp #1 beq .shMoveX1 inx cpx #maxX bcc .shSaveX ldx #0 beq .shSaveX .shMoveX1 dex bpl .shSaveX ldx #maxX-1 .shSaveX stx saucerX .shDisplay jsr displaySaucer jsr flipSaucer jsr displaySaucer jsr flipSaucer .shAnimate lda frameCounter and saucerPhaseMask bne .shDone jsr animateSaucer .shDone rts .shStop lda #0 sta saucerDx sta saucerDy jmp .shMoveY flipSaucer ;flips saucerY by saucerOffset lda saucerY clc adc #saucerOffset cmp #maxY bcc .fpSave sec sbc #maxY .fpSave sta saucerY rts displaySaucer ;pushes a saucer at saucerX /saucerY onto the charQueue ldx saucerY dex ;2 pos offset dex dex ;-1 for top row stx qPosY ldx saucerX dex ;2 pos offset dex stx qPosX lda #$64 sta qScreenCode jsr pushScreenCode ;$64 at x, y-1 ldx qPosY inx stx qPosY ldx qPosX dex stx qPosX lda #$73 sta qScreenCode jsr pushScreenCode ;$73 at x-1, y ldx qPosX inx stx qPosX ldx saucerPhase lda saucerPhases, x sta qScreenCode jsr pushScreenCode ;center code at x, y ldx qPosX inx stx qPosX lda #$6B sta qScreenCode jsr pushScreenCode ;$6B at x+1, y ldx qPosY inx stx qPosY ldx qPosX dex stx qPosX lda #$63 sta qScreenCode jsr pushScreenCode ; $63 at x, y+1 rts clearSaucer ;pushes a saucer at saucerX /saucerY onto the resetQueue ldx saucerY dex ;2 pos offset dex dex ;-1 for top row stx qPosY ldy saucerX dey ;2 pos offset dey sty qPosX jsr getStar sta qScreenCode jsr pushScreenReset ldx qPosY inx stx qPosY ldy qPosX dey sty qPosX jsr getStar sta qScreenCode jsr pushScreenReset ldx qPosY ldy qPosX iny sty qPosX jsr getStar sta qScreenCode jsr pushScreenReset ldx qPosY ldy qPosX iny sty qPosX jsr getStar sta qScreenCode jsr pushScreenReset ldx qPosY inx stx qPosY ldy qPosX dey sty qPosX jsr getStar sta qScreenCode jsr pushScreenReset rts animateSaucer ;saucer center animation lda saucerPhaseDir beq .asLeft ldx saucerPhase inx cpx #10 bne .asNext ldx #0 beq .asNext .asLeft ldx saucerPhase dex bpl .asNext ldx #9 .asNext stx saucerPhase lda saucerPhases, x sta qScreenCode ldx saucerX dex ;2 pos offset dex stx qPosX ldx saucerY dex ;2 pos offset dex stx qPosY jsr pushScreenCode rts ; variables score1 !byte 0 score2 !byte 0 time1 !byte 0 time2 !byte 0 saucerX !byte 0 saucerY !byte 0 saucerDx !byte 0 saucerDy !byte 0 saucerPhase !byte 0 saucerPhaseDir !byte 0 saucerPhaseMask !byte 0 saucerCnt !byte 0 saucerLegCnt !byte 0 ; data starMaskX !byte $20, $00, $40, $0A, $08, $01, $82, $00 !byte $00, $00, $00, $00, $40, $00, $00, $02 !byte $00, $04, $20, $10, $88, $44, $00, $40 !byte $00, $01, $20, $00, $00, $42, $14, $00 !byte $48, $20, $00, $10, $18, $00, $00, $40 starMaskY !byte $40, $00, $01, $00, $00, $08, $00, $04 !byte $00, $40, $00, $02, $00, $00, $01, $00 !byte $04, $00, $10, $00, $20, $00, $01, $00 !byte $80 screenLinesHi !byte $80 !byte $80 !byte $80 !byte $80 !byte $80 !byte $80 !byte $80 !byte $81 !byte $81 !byte $81 !byte $81 !byte $81 !byte $81 !byte $82 !byte $82 !byte $82 !byte $82 !byte $82 !byte $82 !byte $82 !byte $83 !byte $83 !byte $83 !byte $83 !byte $83 screenLinesLo !byte $00 !byte $28 !byte $50 !byte $78 !byte $A0 !byte $C8 !byte $F0 !byte $18 !byte $40 !byte $68 !byte $90 !byte $B8 !byte $E0 !byte $08 !byte $30 !byte $58 !byte $80 !byte $A8 !byte $D0 !byte $F8 !byte $20 !byte $48 !byte $70 !byte $98 !byte $C0 digits ;0 !byte $62,$62 !byte $61,$E1 !byte $61,$E1 !byte $FC,$FE ;1 !byte $20,$6C !byte $20,$E1 !byte $20,$E1 !byte $20,$E1 ;2 !byte $62,$62 !byte $20,$E1 !byte $EC,$E2 !byte $FC,$62 ;3 !byte $62,$62 !byte $20,$E1 !byte $7C,$FB !byte $62,$FE ;4 !byte $7B,$6C !byte $61,$E1 !byte $E2,$FB !byte $20,$E1 ;5 !byte $62,$62 !byte $61,$20 !byte $E2,$FB !byte $62,$FE ;6 !byte $7B,$20 !byte $61,$20 !byte $EC,$FB !byte $FC,$FE ;7 !byte $62,$62 !byte $20,$E1 !byte $20,$E1 !byte $20,$E1 ;8 !byte $62,$62 !byte $61,$E1 !byte $EC,$FB !byte $FC,$FE ;9 !byte $62,$62 !byte $61,$E1 !byte $E2,$FB !byte $20,$E1 digitOffsets !byte 0, 8, 16, 24, 32, 40, 48, 56, 64, 72 saucerPhases !byte $20,$65,$54,$47,$42,$5D,$48,$59,$67,$20
(Assembles to 1,309 bytes of binary code.)
— Stay tuned! —
▶ Next: Episode 7: Rocket (Phew!)
◀ Previous:  Episode 5: Sync!
▲ Back to the index.
April 2017, Vienna, Austria
www.masswerk.at – contact me.
— This series is part of Retrochallenge 2017/04. —