Skip to content
Game 1 Unit 8 of 128 1 hr learning time

Scoring Points

Screen RAM displays a two-digit score in the top-left corner, updated with BCD arithmetic and nybble extraction every time a bullet hits the enemy.

6% of Starfield

The enemy explodes — flash, noise, respawn. But nothing records the hit. Screen RAM changes that. Two bytes at $0400 become a score counter, updated every time a bullet connects.

Screen RAM

The VIC-II reads character data from screen RAM starting at $0400. It’s a 40×25 grid — 1,000 bytes, one per character cell. Write a value and the character appears immediately.

Screen codes for digits: $30 = ‘0’, $31 = ‘1’, … $39 = ‘9’. To display a digit, add $30 to its value and write to screen RAM.

Colour RAM

Each character cell also has a colour byte at $D800+. Same layout as screen RAM — $D800 maps to $0400, $D801 to $0401, and so on. Write a colour value and the character changes colour immediately.

        ; Score starts at zero (BCD)
        lda #$00
        sta score

        ; Write "00" to screen RAM (top-left corner)
        lda #$30            ; Screen code for '0'
        sta $0400           ; Tens digit (row 0, col 0)
        sta $0401           ; Ones digit (row 0, col 1)

        ; Set score colour to white
        lda #$01
        sta $d800
        sta $d801

The init code writes two ‘0’ characters and colours them white. The score sits at row 0, columns 0–1 — top-left corner.

Decimal Mode

The 6502 has a decimal mode. SED activates it, CLD turns it off. In decimal mode, ADC carries at 10 instead of 16. The byte $09 plus $01 gives $10, not $0A. This is BCD — binary-coded decimal.

Each nybble (4 bits) holds one decimal digit. The byte $42 means forty-two: high nybble is 4, low nybble is 2. No conversion needed between the stored value and the display — the digits are right there in the byte.

Important: always CLD after BCD arithmetic. Forgetting leaves decimal mode active, and every ADC/SBC in the game will behave differently.

Updating the Display

        ; Increment score (BCD)
        sed                     ; Decimal mode
        lda score
        clc
        adc #$01
        sta score
        cld                     ; Back to binary mode

        ; Update score display — tens digit
        lda score
        lsr
        lsr
        lsr
        lsr                     ; High nybble in A (0-9)
        clc
        adc #$30                ; Convert to screen code
        sta $0400               ; Write tens digit

        ; Update score display — ones digit
        lda score
        and #$0f                ; Low nybble in A (0-9)
        clc
        adc #$30                ; Convert to screen code
        sta $0401               ; Write ones digit

Three steps: increment the score in decimal mode, extract the tens digit with four LSR shifts, extract the ones digit with AND $0F. Add $30 to each and write to screen RAM.

LSR shifts the accumulator right by one bit. Four shifts move the high nybble down to the low nybble position. AND #$0F masks off the high nybble, leaving only the low four bits.

The Complete Code

; Starfield - Unit 8: Scoring Points
; Assemble with: acme -f cbm -o starfield.prg starfield.asm

; ------------------------------------------------
; Zero-page variables
; ------------------------------------------------
bullet_active = $02     ; 0 = no bullet, 1 = active
bullet_y      = $03     ; Bullet Y position
enemy_x       = $04     ; Enemy X position
enemy_y       = $05     ; Enemy Y position
flash_timer   = $06     ; Explosion flash countdown (0 = no flash)
score         = $07     ; Score (0-99, BCD format)

; ------------------------------------------------
; BASIC stub
; ------------------------------------------------
*= $0801
!byte $0c,$08,$0a,$00,$9e,$32,$30,$36,$31,$00,$00,$00

; ------------------------------------------------
; Initialisation
; ------------------------------------------------
*= $080d
        ; Black screen
        lda #$00
        sta $d020           ; Border colour
        sta $d021           ; Background colour

        ; Clear the screen
        ldx #$00
-       lda #$20
        sta $0400,x
        sta $0500,x
        sta $0600,x
        sta $0700,x
        inx
        bne -

        ; Sprite 0 setup (ship)
        lda #128
        sta $07f8           ; Data pointer (block 128 = $2000)
        lda #172
        sta $d000           ; X position
        lda #220
        sta $d001           ; Y position
        lda #$01
        sta $d027           ; Colour (white)

        ; Sprite 1 setup (bullet)
        lda #129
        sta $07f9           ; Data pointer (block 129 = $2040)
        lda #$07
        sta $d028           ; Colour (yellow)

        ; Sprite 2 setup (enemy)
        lda #130
        sta $07fa           ; Data pointer (block 130 = $2080)
        lda #$05
        sta $d029           ; Colour (green)

        ; Enemy starting position
        lda #100
        sta enemy_x
        sta $d004           ; X position
        lda #$32
        sta enemy_y
        sta $d005           ; Y position

        ; Enable sprites 0 and 2 (bullet starts disabled)
        lda #%00000101
        sta $d015

        ; Bullet starts inactive
        lda #$00
        sta bullet_active

        ; Flash timer starts at zero (no flash)
        lda #$00
        sta flash_timer

        ; Score starts at zero (BCD)
        lda #$00
        sta score

        ; Write "00" to screen RAM (top-left corner)
        lda #$30            ; Screen code for '0'
        sta $0400           ; Tens digit (row 0, col 0)
        sta $0401           ; Ones digit (row 0, col 1)

        ; Set score colour to white
        lda #$01
        sta $d800
        sta $d801

        ; SID setup — voice 1 laser sound
        lda #$0f
        sta $d418           ; Volume to maximum

        lda #$00
        sta $d400           ; Frequency low byte
        lda #$10
        sta $d401           ; Frequency high byte ($1000 = mid-high pitch)

        lda #$09
        sta $d405           ; Attack=0, Decay=9
        lda #$00
        sta $d406           ; Sustain=0, Release=0

!ifdef SCREENSHOT_MODE {
        ; Set a visible score for screenshot
        lda #$05
        sta score
        lda #$30            ; '0' (tens)
        sta $0400
        lda #$35            ; '5' (ones)
        sta $0401

        lda $d000
        sta $d002           ; Bullet X = ship X
        lda #140
        sta $d003           ; Bullet Y mid-screen

        ; Show enemy flashing white (explosion in progress)
        lda #$01
        sta $d029           ; White

        lda #%00000111
        sta $d015           ; Enable all three sprites
}

; ------------------------------------------------
; Game loop — runs once per frame
; ------------------------------------------------
game_loop:
        ; Wait for the raster beam to reach line 255
-       lda $d012
        cmp #$ff
        bne -

        ; --- Read joystick and move ship ---

        ; UP (bit 0)
        lda $dc00
        and #%00000001
        bne not_up
        dec $d001
        dec $d001
not_up:

        ; DOWN (bit 1)
        lda $dc00
        and #%00000010
        bne not_down
        inc $d001
        inc $d001
not_down:

        ; LEFT (bit 2)
        lda $dc00
        and #%00000100
        bne not_left
        dec $d000
        dec $d000
not_left:

        ; RIGHT (bit 3)
        lda $dc00
        and #%00001000
        bne not_right
        inc $d000
        inc $d000
not_right:

        ; --- Fire button (bit 4) ---
        lda $dc00
        and #%00010000
        bne no_fire         ; Bit is 1 = NOT pressed

        ; Fire is pressed — only spawn if no bullet active
        lda bullet_active
        bne no_fire         ; Already active, skip

        ; Spawn bullet at ship position
        lda $d000           ; Ship X → bullet X
        sta $d002
        lda $d001           ; Ship Y → bullet Y
        sta bullet_y

        ; Enable sprite 1 (keep other sprites enabled)
        lda $d015
        ora #%00000010
        sta $d015

        ; Mark bullet active
        lda #$01
        sta bullet_active

        ; Trigger laser sound (gate off then on to retrigger)
        lda #$20
        sta $d404           ; Sawtooth, gate OFF (reset envelope)
        lda #$21
        sta $d404           ; Sawtooth, gate ON (start sound)

no_fire:

        ; --- Update bullet ---
        lda bullet_active
        beq no_bullet       ; Not active, skip

        ; Move bullet up (4 pixels per frame)
        lda bullet_y
        sec
        sbc #$04
        sta bullet_y
        sta $d003           ; Update sprite 1 Y position

        ; Check if bullet has left the screen (Y < 30)
        cmp #$1e
        bcs no_bullet       ; Y >= 30, still on screen

        ; Deactivate bullet
        lda #$00
        sta bullet_active

        ; Disable sprite 1 (keep other sprites enabled)
        lda $d015
        and #%11111101
        sta $d015

no_bullet:

        ; --- Check bullet-enemy collision ---
        lda bullet_active
        beq no_hit              ; No bullet, skip

        ; Check Y distance
        lda bullet_y
        sec
        sbc enemy_y
        cmp #$10                ; Within 16 pixels? (positive direction)
        bcc y_close
        cmp #$f0                ; Within 16 pixels? (negative/wrapped)
        bcc no_hit              ; Too far apart
y_close:
        ; Check X distance
        lda $d002               ; Bullet X
        sec
        sbc enemy_x
        cmp #$10                ; Within 16 pixels? (positive direction)
        bcc hit_enemy
        cmp #$f0                ; Within 16 pixels? (negative/wrapped)
        bcc no_hit              ; Too far apart

hit_enemy:
        ; Deactivate bullet
        lda #$00
        sta bullet_active
        lda $d015
        and #%11111101          ; Disable sprite 1 (bullet)
        sta $d015

        ; Flash enemy white
        lda #$01
        sta $d029               ; Sprite 2 colour = white

        ; Start flash timer
        lda #$08
        sta flash_timer         ; 8 frames of flash

        ; Explosion sound — SID voice 2 (noise)
        lda #$00
        sta $d407               ; Frequency low
        lda #$08
        sta $d408               ; Frequency high (low rumble)
        lda #$09
        sta $d40c               ; Attack=0, Decay=9
        lda #$00
        sta $d40d               ; Sustain=0, Release=0
        lda #$80
        sta $d40b               ; Noise, gate OFF (reset envelope)
        lda #$81
        sta $d40b               ; Noise, gate ON (trigger)

        ; Increment score (BCD)
        sed                     ; Decimal mode
        lda score
        clc
        adc #$01
        sta score
        cld                     ; Back to binary mode

        ; Update score display — tens digit
        lda score
        lsr
        lsr
        lsr
        lsr                     ; High nybble in A (0-9)
        clc
        adc #$30                ; Convert to screen code
        sta $0400               ; Write tens digit

        ; Update score display — ones digit
        lda score
        and #$0f                ; Low nybble in A (0-9)
        clc
        adc #$30                ; Convert to screen code
        sta $0401               ; Write ones digit

no_hit:

        ; --- Update enemy ---
        lda flash_timer
        beq enemy_move          ; Not flashing, normal movement

        ; Enemy is frozen (flashing white)
        dec flash_timer
        bne no_respawn          ; Still flashing, skip everything

        ; Flash done — restore colour and respawn
        lda #$05
        sta $d029               ; Back to green
        lda #$32
        sta enemy_y
        sta $d005
        lda $d012
        and #$7f
        clc
        adc #$30
        sta enemy_x
        sta $d004
        jmp no_respawn

enemy_move:
        lda enemy_y
        clc
        adc #$01                ; Move down 1 pixel per frame
        sta enemy_y
        sta $d005               ; Update sprite 2 Y position

        ; Check if enemy has left the screen (Y > 248)
        cmp #$f8
        bcc no_respawn          ; Y < 248, still on screen

        ; Respawn at top with new X position
        lda #$32
        sta enemy_y
        sta $d005               ; Reset Y to top

        lda $d012               ; Pseudo-random from raster position
        and #$7f                ; Range 0-127
        clc
        adc #$30                ; Range 48-175
        sta enemy_x
        sta $d004               ; New X position

no_respawn:
        jmp game_loop

; ------------------------------------------------
; Sprite data at $2000 (block 128) — ship
; ------------------------------------------------
*= $2000
        !byte $00,$18,$00   ;        ##
        !byte $00,$3c,$00   ;       ####
        !byte $00,$3c,$00   ;       ####
        !byte $00,$7e,$00   ;      ######
        !byte $00,$7e,$00   ;      ######
        !byte $00,$ff,$00   ;     ########
        !byte $00,$ff,$00   ;     ########
        !byte $01,$ff,$80   ;    ##########
        !byte $03,$ff,$c0   ;   ############
        !byte $07,$ff,$e0   ;  ##############
        !byte $07,$ff,$e0   ;  ##############
        !byte $07,$e7,$e0   ;  ###..####..###
        !byte $03,$c3,$c0   ;   ##....##....##
        !byte $01,$ff,$80   ;    ##########
        !byte $00,$ff,$00   ;     ########
        !byte $00,$ff,$00   ;     ########
        !byte $00,$db,$00   ;     ##.##.##
        !byte $00,$db,$00   ;     ##.##.##
        !byte $00,$66,$00   ;      ##..##
        !byte $00,$24,$00   ;       #..#
        !byte $00,$00,$00   ;

; ------------------------------------------------
; Sprite data at $2040 (block 129) — bullet
; ------------------------------------------------
*= $2040
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$18,$00   ;        ##
        !byte $00,$18,$00   ;        ##
        !byte $00,$18,$00   ;        ##
        !byte $00,$18,$00   ;        ##
        !byte $00,$18,$00   ;        ##
        !byte $00,$18,$00   ;        ##
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;

; ------------------------------------------------
; Sprite data at $2080 (block 130) — enemy
; ------------------------------------------------
*= $2080
        !byte $00,$66,$00   ;      ##..##
        !byte $00,$3c,$00   ;       ####
        !byte $00,$7e,$00   ;      ######
        !byte $00,$db,$00   ;     ##.##.##
        !byte $00,$ff,$00   ;     ########
        !byte $01,$ff,$80   ;    ##########
        !byte $01,$7e,$80   ;    #.######.#
        !byte $01,$3c,$80   ;    #..####..#
        !byte $00,$a5,$00   ;     #.#..#.#
        !byte $01,$81,$80   ;    ##......##
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;
        !byte $00,$00,$00   ;

If It Doesn’t Work

  • Score doesn’t appear? Check you’re writing to $0400/$0401 in the init section. The screen must be cleared first (it is — the clear loop runs before the score write).
  • Score shows wrong characters? Screen code for ‘0’ is $30, not $00. If you see ’@’ symbols, you forgot to add $30.
  • Score jumps by strange amounts? Make sure SED is before the ADC and CLD is after. Without decimal mode, $09 + $01 = $0A, which displays as a letter.
  • Score doesn’t update on hit? The increment code must be inside the hit_enemy block, after the flash/sound setup.

Try This: Different Score Position

Move the score to the top-right corner. Row 0, columns 38–39: $0400 + 38 = $0426 and $0400 + 39 = $0427. Change the STA addresses in both init and the display update.

Try This: Score Cap

Stop the score at 99. Before the BCD increment, check if score equals $99. If it does, skip the ADC. One CMP and one BEQ.

What You’ve Learnt

  • Screen RAM at $0400 — direct character output to the 40×25 grid.
  • Colour RAM at $D800 — per-character colour, same layout as screen RAM.
  • BCD / decimal modeSED makes ADC count in tens. Always CLD after.
  • Nybble extractionLSR ×4 for the high nybble, AND #$0F for the low.

What’s Next

One enemy at a time feels like target practice. In Unit 9, indexed addressing lets you track a whole fleet — three enemies descending at once.