Level 29: FINALE: Snake
Task
Write a complete Snake game in assembly!
Related Materials
- MatrixDisplay: matrix display
- Gamepad: gamepad
- LFSR: random number generator
Solution
Complete Snake game code (126 instructions, fits within the 128-instruction word-format limit):
Architecture
| Address | Purpose |
|---|---|
| RAM[0] | dir — direction (0=Up, 1=Down, 2=Left, 3=Right) |
| RAM[1] | snakeX — snake X coordinate (0–15) |
| RAM[2] | snakeY — snake Y coordinate (0–15) |
| RAM[3] | oldX — previous X (for clearing) |
| RAM[4] | oldY — previous Y (for clearing) |
| RAM[5] | gp — gamepad value |
| RAM[6] | constant 1 |
| RAM[7] | constant 2 |
| RAM[8] | constant 4 |
| RAM[9] | constant 8 |
| RAM[10] | constant 15 |
| RAM[11] | delay counter |
| RAM[12] | foodX — food X coordinate |
| RAM[13] | foodY — food Y coordinate |
I/O Ports
| Port | Device |
|---|---|
| 250 | LFSR — random number generator |
| 252 | MatrixDisplay X |
| 253 | MatrixDisplay Y |
| 254 | Gamepad (bit0=Up, bit1=Down, bit2=Left, bit3=Right) |
| 255 | MatrixDisplay Write (0=clear, 1=draw) |
Key Techniques
- LDA N vs ADD N:
LDA 0loads immediate value 0 into the accumulator, whileADD 0reads RAM[0] and adds it to the accumulator. SoLDA 0; ADD 1= snakeX (0 + RAM[1]). - STA chaining: After
LDA 8, accumulator holds 8.STA 1writes 8 to RAM[1], but the accumulator is not cleared. So a followingSTA 2writes the same 8 to RAM[2]. Saves instructions! - Bitmask via AND:
AND 6= Acc & RAM[6] = Acc & 1. Used to test individual gamepad bits. - Increment optimization:
LDA 1; ADD 1= 1 + snakeX = snakeX + 1 (saves one instruction vs.LDA 0; ADD 1; ADD 6). - Bounds check before moving: instead of checking coordinates after movement (tricky due to 8-bit unsigned wraparound), check the wall before moving: if X=15 and going right → Game Over.
Code
; ==========================================
; SNAKE — FINAL PROJECT
; ==========================================
; Controls: gamepad (port 254)
; Memory: RAM[0]=dir, RAM[1]=X, RAM[2]=Y
; RAM[3]=oldX, RAM[4]=oldY, RAM[5]=gp
; RAM[6]=1, RAM[7]=2, RAM[8]=4, RAM[9]=8
; RAM[10]=15, RAM[11]=delay
; RAM[12]=foodX, RAM[13]=foodY
init:
; Draw snake first
LDA 8
STA 252 ; MatrixX = 8
STA 1 ; snakeX = 8
LDA 8
STA 253 ; MatrixY = 8
STA 2 ; snakeY = 8
LDA 1
STA 255 ; light pixel (8,8)
STA 6 ; const 1 = 1
; Constants
LDA 2
STA 7 ; const 2
LDA 4
STA 8 ; const 4
LDA 8
STA 9 ; const 8
LDA 15
STA 10 ; const 15
LDA 3
STA 0 ; dir = Right
; Food
LDA 5
STA 12 ; foodX = 5
STA 13 ; foodY = 5
STA 252 ; MatrixX = 5
STA 253 ; MatrixY = 5
LDA 1
STA 255 ; light food pixel (5,5)
loop:
; 1. Save old position
LDA 0
ADD 1
STA 3 ; oldX = snakeX
LDA 0
ADD 2
STA 4 ; oldY = snakeY
; 2. Read gamepad
LDA 0
ADD 254
STA 5
JZ update_pos ; no press — keep direction
; Up (bit 0)
LDA 0
ADD 5
AND 6 ; gamepad & 1
JZ check_dn
LDA 0
STA 0 ; dir = Up
JMP update_pos
check_dn:
LDA 0
ADD 5
AND 7 ; gamepad & 2
JZ check_lt
LDA 1
STA 0 ; dir = Down
JMP update_pos
check_lt:
LDA 0
ADD 5
AND 8 ; gamepad & 4
JZ check_rt
LDA 2
STA 0 ; dir = Left
JMP update_pos
check_rt:
LDA 0
ADD 5
AND 9 ; gamepad & 8
JZ update_pos
LDA 3
STA 0 ; dir = Right
update_pos:
; 3. Move in current direction
LDA 0
ADD 0 ; Acc = dir
JZ move_up ; dir == 0?
LDA 0
ADD 0
SUB 6 ; dir - 1
JZ move_dn ; dir == 1?
LDA 0
ADD 0
SUB 7 ; dir - 2
JZ move_lt ; dir == 2?
; dir == 3 (Right)
LDA 0
ADD 1 ; Acc = snakeX
SUB 10 ; snakeX - 15
JZ dead ; right wall → Game Over
LDA 1
ADD 1 ; snakeX + 1
STA 1
JMP clear_old
move_up:
LDA 0
ADD 2 ; Acc = snakeY
JZ dead ; top wall → Game Over
SUB 6 ; snakeY - 1
STA 2
JMP clear_old
move_dn:
LDA 0
ADD 2 ; Acc = snakeY
SUB 10 ; snakeY - 15
JZ dead ; bottom wall → Game Over
LDA 1
ADD 2 ; snakeY + 1
STA 2
JMP clear_old
move_lt:
LDA 0
ADD 1 ; Acc = snakeX
JZ dead ; left wall → Game Over
SUB 6 ; snakeX - 1
STA 1
clear_old:
; 4. Clear old position
LDA 0
ADD 3
STA 252
LDA 0
ADD 4
STA 253
LDA 0
STA 255
draw_snake:
; 5. Draw snake
LDA 0
ADD 1
STA 252
LDA 0
ADD 2
STA 253
LDA 1
STA 255
delay:
; 6. Delay (3 iterations — ~0.3 sec at 10 Hz)
LDA 3
STA 11
dloop:
LDA 0
ADD 11
SUB 6
STA 11
JZ loop
JMP dloop
dead:
JMP dead ; Game Over — infinite loop
How It Works
- Initialization: load constants (1,2,4,8,15) into RAM[6..10], set initial snake position (8,8), food position (5,5), direction Right (3), draw food on display.
- Game loop:
- Save current position to oldX/oldY (for later clearing)
- Read gamepad (port 254). If a button is pressed, update direction via AND bitmasks
- Move snake 1 pixel in current direction, checking walls beforehand
- Clear old position (X→252, Y→253, 0→255)
- Draw snake at new position (X→252, Y→253, 1→255)
- Delay ~3 iterations for playable speed
- Game Over: on wall collision — infinite loop
JMP dead. CPU stops via HLT detection.
Limitations & Extensions
This version has static food (no respawn after being eaten). It fits in 126 instructions under the 128-instruction limit (word format: 2 ROM × 256 bytes, 2 bytes per instruction). Possible extensions:
- Food regeneration via LFSR: add
check_foodblock: compare snakeX/foodX and snakeY/foodY; on match — new value fromADD 250; AND 10. Requires ~26 more instructions — trim delay or constants to fit. - Reverse protection: prevent 180° turns (e.g., Up→Down). Adds ~18 instructions.
- Snake tail: store segment array in RAM, use index register (LDX/INX/STAX) for traversal.