Уровень 29: ФИНАЛ: Змейка

Задание

Напишите полную игру «Змейка» на ассемблере!

Материалы по теме

Решение

Полный код игры «Змейка» (126 инструкций, умещается в 128-инструкционный лимит word-формата):

Архитектура решения

АдресНазначение
RAM[0]dir — направление (0=Up, 1=Down, 2=Left, 3=Right)
RAM[1]snakeX — координата X змейки (0–15)
RAM[2]snakeY — координата Y змейки (0–15)
RAM[3]oldX — предыдущая X (для стирания)
RAM[4]oldY — предыдущая Y (для стирания)
RAM[5]gp — значение геймпада
RAM[6]константа 1
RAM[7]константа 2
RAM[8]константа 4
RAM[9]константа 8
RAM[10]константа 15
RAM[11]счётчик задержки
RAM[12]foodX — координата X еды
RAM[13]foodY — координата Y еды

Порты ввода-вывода

ПортУстройство
250LFSR — генератор случайных чисел
252MatrixDisplay X
253MatrixDisplay Y
254Gamepad (bit0=Up, bit1=Down, bit2=Left, bit3=Right)
255MatrixDisplay Write (0=стереть, 1=нарисовать)

Ключевые приёмы

  • LDA N vs ADD N: LDA 0 загружает непосредственное значение 0 в аккумулятор, а ADD 0 читает RAM[0] и прибавляет к аккумулятору. Поэтому LDA 0; ADD 1 = snakeX (0 + RAM[1]).
  • Цепочка STA: После LDA 8 аккумулятор содержит 8. STA 1 записывает 8 в RAM[1], но аккумулятор не сбрасывается. Поэтому STA 2 следом запишет те же 8 в RAM[2]. Это экономит инструкции!
  • Битовые маски через AND: AND 6 = Acc & RAM[6] = Acc & 1. Так проверяем отдельные биты геймпада.
  • Оптимизация инкремента: LDA 1; ADD 1 = 1 + snakeX = snakeX + 1 (экономия одной инструкции по сравнению с LDA 0; ADD 1; ADD 6).
  • Проверка границ до движения: вместо проверки координат после движения (сложно из-за 8-битного беззнакового переполнения), проверяем стену перед движением: если X=15 и идём вправо → Game Over.

Код

; ==========================================
; ЗМЕЙКА — ФИНАЛЬНЫЙ ПРОЕКТ
; ==========================================
; Управление: геймпад (порт 254)
; Память: 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:
  ; Рисуем змейку в первую очередь
  LDA 8
  STA 252            ; MatrixX = 8
  STA 1              ; snakeX = 8
  LDA 8
  STA 253            ; MatrixY = 8
  STA 2              ; snakeY = 8
  LDA 1
  STA 255            ; зажечь пиксель (8,8)
  STA 6              ; const 1 = 1

  ; Константы
  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

  ; Еда
  LDA 5
  STA 12             ; foodX = 5
  STA 13             ; foodY = 5
  STA 252            ; MatrixX = 5
  STA 253            ; MatrixY = 5
  LDA 1
  STA 255            ; зажечь пиксель еды (5,5)

loop:
  ; 1. Сохранить старую позицию
  LDA 0
  ADD 1
  STA 3              ; oldX = snakeX
  LDA 0
  ADD 2
  STA 4              ; oldY = snakeY

  ; 2. Прочитать геймпад
  LDA 0
  ADD 254
  STA 5
  JZ update_pos      ; нет нажатий — не менять направление

  ; 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. Движение по направлению
  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            ; стена справа → Game Over
  LDA 1
  ADD 1              ; snakeX + 1
  STA 1
  JMP clear_old

move_up:
  LDA 0
  ADD 2              ; Acc = snakeY
  JZ dead            ; стена сверху → 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            ; стена снизу → Game Over
  LDA 1
  ADD 2              ; snakeY + 1
  STA 2
  JMP clear_old

move_lt:
  LDA 0
  ADD 1              ; Acc = snakeX
  JZ dead            ; стена слева → Game Over
  SUB 6              ; snakeX - 1
  STA 1

clear_old:
  ; 4. Стереть старую позицию
  LDA 0
  ADD 3
  STA 252
  LDA 0
  ADD 4
  STA 253
  LDA 0
  STA 255

draw_snake:
  ; 5. Нарисовать змейку
  LDA 0
  ADD 1
  STA 252
  LDA 0
  ADD 2
  STA 253
  LDA 1
  STA 255

delay:
  ; 6. Задержка (3 итерации — ~0.3 сек на 10 Hz)
  LDA 3
  STA 11
dloop:
  LDA 0
  ADD 11
  SUB 6
  STA 11
  JZ loop
  JMP dloop

dead:
  JMP dead            ; Game Over — бесконечный цикл

Как это работает

  1. Инициализация: загружаем константы (1,2,4,8,15) в RAM[6..10], устанавливаем начальные координаты змейки (8,8) и еды (5,5), направление Right (3), рисуем еду на дисплее.
  2. Игровой цикл:
    • Сохраняем текущую позицию в oldX/oldY (для последующего стирания)
    • Читаем геймпад (порт 254). Если нажата кнопка — обновляем направление через битовые маски AND
    • Движем змейку на 1 пиксель в текущем направлении, предварительно проверив стену
    • Стираем старую позицию (X→252, Y→253, 0→255)
    • Рисуем змейку в новой позиции (X→252, Y→253, 1→255)
    • Задержка ~3 итерации для играбельной скорости
  3. Game Over: при столкновении со стеной — бесконечный цикл JMP dead. Процессор останавливается по HLT-детекции.

Ограничения и развитие

Данная версия содержит статичную еду (не возрождается после съедания). Это уложилось в 126 инструкций при лимите 128 (word-формат: 2 ROM по 256 байт, инструкции по 2 байта). Возможные расширения:

  • Регенерация еды через LFSR: добавить блок check_food: сравнить snakeX/foodX и snakeY/foodY; при совпадении — новое значение из ADD 250; AND 10. Требует ещё ~26 инструкций — нужно ужаться, убрав задержку или константы.
  • Защита от реверса: запретить разворот на 180° (например, с Up на Down). Добавляет ~18 инструкций.
  • Хвост змейки: хранить массив сегментов в RAM, использовать индексный регистр (LDX/INX/STAX) для обхода.