Chapter 14: Complete Example - A Bouncing Ball Game
In this final chapter, we’ll put everything together by examining a complete game that demonstrates all assembler features.
The Bouncing Ball Demo
This program displays a white ball that bounces around the screen, demonstrating:
- Hardware initialization
- Game loop structure
- Sprite movement and collision
- Video memory access
Memory Map
; Memory-mapped I/O registers
.equ VID_PTR 0xFD ; Video page pointer (page number, not high byte)
.equ RANDOM 0xFE ; Random number generator
.equ INPUT 0xFF ; Input register
; VRAM location
.equ VRAM 0x1000 ; Start of video RAM
.equ VID_PAGE 0x01 ; Video page number (0x1000 >> 12)
; Screen dimensions
.equ WIDTH 64
.equ HEIGHT 64
; Zero page variables
.equ BALL_X 0x00 ; Ball X position
.equ BALL_Y 0x01 ; Ball Y position
.equ VEL_X 0x02 ; X velocity (signed)
.equ VEL_Y 0x03 ; Y velocity (signed)
.equ OLD_X 0x04 ; Previous X position
.equ OLD_Y 0x05 ; Previous Y position
.equ TEMP 0x06 ; Temporary variable
Using .equ for constants makes the code readable and maintainable.
Initialization
.org 0x8000
reset:
; Set video page to VRAM (page 1 = 0x1000-0x1FFF)
lda #VID_PAGE
sta VID_PTR
; Initialize ball position to center
lda #32
sta BALL_X
sta BALL_Y
; Initialize velocity (moving down-right)
lda #1
sta VEL_X
sta VEL_Y
; Clear the screen
jsr clear_screen
The VID_PTR register takes a page number (0-15), where each page is 4KB. Page 1 corresponds to addresses 0x1000-0x1FFF.
The Main Loop
The main loop is driven by the IRQ (VBLANK):
main_loop:
; Save old position for erasing
lda BALL_X
sta OLD_X
lda BALL_Y
sta OLD_Y
; Update ball position
jsr update_ball
; Erase old ball
ldx OLD_X
ldy OLD_Y
lda #0 ; black
jsr draw_pixel
; Draw new ball
ldx BALL_X
ldy BALL_Y
lda #1 ; white
jsr draw_pixel
; Wait for next frame
rti
The rti (return from interrupt) waits until the next VBLANK.
Ball Update with Bouncing
update_ball:
; Update X position
lda BALL_X
clc
adc VEL_X
sta BALL_X
; Check X bounds
cmp #WIDTH - 1
bcs .bounce_x
cmp #0
beq .bounce_x
jmp .check_y
.bounce_x:
; Reverse X velocity: VEL_X = 0 - VEL_X
lda #0
sec
sbc VEL_X
sta VEL_X
; Clamp X position
lda BALL_X
cmp #WIDTH
bcc .clamp_x_done
lda #WIDTH - 2
sta BALL_X
.clamp_x_done:
lda BALL_X
bne .check_y
lda #1
sta BALL_X
.check_y:
; Similar logic for Y...
rts
This demonstrates:
- Local labels (
.bounce_x,.check_y) - Expression in immediate (
#WIDTH - 1) - Conditional branching
Pixel Drawing
; Draw a pixel at (X, Y) with color in A
; X = column (0-63)
; Y = row (0-63)
; A = color
draw_pixel:
sta TEMP ; Save color
; Calculate VRAM offset: Y * 64 + X
tya ; A = Y
asl a ; A = Y * 2
asl a ; A = Y * 4
asl a ; A = Y * 8
asl a ; A = Y * 16
asl a ; A = Y * 32
asl a ; A = Y * 64
; Add X
stx TEMP + 1 ; Save X
clc
adc TEMP + 1 ; A = Y * 64 + X
; Store to VRAM
tax
lda TEMP ; Get color back
sta VRAM,x ; Write to VRAM
rts
Screen Clearing
clear_screen:
ldx #0
lda #0
.loop:
sta VRAM,x
sta VRAM + 0x100,x
sta VRAM + 0x200,x
sta VRAM + 0x300,x
; ... (more pages)
inx
bne .loop
rts
Using expressions like VRAM + 0x100 makes the code clearer.
Interrupt Vectors
; Set up reset and IRQ vectors
.org 0xFFFC
.dw reset ; Reset vector
.dw main_loop ; IRQ vector (VBLANK)
The .dw directive writes 16-bit addresses in little-endian format.
Building and Running
# Assemble the game
cargo run -p byte_asm -- bouncing_ball.s -o game.bin
# Run in emulator
cargo run -p byte_emu -- game.bin
With verbose output:
$ cargo run -p byte_asm -- -v bouncing_ball.s -o game.bin
Assembling: bouncing_ball.s
Parsed 85 statements
Generated 312 bytes
Defined 15 symbols
Wrote: game.bin
Features Demonstrated
This example uses all major assembler features:
| Feature | Example |
|---|---|
.org | .org 0x8000 |
.equ | .equ WIDTH 64 |
.db | (could add strings) |
.dw | .dw reset |
| Labels | reset:, main_loop: |
| Local labels | .bounce_x:, .loop: |
| Immediate | lda #32 |
| Zero Page | sta BALL_X |
| Absolute | sta VRAM,x |
| Indexed | sta VRAM + 0x100,x |
| Expressions | #WIDTH - 1, VRAM + 0x100 |
| Branches | bne .loop, bcs .bounce_x |
| Subroutines | jsr update_ball, rts |
Extending the Example
Ideas for additions:
- User input to control ball direction
- Multiple balls
- Score counter
- Sound effects
- Paddle for a Pong-like game
Complete Source
See byte_asm/examples/bouncing_ball.s for the full source code.
Summary
In this tutorial, we built a complete 6502 assembler from scratch:
- Scanner: Tokenizes source into meaningful chunks
- Parser: Builds an AST representing program structure
- Symbol Table: Tracks labels and constants
- Two-Pass Assembler: Resolves forward references
- Code Generator: Emits correct machine code
- Expression Evaluator: Computes values at assembly time
- Directive Handlers: Processes .org, .db, .dw, .equ
- Error Handling: Provides helpful error messages
- CLI: User-friendly command-line interface
- Testing: Verifies correctness
The result is a fully functional assembler that can build real programs for the Byte fantasy console.
What’s Next?
- Macros: Add
.macroand.endmacrofor code reuse - Conditional Assembly: Add
.if,.else,.endif - Include Path: Search multiple directories for includes
- Listing Files: Generate human-readable assembly listings
- Debug Symbols: Output symbol tables for debuggers
Happy assembling!