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

Screen Edges

The ship can only reach half the screen. The $D010 register adds a ninth bit to each sprite's X position, EOR toggles it at the boundary, and clamping keeps everything visible.

13% of Starfield

Move the ship to the right edge of the screen. It stops halfway — the X position register is 8 bits, giving a range of 0 to 255, but the C64 screen is 320 pixels wide. Sixty-five pixels of space are unreachable. This unit fixes that with a ninth bit, a new instruction for toggling it, and boundary checks that keep sprites visible.

The Ninth Bit

The VIC-II stores each sprite’s X position in a single byte: $D000 for sprite 0, $D002 for sprite 1, and so on. Eight bits give a range of 0–255, but the visible screen extends to around X=344. The missing piece is $D010 — a register where each bit holds the ninth bit of the corresponding sprite’s X position.

Bit 0 of $D010 is the high bit for sprite 0. When it’s clear, sprite 0’s X position is the value in $D000 (0–255). When it’s set, the position is $D000 + 256. Together, the two storage locations give a range of 0–511 — more than enough for the full screen width.

Toggling Bits with EOR

When the ship crosses from X=255 to X=256, the low byte wraps from $FF to $00 and the MSB needs to flip from 0 to 1. When it crosses back, the MSB flips from 1 to 0. EOR handles both directions with one instruction.

EOR #$01 flips bit 0 without touching the other seven bits. If bit 0 was 0, it becomes 1. If it was 1, it becomes 0. The toggle works regardless of the current state — no need to check first.

        ; RIGHT (bit 3) — 9-bit clamp to X <= 320
        lda $dc00
        and #%00001000
        bne not_right

        ; Check right boundary
        lda $d010
        and #$01
        beq right_ok            ; MSB clear, X < 256, safe to move right
        lda $d000
        cmp #63                 ; 320 - 256 - 2 + 1 (room for 2-pixel move)
        bcs not_right           ; Too close to right edge

right_ok:
        ; Increment 1 — check for $FF wrap
        inc $d000
        bne +
        lda $d010               ; Low byte wrapped to $00 — toggle bit 8
        eor #$01
        sta $d010
+
        ; Increment 2
        inc $d000
        bne +
        lda $d010
        eor #$01
        sta $d010
+

not_right:

After each INC $D000, BNE checks whether the result wrapped to zero. If it did, the low byte crossed from $FF to $00, and EOR #$01 toggles the MSB. Left movement uses the same logic in reverse — check before each DEC whether the low byte is $00, toggle the MSB if so, then decrement.

Staying on Screen

Without boundary checks, the ship wraps around the edges. Moving left past X=0 jumps to X=511. Moving down past the bottom reappears at the top. Boundary clamping prevents this by checking the position before each move.

        ; UP (bit 0) — clamp to Y >= 50
        lda $dc00
        and #%00000001
        bne not_up
        lda $d001
        cmp #52                 ; 50 + 2 (room for 2-pixel move)
        bcc not_up
        dec $d001
        dec $d001
not_up:

        ; DOWN (bit 1) — clamp to Y <= 234
        lda $dc00
        and #%00000010
        bne not_down
        lda $d001
        cmp #233                ; 234 - 1 (room for 2-pixel move)
        bcs not_down
        inc $d001
        inc $d001
not_down:

        ; LEFT (bit 2) — 9-bit clamp to X >= 24
        lda $dc00
        and #%00000100
        bne not_left

        ; Check left boundary
        lda $d010
        and #$01
        bne left_ok             ; MSB set, X >= 256, safe to move left
        lda $d000
        cmp #26                 ; 24 + 2 (room for 2-pixel move)
        bcc not_left            ; Too close to left edge

left_ok:
        ; Decrement 1 — check for $00 wrap
        lda $d000
        bne +
        lda $d010               ; Low byte is $00 — toggle bit 8
        eor #$01
        sta $d010
+       dec $d000

        ; Decrement 2
        lda $d000
        bne +
        lda $d010
        eor #$01
        sta $d010
+       dec $d000

not_left:

Y clamping is a single comparison — CMP against the boundary, branch if already there. X clamping needs the MSB: when bit 0 of $D010 is set, the ship is past X=255 and always safe to move left; when it’s clear, CMP against the left boundary works normally. The right boundary reverses the logic.

The collision checks also gain MSB awareness. Enemies always spawn below X=256, so when the ship or bullet has its MSB set, collisions are impossible. A quick check of $D010 skips the distance calculations entirely.

Clean Restarts

Two places reset the game: init_game (starting a new game) and init_title (returning to the title screen). Both now clear $D010 — no stale MSB values carrying across. They also silence all three SID voices by writing zero to each control register. Any sound left playing from the previous session cuts off cleanly.

In life_lost, the ship resets to X=172 — well under 256. The code clears bit 0 of $D010 so the MSB matches the new position.

The Complete Code

; Starfield - Unit 16: Screen Edges
; 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 = title, 1 = playing, 2 = game over
lives          = $12   ; Lives remaining (starts at 3)
death_timer    = $13   ; Death flash countdown (0 = no flash)
star_row       = $14   ; 8 bytes ($14-$1b) — row 0-24
star_col       = $1c   ; 8 bytes ($1c-$23) — column 0-39
frame_count    = $24   ; Frame counter for parallax timing

; $fb-$fc: temporary pointer (used by star routines)

; ------------------------------------------------
; 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

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

        ; Start on the title screen
        jsr clear_screen
        jsr init_title

!ifdef SCREENSHOT_MODE {
        ; Override: start in playing state with ship on the right
        jsr clear_screen
        jsr init_game

        ; Move ship past X=255 (X=296 = 256 + 40)
        lda #40
        sta $d000
        lda $d010
        ora #$01
        sta $d010

        ; Set a visible score
        sed
        lda #$05
        sta score
        cld
        lda #$30
        sta $0400
        lda #$35
        sta $0401
}

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

        ; --- Update stars (all states) ---
        inc frame_count

        ldx #$00
star_loop:
        jsr erase_star

        ; Check if star should move this frame
        cpx #$04
        bcc move_star           ; Close stars (0-3) always move

        ; Distant star — only move on odd frames
        lda frame_count
        and #$01
        beq skip_move           ; Even frame, don't move

move_star:
        inc star_row,x
        lda star_row,x
        cmp #25
        bcc skip_move

        ; Wrap to row 0
        lda #$00
        sta star_row,x

skip_move:
        jsr draw_star

        inx
        cpx #$08
        bne star_loop

        ; --- State dispatch ---
        lda game_state
        beq title_state         ; 0 = title
        cmp #$02
        beq game_over_state     ; 2 = game over
        jmp game_active         ; 1 = playing

title_state:
        ; Redraw title text (repairs any star damage)
        jsr show_title

        ; Poll fire button
        lda $dc00
        and #%00010000
        bne state_done          ; Not pressed

        ; Fire pressed — start the game
        jsr clear_screen
        jsr init_game
        jmp game_loop

game_over_state:
        ; Redraw game over text (repairs any star damage)
        jsr show_game_over

        ; Poll fire button
        lda $dc00
        and #%00010000
        bne state_done          ; Not pressed

        ; Fire pressed — return to title
        jsr clear_screen
        jsr init_title
        jmp game_loop

state_done:
        jmp game_loop

game_active:

        ; --- Death timer (invulnerability flash) ---
        lda death_timer
        beq no_death_flash

        dec death_timer
        bne no_death_flash

        ; Timer expired — restore border to black
        lda #$00
        sta $d020

no_death_flash:

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

        ; UP (bit 0) — clamp to Y >= 50
        lda $dc00
        and #%00000001
        bne not_up
        lda $d001
        cmp #52                 ; 50 + 2 (room for 2-pixel move)
        bcc not_up
        dec $d001
        dec $d001
not_up:

        ; DOWN (bit 1) — clamp to Y <= 234
        lda $dc00
        and #%00000010
        bne not_down
        lda $d001
        cmp #233                ; 234 - 1 (room for 2-pixel move)
        bcs not_down
        inc $d001
        inc $d001
not_down:

        ; LEFT (bit 2) — 9-bit clamp to X >= 24
        lda $dc00
        and #%00000100
        bne not_left

        ; Check left boundary
        lda $d010
        and #$01
        bne left_ok             ; MSB set, X >= 256, safe to move left
        lda $d000
        cmp #26                 ; 24 + 2 (room for 2-pixel move)
        bcc not_left            ; Too close to left edge

left_ok:
        ; Decrement 1 — check for $00 wrap
        lda $d000
        bne +
        lda $d010               ; Low byte is $00 — toggle bit 8
        eor #$01
        sta $d010
+       dec $d000

        ; Decrement 2
        lda $d000
        bne +
        lda $d010
        eor #$01
        sta $d010
+       dec $d000

not_left:

        ; RIGHT (bit 3) — 9-bit clamp to X <= 320
        lda $dc00
        and #%00001000
        bne not_right

        ; Check right boundary
        lda $d010
        and #$01
        beq right_ok            ; MSB clear, X < 256, safe to move right
        lda $d000
        cmp #63                 ; 320 - 256 - 2 + 1 (room for 2-pixel move)
        bcs not_right           ; Too close to right edge

right_ok:
        ; Increment 1 — check for $FF wrap
        inc $d000
        bne +
        lda $d010               ; Low byte wrapped to $00 — toggle bit 8
        eor #$01
        sta $d010
+
        ; Increment 2
        inc $d000
        bne +
        lda $d010
        eor #$01
        sta $d010
+

not_right:

        ; --- Fire button (bit 4) ---
        lda $dc00
        and #%00010000
        bne no_fire

        lda bullet_active
        bne no_fire

        ; Spawn bullet at ship position
        lda $d000
        sta $d002
        lda $d001
        sta bullet_y

        ; Copy ship X bit 8 to bullet X bit 8
        lda $d010
        and #%11111101          ; Clear bullet bit (bit 1)
        sta $d010
        lda $d010
        and #$01                ; Get ship bit (bit 0)
        asl                     ; Shift to bullet position (bit 1)
        ora $d010
        sta $d010

        ; Enable sprite 1
        lda $d015
        ora #%00000010
        sta $d015

        lda #$01
        sta bullet_active

        ; Trigger laser sound
        lda #$20
        sta $d404
        lda #$21
        sta $d404

no_fire:

        ; --- Update bullet ---
        lda bullet_active
        beq no_bullet

        ; Move bullet up
        lda bullet_y
        sec
        sbc #$04
        sta bullet_y
        sta $d003

        ; Off-screen check
        cmp #$1e
        bcs no_bullet

        lda #$00
        sta bullet_active
        lda $d015
        and #%11111101
        sta $d015
        lda $d010
        and #%11111101          ; Clear bullet MSB
        sta $d010

no_bullet:

        ; --- Check bullet-enemy collision ---
        lda bullet_active
        bne check_collision
        jmp no_hit
check_collision:
        ; Skip if bullet past X=255 (enemies are always < 256)
        lda $d010
        and #%00000010
        beq do_collision
        jmp no_hit
do_collision:

        ldx #$00
collision_loop:
        lda flash_tbl,x
        bne next_collision

        ; 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
        sec
        sbc enemy_x_tbl,x
        cmp #$10
        bcc hit_enemy
        cmp #$f0
        bcc next_collision
        jmp hit_enemy

next_collision:
        inx
        cpx #$03
        bne collision_loop
        jmp no_hit

hit_enemy:
        ; Deactivate bullet
        lda #$00
        sta bullet_active
        lda $d015
        and #%11111101
        sta $d015
        lda $d010
        and #%11111101          ; Clear bullet MSB
        sta $d010

        ; Flash this enemy white
        lda #$08
        sta flash_tbl,x
        ldy sprite_colour_off,x
        lda #$01
        sta $d000,y

        ; Explosion sound — SID voice 2 (noise)
        lda #$00
        sta $d407
        lda #$08
        sta $d408
        lda #$09
        sta $d40c
        lda #$00
        sta $d40d
        lda #$80
        sta $d40b
        lda #$81
        sta $d40b

        ; Increment score (BCD)
        sed
        lda score
        clc
        adc #$01
        sta score
        cld

        ; Update score display — tens digit
        lda score
        lsr
        lsr
        lsr
        lsr
        clc
        adc #$30
        sta $0400

        ; Update score display — ones digit
        lda score
        and #$0f
        clc
        adc #$30
        sta $0401

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
        cmp #$f8
        bcc update_enemy_sprite

        ; Respawn at top
        lda #$32
        jsr spawn_enemy
        jmp next_enemy

do_flash:
        dec flash_tbl,x
        bne update_enemy_sprite

        ; Flash done — respawn
        lda #$32
        jsr spawn_enemy
        jmp next_enemy

update_enemy_sprite:
        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 ---
        lda death_timer
        bne skip_ship_collision

        ; Skip if ship past X=255 (enemies are always < 256)
        lda $d010
        and #$01
        bne skip_ship_collision

        ldx #$00
ship_collision_loop:
        lda flash_tbl,x
        bne next_ship_check

        ; Check Y distance
        lda $d001
        sec
        sbc enemy_y_tbl,x
        cmp #$10
        bcc check_ship_x
        cmp #$f0
        bcc next_ship_check

check_ship_x:
        ; Check X distance
        lda $d000
        sec
        sbc enemy_x_tbl,x
        cmp #$10
        bcc ship_hit
        cmp #$f0
        bcc next_ship_check
        jmp ship_hit

next_ship_check:
        inx
        cpx #$03
        bne ship_collision_loop

skip_ship_collision:
        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

        ; Game over
        lda #$02
        sta game_state
        lda #$02
        sta $d027
        lda #$00
        sta $d020
        sta death_timer
        jsr show_game_over
        jmp play_death_sound

life_lost:
        ; Reset ship position (X=172 < 256, clear MSB)
        lda #172
        sta $d000
        lda #220
        sta $d001
        lda $d010
        and #%11111110          ; Clear ship MSB (bit 0)
        sta $d010

        ; Start death flash
        lda #16
        sta death_timer
        lda #$02
        sta $d020

play_death_sound:
        ; SID voice 3 (descending sawtooth)
        lda #$00
        sta $d40e
        lda #$10
        sta $d40f
        lda #$0a
        sta $d412
        lda #$00
        sta $d413
        lda #$20
        sta $d411
        lda #$21
        sta $d411

        jmp game_loop

; ------------------------------------------------
; Subroutine: erase_star
; X = star index. Writes space to the star's screen position.
; ------------------------------------------------
erase_star:
        ldy star_row,x
        lda row_addr_lo,y
        sta $fb
        lda row_addr_hi,y
        sta $fc
        ldy star_col,x
        lda #$20
        sta ($fb),y
        rts

; ------------------------------------------------
; Subroutine: draw_star
; X = star index. Writes character and colour at the star's position.
; ------------------------------------------------
draw_star:
        ldy star_row,x
        lda row_addr_lo,y
        sta $fb
        lda row_addr_hi,y
        sta $fc
        ldy star_col,x

        ; Write character to screen RAM
        lda star_char_tbl,x
        sta ($fb),y

        ; Switch pointer to colour RAM (high byte + $D4)
        lda $fc
        clc
        adc #$d4
        sta $fc

        ; Write colour
        lda star_colour_tbl,x
        sta ($fb),y
        rts

; ------------------------------------------------
; 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_title
; Sets up the title screen
; ------------------------------------------------
init_title:
        ; No sprites on title screen
        lda #$00
        sta $d015
        sta $d010               ; Clear all MSBs
        sta frame_count
        sta game_state          ; 0 = title

        ; Silence all SID voices
        sta $d404
        sta $d40b
        sta $d412

        ; Draw stars and title text
        jsr init_stars
        jsr show_title

        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
        lda #220
        sta $d001
        lda #$01
        sta $d027

        ; Enemy colours
        lda #$05
        sta $d029
        sta $d02a
        sta $d02b

        ; Spawn three enemies at staggered heights
        lda #$32
        ldx #$00
        jsr spawn_enemy
        lda #$82
        ldx #$01
        jsr spawn_enemy
        lda #$d2
        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 score
        sta death_timer
        sta frame_count
        sta $d020               ; Border black
        sta $d010               ; Clear all MSBs

        ; Silence all SID voices
        sta $d404
        sta $d40b
        sta $d412

        lda #$01
        sta game_state          ; 1 = playing

        ; Score display
        lda #$30
        sta $0400
        sta $0401

        ; Lives
        lda #$03
        sta lives
        lda #$33
        sta $0427

        ; Draw stars
        jsr init_stars

        rts

; ------------------------------------------------
; Subroutine: init_stars
; Resets star positions and draws them
; ------------------------------------------------
init_stars:
        ldx #$00
-       lda star_init_row,x
        sta star_row,x
        lda star_init_col,x
        sta star_col,x
        jsr draw_star
        inx
        cpx #$08
        bne -
        rts

; ------------------------------------------------
; Subroutine: show_title
; Writes "STARFIELD" and "PRESS FIRE" to screen RAM
; ------------------------------------------------
show_title:
        ; "STARFIELD" at row 10, col 16 ($05A0)
        lda #$13            ; S
        sta $05a0
        lda #$14            ; T
        sta $05a1
        lda #$01            ; A
        sta $05a2
        lda #$12            ; R
        sta $05a3
        lda #$06            ; F
        sta $05a4
        lda #$09            ; I
        sta $05a5
        lda #$05            ; E
        sta $05a6
        lda #$0c            ; L
        sta $05a7
        lda #$04            ; D
        sta $05a8

        ; Colour to white
        lda #$01
        sta $d9a0
        sta $d9a1
        sta $d9a2
        sta $d9a3
        sta $d9a4
        sta $d9a5
        sta $d9a6
        sta $d9a7
        sta $d9a8

        ; "PRESS FIRE" at row 14, col 15 ($063F)
        lda #$10            ; P
        sta $063f
        lda #$12            ; R
        sta $0640
        lda #$05            ; E
        sta $0641
        lda #$13            ; S
        sta $0642
        lda #$13            ; S
        sta $0643
        lda #$20            ; (space)
        sta $0644
        lda #$06            ; F
        sta $0645
        lda #$09            ; I
        sta $0646
        lda #$12            ; R
        sta $0647
        lda #$05            ; E
        sta $0648

        ; Colour to light grey
        lda #$0f
        sta $da3f
        sta $da40
        sta $da41
        sta $da42
        sta $da43
        sta $da44
        sta $da45
        sta $da46
        sta $da47
        sta $da48

        rts

; ------------------------------------------------
; Subroutine: show_game_over
; Writes "GAME OVER" to screen RAM, row 12, col 16
; ------------------------------------------------
show_game_over:
        lda #$07
        sta $05f0
        lda #$01
        sta $05f1
        lda #$0d
        sta $05f2
        lda #$05
        sta $05f3
        lda #$20
        sta $05f4
        lda #$0f
        sta $05f5
        lda #$16
        sta $05f6
        lda #$05
        sta $05f7
        lda #$12
        sta $05f8

        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
        lda enemy_y_tbl,x
        sta $d001,y

        rts

; ------------------------------------------------
; Lookup tables
; ------------------------------------------------

; VIC-II sprite register offsets
sprite_pos_off:
        !byte $04, $06, $08

sprite_colour_off:
        !byte $29, $2a, $2b

; Screen RAM row start addresses (rows 0-24)
row_addr_lo:
        !byte $00, $28, $50, $78, $a0, $c8, $f0, $18
        !byte $40, $68, $90, $b8, $e0, $08, $30, $58
        !byte $80, $a8, $d0, $f8, $20, $48, $70, $98, $c0

row_addr_hi:
        !byte $04, $04, $04, $04, $04, $04, $04, $05
        !byte $05, $05, $05, $05, $05, $06, $06, $06
        !byte $06, $06, $06, $06, $07, $07, $07, $07, $07

; Star initial positions (8 stars: 0-3 close, 4-7 distant)
star_init_row:
        !byte 2, 8, 14, 20, 5, 11, 17, 23

star_init_col:
        !byte 5, 28, 15, 35, 18, 7, 32, 22

; Star appearance (close = bright asterisk, distant = dim period)
star_char_tbl:
        !byte $2a, $2a, $2a, $2a, $2e, $2e, $2e, $2e

star_colour_tbl:
        !byte $01, $01, $01, $01, $0b, $0b, $0b, $0b

; ------------------------------------------------
; 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

  • Ship still stops at X=255? Check $D010 is being toggled with EOR #$01 when the low byte wraps. Both INC and DEC paths need it.
  • Ship wraps around the screen? Boundary checks must run before the movement code. Verify CMP values: 52 for top, 233 for bottom, 26 for left, 63 for right (when MSB is set).
  • Ship teleports to the right on death? life_lost must clear bit 0 of $D010 with AND #%11111110. The reset position X=172 needs MSB=0.
  • False collisions when ship is on the right? The collision loop must check $D010 AND #$01 and skip when set. Without this, the low byte alone matches nearby enemy positions.
  • Sound lingers between games? init_game and init_title must write $00 to $D404, $D40B, and $D412 to silence all three SID voices.
  • Bullet hits enemies from the wrong side? When the bullet spawns, copy the ship’s MSB (bit 0) to the bullet’s MSB (bit 1) in $D010. Check bit 1 before running the collision loop.

Try This: Wider Enemy Range

Enemies currently spawn between X=48 and X=175 — always in the left half. Increase the range to use the full screen width by setting their MSBs when X > 255. This means the collision code needs to compare MSBs for both ship-enemy and bullet-enemy pairs.

Try This: Screen Wrap Mode

Remove the boundary clamping and let the ship wrap from the right edge to the left. When the MSB toggles past the right boundary, reset it and the ship appears on the other side. Asteroids-style movement in a vertical shooter.

What You’ve Learnt

  • $D010 for 9-bit positions — each bit extends one sprite’s X range past 255.
  • EOR for bit toggling — flipping a single bit without checking its current state.
  • Boundary clampingCMP and branch to keep values in a valid range.
  • MSB-aware collision — skipping distance checks when positions can’t overlap.

Phase 1 Complete

Sixteen units. The game started as a black screen with a single sprite. It now has a title screen, a ship that moves across the full screen width, a bullet, three enemies, a score, lives, a starfield, sound effects, and a state machine cycling between title, playing, and game over.

Along the way: hardware registers, the accumulator, branches, loops, indexed addressing, subroutines, indirect pointers, BCD arithmetic, and the SID sound chip. The foundation is solid.

What’s Next

Phase 2 replaces the simple enemy arrays with structured data tables — type, position, speed, and status packed together. Different enemy types with distinct behaviours. The fleet is coming.