Skip to content
Game 1 Unit 15 of 64 1 hr learning time

Game States

Add a state machine with title screen, game over, and win states.

23% of Neon Nexus

What You’re Building

A complete game flow. Title screen. Playing. Game over. Win.

Game States

The game now starts at a title screen. Press Start to play.

The State Machine

Four states control the game:

STATE_TITLE    = 0
STATE_PLAYING  = 1
STATE_GAMEOVER = 2
STATE_WIN      = 3

Main Loop

The main loop dispatches to state handlers:

main_loop:
    jsr read_controller

    lda game_state
    cmp #STATE_TITLE
    beq @title_state
    cmp #STATE_PLAYING
    beq @playing_state
    cmp #STATE_GAMEOVER
    beq @gameover_state
    cmp #STATE_WIN
    beq @win_state
    jmp main_loop

@title_state:
    jsr handle_title
    jmp main_loop

Each state has its own handler. Clean separation.

Button Edge Detection

We want to detect a new press, not a held button:

BTN_START = %00010000

handle_title:
    lda buttons
    and #BTN_START
    beq @no_start
    lda buttons_prev
    and #BTN_START
    bne @no_start       ; Already pressed

    ; New press! Start the game
    jsr init_game
    lda #STATE_PLAYING
    sta game_state

@no_start:
    lda buttons
    sta buttons_prev
    rts

Compare current buttons with previous frame. Only act on the rising edge.

State Transitions

FromToTrigger
TITLEPLAYINGStart pressed
PLAYINGGAMEOVERLives reach 0
PLAYINGWINAll items collected
GAMEOVERTITLEStart pressed
WINTITLEStart pressed

Screen Displays

Each state shows different sprites:

  • Title: Player with decorative items
  • Playing: Full game (player, enemies, items, lives)
  • Game Over: Enemy sprites as defeat indicator
  • Win: Player surrounded by items

The Code

; =============================================================================
; NEON NEXUS - Unit 15: Game States
; =============================================================================
; Add a state machine with title screen, playing, game over, and win states.
; =============================================================================

PPUCTRL   = $2000
PPUMASK   = $2001
PPUSTATUS = $2002
OAMADDR   = $2003
PPUSCROLL = $2005
PPUADDR   = $2006
PPUDATA   = $2007
OAMDMA    = $4014
APUSTATUS = $4015
JOYPAD1   = $4016

; APU Pulse 1 registers
APU_PULSE1_CTRL = $4000
APU_PULSE1_SWEEP = $4001
APU_PULSE1_LO = $4002
APU_PULSE1_HI = $4003

BTN_START  = %00010000
BTN_UP     = %00001000
BTN_DOWN   = %00000100
BTN_LEFT   = %00000010
BTN_RIGHT  = %00000001

; Game states
STATE_TITLE    = 0
STATE_PLAYING  = 1
STATE_GAMEOVER = 2
STATE_WIN      = 3

PLAYER_START_X  = 124
PLAYER_START_Y  = 116
PLAYER_SPEED    = 2
ENEMY_SPEED     = 1
NUM_ENEMIES     = 4
NUM_ITEMS       = 4
STARTING_LIVES  = 3
COLLISION_DIST  = 6
COLLECT_DIST    = 8
INVULN_TIME     = 90

TILE_BORDER    = 1
TILE_FLOOR     = 2
TILE_CORNER_TL = 3
TILE_CORNER_TR = 4
TILE_CORNER_BL = 5
TILE_CORNER_BR = 6
SPRITE_PLAYER  = 7
SPRITE_ENEMY   = 8
SPRITE_LIFE    = 9
SPRITE_ITEM    = 10

ARENA_LEFT   = 16
ARENA_RIGHT  = 232
ARENA_TOP    = 24
ARENA_BOTTOM = 208

DIR_RIGHT = 1
DIR_LEFT  = $FF
DIR_DOWN  = 1
DIR_UP    = $FF
BG_COLOUR = $0F

.segment "ZEROPAGE"
player_x:       .res 1
player_y:       .res 1
buttons:        .res 1
buttons_prev:   .res 1      ; Previous frame buttons (for edge detection)
temp:           .res 1
row_counter:    .res 1
frame_count:    .res 1
lives:          .res 1
invuln_timer:   .res 1
game_state:     .res 1      ; Current game state
items_collected: .res 1
score_lo:       .res 1
score_hi:       .res 1

enemy_x:        .res NUM_ENEMIES
enemy_y:        .res NUM_ENEMIES
enemy_dir_x:    .res NUM_ENEMIES
enemy_dir_y:    .res NUM_ENEMIES

item_x:         .res NUM_ITEMS
item_y:         .res NUM_ITEMS
item_active:    .res NUM_ITEMS

.segment "OAM"
oam_buffer:     .res 256

.segment "BSS"

.segment "HEADER"
    .byte "NES", $1A, 2, 1, $01, $00, 0,0,0,0,0,0,0,0

.segment "CODE"

reset:
    sei
    cld
    ldx #$40
    stx $4017
    ldx #$FF
    txs
    inx
    stx PPUCTRL
    stx PPUMASK
    stx $4010

@vblank1:
    bit PPUSTATUS
    bpl @vblank1

    lda #0
@clear_ram:
    sta $0000, x
    sta $0100, x
    sta $0200, x
    sta $0300, x
    sta $0400, x
    sta $0500, x
    sta $0600, x
    sta $0700, x
    inx
    bne @clear_ram

@vblank2:
    bit PPUSTATUS
    bpl @vblank2

    jsr load_palette
    jsr draw_arena
    jsr set_attributes

    ; Hide all sprites initially
    lda #$FF
    ldx #0
@hide_all:
    sta oam_buffer, x
    inx
    bne @hide_all

    lda #0
    sta PPUSCROLL
    sta PPUSCROLL

    ; Enable APU channels
    lda #%00000001
    sta APUSTATUS

    ; Start in title state
    lda #STATE_TITLE
    sta game_state

    lda #%10000000
    sta PPUCTRL
    lda #%00011110
    sta PPUMASK

; =============================================================================
; Main Loop - State Machine
; =============================================================================
main_loop:
    jsr read_controller

    lda game_state
    cmp #STATE_TITLE
    beq @title_state
    cmp #STATE_PLAYING
    beq @playing_state
    cmp #STATE_GAMEOVER
    beq @gameover_state
    cmp #STATE_WIN
    beq @win_state
    jmp main_loop

@title_state:
    jsr handle_title
    jmp main_loop

@playing_state:
    jsr handle_playing
    jmp main_loop

@gameover_state:
    jsr handle_gameover
    jmp main_loop

@win_state:
    jsr handle_win
    jmp main_loop

; =============================================================================
; State Handlers
; =============================================================================

handle_title:
    ; Show title screen sprites
    jsr show_title_sprites

    ; Check for Start button press (new press only)
    lda buttons
    and #BTN_START
    beq @no_start
    lda buttons_prev
    and #BTN_START
    bne @no_start           ; Already pressed last frame

    ; Start pressed! Begin game
    jsr init_game
    lda #STATE_PLAYING
    sta game_state
    jsr play_collect_sound  ; Start game sound

@no_start:
    ; Store previous buttons
    lda buttons
    sta buttons_prev
    rts

handle_playing:
    jsr move_player
    jsr move_enemies
    jsr check_enemy_collisions
    jsr check_item_collisions
    jsr check_win_condition
    jsr update_all_sprites

    ; Store previous buttons
    lda buttons
    sta buttons_prev
    rts

handle_gameover:
    ; Show game over display
    jsr show_gameover_sprites

    ; Check for Start to restart
    lda buttons
    and #BTN_START
    beq @no_restart
    lda buttons_prev
    and #BTN_START
    bne @no_restart

    ; Restart game
    lda #STATE_TITLE
    sta game_state

@no_restart:
    lda buttons
    sta buttons_prev
    rts

handle_win:
    ; Show win display
    jsr show_win_sprites

    ; Check for Start to restart
    lda buttons
    and #BTN_START
    beq @no_restart
    lda buttons_prev
    and #BTN_START
    bne @no_restart

    ; Back to title
    lda #STATE_TITLE
    sta game_state

@no_restart:
    lda buttons
    sta buttons_prev
    rts

; =============================================================================
; Display Routines
; =============================================================================

show_title_sprites:
    ; Hide all sprites first
    lda #$FF
    ldx #0
@hide_loop:
    sta oam_buffer, x
    inx
    bne @hide_loop

    ; Show player sprite in center as "logo"
    lda #100                ; Y position
    sta oam_buffer+0
    lda #SPRITE_PLAYER
    sta oam_buffer+1
    lda #0
    sta oam_buffer+2
    lda #124                ; X position
    sta oam_buffer+3

    ; Show some items as decoration
    lda #100
    sta oam_buffer+4
    lda #SPRITE_ITEM
    sta oam_buffer+5
    lda #%00000010
    sta oam_buffer+6
    lda #100
    sta oam_buffer+7

    lda #100
    sta oam_buffer+8
    lda #SPRITE_ITEM
    sta oam_buffer+9
    lda #%00000010
    sta oam_buffer+10
    lda #148
    sta oam_buffer+11
    rts

show_gameover_sprites:
    ; Hide all sprites
    lda #$FF
    ldx #0
@hide_loop:
    sta oam_buffer, x
    inx
    bne @hide_loop

    ; Show enemies as "defeat" indicator
    lda #100
    sta oam_buffer+0
    lda #SPRITE_ENEMY
    sta oam_buffer+1
    lda #%00000001
    sta oam_buffer+2
    lda #112
    sta oam_buffer+3

    lda #100
    sta oam_buffer+4
    lda #SPRITE_ENEMY
    sta oam_buffer+5
    lda #%00000001
    sta oam_buffer+6
    lda #128
    sta oam_buffer+7

    lda #100
    sta oam_buffer+8
    lda #SPRITE_ENEMY
    sta oam_buffer+9
    lda #%00000001
    sta oam_buffer+10
    lda #144
    sta oam_buffer+11
    rts

show_win_sprites:
    ; Hide all sprites
    lda #$FF
    ldx #0
@hide_loop:
    sta oam_buffer, x
    inx
    bne @hide_loop

    ; Show player and items as "victory" indicator
    lda #100
    sta oam_buffer+0
    lda #SPRITE_PLAYER
    sta oam_buffer+1
    lda #0
    sta oam_buffer+2
    lda #124
    sta oam_buffer+3

    ; Items around player
    lda #92
    sta oam_buffer+4
    lda #SPRITE_ITEM
    sta oam_buffer+5
    lda #%00000010
    sta oam_buffer+6
    lda #116
    sta oam_buffer+7

    lda #92
    sta oam_buffer+8
    lda #SPRITE_ITEM
    sta oam_buffer+9
    lda #%00000010
    sta oam_buffer+10
    lda #132
    sta oam_buffer+11

    lda #108
    sta oam_buffer+12
    lda #SPRITE_ITEM
    sta oam_buffer+13
    lda #%00000010
    sta oam_buffer+14
    lda #116
    sta oam_buffer+15

    lda #108
    sta oam_buffer+16
    lda #SPRITE_ITEM
    sta oam_buffer+17
    lda #%00000010
    sta oam_buffer+18
    lda #132
    sta oam_buffer+19
    rts

; =============================================================================
; Game Initialisation
; =============================================================================

init_game:
    lda #STARTING_LIVES
    sta lives
    lda #0
    sta invuln_timer
    sta items_collected
    sta score_lo
    sta score_hi

    lda #PLAYER_START_X
    sta player_x
    lda #PLAYER_START_Y
    sta player_y

    jsr init_enemies
    jsr init_items
    rts

init_enemies:
    lda #48
    sta enemy_x+0
    sta enemy_y+0
    lda #DIR_RIGHT
    sta enemy_dir_x+0
    lda #DIR_DOWN
    sta enemy_dir_y+0

    lda #200
    sta enemy_x+1
    lda #48
    sta enemy_y+1
    lda #DIR_LEFT
    sta enemy_dir_x+1
    lda #DIR_DOWN
    sta enemy_dir_y+1

    lda #48
    sta enemy_x+2
    lda #176
    sta enemy_y+2
    lda #DIR_RIGHT
    sta enemy_dir_x+2
    lda #DIR_UP
    sta enemy_dir_y+2

    lda #200
    sta enemy_x+3
    lda #176
    sta enemy_y+3
    lda #DIR_LEFT
    sta enemy_dir_x+3
    lda #DIR_UP
    sta enemy_dir_y+3
    rts

init_items:
    lda #80
    sta item_x+0
    lda #64
    sta item_y+0
    lda #1
    sta item_active+0

    lda #168
    sta item_x+1
    lda #64
    sta item_y+1
    lda #1
    sta item_active+1

    lda #80
    sta item_x+2
    lda #160
    sta item_y+2
    lda #1
    sta item_active+2

    lda #168
    sta item_x+3
    lda #160
    sta item_y+3
    lda #1
    sta item_active+3
    rts

; =============================================================================
; Collision Detection
; =============================================================================

check_enemy_collisions:
    lda invuln_timer
    beq @check
    dec invuln_timer
    rts

@check:
    ldx #0
@check_enemy:
    lda player_x
    sec
    sbc enemy_x, x
    bpl @check_x_pos
    eor #$FF
    clc
    adc #1
@check_x_pos:
    cmp #COLLISION_DIST
    bcs @next_enemy

    lda player_y
    sec
    sbc enemy_y, x
    bpl @check_y_pos
    eor #$FF
    clc
    adc #1
@check_y_pos:
    cmp #COLLISION_DIST
    bcs @next_enemy

    jsr player_hit
    rts

@next_enemy:
    inx
    cpx #NUM_ENEMIES
    bne @check_enemy
    rts

check_item_collisions:
    ldx #0
@check_item:
    lda item_active, x
    beq @next_item

    lda player_x
    sec
    sbc item_x, x
    bpl @item_x_pos
    eor #$FF
    clc
    adc #1
@item_x_pos:
    cmp #COLLECT_DIST
    bcs @next_item

    lda player_y
    sec
    sbc item_y, x
    bpl @item_y_pos
    eor #$FF
    clc
    adc #1
@item_y_pos:
    cmp #COLLECT_DIST
    bcs @next_item

    ; Collected!
    lda #0
    sta item_active, x
    inc items_collected
    lda score_lo
    clc
    adc #100
    sta score_lo
    lda score_hi
    adc #0
    sta score_hi
    jsr play_collect_sound

@next_item:
    inx
    cpx #NUM_ITEMS
    bne @check_item
    rts

player_hit:
    jsr play_death_sound
    dec lives
    lda lives
    beq @game_over

    lda #PLAYER_START_X
    sta player_x
    lda #PLAYER_START_Y
    sta player_y

    lda #INVULN_TIME
    sta invuln_timer
    rts

@game_over:
    lda #STATE_GAMEOVER
    sta game_state
    rts

; =============================================================================
; Win Condition
; =============================================================================

check_win_condition:
    lda items_collected
    cmp #NUM_ITEMS
    bne @not_yet
    lda game_state
    cmp #STATE_WIN
    beq @not_yet            ; Already won
    lda #STATE_WIN
    sta game_state
    jsr play_victory_sound
@not_yet:
    rts

; =============================================================================
; Sound Effects
; =============================================================================

play_collect_sound:
    lda #%10011111
    sta APU_PULSE1_CTRL
    lda #0
    sta APU_PULSE1_SWEEP
    lda #$C4
    sta APU_PULSE1_LO
    lda #%00001000
    sta APU_PULSE1_HI
    rts

play_death_sound:
    lda #%10011111
    sta APU_PULSE1_CTRL
    lda #%10001111
    sta APU_PULSE1_SWEEP
    lda #$00
    sta APU_PULSE1_LO
    lda #%00001011
    sta APU_PULSE1_HI
    rts

play_victory_sound:
    lda #%10011111
    sta APU_PULSE1_CTRL
    lda #%10000111
    sta APU_PULSE1_SWEEP
    lda #$FF
    sta APU_PULSE1_LO
    lda #%00000011
    sta APU_PULSE1_HI
    rts

; =============================================================================
; Sprite Updates
; =============================================================================

update_all_sprites:
    jsr update_player_sprite
    jsr update_enemy_sprites
    jsr update_item_sprites
    jsr update_lives_display
    rts

update_player_sprite:
    lda game_state
    cmp #STATE_PLAYING
    bne @hide

    lda invuln_timer
    beq @show
    and #%00000100
    beq @show

@hide:
    lda #$FF
    sta oam_buffer+0
    rts

@show:
    lda player_y
    sta oam_buffer+0
    lda #SPRITE_PLAYER
    sta oam_buffer+1
    lda #0
    sta oam_buffer+2
    lda player_x
    sta oam_buffer+3
    rts

update_enemy_sprites:
    ldx #0
    ldy #4
@loop:
    lda enemy_y, x
    sta oam_buffer, y
    iny
    lda #SPRITE_ENEMY
    sta oam_buffer, y
    iny
    lda #%00000001
    sta oam_buffer, y
    iny
    lda enemy_x, x
    sta oam_buffer, y
    iny
    inx
    cpx #NUM_ENEMIES
    bne @loop
    rts

update_item_sprites:
    ldx #0
    ldy #20
@loop:
    lda item_active, x
    beq @hide_item

    lda item_y, x
    sta oam_buffer, y
    iny
    lda #SPRITE_ITEM
    sta oam_buffer, y
    iny
    lda #%00000010
    sta oam_buffer, y
    iny
    lda item_x, x
    sta oam_buffer, y
    iny
    jmp @next_item

@hide_item:
    lda #$FF
    sta oam_buffer, y
    iny
    iny
    iny
    iny

@next_item:
    inx
    cpx #NUM_ITEMS
    bne @loop
    rts

update_lives_display:
    ldy #36
    ldx lives
    beq @hide_all

    lda #8
    sta oam_buffer, y
    iny
    lda #SPRITE_LIFE
    sta oam_buffer, y
    iny
    lda #0
    sta oam_buffer, y
    iny
    lda #16
    sta oam_buffer, y
    iny

    cpx #1
    beq @hide_rest

    lda #8
    sta oam_buffer, y
    iny
    lda #SPRITE_LIFE
    sta oam_buffer, y
    iny
    lda #0
    sta oam_buffer, y
    iny
    lda #26
    sta oam_buffer, y
    iny

    cpx #2
    beq @hide_rest

    lda #8
    sta oam_buffer, y
    iny
    lda #SPRITE_LIFE
    sta oam_buffer, y
    iny
    lda #0
    sta oam_buffer, y
    iny
    lda #36
    sta oam_buffer, y
    rts

@hide_rest:
@hide_all:
    rts

; =============================================================================
; Enemy Movement
; =============================================================================

move_enemies:
    ldx #0
@enemy_loop:
    lda enemy_x, x
    clc
    adc enemy_dir_x, x
    sta enemy_x, x
    cmp #ARENA_LEFT
    bcs @check_right
    lda #DIR_RIGHT
    sta enemy_dir_x, x
    lda #ARENA_LEFT
    sta enemy_x, x
    jmp @move_y
@check_right:
    cmp #ARENA_RIGHT
    bcc @move_y
    lda #DIR_LEFT
    sta enemy_dir_x, x
    lda #ARENA_RIGHT
    sec
    sbc #1
    sta enemy_x, x
@move_y:
    lda enemy_y, x
    clc
    adc enemy_dir_y, x
    sta enemy_y, x
    cmp #ARENA_TOP
    bcs @check_bottom
    lda #DIR_DOWN
    sta enemy_dir_y, x
    lda #ARENA_TOP
    sta enemy_y, x
    jmp @next_enemy
@check_bottom:
    cmp #ARENA_BOTTOM
    bcc @next_enemy
    lda #DIR_UP
    sta enemy_dir_y, x
    lda #ARENA_BOTTOM
    sec
    sbc #1
    sta enemy_y, x
@next_enemy:
    inx
    cpx #NUM_ENEMIES
    bne @enemy_loop
    rts

; =============================================================================
; PPU Setup
; =============================================================================

load_palette:
    bit PPUSTATUS
    lda #$3F
    sta PPUADDR
    lda #$00
    sta PPUADDR
    ldx #0
@loop:
    lda palette_data, x
    sta PPUDATA
    inx
    cpx #32
    bne @loop
    rts

draw_arena:
    bit PPUSTATUS
    lda #$20
    sta PPUADDR
    lda #$00
    sta PPUADDR
    lda #0
    sta row_counter
@draw_row:
    lda row_counter
    cmp #0
    beq @top_row
    cmp #1
    beq @top_row
    cmp #28
    beq @bottom_row
    cmp #29
    beq @bottom_row
    jmp @middle_row
@top_row:
    lda row_counter
    cmp #0
    bne @top_row_inner
    lda #TILE_CORNER_TL
    sta PPUDATA
    lda #TILE_BORDER
    ldx #30
@top_fill:
    sta PPUDATA
    dex
    bne @top_fill
    lda #TILE_CORNER_TR
    sta PPUDATA
    jmp @next_row
@top_row_inner:
    lda #TILE_BORDER
    ldx #32
@top_inner_fill:
    sta PPUDATA
    dex
    bne @top_inner_fill
    jmp @next_row
@bottom_row:
    lda row_counter
    cmp #29
    bne @bottom_row_inner
    lda #TILE_CORNER_BL
    sta PPUDATA
    lda #TILE_BORDER
    ldx #30
@bottom_fill:
    sta PPUDATA
    dex
    bne @bottom_fill
    lda #TILE_CORNER_BR
    sta PPUDATA
    jmp @next_row
@bottom_row_inner:
    lda #TILE_BORDER
    ldx #32
@bottom_inner_fill:
    sta PPUDATA
    dex
    bne @bottom_inner_fill
    jmp @next_row
@middle_row:
    lda #TILE_BORDER
    sta PPUDATA
    sta PPUDATA
    lda #TILE_FLOOR
    ldx #28
@floor_fill:
    sta PPUDATA
    dex
    bne @floor_fill
    lda #TILE_BORDER
    sta PPUDATA
    sta PPUDATA
@next_row:
    inc row_counter
    lda row_counter
    cmp #30
    beq @done_drawing
    jmp @draw_row
@done_drawing:
    rts

set_attributes:
    bit PPUSTATUS
    lda #$23
    sta PPUADDR
    lda #$C0
    sta PPUADDR
    ldx #8
    lda #$00
@attr_top:
    sta PPUDATA
    dex
    bne @attr_top
    ldx #6
@attr_floor:
    lda #$00
    sta PPUDATA
    lda #%01010101
    sta PPUDATA
    sta PPUDATA
    sta PPUDATA
    sta PPUDATA
    sta PPUDATA
    sta PPUDATA
    lda #$00
    sta PPUDATA
    dex
    bne @attr_floor
    ldx #8
    lda #$00
@attr_bottom:
    sta PPUDATA
    dex
    bne @attr_bottom
    rts

; =============================================================================
; Input
; =============================================================================

read_controller:
    lda #1
    sta JOYPAD1
    lda #0
    sta JOYPAD1
    ldx #8
@read_loop:
    lda JOYPAD1
    lsr a
    rol buttons
    dex
    bne @read_loop
    rts

move_player:
    lda buttons
    and #BTN_UP
    beq @check_down
    lda player_y
    sec
    sbc #PLAYER_SPEED
    cmp #ARENA_TOP
    bcc @check_down
    sta player_y
@check_down:
    lda buttons
    and #BTN_DOWN
    beq @check_left
    lda player_y
    clc
    adc #PLAYER_SPEED
    cmp #ARENA_BOTTOM
    bcs @check_left
    sta player_y
@check_left:
    lda buttons
    and #BTN_LEFT
    beq @check_right
    lda player_x
    sec
    sbc #PLAYER_SPEED
    cmp #ARENA_LEFT
    bcc @check_right
    sta player_x
@check_right:
    lda buttons
    and #BTN_RIGHT
    beq @done
    lda player_x
    clc
    adc #PLAYER_SPEED
    cmp #ARENA_RIGHT
    bcs @done
    sta player_x
@done:
    rts

; =============================================================================
; NMI Handler
; =============================================================================

nmi:
    pha
    txa
    pha
    tya
    pha
    lda #0
    sta OAMADDR
    lda #>oam_buffer
    sta OAMDMA
    inc frame_count
    lda #0
    sta PPUSCROLL
    sta PPUSCROLL
    pla
    tay
    pla
    tax
    pla
    rti

irq:
    rti

; =============================================================================
; Data
; =============================================================================

palette_data:
    .byte BG_COLOUR, $11, $21, $31
    .byte BG_COLOUR, $13, $23, $33
    .byte BG_COLOUR, $19, $29, $39
    .byte BG_COLOUR, $16, $26, $36
    .byte BG_COLOUR, $30, $27, $17    ; Player
    .byte BG_COLOUR, $16, $26, $36    ; Enemies
    .byte BG_COLOUR, $1A, $2A, $3A    ; Items (green)
    .byte BG_COLOUR, $30, $27, $17

.segment "VECTORS"
    .word nmi
    .word reset
    .word irq

.segment "CHARS"
; Tile 0: Empty
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
; Tile 1: Border
.byte %11111111,%10000001,%10000001,%11111111,%11111111,%00010001,%00010001,%11111111
.byte %00000000,%01111110,%01111110,%00000000,%00000000,%11101110,%11101110,%00000000
; Tile 2: Floor
.byte %00000000,%00000000,%00000000,%00000000,%00000000,%00000000,%00000000,%10000001
.byte %00000000,%00000000,%00000000,%00000000,%00000000,%00000000,%00000000,%00000000
; Tile 3: Corner TL
.byte %11111111,%11000000,%10100000,%10010000,%10001000,%10000100,%10000010,%10000001
.byte %00000000,%00111111,%01011111,%01101111,%01110111,%01111011,%01111101,%01111110
; Tile 4: Corner TR
.byte %11111111,%00000011,%00000101,%00001001,%00010001,%00100001,%01000001,%10000001
.byte %00000000,%11111100,%11111010,%11110110,%11101110,%11011110,%10111110,%01111110
; Tile 5: Corner BL
.byte %10000001,%10000010,%10000100,%10001000,%10010000,%10100000,%11000000,%11111111
.byte %01111110,%01111101,%01111011,%01110111,%01101111,%01011111,%00111111,%00000000
; Tile 6: Corner BR
.byte %10000001,%01000001,%00100001,%00010001,%00001001,%00000101,%00000011,%11111111
.byte %01111110,%10111110,%11011110,%11101110,%11110110,%11111010,%11111100,%00000000
; Tile 7: Player (sprite)
.byte %00011000,%00011000,%00111100,%01111110,%11111111,%10111101,%00100100,%00100100
.byte %00000000,%00011000,%00011000,%00111100,%01000010,%01000010,%00011000,%00000000
; Tile 8: Enemy (sprite)
.byte %00011000,%00111100,%01111110,%11111111,%11111111,%01111110,%00111100,%00011000
.byte %00000000,%00011000,%00100100,%01000010,%01000010,%00100100,%00011000,%00000000
; Tile 9: Life icon (sprite)
.byte %00000000,%00100100,%01111110,%01111110,%00111100,%00011000,%00000000,%00000000
.byte %00000000,%00000000,%00000000,%00100100,%00011000,%00000000,%00000000,%00000000
; Tile 10: Item (data core - diamond)
.byte %00000000,%00011000,%00111100,%01111110,%01111110,%00111100,%00011000,%00000000
.byte %00000000,%00000000,%00011000,%00100100,%00100100,%00011000,%00000000,%00000000

.res 8192 - 176, $00

Build It

ca65 nexus.asm -o nexus.o
ld65 -C nes.cfg nexus.o -o nexus.nes

The game now has complete flow. Title, play, end, restart.

Next

Unit 16 brings Phase 1 to a close. Time to review what we’ve built.

What Changed

Unit 14 → Unit 15
+346-67
11 ; =============================================================================
2-; NEON NEXUS - Unit 14: Win Condition
2+; NEON NEXUS - Unit 15: Game States
33 ; =============================================================================
4-; Add a win condition - collecting all items completes the level.
4+; Add a state machine with title screen, playing, game over, and win states.
55 ; =============================================================================
66
77 PPUCTRL = $2000
...
2121 APU_PULSE1_LO = $4002
2222 APU_PULSE1_HI = $4003
2323
24+BTN_START = %00010000
2425 BTN_UP = %00001000
2526 BTN_DOWN = %00000100
2627 BTN_LEFT = %00000010
2728 BTN_RIGHT = %00000001
29+
30+; Game states
31+STATE_TITLE = 0
32+STATE_PLAYING = 1
33+STATE_GAMEOVER = 2
34+STATE_WIN = 3
2835
2936 PLAYER_START_X = 124
3037 PLAYER_START_Y = 116
...
6370 player_x: .res 1
6471 player_y: .res 1
6572 buttons: .res 1
73+buttons_prev: .res 1 ; Previous frame buttons (for edge detection)
6674 temp: .res 1
6775 row_counter: .res 1
6876 frame_count: .res 1
6977 lives: .res 1
7078 invuln_timer: .res 1
71-game_over: .res 1
79+game_state: .res 1 ; Current game state
7280 items_collected: .res 1
73-score_lo: .res 1 ; Score low byte
74-score_hi: .res 1 ; Score high byte (for scores > 255)
75-level_complete: .res 1 ; Non-zero when all items collected
81+score_lo: .res 1
82+score_hi: .res 1
7683
7784 enemy_x: .res NUM_ENEMIES
7885 enemy_y: .res NUM_ENEMIES
...
8188
8289 item_x: .res NUM_ITEMS
8390 item_y: .res NUM_ITEMS
84-item_active: .res NUM_ITEMS ; Non-zero if item exists
91+item_active: .res NUM_ITEMS
8592
8693 .segment "OAM"
8794 oam_buffer: .res 256
...
129136 jsr load_palette
130137 jsr draw_arena
131138 jsr set_attributes
132- jsr init_game
133139
140+ ; Hide all sprites initially
134141 lda #$FF
135142 ldx #0
136143 @hide_all:
137144 sta oam_buffer, x
138145 inx
139146 bne @hide_all
140-
141- jsr update_all_sprites
142147
143148 lda #0
144149 sta PPUSCROLL
145150 sta PPUSCROLL
146151
147152 ; Enable APU channels
148- lda #%00000001 ; Enable pulse 1
153+ lda #%00000001
149154 sta APUSTATUS
155+
156+ ; Start in title state
157+ lda #STATE_TITLE
158+ sta game_state
150159
151160 lda #%10000000
152161 sta PPUCTRL
153162 lda #%00011110
154163 sta PPUMASK
155164
165+; =============================================================================
166+; Main Loop - State Machine
167+; =============================================================================
156168 main_loop:
157- lda game_over
158- bne @game_over_loop
159- lda level_complete
160- bne @level_complete_loop
161169 jsr read_controller
170+
171+ lda game_state
172+ cmp #STATE_TITLE
173+ beq @title_state
174+ cmp #STATE_PLAYING
175+ beq @playing_state
176+ cmp #STATE_GAMEOVER
177+ beq @gameover_state
178+ cmp #STATE_WIN
179+ beq @win_state
180+ jmp main_loop
181+
182+@title_state:
183+ jsr handle_title
184+ jmp main_loop
185+
186+@playing_state:
187+ jsr handle_playing
188+ jmp main_loop
189+
190+@gameover_state:
191+ jsr handle_gameover
192+ jmp main_loop
193+
194+@win_state:
195+ jsr handle_win
196+ jmp main_loop
197+
198+; =============================================================================
199+; State Handlers
200+; =============================================================================
201+
202+handle_title:
203+ ; Show title screen sprites
204+ jsr show_title_sprites
205+
206+ ; Check for Start button press (new press only)
207+ lda buttons
208+ and #BTN_START
209+ beq @no_start
210+ lda buttons_prev
211+ and #BTN_START
212+ bne @no_start ; Already pressed last frame
213+
214+ ; Start pressed! Begin game
215+ jsr init_game
216+ lda #STATE_PLAYING
217+ sta game_state
218+ jsr play_collect_sound ; Start game sound
219+
220+@no_start:
221+ ; Store previous buttons
222+ lda buttons
223+ sta buttons_prev
224+ rts
225+
226+handle_playing:
162227 jsr move_player
163228 jsr move_enemies
164229 jsr check_enemy_collisions
165230 jsr check_item_collisions
166231 jsr check_win_condition
167232 jsr update_all_sprites
168- jmp main_loop
169233
170-@game_over_loop:
171- jmp @game_over_loop
234+ ; Store previous buttons
235+ lda buttons
236+ sta buttons_prev
237+ rts
172238
173-@level_complete_loop:
174- jsr update_all_sprites ; Keep showing win state
175- jmp @level_complete_loop
239+handle_gameover:
240+ ; Show game over display
241+ jsr show_gameover_sprites
242+
243+ ; Check for Start to restart
244+ lda buttons
245+ and #BTN_START
246+ beq @no_restart
247+ lda buttons_prev
248+ and #BTN_START
249+ bne @no_restart
250+
251+ ; Restart game
252+ lda #STATE_TITLE
253+ sta game_state
254+
255+@no_restart:
256+ lda buttons
257+ sta buttons_prev
258+ rts
259+
260+handle_win:
261+ ; Show win display
262+ jsr show_win_sprites
263+
264+ ; Check for Start to restart
265+ lda buttons
266+ and #BTN_START
267+ beq @no_restart
268+ lda buttons_prev
269+ and #BTN_START
270+ bne @no_restart
271+
272+ ; Back to title
273+ lda #STATE_TITLE
274+ sta game_state
275+
276+@no_restart:
277+ lda buttons
278+ sta buttons_prev
279+ rts
280+
281+; =============================================================================
282+; Display Routines
283+; =============================================================================
284+
285+show_title_sprites:
286+ ; Hide all sprites first
287+ lda #$FF
288+ ldx #0
289+@hide_loop:
290+ sta oam_buffer, x
291+ inx
292+ bne @hide_loop
293+
294+ ; Show player sprite in center as "logo"
295+ lda #100 ; Y position
296+ sta oam_buffer+0
297+ lda #SPRITE_PLAYER
298+ sta oam_buffer+1
299+ lda #0
300+ sta oam_buffer+2
301+ lda #124 ; X position
302+ sta oam_buffer+3
303+
304+ ; Show some items as decoration
305+ lda #100
306+ sta oam_buffer+4
307+ lda #SPRITE_ITEM
308+ sta oam_buffer+5
309+ lda #%00000010
310+ sta oam_buffer+6
311+ lda #100
312+ sta oam_buffer+7
313+
314+ lda #100
315+ sta oam_buffer+8
316+ lda #SPRITE_ITEM
317+ sta oam_buffer+9
318+ lda #%00000010
319+ sta oam_buffer+10
320+ lda #148
321+ sta oam_buffer+11
322+ rts
323+
324+show_gameover_sprites:
325+ ; Hide all sprites
326+ lda #$FF
327+ ldx #0
328+@hide_loop:
329+ sta oam_buffer, x
330+ inx
331+ bne @hide_loop
332+
333+ ; Show enemies as "defeat" indicator
334+ lda #100
335+ sta oam_buffer+0
336+ lda #SPRITE_ENEMY
337+ sta oam_buffer+1
338+ lda #%00000001
339+ sta oam_buffer+2
340+ lda #112
341+ sta oam_buffer+3
342+
343+ lda #100
344+ sta oam_buffer+4
345+ lda #SPRITE_ENEMY
346+ sta oam_buffer+5
347+ lda #%00000001
348+ sta oam_buffer+6
349+ lda #128
350+ sta oam_buffer+7
351+
352+ lda #100
353+ sta oam_buffer+8
354+ lda #SPRITE_ENEMY
355+ sta oam_buffer+9
356+ lda #%00000001
357+ sta oam_buffer+10
358+ lda #144
359+ sta oam_buffer+11
360+ rts
361+
362+show_win_sprites:
363+ ; Hide all sprites
364+ lda #$FF
365+ ldx #0
366+@hide_loop:
367+ sta oam_buffer, x
368+ inx
369+ bne @hide_loop
370+
371+ ; Show player and items as "victory" indicator
372+ lda #100
373+ sta oam_buffer+0
374+ lda #SPRITE_PLAYER
375+ sta oam_buffer+1
376+ lda #0
377+ sta oam_buffer+2
378+ lda #124
379+ sta oam_buffer+3
380+
381+ ; Items around player
382+ lda #92
383+ sta oam_buffer+4
384+ lda #SPRITE_ITEM
385+ sta oam_buffer+5
386+ lda #%00000010
387+ sta oam_buffer+6
388+ lda #116
389+ sta oam_buffer+7
390+
391+ lda #92
392+ sta oam_buffer+8
393+ lda #SPRITE_ITEM
394+ sta oam_buffer+9
395+ lda #%00000010
396+ sta oam_buffer+10
397+ lda #132
398+ sta oam_buffer+11
399+
400+ lda #108
401+ sta oam_buffer+12
402+ lda #SPRITE_ITEM
403+ sta oam_buffer+13
404+ lda #%00000010
405+ sta oam_buffer+14
406+ lda #116
407+ sta oam_buffer+15
408+
409+ lda #108
410+ sta oam_buffer+16
411+ lda #SPRITE_ITEM
412+ sta oam_buffer+17
413+ lda #%00000010
414+ sta oam_buffer+18
415+ lda #132
416+ sta oam_buffer+19
417+ rts
418+
419+; =============================================================================
420+; Game Initialisation
421+; =============================================================================
176422
177423 init_game:
178424 lda #STARTING_LIVES
179425 sta lives
180426 lda #0
181- sta game_over
182- sta level_complete
183427 sta invuln_timer
184428 sta items_collected
185429 sta score_lo
...
232476 rts
233477
234478 init_items:
235- ; Place 4 items around the arena
236479 lda #80
237480 sta item_x+0
238481 lda #64
...
261504 lda #1
262505 sta item_active+3
263506 rts
507+
508+; =============================================================================
509+; Collision Detection
510+; =============================================================================
264511
265512 check_enemy_collisions:
266513 lda invuln_timer
...
330577 cmp #COLLECT_DIST
331578 bcs @next_item
332579
333- ; Collected! Add 100 points
580+ ; Collected!
334581 lda #0
335582 sta item_active, x
336583 inc items_collected
337- ; Add 100 to score (100 = $64)
338584 lda score_lo
339585 clc
340586 adc #100
...
366612 rts
367613
368614 @game_over:
369- lda #1
370- sta game_over
615+ lda #STATE_GAMEOVER
616+ sta game_state
371617 rts
372618
373-; -----------------------------------------------------------------------------
619+; =============================================================================
620+; Win Condition
621+; =============================================================================
622+
623+check_win_condition:
624+ lda items_collected
625+ cmp #NUM_ITEMS
626+ bne @not_yet
627+ lda game_state
628+ cmp #STATE_WIN
629+ beq @not_yet ; Already won
630+ lda #STATE_WIN
631+ sta game_state
632+ jsr play_victory_sound
633+@not_yet:
634+ rts
635+
636+; =============================================================================
374637 ; Sound Effects
375-; -----------------------------------------------------------------------------
638+; =============================================================================
639+
376640 play_collect_sound:
377- ; High-pitched short beep
378- lda #%10011111 ; Duty 50%, length counter disabled, constant vol, vol 15
641+ lda #%10011111
379642 sta APU_PULSE1_CTRL
380643 lda #0
381644 sta APU_PULSE1_SWEEP
382- lda #$C4 ; Low byte of period (high pitch)
645+ lda #$C4
383646 sta APU_PULSE1_LO
384- lda #%00001000 ; Length counter load, high bits of period
647+ lda #%00001000
385648 sta APU_PULSE1_HI
386649 rts
387650
388651 play_death_sound:
389- ; Low-pitched descending sound
390- lda #%10011111 ; Duty 50%, constant vol, vol 15
652+ lda #%10011111
391653 sta APU_PULSE1_CTRL
392- lda #%10001111 ; Sweep enabled, down, fast
654+ lda #%10001111
393655 sta APU_PULSE1_SWEEP
394- lda #$00 ; Low byte of period (low pitch)
656+ lda #$00
395657 sta APU_PULSE1_LO
396- lda #%00001011 ; Length counter load, high bits
658+ lda #%00001011
397659 sta APU_PULSE1_HI
398660 rts
399661
400662 play_victory_sound:
401- ; Ascending triumphant sound
402- lda #%10011111 ; Duty 50%, constant vol, vol 15
663+ lda #%10011111
403664 sta APU_PULSE1_CTRL
404- lda #%10000111 ; Sweep enabled, up, fast
665+ lda #%10000111
405666 sta APU_PULSE1_SWEEP
406- lda #$FF ; Low byte of period (start low)
667+ lda #$FF
407668 sta APU_PULSE1_LO
408- lda #%00000011 ; Length counter load, high bits
669+ lda #%00000011
409670 sta APU_PULSE1_HI
410671 rts
411672
412-; -----------------------------------------------------------------------------
413-; Win Condition Check
414-; -----------------------------------------------------------------------------
415-check_win_condition:
416- lda items_collected
417- cmp #NUM_ITEMS ; Have we collected all items?
418- bne @not_yet
419- lda level_complete ; Already triggered?
420- bne @not_yet
421- lda #1
422- sta level_complete
423- jsr play_victory_sound
424-@not_yet:
425- rts
673+; =============================================================================
674+; Sprite Updates
675+; =============================================================================
426676
427677 update_all_sprites:
428678 jsr update_player_sprite
...
432682 rts
433683
434684 update_player_sprite:
435- lda game_over
436- beq @alive
437- lda #$FF
438- sta oam_buffer+0
439- rts
685+ lda game_state
686+ cmp #STATE_PLAYING
687+ bne @hide
440688
441-@alive:
442689 lda invuln_timer
443690 beq @show
444691 and #%00000100
445692 beq @show
693+
694+@hide:
446695 lda #$FF
447696 sta oam_buffer+0
448697 rts
...
481730
482731 update_item_sprites:
483732 ldx #0
484- ldy #20 ; OAM offset after enemies
733+ ldy #20
485734 @loop:
486735 lda item_active, x
487736 beq @hide_item
...
492741 lda #SPRITE_ITEM
493742 sta oam_buffer, y
494743 iny
495- lda #%00000010 ; Palette 2 (green)
744+ lda #%00000010
496745 sta oam_buffer, y
497746 iny
498747 lda item_x, x
...
515764 rts
516765
517766 update_lives_display:
518- ldy #36 ; OAM offset for lives
767+ ldy #36
519768 ldx lives
520769 beq @hide_all
521770
...
567816 @hide_rest:
568817 @hide_all:
569818 rts
819+
820+; =============================================================================
821+; Enemy Movement
822+; =============================================================================
570823
571824 move_enemies:
572825 ldx #0
...
617870 cpx #NUM_ENEMIES
618871 bne @enemy_loop
619872 rts
873+
874+; =============================================================================
875+; PPU Setup
876+; =============================================================================
620877
621878 load_palette:
622879 bit PPUSTATUS
...
7541011 dex
7551012 bne @attr_bottom
7561013 rts
1014+
1015+; =============================================================================
1016+; Input
1017+; =============================================================================
7571018
7581019 read_controller:
7591020 lda #1
...
8111072 sta player_x
8121073 @done:
8131074 rts
1075+
1076+; =============================================================================
1077+; NMI Handler
1078+; =============================================================================
8141079
8151080 nmi:
8161081 pha
...
8351100
8361101 irq:
8371102 rti
1103+
1104+; =============================================================================
1105+; Data
1106+; =============================================================================
8381107
8391108 palette_data:
8401109 .byte BG_COLOUR, $11, $21, $31
...
8521121 .word irq
8531122
8541123 .segment "CHARS"
1124+; Tile 0: Empty
8551125 .byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
1126+; Tile 1: Border
8561127 .byte %11111111,%10000001,%10000001,%11111111,%11111111,%00010001,%00010001,%11111111
8571128 .byte %00000000,%01111110,%01111110,%00000000,%00000000,%11101110,%11101110,%00000000
1129+; Tile 2: Floor
8581130 .byte %00000000,%00000000,%00000000,%00000000,%00000000,%00000000,%00000000,%10000001
8591131 .byte %00000000,%00000000,%00000000,%00000000,%00000000,%00000000,%00000000,%00000000
1132+; Tile 3: Corner TL
8601133 .byte %11111111,%11000000,%10100000,%10010000,%10001000,%10000100,%10000010,%10000001
8611134 .byte %00000000,%00111111,%01011111,%01101111,%01110111,%01111011,%01111101,%01111110
1135+; Tile 4: Corner TR
8621136 .byte %11111111,%00000011,%00000101,%00001001,%00010001,%00100001,%01000001,%10000001
8631137 .byte %00000000,%11111100,%11111010,%11110110,%11101110,%11011110,%10111110,%01111110
1138+; Tile 5: Corner BL
8641139 .byte %10000001,%10000010,%10000100,%10001000,%10010000,%10100000,%11000000,%11111111
8651140 .byte %01111110,%01111101,%01111011,%01110111,%01101111,%01011111,%00111111,%00000000
1141+; Tile 6: Corner BR
8661142 .byte %10000001,%01000001,%00100001,%00010001,%00001001,%00000101,%00000011,%11111111
8671143 .byte %01111110,%10111110,%11011110,%11101110,%11110110,%11111010,%11111100,%00000000
1144+; Tile 7: Player (sprite)
8681145 .byte %00011000,%00011000,%00111100,%01111110,%11111111,%10111101,%00100100,%00100100
8691146 .byte %00000000,%00011000,%00011000,%00111100,%01000010,%01000010,%00011000,%00000000
1147+; Tile 8: Enemy (sprite)
8701148 .byte %00011000,%00111100,%01111110,%11111111,%11111111,%01111110,%00111100,%00011000
8711149 .byte %00000000,%00011000,%00100100,%01000010,%01000010,%00100100,%00011000,%00000000
1150+; Tile 9: Life icon (sprite)
8721151 .byte %00000000,%00100100,%01111110,%01111110,%00111100,%00011000,%00000000,%00000000
8731152 .byte %00000000,%00000000,%00000000,%00100100,%00011000,%00000000,%00000000,%00000000
8741153 ; Tile 10: Item (data core - diamond)