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:
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):
- 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:
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.
April 2017, Vienna, Austria
www.masswerk.at – contact me.
— This series is part of Retrochallenge 2017/04. —