Retrochallenge 2018/04 (Now in COLOR)
Refraction for the Atari 2600

Episode 8: Completing the Game Mechanics

We are finally seeing light at the end of what's not a tunnel: In this episode, we complete, if not finalize, the game mechanics. What's currently missing are the player controls for the eponymous refraction action and hit detection, including everything that comes with it, like object states, visual effects and score keeping.

Controlling Refractions

This is rather straight forward. Notwithstanding last episode's consideration, we go with the simple "asl ... rol" scheme for multiplication. There's simply not much to gain otherwise. What we do add now to this, is a check of the joystick/controller states in register SWCHA (compare episode 6). The action, we're looking for first, is the player pulling the stick towards her side of the playfield. We could do this after we computed the distance of the missile from the vertical center and flip the results accordingly. However, this is a 16-bit value and it's probably less expensive to do the test in advance and to branch to a subtraction in the appropriate order of arguments, by this optaining the right sign without much overhead.

Next it's about augmenting the delta (or angle). If either of the two inputs for left or right are low (i.e. active), we add half of what we already determined in the beginning. Then, regardless of whether there had been any controller input at all, we proceed with the multiplication by 4, we discussed last time, and finally update the missile state to refect the fact that the missile has passed the barrier.

Hit Detection

Hit detection (or collisions) is implemented in the hardware of the TIA. If there are any objects overlapping in a scan line, bits in the internal collision registers are set accordingly. These are latches registers and we have to strobe register CXCLR to reset them, which we do just after the vertical sync signal (VSYNC) at the beginning of each frame. Therefor any collisions occuring in a frame, will be available in overscan, where we are going to check the registers.

Until now, we were mostly writing to the TIA (the controller ports and registers SWCHA, SWCHAB are part of the PIA/RIOT), But here, the TIA reveals a total different side, when we attempt to read from it. On read access, the TIA exhibits a set of collision registers and the input ports (we already know INPT4 and INPT5 for reading controller buttons) via a 6-bit address scheme:

TIA – Read Access

RegisterBits used (active HI)FunctionSemantics
6-bit Addr.Name76543210D7D6
0CXM0PXX......collisionM0–P1M0–P0
1CXM1PXX......collisionM1–P0M1–P1
2CXP0FBXX......collisionP0–PFP0–BL
3CXP1FBXX......collisionP1–PFP1–BL
4CXM0FBXX......collisionM0–PFM0–BL
5CXM1FBXX......collisionM1–PFM1–BL
6CXBLPFX.......collisionBL–PFunused
7CXPPMMXX......collisionP0–P1M0–M1
8INPT0X.......pot port
9INPT1X.......pot port
AINPT2X.......pot port
BINPT3X.......pot port
CINPT4X.......input (button)
DINPT5X.......input (button)

As may be observed, there's a bit to represent any of the collisions that may occur. For a ship, we're particularly interested in it colliding with the ball (CXM0FB, bit D6 for Player0) and with the opponent's missile (CXM1P, bit D7 for Player0). As the interesting bits are at the left or most significant end, we're going to check them using the sign bit (and a shift, when necessary):


CheckShipHit0              ; check collisions for ship0
    lda ship0State
    bne shipHit0Done       ; inactive
    lda CXM1P              ; hit by missile1?
    bmi shipHit0
    lda CXP0FB             ; hit by ball?
    asl
    bpl shipHit0Done
shipHit0
    ldx #0
    jsr ShipHit
shipHit0Done

As we may see, there's a new subroutine for handling a hit and there's also a new variable to reflect the state of a ship. This is, because we're not simply resetting a ship, but we're going to insert an effect sequence to represent the game event. For this, we will also disable several objects, like missiles or the ball, while this sequence is in effect. Therefore, we add a state-variable for any of the objects and we put any code for handling any of those objects inside checks for the state. (As a side effect, we also rearrange some of the existing code, so that anything, which is related to a particular state may go inside a single check.)

Similarly, we arrange for the missiles, which will expire when hitting the ball. While we're at it, we add yet another 16-bit counter to countdown a missiles life until it finally expires, to prevent it from bouncing around for ever.

Simple Effects

We're using extensive amounts of memory for our approach to sprites, so we won't add another one (or two, or three) for an exploding ship. We're going for a flckering effect, which also matches the abstract style of our game. To keep things simple, we just switch the color of the player sprite. Towards the end, we reposition the ship at the vertical center and add a bit of a fade-in effect.

At this point, we reactivate all objects, we've previously disabled and also reset the ball.

Pitfall — But not by ActiVision

Repositioning the ball shouldn't be outrageously difficult. To provide a bit of variety, we will set up a random speed and direction (based on the frame counter) and also a random vertical start position (one out of eight). However, things aren't always that easy as expected. It's just a tiny bit of code, selecting one out of four possible velocities for any of the two axis, and we'll do so by simple table lookup. In the end, it looks like this:


ResetBall
    (...)                      ; get random bits
    and #3                     ; reduce to 3 max
    asl                        ; now of 0, 2, 4, 6
    tax
    lda ballVelocityX,X
    sta ballDX
    lda ballVelocityX + 1,X
    sta ballDX + 1
    (...)                      ; get random bits
    and #3
    asl
    tax
    lda ballVelocityY,X
    sta ballDY
    lda ballVelocityY + 1,X
    sta ballDY + 1
    (...)

ballVelocityX
    .word $0100
    .word $0180
    .word $FF00
    .word $FE80
ballVelocityY
    .word $0080
    .word $0100
    .word $FE80
    .word $FF00

The problem here, over which I spent considerable time, is the innocent assembler directive ".word". It's the proper representation for 16-bit values. In my understanding it just represented two consecutive bytes (same as ".byte" … ".byte"). However, the DASM assembler isn't just bare-bones, it cares for the user and automatically converts a HI-byte/LO-byte notation into a LO-byte/HI-byte double in memory. For whatever reason, I didn't expect this and read the bytes in reverse order (in relation to the order in the source code), by this undoing what the assembler was doing for me already. Consequently, results were weird, to say the least. Since the HI-byte values exceeded the height of playfield, the ball not only moved erratically, but also showed up in multiple positions at once (also, in distorted shapes like bands). It was only after some considerable amount of head scratching that I discovered the source of the evil. — Another lesson learned.

However, in the end all was good. By this, we've implemented the basic game machanics in their entirety and there's also a basic, playable game. We may reconsider some of this after a bit of testing. E.g., we may want to move the barriers and the ship positions a bit towards the center, if we find the game too hard. We'll see.

By now, we're missing the score display (we're already mainting scores internally), a nice title screen, and, of course, sound.

Game states

Two game states.

(BTW, I remapped the keys for the online emulation, as the old one proved to be rather tiresome.)

Code

And here's the code, so far:


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Program:        Refraction  rev.0.5
; Implements:     Game Mechanics
; System:         Atari 2600
; Source Format:  DASM
; Author:         N. Landsteiner, 2018
;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

    processor 6502
    include vcs.h
    include macro.h


    SEG.U config

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Constants
;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

; tv standard specifics
; uncomment for PAL
;PAL = 1

    ifnconst  PAL
;----------------------------- NTSC

; 262 lines: 3+37 VBlank, 192 kernel, 30 overscan
; timers (@ 64 cycles)
;  VBlank    43 * 64 = 2752 cycles = 36.21 lines
;  Overscan  35 * 64 = 2240 cycles = 29.47 lines

ScanLines      = 192
T64VBlank      =  43
T64Overscan    =  35

BorderHeight   =   6
BorderClr      = $64  ; purple
ScoreClr       = $EC  ; yellow
PlayerClr      = $0C  ; light grey
ResetClr       = $60  ; dark purple

;-----------------------------
    else
;----------------------------- PAL

; 312 lines: 3+45 VBlank, 228 kernel, 36 overscan
; timers (@ 64 cycles)
;  VBlank    53 * 64 = 3392 cycles = 44.63 lines
;  Overscan  42 * 64 = 2688 cycles = 35.36 lines

ScanLines      = 228
T64VBlank      =  53
T64Overscan    =  42

BorderHeight   =   7
BorderClr      = $C4
ScoreClr       = $2C
PlayerClr      = $0C
ResetClr       = $C0

;-----------------------------
    endif

; general definitions

ScoresHeight   =  10
PFHeight = ScanLines - ScoresHeight - 2 * BorderHeight

shipVelocity  = $0180

mslVelocity   = $0180
mslCooling    =   $30  ; frames

MissileLife   = $0140  ; frames

; ship X coordinates (static)

ship0X = 20
ship1X = 134

; vars

frCntr      = $80
toggle      = $81

; sprite coordinates (16-bit, HI-byte used for display)
; sprite specific horizontal offsets of TIA coordinates vs logical X:
;   players:         X+1 (1...160)
;   missiles, ball:  X+2 (2...161)
; (ball and missiles start 1 px left/early as compared to player sprites)

ship0Y      = $82  ; 2 bytes
ship1Y      = $84  ; 2 bytes

; order and grouping is important for selecting objects by index

ballX       = $86  ; 2 bytes
msl0X       = $88  ; 2 bytes
msl1X       = $8A  ; 2 bytes

ballY       = $8C  ; 2 btyes
msl0Y       = $8E  ; 2 bytes
msl1Y       = $90  ; 2 bytes

ballDX      = $92  ; 2 bytes
msl0DX      = $94  ; 2 bytes
msl1DX      = $96  ; 2 bytes

ballDY      = $98  ; 2 bytes
msl0DY      = $9A  ; 2 bytes
msl1DY      = $9C  ; 2 bytes

msl0State   = $9E
msl0Cooling = $9F
msl1State   = $A0
msl1Cooling = $A1
msl0Life    = $A2
msl1Life    = $A4

ship0State  = $A6
score1      = $A7
ship1State  = $A8
score0      = $A9

ballState   = $AA


; addresses for relocated playfield scan line routine

PFRoutine = $B0  ; where to place the scan line routine

M1Ptr = PFRoutine + $03
S0Ptr = PFRoutine + $08
M0Ptr = PFRoutine + $0d
S1Ptr = PFRoutine + $12
BlPtr = PFRoutine + $1f
BrPtr = PFRoutine + $17


    SEG cartridge

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Initialization
;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

    org $F000

Start
    sei                 ; disable interrupts
    cld                 ; clear BCD mode

    ldx #$FF
    txs                 ; reset stack pointer
    lda #$00
    ldx #$28            ; clear TIA registers ($04-$2C)
TIAClear
    sta $04,X
    dex
    bpl TIAClear        ; loop exits with X=$FF

;    ldx #$FF
RAMClear
    sta $00,X           ; clear RAM ($FF-$80)
    dex
    bmi RAMClear        ; loop exits with X=$7F

    sta SWBCNT          ; set console I/O to INPUT
    sta SWACNT          ; set controller I/O to INPUT


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Game Init
;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

    lda #1 + 32
    sta CTRLPF          ; set up symmetric playfield, 2x ball width
    lda #0
    sta toggle
    sta frCntr
    lda #PlayerClr      ; set player sprite colors
    sta COLUP0
    sta COLUP1
    lda #8              ; flip player 1 horizontally
    sta REFP1

    jsr relocatePFRoutine

    lda #0
    sta ship0Y
    sta ship1Y
    sta ballX
    sta ballY
    sta msl0Cooling
    sta msl1Cooling
    sta msl0X + 1
    sta msl1X + 1
    sta ship0State
    sta ship1State
    sta score0
    sta score1
    lda #PFHeight / 2 - 5
    sta ship0Y + 1
    sta ship1Y + 1
    lda #81            ; 80 + 2 offset - 1 (size = 2)
    sta ballX + 1
    lda #10
    sta ballY + 1
    lda #PFHeight
    sta msl0Y + 1
    sta msl1Y + 1

    jsr ResetBall


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Start a new Frame / VBLANK
;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

Frame
    lda #$02
    sta WSYNC           ; wait for horizontal sync
    sta VBLANK          ; turn on VBLANK
    sta VSYNC           ; turn on VSYNC
    sta WSYNC           ; leave VSYNC on for 3 lines
    sta WSYNC
    sta WSYNC
    lda #$00
    sta VSYNC           ; turn VSYNC off

    lda #T64VBlank      ; set timer for VBlank
    sta TIM64T
    sta CXCLR           ; clear collision registers

ReadInput
    lda SWCHB
    and #1              ; D0: reset
    bne ShipSelect
    jmp Start

ShipSelect              ; set up ship base addresses (select shape)
    lda SWCHB
    and #$8             ; D3: color/bw switch
    beq shipSelect2
shipSelect1
    lda #<[Ship1 - PFHeight]
    sta S0Ptr
    lda #>[Ship1 - PFHeight]
    sta S0Ptr + 1
    lda #<[Ship1 - PFHeight]
    sta S1Ptr
    lda #>[Ship1 - PFHeight]
    sta S1Ptr + 1
    jmp shipSelectDone
shipSelect2
    lda #<[Ship2 - PFHeight]
    sta S0Ptr
    lda #>[Ship2 - PFHeight]
    sta S0Ptr + 1
    lda #<[Ship2 - PFHeight]
    sta S1Ptr
    lda #>[Ship2 - PFHeight]
    sta S1Ptr + 1
shipSelectDone

Ship0Handler
    ldx #0              ; payer0
    lda ship0State
    beq ship0Active
    jsr ShipExplode
    jmp ship0Done
ship0Active
    jsr SteerShip
    lda ship1State      ; fire only, if opponent active
    bne ship0Done
    ldy INPT4
    jsr FireMissile
ship0Done

Ship1Handler
    ldx #2              ; payer1
    lda ship1State
    beq ship1Active
    jsr ShipExplode
    jmp ship1Done
ship1Active
    jsr SteerShip
    lda ship0State      ; fire only, if opponent active
    bne ship1Done
    ldy INPT5
    jsr FireMissile
ship1Done

VPositioning            ; vertical sprite positions (off: y = PFHeight)
    lda S0Ptr
    clc
    adc ship0Y + 1
    sta S0Ptr
    bcc s0Done
    inc S0Ptr + 1
s0Done

    lda S1Ptr
    clc
    adc ship1Y + 1
    sta S1Ptr
    bcc s1Done
    inc S1Ptr + 1
s1Done

    lda #<[SpriteM - PFHeight]
    clc
    adc msl0Y + 1
    sta M0Ptr
    lda #0
    adc #>[SpriteM - PFHeight]
    sta M0Ptr + 1

    lda #<[SpriteM - PFHeight]
    clc
    adc msl1Y + 1
    sta M1Ptr
    lda #0
    adc #>[SpriteM - PFHeight]
    sta M1Ptr + 1

    lda #<[SpriteBL - PFHeight]
    clc
    adc ballY + 1
    sta BlPtr
    lda #0
    adc #>[SpriteBL - PFHeight]
    sta BlPtr + 1


HPositioning            ; horizontal sprite positioning
    sta WSYNC
    lda #ship0X         ; player0
    ldx #0
    jsr bzoneRepos
    lda #ship1X         ; player1
    ldx #1
    jsr bzoneRepos
    lda msl0X + 1       ; missile0
    ldx #2
    jsr bzoneRepos
    lda msl1X + 1       ; missile1
    ldx #3
    jsr bzoneRepos
    lda ballX + 1       ; ball
    ldx #4
    jsr bzoneRepos
    sta WSYNC

VBlankWait
    lda     INTIM
    bne VBlankWait      ; wait for timer
    sta WSYNC           ; finish current line
    sta HMOVE           ; put movement registers into effect
    sta VBLANK          ; turn off VBLANK


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Visible Kernel
;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

Scores
    ; just a dummy, render alternating lines
    ldy #ScoresHeight-1
    ldx #0
    stx COLUBK

ScoresLoop
    sta WSYNC
    tya
    and #1
    beq s1
    lda #ScoreClr
s1
    sta COLUBK
    dey
    bpl ScoresLoop


TopBorder
    sta WSYNC
    lda #BorderClr
    sta COLUBK
    sta COLUPF          ; playfield color
    lda #16             ; playfield border (will not show in front of bg)
    sta PF0
    lda toggle
    sta PF1
    sta BrPtr

    ldy #PFHeight-1
    lda (BlPtr),Y       ; load ball in advance
    dec BlPtr           ; compensate for loading before dey in the pf-routine

    ldx #BorderHeight-1
topLoop
    sta WSYNC
    dex
    bne topLoop

                        ; last line of border
    sleep 68
    stx COLUBK          ; we're exactly at the right border
                        ; next scan-line starts

Playfield
    jmp PFRoutine       ; we'll start 3 cycles into the scan line,
                        ;  same as branch after WSYNC

BottomBorder
    lda #BorderClr
    sta COLUBK

    lda #0
    sta ENABL           ; all sprites off
    sta ENAM0
    sta ENAM1
    sta GRP0
    sta GRP1
    sta PF0             ; playfield off
    sta PF1
    sta PF2

    ldy #BorderHeight
btmLoop
    sta WSYNC
    dey
    bne btmLoop

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Overscan
;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

OverscanStart
    lda #$02
    sta VBLANK
    sta WSYNC
    lda #T64Overscan    ; set timer for overscan
    sta TIM64T

    inc frCntr          ; increment frame counter
    lda frCntr
    and #3
    bne CheckShipHit0
    lda toggle          ; flip toggle (barrier mask bit)
    eor #1
    sta toggle

; collisions / hit detection

CheckShipHit0              ; check collisions for ship0
    lda ship0State
    bne shipHit0Done
    lda CXM1P              ; hit by missile 1?
    bmi shipHit0
    lda CXP0FB             ; hit by ball?
    asl
    bpl shipHit0Done
shipHit0
    ldx #0
    jsr ShipHit
shipHit0Done

CheckShipHit1              ; check collisions for ship1
    lda ship1State
    bne shipHit1Done
    lda CXM0P              ; hit by missile 0?
    bmi shipHit1
    lda CXP1FB             ; hit by ball?
    asl
    bpl shipHit1Done
shipHit1
    ldx #2
    jsr ShipHit
shipHit1Done

CheckMslCollisions
    lda msl0State
    beq checkMsl1Bl
    lda msl1State
    beq checkMsl0Bl
    lda CXPPMM
    asl
    bpl checkMsl0Bl
    lda #0
    jsr ResetMissile
    lda #2
    jsr ResetMissile
    jmp checkMslDone
checkMsl0Bl
    lda msl0State
    beq checkMsl1Bl
    lda CXM0FB
    asl
    bpl checkMsl1Bl
    lda #0
    jsr ResetMissile
checkMsl1Bl
    lda msl1State
    beq checkMslDone
    lda CXM1FB
    asl
    bpl checkMslDone
    lda #1
    jsr ResetMissile
checkMslDone

; motions

MoveBall
    lda ballState
    beq moveBallDone    ; inactive
    ldx #0              ; select ball X
    jsr MoveObject
    ldx #6              ; select ball Y
    jsr MoveObject
moveBallDone

MoveMissile0
    ldx #0
    lda msl0State
    beq moveMsl0Done    ; inactive
    bpl moveMsl0Cnt     ; already refracted
    lda msl0X + 1
    cmp #116            ; crossed the barrier?
    bcc moveMsl0Cnt
    jsr Refract
    jmp moveMsl0
moveMsl0Cnt
    dec msl0Life        ; count down (16-bit) towards expery
    bne moveMsl0
    dec msl0Life + 1
    bpl moveMsl0
    jsr ResetMissile
    jmp moveMsl0Done
moveMsl0
    ldx #2              ; select missile0 Y
    jsr MoveObject
    ldx #8              ; select missile0 Y
    jsr MoveObject
moveMsl0Done

MoveMissile1
    ldx #2
    lda msl1State
    beq moveMsl1Done    ; inactive
    bpl moveMsl1Cnt     ; already refracted
    lda msl1X + 1
    cmp #44             ; crossed the barrier?
    bcs moveMsl1Cnt
    jsr Refract
    jmp moveMsl1
moveMsl1Cnt
    dec msl1Life        ; count down (16-bit) towards expery
    bne moveMsl1
    dec msl1Life + 1
    bpl moveMsl1
    jsr ResetMissile
    jmp moveMsl1Done
moveMsl1
    ldx #4              ; select missile1 X
    jsr MoveObject
    ldx #10             ; select missile1 Y
    jsr MoveObject
moveMsl1Done


OverscanWait
    lda INTIM
    bne OverscanWait    ; wait for timer
    jmp Frame


; some subroutines

SteerShip                 ; X: ship (0, 2)
    lda ctrlYPlayer0,X
    and SWCHA             ; joystick up?
    bne steerDown         ; active LO!
    sec
    lda ship0Y,X
    sbc #<shipVelocity
    sta ship0Y,X
    lda ship0Y + 1,X
    sbc #>shipVelocity
    cmp #$F0
    bcc steerSaveX
    lda #0
steerSaveX
    sta ship0Y + 1,X
steerDown
    lda ctrlYPlayer0+1,X
    and SWCHA            ; joystick down?
    bne steerDone
    clc
    lda ship0Y,X
    adc #<shipVelocity
    sta ship0Y,X
    lda ship0Y + 1,X
    adc #>shipVelocity
    cmp #PFHeight - 13
    bcc steerSaveY
    lda #PFHeight - 12
steerSaveY
    sta ship0Y + 1,X
steerDone
    rts

MoveObject               ; subroutine to move an object (x selects object and axis)
    clc                  ; DX:  0 ball,  2 missile0,  4 missile1
    lda ballX,X          ; DY:  6 ball,  8 missile0, 10 missile1
    adc ballDX,X
    sta ballX,X
    lda ballX  + 1,X
    adc ballDX + 1,X
    sta ballX  + 1,X
    ldy ballDX + 1,X
    bpl moveInc          ; branch on positive delta (incrementing)
moveDec
    ldy minMaxBallX,X    ; are we comparing to zero?
    beq moveCmp0
    cmp minMaxBallX,X    ; lower boundary from table
    bcs moveDone         ; branch on greater or equal than boundary
    lda minMaxBallX,X    ; new value = boundary
    jmp Bounce
moveCmp0
    cmp #$F0             ; deal with wrap around
    bcc moveDone         ; branch on less than $F0
    lda #0
    jmp Bounce
moveInc
    cmp minMaxBallX+1,X  ; upper boundary from table
    bcc moveDone         ; branch on less than boundary
    lda minMaxBallX+1,X
    sbc #1               ; new value = boundary - 1; carry already set
    jmp Bounce
moveDone
    rts

Bounce                   ; (sub)routine to invert an object's motion
    sta ballX + 1,X      ; A: new pos HI-btye
    lda #0               ; X = DX:  0 ball,  2 missile0,  4 missile1
    sta ballX,X          ;     DY:  6 ball,  8 missile0, 10 missile1
    sec
    sbc ballDX,X
    sta ballDX,X
    lda #0
    sbc ballDX + 1,X
    sta ballDX + 1,X
    rts

FireMissile              ; X = ship/player (0, 2), button input in Y
    lda msl0Cooling,X    ; missile available?
    beq fire
    dec msl0Cooling,X
    rts
fire
    tya
    bmi fireDone
    lda #mslCooling
    sta msl0Cooling,X
    lda ship0Y,X
    sta msl0Y,X
    lda ship0Y + 1,X
    clc
    adc #5
    sta msl0Y + 1,X
    lda originMsl0,X
    sta msl0X + 1,X
    lda #0
    sta msl0X,X
    sta msl0DY,X
    sta msl0DY + 1,X
    lda msl0Velocity,X
    sta msl0DX,X
    lda msl0Velocity + 1,X
    sta msl0DX + 1,X
    lda #$FF
    sta msl0State,X
    lda #<MissileLife
    sta msl0Life,X
    lda #>MissileLife
    sta msl0Life + 1,X
fireDone
    rts

Refract
    sec
    lda ctrlXPlayer0,X
    and SWCHA
    beq refractInv         ; stick pulled
    lda #PFHeight/2 + 5
    sbc msl0Y + 1,X
    jmp refractDif
refractInv
    lda msl0Y + 1,X
    sbc #PFHeight/2 + 5
refractDif
    bcs refractAdd
    sec
    sbc #$20
    sta msl0DY,X
    lda #$FF
    sta msl0DY + 1,X
    jmp refractCtrl
refractAdd
    clc
    adc #20
    sta msl0DY,X
refractCtrl
    lda ctrlXPlayer0 + 1,X   ; check, if stick pushed or pulled
    and SWCHA
    cmp ctrlXPlayer0 + 1,X
    beq refractMult
    asl msl0DY + 1,X       ; DY x 1.5, get carry (DY+1 is either $FF or 0)
    lda msl0DY,X
    ror
    clc
    adc msl0DY,X
    sta msl0DY,X
    bcc refractMult
    inc msl0DY + 1,X
refractMult                ; multiply by 4 (2 16-bit shifts left)
    asl msl0DY,X
    rol msl0DY + 1,X
    asl msl0DY,X
    rol msl0DY + 1,X
    lda #1                 ; update missile state
    sta msl0State,X
refractEnd
    rts

ShipHit                    ; ship in X (0, 2)
    lda #0
    sta msl1State
    sta msl0State
    sta msl0Cooling
    sta msl1Cooling
    lda #PFHeight
    sta msl0Y + 1
    sta msl1Y + 1
    lda #$58
    sta ship0State,X
    inc score1,X           ; score0, score1 are stored in reverse order
    lda #0                 ; ball off
    sta ballState
    lda #PFHeight
    sta ballY + 1
    rts

ResetShip                  ; ship in X (0, 2)
    lda #16
    sta msl0Cooling
    sta msl1Cooling
    lda #PlayerClr
    cpx #0
    bne resetShip1
    sta COLUP0
    jmp resetShipDone
resetShip1
    sta COLUP1
resetShipDone
    jmp ResetBall

ShipExplode                ; ship in X (0, 2)
    dec ship0State,X       ; decrement counter
    beq ResetShip          ; done with countdown, jump to reset
    cmp #34                ; equal 34?
    beq shipExRst          ; yes, reset ship to center
    bcs shipExFlicker      ; greater than 24, set blinking color
    cmp #8                 ; is it the 8th-last frame?
    bne shipExDone         ; no, return
    lda #BorderClr         ; set ship to border color
    jmp shipExSetClr
shipExFlicker
    lda frCntr             ; select either PlayerClr (light) or ResetClr (dark)
    and #4                 ; change every 4th frame
    beq shipExDark
    lda #PlayerClr
    jmp shipExSetClr
shipExRst
    lda #0
    sta ship0Y,X
    lda #PFHeight / 2 - 5
    sta ship0Y + 1,X
shipExDark
    lda #ResetClr
shipExSetClr               ; set the ship color
    cpx #0
    bne shipEx1
    sta COLUP0
    jmp shipExDone
shipEx1
    sta COLUP1
shipExDone
    rts

ResetMissile               ; missile in X (0, 2)
    lda #0
    sta msl0State,X
    sta msl0Cooling,X
    sta msl0Y,X
    lda #PFHeight
    sta msl0Y + 1,X
    rts

ResetBall
    lda frCntr
    tay
    and #3
    asl
    tax
    lda ballVelocityX,X
    sta ballDX
    lda ballVelocityX + 1,X
    sta ballDX + 1
    tya
    ror
    ror
    tay
    and #3
    asl
    tax
    lda ballVelocityY,X
    sta ballDY
    lda ballVelocityY + 1,X
    sta ballDY + 1
    lda #0
    sta ballY
    sta ballX
    sta ballY + 1
    tya
    ror
    ror
    eor frCntr
    and #7
    tax
    clc
rstBallLoop
    adc #PFHeight/9
    dex
    bpl rstBallLoop
    sta ballY + 1
    lda #81
    sta ballX + 1
    lda #1
    sta ballState
    rts


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Tables for subroutines / object selection
;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

; table joystick test patterns

ctrlYPlayer0
    .byte %00010000  ; up
    .byte %00100000  ; down
ctrlYPlayer1
    .byte %00000001  ; up
    .byte %00000010  ; down

ctrlXPlayer0
    .byte %01000000  ; left
    .byte %11000000  ; left or right
ctrlXPlayer1
    .byte %00001000  ; right
    .byte %00001100  ; left or right

; table of boundaries for various motions

minMaxBallX
    .byte 6
    .byte 156
minMaxMsl0X
    .byte 162-40-4
    .byte 158
minMaxMsl1X
    .byte 6
    .byte 40+4+2

minMaxBallY
    .byte 0
    .byte PFHeight - 7
minMaxMsl0Y
    .byte 4
    .byte PFHeight - 4
minMaxMsl1Y
    .byte 4
    .byte PFHeight - 4

originMsl0
    .byte ship0X + 10
    .byte 0
originMsl1
    .byte ship1X - 1
    .byte 0
msl0Velocity
    .byte <mslVelocity
    .byte >mslVelocity
msl1Velocity
    .byte 255 - <mslVelocity
    .byte 255 - >mslVelocity

ballVelocityX              ; (HHLL assembled to LLHH)
    .word $0100
    .word $0180
    .word $FF00
    .word $FE80
ballVelocityY              ; (HHLL assembled to LLHH)
    .word $0080
    .word $0100
    .word $FE80
    .word $FF00

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Playfield Scan Line Routine
;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

; kernel scan-line routine to be relocated to RAM (addr. PFRoutine)
; relocation via 'rorg ... rend' breaks DASM (why?), so let's do it the hard way

PFStart
                   ;   pfLoop
    hex 85 1f      ; 00  sta ENABL     ; draw ball
    hex b9 00 00   ; 02  lda 00,Y      ; draw missile 1 (addr at $03)
    hex 85 1e      ; 05  sta ENAM1
    hex b9 00 00   ; 07  lda 00,Y      ; draw player 0  (addr at $08)
    hex 85 1b      ; 0A  sta GRP0
    hex b9 00 00   ; 0C  lda 00,Y      ; draw missile 0 (addr at $0D)
    hex 85 1d      ; 0F  sta ENAM0
    hex b9 00 00   ; 11  lda 00,Y      ; draw player 1  (addr at $12)
    hex 85 1c      ; 14  sta GRP1

                   ;   barrier         ; alternating barrier animation
    hex a9 00      ; 16  lda #0        ; (pointer for start at $17)
    hex 49 01      ; 18  eor #1        ; the two barriers will be out of sync, because
    hex 85 0e      ; 1A  sta PF1       ; at this point we already missed PF1 at the left.
    hex 85 17      ; 1C  sta barrier+1 ; store pattern with D0 flipped (self-modifying)

    hex b9 00 00   ; 1E  lda 00,Y      ; load ball for next line (addr at $1F)
    hex 88         ; 21  dey
    hex 85 02      ; 22  sta WSYNC
    hex d0 da      ; 24  bne pfLoop    ; start over 3 cycles into the scan line
    hex 4c         ; 26  jmp
PFEnd

; 38 + 2 bytes in total ($28)

; subroutine to move it to RAM

PfReturnLoc = PFEnd - PFStart + PFRoutine

relocatePFRoutine
    ldx #PFEnd-PFStart
mvCode
    lda PFStart,X
    sta PFRoutine,X
    dex
    bpl mvCode

    lda #<BottomBorder     ; fix up return vector
    sta PfReturnLoc
    lda #>BottomBorder
    sta PfReturnLoc + 1

    lda #PFRoutine + $17   ; fix up the self-modifying rewrite addr
    sta PFRoutine + $1d

    rts


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Horizontal Positioning
;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

    org $F800

;-----------------------------
; This table is on a page boundary to guarantee the processor
; will cross a page boundary and waste a cycle in order to be
; at the precise position
; (lookup index is negative underflow of 241...255, 0)

fineAdjustBegin
    .byte %01110000 ; Left 7
    .byte %01100000 ; Left 6
    .byte %01010000 ; Left 5
    .byte %01000000 ; Left 4
    .byte %00110000 ; Left 3
    .byte %00100000 ; Left 2
    .byte %00010000 ; Left 1
    .byte %00000000 ; No movement.
    .byte %11110000 ; Right 1
    .byte %11100000 ; Right 2
    .byte %11010000 ; Right 3
    .byte %11000000 ; Right 4
    .byte %10110000 ; Right 5
    .byte %10100000 ; Right 6
    .byte %10010000 ; Right 7

fineAdjustTable = fineAdjustBegin - %11110001   ; Note: %11110001 = -15

; Battlezone style exact horizontal repositioning (modified)
;
; X = object      A = position in px
; --------------------------------------
; 0 = Player0     offset 1, 1...160
; 1 = Player1     offset 1, 1...160
; 2 = Missile0    offset 2, 2...161
; 3 = Missile1    offset 2, 2...161
; 4 = Ball        offset 2, 2...161

bzoneRepos                 ; cycles
    sta WSYNC              ; 3    wait for next scanline
    sec                    ; 2    start of scanline (0), set carry flag
divideby15
    sbc #15                ; 2    waste 5 cycles by dividing X-pos by 15
    bcs divideby15         ; 2/3  now at 6/11/16/21/...

    tay                    ; 2    now at 8/13/18/23/...
    lda fineAdjustTable,Y  ; 5    5 cycles, as we cross a page boundary
    nop                    ; 2    now at 15/20/25/30/...
    sta HMP0,X             ; 4    store fine adjustment
    sta RESP0,X            ; 4    (19/24/29/34/...) strobe position
    rts                    ; 6

; Note: "bcs divideby15" must not cross a page boundary


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Data
;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

Sprite0

    repeat PFHeight
    .byte $00
    repend

    .byte $10 ; |   X    |
    .byte $10 ; |   X    |
    .byte $58 ; | X XX   |
    .byte $BE ; |X XXXXX |
    .byte $73 ; | XXX  XX|
    .byte $6D ; | XX XX X|
    .byte $73 ; | XXX  XX|
    .byte $BE ; |X XXXXX |
    .byte $58 ; | X XX   |
    .byte $10 ; |   X    |
    .byte $10 ; |   X    |
Ship1

    repeat PFHeight
    .byte $00
    repend

    .byte $70 ; | XXX    |
    .byte $78 ; | XXXX   |
    .byte $5C ; | X XXX  |
    .byte $9E ; |X  XXXX |
    .byte $C3 ; |XX    XX|
    .byte $BC ; |X XXXX  |
    .byte $C3 ; |XX    XX|
    .byte $9E ; |X  XXXX |
    .byte $5C ; | X XXX  |
    .byte $78 ; | XXXX   |
    .byte $70 ; | XXX    |
Ship2

    repeat PFHeight
    .byte $00
    repend

    .byte $02 ; missile
SpriteM

    repeat PFHeight
    .byte $00
    repend

    .byte $02 ; ball
    .byte $02
    .byte $02
    .byte $02
    .byte $02
    .byte $02
    .byte $02
SpriteBL

    repeat PFHeight
    .byte $00
    repend

    .byte $00

SpriteEnd


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Interrupt and reset vectors
;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

    org $FFFA
    .word Start       ; NMI
    .word Start       ; Reset
    .word Start       ; IRQ

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    end
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

 

 

Next:  Episode 9: Scores!

Previous:   Episode 7: Let's Save Some Bytes!

Back to the index.

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