Skip to content

Repository files navigation

SPACE — реверс-инжиниринг игры для Радио-86РК

Дизассемблированный исходник игры SPACE (V1.1) для Радио-86РК — посимвольно-точная реконструкция оригинального кассетного образа tape/SPACE.RK в виде аннотированного i8080 ассемблера.

Автор ВЕЛНОВ П.А. из г. Отрадный Куйбышевской области (середина-конец 1980-х). На титульном экране автор оставил свой домашний адрес и просит писать ему бумажные письма — артефакт эпохи.

Статус

  • Только для образовательных целей.
  • Все права на оригинальный код принадлежат автору. Этот репозиторий — не публикация и не редистрибуция игры, а результат разбора уже известного публичного кассетного образа в учебных целях.
  • Бинарный образ tape/SPACE.RK сохранён без изменений; любые правки производятся только над SPACE.asm, после чего just ci гарантирует побайтовую идентичность собранного файла оригиналу.

Поиграть онлайн

Открыть в эмуляторе РК86 в браузере:

https://rk86.ru/index.html?run=SPACE.RK

Управление по сюжету: «ПУСК РАКЕТЫ — .»; секретная клавиша 9 запускает бонусную «лестницу» (см. ниже).

Скриншоты

Заставка

Игровое поле

Геймплей

Используемый навык

Реверс выполнен с помощью навыка rk86-reversal для Claude — он описывает трёхпроходный процесс разбора, идиомы i8080-кода под РК86 и содержит шаблоны (disasm.py, tobin.py, Justfile) под полную побайтовую round-trip-сборку.

Сборка и проверка

just ci # ассемблировать SPACE.asm и сравнить с tape/SPACE.RK# пустой diff = всё в порядке
just initial # пересобрать SPACE.bin из кассетного образа (редко)
just disasm # перегенерировать SPACE.asm (ВНИМАНИЕ: затрёт правки)
just clean # удалить промежуточные артефакты

CI на GitHub Actions (.github/workflows/build.yaml) запускает just ci на каждый push / PR — пустой diff обязателен.

Зависимости:


Находки и устройство кода

Кассетный формат

ПолеЗначение
org0100h
Длина3392 байта по заголовку, 3394 в файле
Конецзаголовок: 0E3F, фактически: 0E41

Образ записан с известным дефектом ROM-кодировщика: поле end_addr в заголовке на 2 байта меньше реального хвоста полезной нагрузки (между концом данных и трейлером лежат два лишних нулевых байта). В Justfile это лечится передачей --trailer-padding 2 ассемблеру и обрезанием последних двух байт SPACE.asm так, чтобы вычисленный end_addr совпал с оригинальным 0E3F.

См. также playbook/common-pitfalls.md в rk86-reversal — случаи N=2 и N=1 встречаются «в дикой природе».

Карта памяти

0100h .. 0188h init: заставка → флип-анимация → start_game
0189h .. 018Dh мёртвая зона (часть jmp + 5 байт)
018Eh .. 01AEh flip_table — 16 пар (key,value) + 00h-стоп
01AFh .. 05B3h intro_screen — анимированный сплеш
05B4h .. 05F6h start_game / init_game_screen / fill_row
05F7h .. 0653h hud_panel — рамка игрового поля
0654h .. 0A21h игровая логика (start_round → game_over)
0A22h .. 0A33h переменные (BSS-перезаписываемые)
0A34h .. 0BFAh 7 спрайтов по 65 байт (шаг 0x41)
0BFBh .. 0CAFh msg_a / msg_b — анимация ракеты
0CB0h .. 0D23h msg_c — следующая стадия анимации
0D24h .. 0D6Ch hud_layout — SCORE / SHIPS / ROCKETS / ATTEMPTS
0D6Dh .. 0D78h end_msg — «= E N D =»
0D79h .. 0D93h priz_game — «P R I Z G A M E»
0D94h .. 0E3Fh title_screen — титул с адресом автора
0E40h .. active_sprite — RAM-буфер на 66 байт
(за пределами файла; копируется sprite_N → сюда)

Переменные

Все переменные 0A22h..0A33h — BSS-style: поле всегда записывается через sta/shldдо первого чтения, поэтому байты в файле — мусор от компиляции, а не значения по умолчанию. Объявлены поимённо в vars блоке для читаемости:

АдресИмяНазначение
0A22hrocket_pos(word) текущий адрес ракеты в видеопамяти
0A24hframe_tickсчётчик кадров анимации (порог 1Bh)
0A25hrng_seed(word) состояние LFSR для rng16
0A27hwave_stepиндекс волны (0..2), задаёт строку
0A28hvar_28gap, не используется напрямую
0A29hcheat_pos(word) видеоадрес «лестницы» бонуса
0A2Bhcheat_flag1 = нажата клавиша бонуса (19h)
0A2Chbonus_done1 = «лестница» дошла до верха
0A2Dhactor_state(word) аккумулятор внутри proc_actor
0A2Fhside_flagбит rng — выбор лево/право
0A30hlast_actor_dпредыдущий D в proc_actor
0A31hsprite2_flag3 в ветке sprite_2, иначе 0
0A32hmode_flag0 = обычный режим, 1 = после бонусного приза
0A33hattempts_digitзеркало правой цифры HUD-счётчика ATTEMPTS

Подпрограммы

rng16 — 16-битный LFSR

rng16: lhld rng_seed mvi c,10h ; 16 сдвигов@loop:mov a, h dad h ; HL <<= 1 ani 60h ; отводы 14 и 13jpe @skip inx h ; обратная связь по нечётной чётности@skip: dcr cjnz @loopshld rng_seedret

Классический «параллельный LFSR за один проход»: dad h сдвигает влево, отводы берутся через ani 60h + jpe (чётность). Семя 0A25h персистится — игра становится детерминированной, если поведение игрока повторяется.

gotoxy — обёртка над ROM-вызовом

ROM-функция 0FD27h ожидает B,C начиная с 1; обёртка 0888Eh инкрементит оба регистра, затем восстанавливает их через push b / pop b. Это позволяет вызывающему коду работать в нуль-индексных координатах.

fill_row — заливка рамки

HL=видеоадрес, D=символ, BC=шаг, E=количество. Используется четырьмя вызовами в init_game_screen для рисования рамки игрового поля — горизонтальные и вертикальные линии.

dec_digit_chain / inc_digit_chain — десятичные счётчики

Изящный приём для счётчиков на экране: цифры хранятся в ASCII прямо в видеопамяти. dec_digit_chain декрементирует младшую цифру; при переходе через '0' - 1 = '/' (0x2F) цифра заворачивается в '9', а перенос распространяется влево через dcx h и рекурсивный цикл. inc_digit_chain — зеркальная операция через '9' + 1 = ':' (0x3A)'0'.

proc_actor — главная диспетчерская

Вызывается дважды за цикл: один раз с D=03h (быстрая ветка), один раз с D=значением шага. Сохраняет состояние между вызовами в actor_state, при изменении D сбрасывается. Внутри:

  • если cheat_flag=1 → шагнуть бонусную «лестницу» (step_cheat_ladder);
  • иначе опросить клавиатуру; если нажата 19h (клавиша «9») — взвести cheat_flag и установить cheat_pos на стартовую позицию video_memory + 21*78 + 40.

step_cheat_ladder / award_bonus — пасхальное яйцо

Спрятанная клавиша 9 (код 19h) включает «лестницу»: каждую итерацию proc_actor курсор-маркер 0Bh поднимается на одну строку вверх (HL += -78, см. row_up). Если по дороге встречается RK86-псевдографика (01h..17h) — вызывается award_bonus: счётчик призов в правом верхнем углу инкрементится; на пятом призе игра переходит в mode_flag=1 (жёсткий режим — выводится «P R I Z G A M E» и стартовые цифры SHIPS / ROCKETS принудительно становятся '2').

lose_life / game_over

Анимация «взрыва» — курсор пробегает строку B=14h..0 и обратно, после чего декрементится 3-цифровой счётчик ATTEMPTS через dec_digit_chain. Если все три цифры — '0', проваливается в game_over: печатается «= E N D =», программа возвращается в wait_intro_screen.

Знаковые константы

Отрицательные дельты для dad b над видеоуказателем:

КонстантаИмяЗначениеЭффект
0FFB2hrow_up-78подняться на одну строку вверх
0FFCEhrow_up_short-50вариант для side_flag==1

Адреса видеопамяти

Все обращения к ВРАМ переписаны через выражение video_memory + y*78 + x, где video_memory equ 76D0h и 78 — стандартный шаг строки РК86. Это превращает магические 7E79h в читаемое video_memory + 25*78 + 11 без потери ни одного байта.

Спрайты и их особенность

Семь блоков по 65 байт начиная с 0A34h (шаг 0x41). Циклы копирования читают 66 байт (cpi 42h), захватывая первый байт следующего спрайта — это закладка автора, в коде не дефект: последний байт спрайта при отрисовке игнорируется.

Активный спрайт копируется в active_sprite (0E40h, BSS) и выводится puts-ом — то есть 00h-байт в спрайте служит терминатором, а ESC-Y-последовательности задают позиционирование относительно курсора.

Анимация заставки (intro flip)

Самый красивый трюк программы. После puts(intro_screen) рамка заполнена RK86-псевдографикой. Затем:

  1. Цикл считывает каждый байт из видеопамяти начиная с video_memory + 3*78 + 8 (верх-лево окна 64×25);
  2. Прогоняет через flip_table — таблицу пар (ключ, значение) с 00h-стопом и значением по умолчанию 17h;
  3. Записывает результат обратно в ту же ячейку.

Таблица отображает квадрантную графику в её «зеркальное» представление (вертикальный флип): 01↔16, 02↔15, 03↔14, 04↔13 и т.п. Плюс 17↔20 (заполненный квадрант ↔ пробел), что и даёт мерцающий «дыхательный» эффект на буквах SPACE.

Цикл прерывается опросом scan_kbd — любая клавиша вызывает jmp start_game.

Кодировка строк

ВРАМ-байты 60h..7Fh отрисовываются как кириллические заглавные буквы (KOI-7Н). Поэтому в исходнике строки выглядят как прописная латиница, например:

db'g. otradnyj , ul. otradnaq 17-16' ; Г. ОТРАДНЫЙ , УЛ. ОТРАДНАЯ 17-16db'kujby{ewskaq obl.,' ; КУЙБЫШЕВСКАЯ ОБЛ.,db' pi{ite ! ! !' ; ПИШИТЕ ! ! !

{Ш, }Щ, ~Ч, |Э и т.д. — стандартное соответствие KOI-7Н (см. references/video.md в навыке).

ESC-Y позиционирование

Управляющая последовательность 1Bh 59h ROW COL, где ROW и COL — ASCII-сдвинутые координаты (значение - 0x20). В дампе строк она выделена в отдельные db-чанки, например:

db1Bh,59h,21h,24h ; ESC-Y row= 1 col= 4

Локальные метки

В чистых подпрограммах (rng16, proc_actor, fill_row, step_cheat_ladder, award_bonus, lose_life, game_over, dec_digit_chain, inc_digit_chain, wait_intro_screen) внутренние метки переписаны как @name — синтаксис локальных меток asm8080, область видимости — до следующего глобального лейбла. Это убрало шум loc_XXXX без влияния на сборку.

Большой монолит start_round → next_rocket → rocket_step оставлен на глобальных метках: эти три точки взаимопереходимы (jmp через границу), поэтому единый @-скоуп для них невозможен.

Лицензия и оговорка

Это учебный материал. Все права на оригинальную игру SPACE принадлежат её автору ВЕЛНОВУ П.А. Если вы — автор или правообладатель и считаете, что этот разбор нужно изменить или удалить, откройте issue или напишите.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages