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.
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/$0401in 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
SEDis before theADCandCLDis 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_enemyblock, 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 mode —
SEDmakesADCcount in tens. AlwaysCLDafter. - Nybble extraction —
LSR×4 for the high nybble,AND #$0Ffor 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.