Уровень 29: ФИНАЛ: Змейка
Задание
Напишите полную игру «Змейка» на ассемблере!
Материалы по теме
- Указатели и индексные регистры (IX)
- Ветвления и циклы (JZ, JN, JMP)
- Справка по ассемблеру →
- MatrixDisplay — матричный дисплей
- Gamepad — геймпад
- LFSR — генератор случайных чисел
Решение
Полный код игры «Змейка» (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 еды |
Порты ввода-вывода
| Порт | Устройство |
|---|---|
| 250 | LFSR — генератор случайных чисел |
| 252 | MatrixDisplay X |
| 253 | MatrixDisplay Y |
| 254 | Gamepad (bit0=Up, bit1=Down, bit2=Left, bit3=Right) |
| 255 | MatrixDisplay 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,2,4,8,15) в RAM[6..10], устанавливаем начальные координаты змейки (8,8) и еды (5,5), направление Right (3), рисуем еду на дисплее.
- Игровой цикл:
- Сохраняем текущую позицию в oldX/oldY (для последующего стирания)
- Читаем геймпад (порт 254). Если нажата кнопка — обновляем направление через битовые маски AND
- Движем змейку на 1 пиксель в текущем направлении, предварительно проверив стену
- Стираем старую позицию (X→252, Y→253, 0→255)
- Рисуем змейку в новой позиции (X→252, Y→253, 1→255)
- Задержка ~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) для обхода.