Level 29: FINALE: Snake

Task

Write a complete Snake game in assembly!

Related Materials

Solution

Complete Snake game code (126 instructions, fits within the 128-instruction word-format limit):

Architecture

AddressPurpose
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

PortDevice
250LFSR — random number generator
252MatrixDisplay X
253MatrixDisplay Y
254Gamepad (bit0=Up, bit1=Down, bit2=Left, bit3=Right)
255MatrixDisplay Write (0=clear, 1=draw)

Key Techniques

  • LDA N vs ADD N: LDA 0 loads immediate value 0 into the accumulator, while ADD 0 reads RAM[0] and adds it to the accumulator. So LDA 0; ADD 1 = snakeX (0 + RAM[1]).
  • STA chaining: After LDA 8, accumulator holds 8. STA 1 writes 8 to RAM[1], but the accumulator is not cleared. So a following STA 2 writes 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

  1. 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.
  2. 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
  3. 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_food block: compare snakeX/foodX and snakeY/foodY; on match — new value from ADD 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.

Assembler help →