Three Lives
A lives counter at the top-right gives the player three chances. DEC decrements the count, and game over waits until the last life is spent.
One hit, game over. That’s harsh. This unit gives the player three lives. Each death costs one — the game only ends when the last life is spent.
Tracking Lives
A new zero-page variable:
lives = $12 ; Lives remaining (starts at 3)
init_game sets it to 3. The value is a plain byte — not BCD, not packed. One variable, one digit, values 0 to 3. Simple.
The Lives Display
; --- init_game (lives section) ---
; Lives
lda #$03
sta lives
lda #$33 ; Screen code for '3'
sta $0427
; --- One-time setup (colour) ---
; Set lives colour to white (persists across restarts)
lda #$01
sta $d827
The lives count appears at column 39, row 0 — the top-right corner. Screen RAM address: $0400 + 39 = $0427. Colour RAM: $D827, set to white in the one-time setup (it never changes, even across restarts).
Converting the count to a screen code: add $30. Lives = 3 becomes screen code $33, which displays as ‘3’. The same trick as the score digits — all single-digit values map to $30–$39.
Death Without Game Over
When an enemy hits the ship, DEC lives subtracts one. Then the code branches: if lives is zero, game over. If lives is non-zero, the ship resets to its starting position and the game continues.
ship_hit:
; --- Decrement lives ---
dec lives
; Update lives display
lda lives
clc
adc #$30
sta $0427
; Check if lives exhausted
lda lives
bne life_lost
; --- No lives left — game over ---
lda #$01
sta game_state
; Turn ship red
lda #$02
sta $d027
; Show GAME OVER text
jsr show_game_over
jmp play_death_sound
life_lost:
; --- Lives remaining — reset ship position ---
lda #172
sta $d000
lda #220
sta $d001
play_death_sound:
; Death sound plays in both cases
; ...
DEC is the zero-page decrement instruction — it subtracts 1 from the byte in memory and sets the zero flag if the result is 0. The BNE life_lost branch tests that flag: non-zero means lives remain, zero means the last life is gone.
Both paths play the death sound. The game over path also turns the ship red and displays “GAME OVER”. The life lost path just moves the ship back to its starting position — the enemies keep descending, the score stays, and the game continues immediately.
The Complete Code
; Starfield - Unit 12: Three Lives
; 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
score = $07 ; Score (0-99, BCD format)
enemy_x_tbl = $08 ; 3 bytes ($08, $09, $0a)
enemy_y_tbl = $0b ; 3 bytes ($0b, $0c, $0d)
flash_tbl = $0e ; 3 bytes ($0e, $0f, $10)
game_state = $11 ; 0 = playing, 1 = game over
lives = $12 ; Lives remaining (starts at 3)
; ------------------------------------------------
; BASIC stub
; ------------------------------------------------
*= $0801
!byte $0c,$08,$0a,$00,$9e,$32,$30,$36,$31,$00,$00,$00
; ------------------------------------------------
; One-time hardware setup
; ------------------------------------------------
*= $080d
; Black screen
lda #$00
sta $d020 ; Border colour
sta $d021 ; Background colour
; Sprite 1 colour (yellow, never changes)
lda #$07
sta $d028
; Set score colour to white (persists across restarts)
lda #$01
sta $d800
sta $d801
; Set lives colour to white (persists across restarts)
sta $d827
; 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
; First-time init
jsr clear_screen
jsr init_game
!ifdef SCREENSHOT_MODE {
; Set a visible score
lda #$05
sta score
lda #$30
sta $0400
lda #$35
sta $0401
; Lives at 2 (lost one life)
lda #$02
sta lives
lda #$32 ; '2'
sta $0427
; Position bullet mid-screen
lda $d000
sta $d002
lda #140
sta $d003
; Enable all sprites (ship + bullet + 3 enemies)
lda #%00011111
sta $d015
; Freeze the game (prevents enemies moving during capture)
lda #$01
sta game_state
}
; ------------------------------------------------
; Game loop — runs once per frame
; ------------------------------------------------
game_loop:
; Wait for the raster beam to reach line 255
- lda $d012
cmp #$ff
bne -
; --- Check game state ---
lda game_state
beq game_active
; --- Game over: poll fire button ---
lda $dc00
and #%00010000
bne game_loop ; Not pressed, keep waiting
; Fire pressed — restart the game
jsr clear_screen
jsr init_game
jmp game_loop
game_active:
; --- 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
bne check_collision
jmp no_hit
check_collision:
ldx #$00
collision_loop:
lda flash_tbl,x
bne next_collision ; Skip flashing enemies
; Check Y distance
lda bullet_y
sec
sbc enemy_y_tbl,x
cmp #$10
bcc check_x
cmp #$f0
bcc next_collision
check_x:
; Check X distance
lda $d002 ; Bullet X (sprite 1)
sec
sbc enemy_x_tbl,x
cmp #$10
bcc hit_enemy
cmp #$f0
bcc next_collision
jmp hit_enemy ; >= $F0 = close (negative)
next_collision:
inx
cpx #$03
bne collision_loop
jmp no_hit
hit_enemy:
; X = index of hit enemy
; Deactivate bullet
lda #$00
sta bullet_active
lda $d015
and #%11111101
sta $d015
; Flash this enemy white
lda #$08
sta flash_tbl,x
ldy sprite_colour_off,x
lda #$01
sta $d000,y ; White
; 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 all enemies ---
ldx #$00
enemy_loop:
lda flash_tbl,x
bne do_flash
; Move down 1 pixel per frame
lda enemy_y_tbl,x
clc
adc #$01
sta enemy_y_tbl,x
; Off-screen check (Y >= 248)
cmp #$f8
bcc update_sprite
; Respawn at top
lda #$32
jsr spawn_enemy
jmp next_enemy
do_flash:
dec flash_tbl,x
bne update_sprite ; Still flashing
; Flash done — respawn
lda #$32
jsr spawn_enemy
jmp next_enemy
update_sprite:
; Copy position to VIC-II
ldy sprite_pos_off,x
lda enemy_x_tbl,x
sta $d000,y
lda enemy_y_tbl,x
sta $d001,y
next_enemy:
inx
cpx #$03
bne enemy_loop
; --- Check ship-enemy collision ---
ldx #$00
ship_collision_loop:
lda flash_tbl,x
bne next_ship_check ; Skip flashing enemies
; Check Y distance (ship Y vs enemy Y)
lda $d001 ; Ship Y
sec
sbc enemy_y_tbl,x
cmp #$10
bcc check_ship_x
cmp #$f0
bcc next_ship_check
check_ship_x:
; Check X distance (ship X vs enemy X)
lda $d000 ; Ship X
sec
sbc enemy_x_tbl,x
cmp #$10
bcc ship_hit
cmp #$f0
bcc next_ship_check
jmp ship_hit ; >= $F0 = close (negative)
next_ship_check:
inx
cpx #$03
bne ship_collision_loop
jmp game_loop
ship_hit:
; --- Decrement lives ---
dec lives
; Update lives display
lda lives
clc
adc #$30
sta $0427
; Check if lives exhausted
lda lives
bne life_lost
; --- No lives left — game over ---
lda #$01
sta game_state
; Turn ship red
lda #$02
sta $d027
; Show GAME OVER text
jsr show_game_over
jmp play_death_sound
life_lost:
; --- Lives remaining — reset ship position ---
lda #172
sta $d000
lda #220
sta $d001
play_death_sound:
; Death sound — SID voice 3 (descending sawtooth)
lda #$00
sta $d40e ; Frequency low
lda #$10
sta $d40f ; Frequency high
lda #$0a
sta $d412 ; Attack=0, Decay=10 (long decay)
lda #$00
sta $d413 ; Sustain=0, Release=0
lda #$20
sta $d411 ; Sawtooth, gate OFF (reset envelope)
lda #$21
sta $d411 ; Sawtooth, gate ON (trigger)
jmp game_loop
; ------------------------------------------------
; Subroutine: clear_screen
; Fills screen RAM with spaces ($20)
; ------------------------------------------------
clear_screen:
ldx #$00
- lda #$20
sta $0400,x
sta $0500,x
sta $0600,x
sta $0700,x
inx
bne -
rts
; ------------------------------------------------
; Subroutine: init_game
; Resets all game state for a new game
; ------------------------------------------------
init_game:
; Sprite data pointers (must be set after clear_screen)
lda #128
sta $07f8 ; Ship (block 128 = $2000)
lda #129
sta $07f9 ; Bullet (block 129 = $2040)
lda #130
sta $07fa ; Enemy 0 (block 130 = $2080)
sta $07fb ; Enemy 1
sta $07fc ; Enemy 2
; Ship position and colour
lda #172
sta $d000 ; X position
lda #220
sta $d001 ; Y position
lda #$01
sta $d027 ; Colour (white)
; Enemy colours
lda #$05
sta $d029
sta $d02a
sta $d02b
; Spawn three enemies at staggered heights
lda #$32 ; Top
ldx #$00
jsr spawn_enemy
lda #$82 ; Middle
ldx #$01
jsr spawn_enemy
lda #$d2 ; Lower
ldx #$02
jsr spawn_enemy
; Enable sprites 0, 2, 3, 4 (bullet starts disabled)
lda #%00011101
sta $d015
; Reset state
lda #$00
sta bullet_active
sta game_state
sta score
; Score display
lda #$30
sta $0400
sta $0401
; Lives
lda #$03
sta lives
lda #$33 ; Screen code for '3'
sta $0427
rts
; ------------------------------------------------
; Subroutine: show_game_over
; Writes "GAME OVER" to screen RAM, row 12, col 16
; ------------------------------------------------
show_game_over:
; Screen codes: G=7, A=1, M=13, E=5, space=32, O=15, V=22, E=5, R=18
; Position: row 12 × 40 + col 16 = 496 → $0400 + $01F0 = $05F0
lda #$07 ; G
sta $05f0
lda #$01 ; A
sta $05f1
lda #$0d ; M
sta $05f2
lda #$05 ; E
sta $05f3
lda #$20 ; (space)
sta $05f4
lda #$0f ; O
sta $05f5
lda #$16 ; V
sta $05f6
lda #$05 ; E
sta $05f7
lda #$12 ; R
sta $05f8
; Colour to white
lda #$01
sta $d9f0
sta $d9f1
sta $d9f2
sta $d9f3
sta $d9f4
sta $d9f5
sta $d9f6
sta $d9f7
sta $d9f8
rts
; ------------------------------------------------
; Subroutine: spawn_enemy
; A = starting Y position, X = enemy index
; ------------------------------------------------
spawn_enemy:
sta enemy_y_tbl,x
; Random X from raster
lda $d012
and #$7f
clc
adc #$30
sta enemy_x_tbl,x
; Clear flash timer
lda #$00
sta flash_tbl,x
; Restore sprite colour to green
ldy sprite_colour_off,x
lda #$05
sta $d000,y
; Update VIC-II sprite position
ldy sprite_pos_off,x
lda enemy_x_tbl,x
sta $d000,y ; Sprite X
lda enemy_y_tbl,x
sta $d001,y ; Sprite Y (offset + 1)
rts
; ------------------------------------------------
; Lookup tables
; ------------------------------------------------
sprite_pos_off:
!byte $04, $06, $08 ; VIC-II X-position offsets for sprites 2, 3, 4
sprite_colour_off:
!byte $29, $2a, $2b ; VIC-II colour register offsets
; ------------------------------------------------
; 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
- Lives display doesn’t appear? Check
$0427is written ininit_gameand$D827is set to white in the one-time setup. - Lives never decrement?
DEC livesmust be in theship_hitsection. Check the ship-enemy collision loop still works. - Game over on first hit?
init_gamemust setlivesto 3. If it starts at 0,DECwraps to 255 — you’ll never reach zero again. - Display shows wrong digit? Make sure you add
$30to convert the count to a screen code. Lives = 2 should write$32, not$02. - Ship doesn’t reset after hit? The
life_lostpath must write 172 to$D000and 220 to$D001.
Try This: Extra Life at Score Threshold
After the BCD score increment, compare the score to $10 (BCD for 10). If it matches and lives is less than 3, increment lives. One CMP, one BNE, one INC. Reward the player for accuracy.
Try This: Display Lives as Sprites
Instead of a digit, show small ship icons. Three sprites along the top — or simpler, write ship-shaped characters to screen RAM. More visual, same data.
What You’ve Learnt
DECfor counters — decrement a zero-page byte, zero flag set when it reaches 0.- Conditional state transition — game over only when lives reach zero, not on every death.
- Display update — keeping screen RAM in sync with the game state after every change.
What’s Next
Death is instant and silent — the ship just resets. In Unit 13, the border flashes red, a mournful tone plays, and a brief invulnerability timer prevents the ship from dying twice in a row.