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:
▶ 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):
- Added an option to assemble the program for 80 column PETs.
- Universal keyboard reading routine (much faster than the ROM routine).
- Rocket controls and thrust animation.
- Title Screen.
- Attract mode.
Still to do:
- Missiles (both for rocket and saucers).
- Colission / hit detection.
- Score increments.
- Reset routines for rocket and saucer(s).
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:
▶ 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.
April 2017, Vienna, Austria
www.masswerk.at – contact me.
— This series is part of Retrochallenge 2017/04. —