Retrochallenge 2017/04:
Personal Computer Space Transactor 2001

Episode 8: Space Commander

A short update on lengthy coding and some progress — read, fully piloted space flight…

Quietly I was busy on the project, the last two days. And while there haves been delays, we eventually added substantially to the game, leaving "only" missiles and collisions on the to-do list. With our current version, we're already in command of a maneuverable rocket ship and may cruise outer space, having a look at the — still friendly — saucers:

Rocket ship in Personal Computer Space Transactor 2001

Outer space piloting.

Try it in in-browser emulation.

 

We had also to tweek the emulator for this, since it came originally with emulated key-repeat, a feature not to be found on the PET 2001. While key-repeat is a great thing for typing BASIC programs and editing screens, it adds a delay between key presses, which renders games comparatively inresponsive. Therefor we added an option to disable key-repeat for the best of all worlds.

Progress Update

(I'm keeping this rather short in order to get this write-up done at all…)

Things that have been done (since last episode):

Still to do:

While the score management and hit-detection isn't much of an issue, the guided (steerable) rocket missile promises some "fun"…

Generally, I found this much more complicated than doing the same in PDP-1 assembler. I'm coming more and more to the conclusion that we may well have no video games at all, hasn't it been for the PDP-1 (the machine, on which the very first interactive real-time game, Spacewar!, was done) and its favorable instruction set.

However some may be related to the particular mental (dis)abilities of your humble author:

Stupid Errors

Much time has been wasted due to typos and stupid errors. E.g., I found the reason of last episode's nerve-wracking issue: Apparently I had messed up the code for backing-up the registers in the IRQ routine (now fixed). Since the clear-rocket routine was the last to be executed, this was also the most likely point, where we may have been "bitten" by the interrupt, leaving one of our registers in a dirty state. (Hours of coding, recoding, debugging, spend on this…). Just the same, I somehow used the X-position of the rocket both for the x- and y-coordinates in the drawing & clearing routines. Somehow unable to see this in the code, I spent some hours on the movement/acceleration routines for the rocket, searching an error in vain, once even starting over from scratch, as I was unable to find the fault in the approach taken.

Since I'm doing this project on a MacBook Air, its true-PET-style chiclet keyboard (or rather, my use of it) adds to the mess, providing typographical surprises, remnant of Easter days.

So, with some extra hours spent already, I'm closing early. Here's a screenshot from the title screen as seen in the emulator:

Personal Computer Space Transactor 2001 (title screen)

The implemented title screen in emulation.

Try it in in-browser emulation.

Code

Sadly, I havent much time left to go into detail. Some parts may be of interest, e.g., the rocket motion routine and the code for reading the keyboard.

Rocket Motion, Subsequently Subpixeled Sub-Charactered

I haven't found an example of 6502 code for an Asteroids-like movement with velocity constraints, so this may be of interest for some. The motion routine consists of two parts, the first one bing a thrust routine, where we add to the fractional part of a 16-bit, signed dx/dy motion vector (fixed point, fractional point at byte borders) . The hi-byte will be always either $FF (for negative values) or $00 (for positive ones). The result is then checked for max-velocity constraints. Here, it is important that the sum of the max-velocity and the greatest extent of any delta is less than a half-byte (127), else the branching for negative values may fail on an overrun into the sign bit.

In a second part, executed each frame, we add this 16-bit fractional motion vector to the current x/y coordinates, which are also 16-bit, but only the respective hi-bytes are of interest for any other logic of the game (treated as a unsigned 8-bit values for the rest of the game). Since the granularity of this 16-bit motion vector is still rather coarse (that is, for the very purpose of character graphics), we also apply an arithmetic shift to the left to the motion delta before summing it with the current positional coordinates. Since the 6502 doesn't provide an arithmetic shift to the left, we'll have to do it on our own, by a rotate left instruction (rol), for which we setup the carry to be rotated in at the highest significant bit position (hsb) according to the sign-bit (1 for negative, 0 for positive). (Note: The same could be achieved by laoding the respective value, rotating it to the right first, to shift the sign-bit into the carry, loading it again, and applying the final rotate to the left.) The rest of the motion routine is then about checking constraints and wrapping at the edges of the screen for toroidal space.

Update: we eventually settle for the following construct, which is about the same in terms of cycle counts, but much clearer:

                pha         ;emulate 'asr' instruction
                rol
                pla
                ror

Reading the Keyboard

Of more general interst may be the keyboard routine. The tricky part is in forcing the OS into key-repeat, which doesn't come with the PET 2001 out of the box. If wouldn't work around this, a key would be only registered once for an elongated key-press, while we really want to read/access the current key-press value in each frame.

The trick is in writing a value of $FF (for no key registered) into the very memory location, where the OS stores the last column value read from keyboard matrix. While we could go on using the universal Commodore jump vector at $FFE4 for reading the last value registered during system interrupt, a brief investigation of a ROM disassembly reveals a tour through a number of various subroutines, which isn't only costly in terms of runtime, but rather superfluous, since the value is already stored in the keyboard buffer. Therefore, we procede to read the last value in the keyboard buffer, if there's any at all, and reset the keyboard buffer index to zero, for a fresh start at the next keyboard scan (else, values may pile up and we won't get the latest keyboard scan at all). Therefor, our routine doubles also as a keyboard reset. Any character found in the keyboard buffer will be returned in the accumulator (AC) as a PETSCII value, or a zero, if the buffer was empty. The function is universial, as for keyboard layouts and localizations, since these are already taken care of by the OS. (Otherwise, if we were scanning the keyboard matrix manually, we would have to decode differently for a business keyboard than we do for the original chiclet keyboard, and so on.)

The only problem left is in the different locations of the keyboard buffer and the backup of the last keyboard column's scan value for ROM 1.0. We simply set a flag (newROM, 0 for the original ROM 1.0, else 1), where we already configure for the ROM version in order to setup the interrupt vector, and branch on the flag to service all ROMs.

(BTW, while not mentioned at the title screen, we're secretly adding optional key mappings for fire, namely "D", normally "S", and left turn, "J", normally "K", in order to provide usabilty and comfort to those with bigger hands.)

readKbd ;reads a character from keyboard, returns char in AC (0 = empty)
                lda newRom
                beq .rkbdRom1
.rkbdRom2       ldx $9E                ;get # of chars in keyboard buffer
                beq .kbdEmpty
                lda $026E,x            ;get latest char from buffer    
                ldx #$FF               ;reset keboard matrix
                stx $97                ; for key repeat
                ldx #0                 ;reset  keyboard buffer index
                stx $9E                ; to clear the queue
                rts
.rkbdRom1    ldx $020D                 ;same as above for ROM 1.0
                beq .kbdEmpty
                lda $020E,x
                ldx #$FF
                stx $0203
                ldx #0
                stx $020D
                rts
.kbdEmpty
                lda #0                 ;even 1 byte shorter when using 'txa' instead
                rts

So much for educational content. ;-)

*****

Code Listing

And here is our code, so far:

!to "rocket-rc2.prg", cbm ;set output file and format

; symbols / constants

screenCols = 40       ;number of screen columns: 40/80 (only 40 cols tested)
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

rocketFreq = 12       ;frames (update frequency, responsiveness)
saucerFreq = 8        ;frames (update frequency, speed)
saucerOffset = 15     ;screen lines y offset (maxY/2)
rocketVMax = 32       ;max velocity fractional value

; 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)
newRom = $2A          ;flag ROM 1.0 (0) / ROM 2.0+ (1)
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)
frameCounter = $31    ;counter for animations
ran = $32             ;random number (1 byte)
PT1 = $50             ;versatile pointer (2 bytes)
PT2 = $52             ;versatile pointer (2 bytes)
IPT1 = $54            ;versatile pointer for interrupt tasks (2 bytes)
IPT2 = $56            ;versatile pointer for interrupt tasks (2 bytes)

; intro

; insert a tiny BASIC program, calling our code at $044C (1100)
;
; 10 REM PERSONAL COMPUTER SPACE
; 20 REM TRANSACTOR 2001, V.0.1, 2017
; 30 SYS 1103

                * = $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, $42, $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, $2C, $20, $56, $2E, $30 ; $0431
                !byte $2E, $31, $2C, $20, $32, $30, $31, $37 ; $0439
                !byte $00, $4D, $04, $1E, $00, $9E, $20, $31 ; $0441
                !byte $31, $30, $33, $00, $00, $00  ; $0449 .. $044E


; main

                * = $044F
                ; 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
                lda #1
                sta newRom
                jmp .setupDone
.rom1           lda $219
                sta IRQVector
                lda $21A
                sta IRQVector+1
                lda #<irqRoutine
                sta $219
                lda #>irqRoutine
                sta $21A
                lda #0
                sta newRom
.setupDone      cli


title
                jsr drawTitleScreen
                jsr readKbd           ; reset the keyboard
                lda #1
                sta fIRQ
                
titleLoop
                lda fIRQ
                bne titleLoop
                jsr readKbd
                cmp #0
                bne init
                lda #1
                sta fIRQ
                jmp titleLoop
                

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
                sta rocketCnt
                
                lda $E844            ; initialize random number from VIA timer 1 
                sta ran

                jsr readKbd          ; reset the keyboard

                lda #10
                sta saucerY
                lda #18
                sta saucerX
                jsr displaySaucer
                jsr animateSaucer

                lda #3
                sta rocketDir
                lda #10
                sta rocketX
                lda #12
                sta rocketY

                lda #1
                sta fRepaint
                sta fIRQ


; main job loop
loop
                lda fIRQ
                bne loop

                lda #0                 ;reset top-of-queue pointers
                sta charQueuePtr
                sta resetQueuePtr
                
                lda gameState
                bne .gameRunning

.attractMode
                lda rocketCnt           ;reused as a temp. counter
                cmp #30                 ; for 30 frames minimum offset
                beq .amkbd
                jsr readKbd             ;reset keyboard
                inc rocketCnt
                bpl .gameFrame2
.amkbd          jsr readKbd
                cmp #0
                beq .gameFrame2
                lda #0                  ;start the game
                sta ticks
                sta rocketCnt
                inc gameState
                jsr displayRocket

.gameRunning
                ;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 rocketHandler
.gameFrame2
                jsr saucerHandler

.loopIter sei
                lda scoreRepaint
                ora resetQueuePtr
                ora charQueuePtr
                sta fRepaint
.loopEnd        lda #1
                sta fIRQ
                cli
                jmp loop


; irq handling

irqRoutine
                pha                   ;save registers
                txa
                pha
                tya
                pha

                inc ticks             ;manage time
                inc frameCounter

.checkRepaint
                lda fRepaint
                beq .irqDone
                jsr drawResetQueue
                jsr drawScores
                jsr drawCharQueue

.irqDone
                lda #0
                sta fRepaint
                sta fIRQ
                pla                    ;restore register
                tay
                pla
                tax
                pla
                jmp (IRQVector)


; subroutines

readKbd ;reads a character from keyboard, returns char in AC (0 = empty)
                lda newRom
                beq .rkbdRom1
.rkbdRom2       ldx $9E                ;get # of chars in keyboard buffer
                beq .kbdEmpty
                lda $026E,x            ;get char from buffer    
                ldx #$FF               ;reset keboard matrix
                stx $97                ; for key repeat
                ldx #0                 ;reset index of keyboard buffer
                stx $9E                ; to clear the queue
                rts
.rkbdRom1    ldx $020D                 ;same as above for ROM 1.0
                beq .kbdEmpty
                lda $020E,x
                ldx #$FF
                stx $0203
                ldx #0
                stx $020D
                rts
.kbdEmpty
                lda #0
                rts


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*screenCols + 36
screenAddressScore2 = $8000 + 10*screenCols + 36
screenAddressTime1 = $8000 + 16*screenCols + 36
screenAddressTime2 = $8000 + 16*screenCols + 33

drawScores ;draws scores and time display
                ldy score1
                lda #<screenAddressScore1
                sta IPT1
                lda #>screenAddressScore1
                sta IPT1+1
                jsr drawDigit
                ldy score2
                lda #<screenAddressScore2
                sta IPT1
                lda #>screenAddressScore2
                sta IPT1+1
                jsr drawDigit
                ldy time1
                lda #<screenAddressTime1
                sta IPT1
                lda #>screenAddressTime1
                sta IPT1+1
                jsr drawDigit
                ldy time2
                lda #<screenAddressTime2
                sta IPT1
                lda #>screenAddressTime2
                sta IPT1+1
                jsr drawDigit
                rts

drawDigit ;draws a digit (screen address in IPT1, digit in Y)
                ldx digitOffsets, y
                ldy #0
                lda #4
                sta IPT2
.dgRow          lda digits, x
                eor videoMask   ;adjust for normal/reverse video
                sta (IPT1), y
                inx
                iny
                lda digits, x
                eor videoMask
                sta (IPT1), y
                dec IPT2
                beq .dgDone
                inx
                dey             ;reset y to zero and increment IPT1 by a screen line
                clc
                lda IPT1
                adc #screenCols
                sta IPT1
                bcc .dgRow
                inc IPT1+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
.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
.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  ;negative
                cmp #25       ;gte 25 (off-screen to the bottom)?
                bcs .psrDone
                lda qPosX
                bmi .psrDone  ;negative
                cmp #40       ;gte 40 (off-screen to the right)?
                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 #saucerFreq
                sta saucerCnt
                jsr clearSaucer
                jsr flipSaucer
                jsr clearSaucer
                jsr flipSaucer

                dec saucerLegCnt
                bpl .shMoveY
                jsr random
                and #15
                clc
                adc #7
                sta saucerLegCnt
                lda ran
                and #1
                sta saucerPhaseDir
                ldx #3
                lda ran
                bpl .shAnimSpeed
                ldx #7
.shAnimSpeed    stx saucerPhaseMask
                jsr random
                and #$3F
                beq .shStop
                and #3
                cmp #3
                bne .shSaveDx
                lda #0
.shSaveDx       sta saucerDx
                jsr random
                and #3
                cmp #3
                bne .shSaveDy
                lda #0
.shSaveDy       sta saucerDy

.shMoveY        ldx saucerY
                lda saucerDy
                beq .shMoveX
                cmp #1
                beq .shMoveY1
                inx
                cpx #maxY
                bcc .shSaveY
                ldx #0
                jmp .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
                jmp .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


; rocket

rocketHandler
                lda #0
                sta rocketRedraw
                lda rocketX
                sta rocketXN
                lda rocketY
                sta rocketYN
                lda rocketDir
                sta rocketDirN
                lda #0
                sta rocketThrustingN

                jsr readKbd
                cmp #$4B              ;K
                beq .rhLeft
                cmp #$4A              ;J (for big hands)
                beq .rhLeft
                cmp #$4C              ;L
                beq .rhRight
                cmp #$41              ;A
                beq .rhThrust
;               cmp #$53              ;S
;               beq .rhFire
;               cmp #$44              ;D (for big hands)
;               beq .rhFire
                jmp .rhMove

.rhLeft lda #$FF
                sta rocketTurn
                jmp .rhMove

.rhRight lda #$01
                sta rocketTurn
                jmp .rhMove

.rhThrust
                inc rocketThrustingN
                ldx rocketDirN              ;direction index in X
                clc                         ;inc dx
                lda rocketDxLo
                adc rocketDirDx, x
                bmi .rhThrustXM             ;process negative value
                cmp #rocketVMax             ;check max velocity (positive)
                bcc .rhThrustX1
                lda #rocketVMax-1
.rhThrustX1     sta rocketDxLo
                lda #0                      ;set HI-byte / sign
                jmp .rhThrustX3
.rhThrustXM     cmp #-rocketVMax            ;check max velocity (negative)
                bcs .rhThrustX2
                lda #-rocketVMax+1
.rhThrustX2     sta rocketDxLo
                lda #$FF                    ;set HI-byte / sign
.rhThrustX3     sta rocketDx
.rhThrustY
                clc                         ;inc dy
                lda rocketDyLo
                adc rocketDirDy, x
                bmi .rhThrustYM             ;process negative value
                cmp #rocketVMax             ;check max velocity (positive)
                bcc .rhThrustY1
                lda #rocketVMax-1
.rhThrustY1     sta rocketDyLo
                lda #0                      ;set HI-byte / sign
                jmp .rhThrustY3
.rhThrustYM     cmp #-rocketVMax            ;check max velocity (negative)
                bcs .rhThrustY2
                lda #-rocketVMax+1
.rhThrustY2     sta rocketDyLo
                lda #$FF                    ;set HI-byte / sign
.rhThrustY3     sta rocketDy
                ;jmp .rhMove

.rhMove         dec rocketCnt               ;process Turn on rocketCnt underflow
                bpl .rhMoveX
                lda #rocketFreq
                sta rocketCnt
                lda rocketTurn
                beq .rhMoveX                ;empty / no turn
                clc
                adc rocketDir
                and #7
                sta rocketDirN
                inc rocketRedraw            ;flag for redraw
                lda #0                      ;reset turn
                sta rocketTurn
                
.rhMoveX                                    ;move by dx
                lda rocketDxLo
                bmi .rhMoveXRM              ;emulate asr instruction
                clc
                jmp .rhMoveXR
.rhMoveXRM      sec
.rhMoveXR       ror
                clc                         ;sum it
                adc rocketXLo
                sta rocketXLo
                lda rocketX
                adc rocketDx
                bmi .rhMoveXM               ;branch to warp to right on negativ
                cmp #maxX
                bcc .rhSaveX
                lda #0                      ;wrap to left
                jmp .rhSaveX
.rhMoveXM       lda #maxX-1
.rhSaveX        sta rocketXN                ;save updated value
                cmp rocketX                 ;evaluate redraw
                beq .rhMoveY
                inc rocketRedraw
.rhMoveY                                    ;move by dy
                lda rocketDyLo
                bmi .rhMoveYRM              ;emulate asr instruction
                clc
                jmp .rhMoveYR
.rhMoveYRM      sec
.rhMoveYR       ror
                clc                         ;sum it
                adc rocketYLo
                sta rocketYLo
                lda rocketY
                adc rocketDy
                bmi .rhMoveYM               ;branch to wrap to bottom on negative
                cmp #maxY
                bcc .rhSaveY
                lda #0                      ;wrap to top
                jmp .rhSaveY
.rhMoveYM       lda #maxY-1
.rhSaveY        sta rocketYN                ;save updated value
                cmp rocketY                 ;evaluate redraw
                beq .rhRedraw
                inc rocketRedraw
                
.rhRedraw
                lda rocketThrusting
                beq .rhRedraw2
                cmp rocketThrustingN
                bne .rhRedraw1
                lda rocketRedraw
                beq .rhRedraw3
.rhRedraw1      jsr clearThrust
.rhRedraw2      lda rocketRedraw
                beq .rhRedraw3
                jsr clearRocket
                lda rocketDirN
                sta rocketDir
                lda rocketXN
                sta rocketX
                lda rocketYN
                sta rocketY
                jsr displayRocket
.rhRedraw3      lda rocketThrustingN
                sta rocketThrusting
                beq .rhDone
                jsr drawThrust
.rhDone         rts


; display routines (display and clear moving objects)

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


displayRocket ; pushes the rocket to the charQueue
                ldx rocketY
                dex                   ;2 pos offset
                dex
                stx qPosY
                ldy rocketX
                dey                   ;2 pos offset
                dey
                sty qPosX
                ldy rocketDir         ;dispatch on value in rocketDir
                lda .drJumpTableHi,y
                sta .drJmp+2
                lda .drJumpTableLo,y
                sta .drJmp+1
.drJmp          jmp $ffff             ;dummy address (fixed up)
.dr0
                lda #$1C
                sta qScreenCode
                jsr pushScreenCode
                ldx qPosX
                dex
                stx qPosX
                lda #$67
                sta qScreenCode
                jsr pushScreenCode
                rts
.dr1
                lda #$2F
                sta qScreenCode
                jsr pushScreenCode
                ldx qPosX
                inx
                stx qPosX
                lda #$65
                sta qScreenCode
                jsr pushScreenCode
                rts
.dr2
                lda #$2F
                sta qScreenCode
                jsr pushScreenCode
                ldx qPosY
                dex
                stx qPosY
                lda #$64
                sta qScreenCode
                jsr pushScreenCode
                rts
.dr3
                lda #$1C
                sta qScreenCode
                jsr pushScreenCode
                ldx qPosY
                inx
                stx qPosY
                lda #$63
                sta qScreenCode
                jsr pushScreenCode
                rts
.dr4
                lda #$1C
                sta qScreenCode
                jsr pushScreenCode
                ldx qPosX
                inx
                stx qPosX
                lda #$65
                sta qScreenCode
                jsr pushScreenCode
                rts
.dr5
                lda #$2F
                sta qScreenCode
                jsr pushScreenCode
                ldx qPosX
                dex
                stx qPosX
                lda #$67
                sta qScreenCode
                jsr pushScreenCode
                rts
.dr6
                lda #$2F
                sta qScreenCode
                jsr pushScreenCode
                ldx qPosY
                inx
                stx qPosY
                lda #$63
                sta qScreenCode
                jsr pushScreenCode
                rts
.dr7
                lda #$1C
                sta qScreenCode
                jsr pushScreenCode
                ldx qPosY
                dex
                stx qPosY
                lda #$64
                sta qScreenCode
                jsr pushScreenCode
                rts

.drJumpTableLo
                !byte <.dr0
                !byte <.dr1
                !byte <.dr2
                !byte <.dr3
                !byte <.dr4
                !byte <.dr5
                !byte <.dr6
                !byte <.dr7

.drJumpTableHi
                !byte >.dr0
                !byte >.dr1
                !byte >.dr2
                !byte >.dr3
                !byte >.dr4
                !byte >.dr5
                !byte >.dr6
                !byte >.dr7


clearRocket ; pushes the rocket to the resetQueue
                ldx rocketY
                dex                   ;2 pos offset
                dex
                stx qPosY
                ldy rocketX
                dey                   ;2 pos offset
                dey
                sty qPosX
                ldy rocketDir         ;dispatch on value in rocketDir
                lda .crJumpTableHi,y
                sta .crJmp+2
                lda .crJumpTableLo,y
                sta .crJmp+1
.crJmp          jmp $ffff             ;dummy address (fixed up)
.cr0
                ldy qPosX
                ldx qPosY
                jsr getStar
                sta qScreenCode
                jsr pushScreenReset
                ldy qPosX
                dey
                sty qPosX
                ldx qPosY
                jsr getStar
                sta qScreenCode
                jsr pushScreenReset
                rts
.cr1
                ldy qPosX
                ldx qPosY
                jsr getStar
                sta qScreenCode
                jsr pushScreenReset
                ldy qPosX
                iny
                sty qPosX
                ldx qPosY
                jsr getStar
                sta qScreenCode
                jsr pushScreenReset
                rts
.cr2
                ldy qPosX
                ldx qPosY
                jsr getStar
                sta qScreenCode
                jsr pushScreenReset
                ldy qPosX
                ldx qPosY
                dex
                stx qPosY
                jsr getStar
                sta qScreenCode
                jsr pushScreenReset
                rts
.cr3
                ldy qPosX
                ldx qPosY
                jsr getStar
                sta qScreenCode
                jsr pushScreenReset
                ldy qPosX
                ldx qPosY
                inx
                stx qPosY
                jsr getStar
                sta qScreenCode
                jsr pushScreenReset
                rts

.crJumpTableLo
                !byte <.cr0
                !byte <.cr1
                !byte <.cr2
                !byte <.cr3
                !byte <.cr1
                !byte <.cr0
                !byte <.cr3
                !byte <.cr2

.crJumpTableHi
                !byte >.cr0
                !byte >.cr1
                !byte >.cr2
                !byte >.cr3
                !byte >.cr1
                !byte >.cr0
                !byte >.cr3
                !byte >.cr2


drawThrust ;pushes the exhaust onto the charQueue
                ldx rocketDir
                clc
                lda rocketX
                adc thrustOffsetX, x
                sta qPosX
                clc
                lda rocketY
                adc thrustOffsetY, x
                sta qPosY
                lda #$2A
                sta qScreenCode
                jsr pushScreenCode
                rts

clearThrust ;pushes background code for exhaust onto the resetQueue
                ldx rocketDir
                clc
                lda rocketX
                adc thrustOffsetX, x
                sta qPosX
                tay
                clc
                lda rocketY
                adc thrustOffsetY, x
                sta qPosY
                tax
                jsr getStar
                sta qScreenCode
                jsr pushScreenReset
                rts


drawTitleScreen ;draws the title screen (directly into screen memory)
                lda screenLinesLo
                sta PT1
                lda screenLinesHi
                sta PT1+1
                lda #<titleScreen
                sta PT2
                lda #>titleScreen
                sta PT2+1
                ldx #24
.dtsRow         ldy #39
.dtsCol         lda (PT2), y
                sta (PT1), y
                dey
                bpl .dtsCol
                dex
                bmi .dtsDone
                clc
                lda PT1
                adc #screenCols
                sta PT1
                lda #0
                adc PT1+1
                sta PT1+1
                clc
                lda PT2
                adc #screenCols
                sta PT2
                lda #0
                adc PT2+1
                sta PT2+1
                jmp .dtsRow
.dtsDone        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

rocketX         !byte 0
rocketY         !byte 0
rocketXLo       !byte 0
rocketYLo       !byte 0
rocketDx        !byte 0
rocketDy        !byte 0
rocketDxLo      !byte 0
rocketDyLo      !byte 0
rocketDir       !byte 0
rocketThrust    !byte 0
rocketCnt       !byte 0
rocketXN        !byte 0
rocketYN        !byte 0
rocketDirN      !byte 0
rocketRedraw    !byte 0
rocketTurn      !byte 0
rocketThrusting !byte 0
rocketThrustingN !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 >($8000 + screenCols * 0)
                !byte >($8000 + screenCols * 1)
                !byte >($8000 + screenCols * 2)
                !byte >($8000 + screenCols * 3)
                !byte >($8000 + screenCols * 4)
                !byte >($8000 + screenCols * 5)
                !byte >($8000 + screenCols * 6)
                !byte >($8000 + screenCols * 7)
                !byte >($8000 + screenCols * 8)
                !byte >($8000 + screenCols * 9)
                !byte >($8000 + screenCols * 10)
                !byte >($8000 + screenCols * 11)
                !byte >($8000 + screenCols * 12)
                !byte >($8000 + screenCols * 13)
                !byte >($8000 + screenCols * 14)
                !byte >($8000 + screenCols * 15)
                !byte >($8000 + screenCols * 16)
                !byte >($8000 + screenCols * 17)
                !byte >($8000 + screenCols * 18)
                !byte >($8000 + screenCols * 19)
                !byte >($8000 + screenCols * 20)
                !byte >($8000 + screenCols * 21)
                !byte >($8000 + screenCols * 22)
                !byte >($8000 + screenCols * 23)
                !byte >($8000 + screenCols * 24)

screenLinesLo
                !byte <($8000 + screenCols * 0)
                !byte <($8000 + screenCols * 1)
                !byte <($8000 + screenCols * 2)
                !byte <($8000 + screenCols * 3)
                !byte <($8000 + screenCols * 4)
                !byte <($8000 + screenCols * 5)
                !byte <($8000 + screenCols * 6)
                !byte <($8000 + screenCols * 7)
                !byte <($8000 + screenCols * 8)
                !byte <($8000 + screenCols * 9)
                !byte <($8000 + screenCols * 10)
                !byte <($8000 + screenCols * 11)
                !byte <($8000 + screenCols * 12)
                !byte <($8000 + screenCols * 13)
                !byte <($8000 + screenCols * 14)
                !byte <($8000 + screenCols * 15)
                !byte <($8000 + screenCols * 16)
                !byte <($8000 + screenCols * 17)
                !byte <($8000 + screenCols * 18)
                !byte <($8000 + screenCols * 19)
                !byte <($8000 + screenCols * 20)
                !byte <($8000 + screenCols * 21)
                !byte <($8000 + screenCols * 22)
                !byte <($8000 + screenCols * 23)
                !byte <($8000 + screenCols * 24)

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

rocketDirDx
                !byte -1,  1,  4,  4,  1, -1, -4, -4

rocketDirDy
                !byte -4, -4, -1,  1,  4,  4,  1, -1

thrustOffsetX
                !byte -2, -2, -3, -3, -2, -2, -1, -1

thrustOffsetY
                !byte -1, -1, -2, -2, -3, -3, -2, -2


titleScreen
                !byte $20,$20,$20,$20,$20,$20,$20,$20 ;0
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$4F,$63,$A0,$20 ;1
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$6C,$61
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$65,$20,$77,$20 ;2
                !byte $A0,$50,$67,$63,$A0,$63,$A0,$20
                !byte $A0,$50,$20,$A0,$67,$20,$FB,$EC
                !byte $20,$4F,$A0,$20,$A0,$50,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$65,$20,$6F,$20 ;3
                !byte $A0,$67,$67,$20,$A0,$20,$A0,$20
                !byte $A0,$67,$20,$A0,$67,$20,$E1,$61
                !byte $20,$4F,$63,$20,$A0,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$4C,$64,$A0,$20 ;4
                !byte $A0,$7A,$67,$20,$A0,$20,$A0,$20
                !byte $A0,$7A,$20,$A0,$7A,$20,$E1,$61
                !byte $20,$4C,$A0,$20,$A0,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20 ;5
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $A0,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$62,$20,$20,$20 ;6
                !byte $20,$20,$20,$20,$A0,$63,$50,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$6C,$A0,$A0,$FB,$7B,$20 ;7
                !byte $20,$20,$20,$20,$A0,$64,$64,$20
                !byte $A0,$50,$20,$A0,$50,$20,$4F,$A0
                !byte $20,$4F,$A0,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$A0,$FE,$FB,$A0,$A0,$20 ;8
                !byte $20,$20,$20,$20,$20,$20,$A0,$20
                !byte $A0,$67,$20,$64,$7A,$20,$65,$20
                !byte $20,$4F,$63,$20,$20,$20,$20,$03
                !byte $0F,$0E,$14,$12,$0F,$0C,$13,$20
                !byte $20,$20,$A0,$FE,$A0,$FB,$A0,$20 ;9
                !byte $20,$20,$20,$20,$4C,$64,$A0,$20
                !byte $A0,$7A,$20,$4C,$A0,$20,$4C,$A0
                !byte $20,$4C,$A0,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$7C,$A0,$A0,$A0,$7E,$20 ;10
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $A0,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$81
                !byte $20,$14,$08,$12,$15,$13,$14,$20
                !byte $20,$20,$20,$20,$E2,$20,$20,$20 ;11
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20 ;12
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$93
                !byte $20,$06,$09,$12,$05,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20 ;13
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $4F,$A0,$20,$4F,$50,$20,$4F,$50
                !byte $20,$7A,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20 ;14
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $64,$A0,$20,$65,$67,$20,$65,$67
                !byte $20,$67,$20,$20,$20,$20,$20,$8B
                !byte $20,$0C,$05,$06,$14,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20 ;15
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $65,$20,$20,$A0,$67,$20,$A0,$67
                !byte $20,$67,$62,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20 ;16
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $4C,$A0,$20,$A0,$7A,$20,$A0,$7A
                !byte $20,$7A,$A0,$20,$20,$20,$20,$8C
                !byte $20,$12,$09,$07,$08,$14,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20 ;17
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20 ;18
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20 ;19
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$22,$03,$0F,$0D,$10,$15,$14 ;20
                !byte $05,$12,$20,$13,$10,$01,$03,$05
                !byte $22,$20,$0F,$12,$09,$07,$09,$0E
                !byte $01,$0C,$20,$01,$12,$03,$01,$04
                !byte $05,$20,$07,$01,$0D,$05,$20,$20
                !byte $20,$28,$03,$29,$20,$31,$39,$37 ;21
                !byte $31,$20,$0E,$15,$14,$14,$09,$0E
                !byte $07,$20,$01,$13,$13,$0F,$03,$09
                !byte $01,$14,$05,$13,$2C,$20,$09,$0E
                !byte $03,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$02,$19,$20 ;22
                !byte $0E,$0F,$0C,$01,$0E,$20,$02,$15
                !byte $13,$08,$0E,$05,$0C,$0C,$20,$01
                !byte $0E,$04,$20,$14,$05,$04,$20,$04
                !byte $01,$02,$0E,$05,$19,$3B,$20,$20
                !byte $20,$10,$05,$14,$20,$32,$30,$30 ;23
                !byte $31,$20,$07,$01,$0D,$05,$20,$02
                !byte $19,$20,$0E,$2E,$20,$0C,$01,$0E
                !byte $04,$13,$14,$05,$09,$0E,$05,$12
                !byte $2C,$20,$32,$30,$31,$37,$2E,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20 ;24
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20
                !byte $20,$20,$20,$20,$20,$20,$20,$20

(Assembles to 3,326 bytes of binary code.)

 

— Stay tuned! —

 

Next:  Episode 9: Progress Update (The End is Nigh)

Previous:  Episode 7: Rocket (Phew!)

Back to the index.

— This series is part of Retrochallenge 2017/04. —