ПакМен: портирую игру из детства на JavaScript

В детстве на ПК МК-88 с ОС Альфа-дос была пятидюймовая дискета с играми. Одна из игр — ПакМен, сделанный, судя по стартовому экрану, где-то в Киеве в 1990 году. Не оригинальный Pac-Man, а некий советский клон - с «кровавыми жуками» вместо призраков и своим характером. Решил её портировать в браузер.

Бинарник нашёлся — PACKMAN.EXE, 42 килобайта. MZ DOS executable, запускается в DOSBox.

Проблема в том, что исходников нет. Только бинарь.

Дизассемблирование

Для декомпиляции использовал Reko Decompiler. Он выдал:

  • 6944-строчный листинг на x86 ASM
  • приблизительный декомпилированный C

C-вариант Reko-а читается удобнее, но местами врёт — переменные без имён, логика угадана неточно. Поэтому правило: если ASM и C расходятся, верим ASM.

Дальше — ручной разбор ключевых функций: инициализация уровней, движение сущностей, AI врагов, обработка клавиатуры, коллизии, подсчёт очков.

BGI и CGA

Игра написана на Turbo C и использует BGI (Borland Graphics Interface) — стандартную графическую библиотеку для DOS-программ от Borland. В бинаре прямо прописана строка "CGA Device Driver 2.00 — Mar 21 1988" — встроенный BGI-драйвер для CGA.

CGA-режим: 320×200 пикселей, 4 цвета из фиксированной палитры. Никакого antialiasing, никаких полутонов — чистый пиксель.

BGI предоставлял набор функций для рисования:

  • outtextxy — вывод текста в нужной точке
  • bar — закрашенный прямоугольник
  • putimage — спрайт из памяти
  • settextstyle — выбор шрифта: Gothic (#4) для заголовка, Triplex (#1) для текста меню
  • setcolor, setfillstyle — управление цветом и заливкой

Спрайты в игре — 2bpp CGA-формат: каждый байт хранит 4 пикселя, по 2 бита на пиксель. Каждый спрайт — 16×14 px, 56 байт. Всего 29 спрайтов: стены лабиринта, точки, кадры анимации ПакМена и врагов.

Уровень 2

Что внутри

Вытащил из бинарника всё что можно, зная формулу смещения: file_offset = DS_offset + 0x400.

Интересные детали, найденные в ASM:

Количество жизней — вычисляется как 0x23 / 7 = 5.

Скорость — хранится инвертированно: нажатие 0 даёт внутреннее значение 9 (медленно), нажатие 9 — 0 (быстро). Формула: speed = '9' - key. Отображаемое значение 9 - speed.

Движение асимметрично: горизонтально 4 пикселя за шаг, вертикально 2. Это не баг — так в ASM.

AI врагов: при совпадении строки или столбца с ПакМеном — преследование, иначе — следование предопределённому паттерну из 16 направлений. Три разных паттерна, назначаются случайно при старте уровня.

Нет режима испуга. Никакого. Столкновение с врагом — смерть.

«Победа !» показывается только после прохождения всех 15 уровней. Между уровнями игра стартует следующий.

Уровень 13 — это отдельная история.

Формат уровней

Данные уровней хранятся начиная с DS:0x6FFC в исполняемом файле. Каждый уровень — 480 байт: 20 столбцов × 12 строк × 2 байта на тайл. 13-й уровень (индекс 12) - по смещению DS:0x867C (файловое 0x8A7C).

Тайлы стен лабиринта:

ID Символ Значение
0x02 угол: верхний левый
0x07 угол: верхний правый
0x08 угол: нижний правый
0x09 угол: нижний левый
0x0A вертикальная стена
0x0B горизонтальная стена
0x00 пустое / проходимое
0x01 · точка (2 очка)

Карта 13-го уровня из бинарника:

1
2
3
4
5
6
7
8
9
10
11
12
╭══════════════════╮     строка 0
│··················│ строка 1
╰╮·╭╮·╭╮·╭╮·╭╮·╭╮·╭╯ строка 2
│·╰╯·╰╯·╰╯·╰╯·╰╯·╰╮ строка 3 ← col 0 = 0x00 (баг!)
╭╯·················│ строка 4
│·╭═╮·╭╮·╭╮·╭╮·╭╮·╭╯ строка 5
│ᗧ╰═╯·╰╯·╰╯·╰╯·╰╯·╰╮ строка 6
╰╮·················│ строка 7
│·╭╮·╭╮·╭╮·╭╮·╭╮·╭╯ строка 8 ← col 0 = 0x00 (баг!)
╭╯·╰╯·╰╯·╰╯·╰╯·╰╯·╰╮ строка 9
│··················│ строка 10
╰══════════════════╯ строка 11

Уровень 13

В детстве я дошёл до 13-го уровня один-единственный раз. И обнаружил, что он непроходим — ПакМен заперт в маленьком участке лабиринта, где всего два яблочка. Жуки бегают снаружи, туда не попасть.

Игра и так довольно сложная, а без power-up и сохранений и упереться в баг - втройне обидно.

Одной из целей этого порта было проверить: правда ли 13-й уровень сломан, или я что-то не так помню? Ответ нашёлся в данных.

Уровень 13 - ПакМен заперт

ПакМен выходит слева и сразу упирается в стену. Уйти можно только на одну клетку вверх — а дальше ни наискосок, ни вниз. Визуально видно: все секции лабиринта — квадраты 2×2, а та, в которую упирается ПакМен, — прямоугольник.

Когда я привёл этот прямоугольник к размеру 2×2, замысел авторов стал понятен - уровень должен был быть полностью симметричным.

Баг, скорее всего, - ошибка при ручном вводе карты уровня. В экстракторе я его исправил: восстановил угловые тайлы в левой стене и открыл проходы между горизонтальными полосами лабиринта, восстановив симметрию с правой стороной. Уровень стал проходимым.

Уровень 13 — после исправления

Теперь можно пройти все уровни, с первого и до последнего. Делать этого, конечно, не буду - все-таки игра довольно сложная, но с power-up’сами - вполне.

Порт на JavaScript

Cборка не требуется - только статика - js-файлы и index.html. Можно открываеть в браузере.

1
2
3
4
5
data.js      — уровни, спрайты, палитры (генерируется из EXE)
input.js — состояние клавиатуры
sound.js — эмуляция PC-спикера через Web Audio API
renderer.js — 2bpp декодер спрайтов, Canvas 2D
game.js — стейт-машина, движение, AI, коллизии

Спрайты декодируются при старте из сырых байт в ImageData — один раз на палитру. Текст меню в оригинале закодирован в CP866, вытащил и записал в UTF-8.

Звуки — квадратные волны через OscillatorNode, приближение к PC-спикеру.

Итого

Стартовый экран

Многое в этом проекте я разбирал сам — вручную листал листинг от Reko, сверял ASM с декомпилированным C, угадывал намерения автора и подолгу гонял игру в DOSBox, чтобы понять, как именно та или иная функция должна себя вести. Но без LLM такое восстановление по бинарнику было бы заметно тяжелее: модель помогала быстро формулировать гипотезы о незнакомых конструкциях BGI, расшифровывать неочевидные участки ассемблера и подсказывать, где искать формат данных, когда зацепиться было не за что. Всё равно занятие непростое — много раз приходилось возвращаться к одним и тем же фрагментам, перепроверять найденное в браузере и переписывать уже работающие куски, когда выяснялось, что в оригинале логика хитрее, чем казалось на первый взгляд.

Зато в конце в браузере запускается то самое, из детства. Кровавые жуки бегают по лабиринту, ПакМен ест точки, снизу написано «Очки: 0».

Исходники открыты: github.com/esix/packman.