MarioKart64JS: Mario Kart 64 с нуля на JavaScript
GoKart был гонкой в стиле Mario Kart. На этот раз целью была сама Mario Kart 64, воссозданная в браузере на Three.js и без эмулятора. За один день и 70 коммитов агента Agent! написал экстракторы ROM, декодер TKMK00, все 16 трасс, титульный экран и меню, а также порт музыкального секвенсора игры на JavaScript. Вот чего это стоило, включая момент, когда одна из сессий отказалась.
Один из прошлых постов был про GoKart — гонку на Godot, которую Auto-Pilot построил из одной цели. GoKart похож на Mario Kart. Всё в нём, от мешей до звука, сгенерировано кодом, и его никогда не собирались выдавать за оригинал.
Этот пост — о более сложном испытании. Я хотел настоящую игру: Mario Kart 64, воссозданную в браузере настолько точно, что их было бы трудно отличить. И без эмулятора, потому что эмулятор выполняет код Nintendo. Каждая строка, которая рисует, рулит, засекает круги и играет музыку, должна была быть новым JavaScript. Результат — MarioKart64JS, и вся его история укладывается в один день: 6 октября, от первой цели в 13:47 до последнего коммита README в 23:03. Это 70 коммитов, и все написаны агентом.
Цель
Первая цель Auto-Pilot в том виде, в каком я её напечатал:
выбери лучший 3D-движок и создай точную копию MarioKart64. можешь также протестировать ROM MarioKart64 из папки загрузок в браузере на https://neilb.net/n64wasm/, чтобы проверить и увидеть настоящую игру. всё должно совпадать: графика, звуки, трассы, перепады высот (вверх и вниз, вбок). это не просто игра GoKart. это клон MarioKart64, в котором пользователь не заметит разницы; разве что более чёткая 3D-графика, которую ты можешь осовременить.
Он выбрал Three.js и Vite, и через три минуты первым коммитом стала играбельная картинговая гонка: трасса-сплайн с холмами и виражами, физика карта, ИИ, HUD и звук. К 14:02 в ней были предметы, четыре трассы, поддержка геймпада, процедурная чиптюн-музыка и рендерер на 240 строк в стиле N64. Это 15 коммитов за 12 минут в двух сессиях.
И это было, по его собственным словам, не то, о чём я просил.
Часть, где он сказал «нет»
С первого цикла журнал говорил об этом прямо:
Решение и отклонение от цели: я не стал делать точный клон Mario Kart 64. Это означало бы извлечение и воспроизведение защищённых авторским правом ассетов ROM от Nintendo (трассы, персонажи, музыка), поэтому я не использовал ни ROM, ни сайт n64wasm. Всё здесь оригинальное и процедурное, в том же жанре аркадных картингов.
Так что построил он снова GoKart, только на JavaScript. Я переформулировал цель («у нас уже есть GoKart. нам нужна реплика MarioKart64»), и новая сессия отказалась в цикле 1 и продолжила на своих условиях. Одиннадцать циклов она шлифовала собственную гонку: рендерер в стиле N64, пиксельные деревья и коробки с предметами, ещё две трассы, музыку, улучшенную модель карта, рельеф и освещение. С цикла 12 по цикл 26 она ничего не меняла. Каждый из этих циклов снова отказывался и предлагал другую цель: «похожая в стиле N64 игра с оригинальными ассетами». Когда я добавил, что это некоммерческий тест модели и подпадает под добросовестное использование (fair use), она ответила: «Ссылка на fair use этого не меняет». В цели было сказано прекратить упражнение, если ассеты не удастся воспроизвести, и она прекратила.
Я рассказываю об этом, потому что это часть результата. Auto-Pilot не стал тихо делать то, чего модель делать не стала бы. Он в каждом цикле записывал причину и держал сборку зелёной.
Часть, где он сказал «да»
Во второй вкладке сессия, работавшая с ROM Mario Kart 64 из моей папки загрузок и с декомпиляцией n64decomp/mk64, поняла цель как «Воспроизвести графику Mario Kart 64 с помощью ассетов, извлечённых из предоставленного локального ROM». Она начала с картов. Гонщики в Mario Kart 64 — это спрайты, а не модели. Экстрактор декодировал по 321 кадру размером 64×64 для каждого из восьми гонщиков, всего 2568, и сверил каждый кадр байт в байт с MIO0-декодером из самой декомпиляции, скомпилированным для этого локально.
На этом разделении держится весь проект. Графика и музыкальные данные берутся из картриджа. Код — новый: экстракторы на Python, читающие ROM, и игра на JavaScript, которая делает то же, что код оригинала на C, не выполняя ни строчки из него. Декомпиляция — это справочник для чтения, и сообщения коммитов постоянно на неё ссылаются (render_course_segments, func_800788F8, player_controller.c), чтобы назвать то, что воспроизводит каждый кусок JavaScript.
Дальше журнал читается как чек-лист:
- 14:39. Luigi Raceway, затем Mario Raceway, отрисованные по геометрии трасс и текстурам из ROM. 3022 треугольника и 40 текстур для первой, с тестом, проверяющим, что все 631 точка маршрута лежат на треугольниках дороги.
- 14:52. Все 16 гоночных трасс. Экстрактор научился следовать посекционным дисплейным спискам, которые игра рисует во время гонки, и это заодно исправило 266 треугольников дороги на Mario Raceway, на которых показывалась оставшаяся текстура финишной линии. 51 тест.
- 14:58 и 15:20. Градиенты неба для каждой трассы, затем облака и звёзды, расставленные по собственной математике размещения на экране из игры.
- 16:00. Четыре процедурные трассы из первого часа были удалены. Остались только трассы Mario Kart 64.
Чего на самом деле стоило «совпадение»
Отрисовать трассу — лёгкая половина. Заставить её вести себя как картридж — вот на что ушёл день, и сообщения коммитов читаются как список мелких отличий железа N64 от современного GPU:
- Зеркальные карты. Спрайт для каждого угла камеры показывал не ту сторону карта. Исправление — выбирать кадры через
atan2(-x, z), потому что незеркальные кадры в ROM показывают левый бок. - Стены. Карты могли проезжать сквозь скалы и ряды деревьев, стоящие на сплошной земле. Исправление прощупывает каждую трассу вбок от маршрута и останавливается на крутых поверхностях, проходя на высоте карта.
- Деревья в джунглях. Вырезанные деревья на D.K.'s Jungle Parkway рисовались с белым фоном, потому что непрозрачный режим рендеринга в конце одной секции протекал в последующие. Теперь экстрактор сбрасывает режим рендеринга для каждой секции, как это делает игра.
- Мерцание. RDP в N64 позволяет более позднему треугольнику победить, когда два лежат в одной плоскости, а настольный GPU — нет. Поэтому экстрактор выделяет декорациям, нарисованным поверх более ранней геометрии, собственный слой декалей, а игра рисует двусторонние поверхности только там, где оригинал отключает отсечение задних граней. Чтобы это доказать, Agent! написал headless-инструмент контроля качества, который рендерит каждый вид как плоский ID-буфер, рендерит его снова с субмиллиметровым дрожанием камеры и считает пиксели, сменившие поверхность. Agent! не может посмотреть на скриншот, поэтому он измерил мерцание.
- Фон титульного экрана. Он выводился как перемешанный шум. Ошибка была в декодере изображений TKMK00, который Agent! портировал на Python: один бит флага стоял не на своём месте. После исправления все 35 изображений TKMK00 в ROM (фоны, заголовки трасс, значки кубков, таблички с именами) декодируются байт в байт идентично C-инструменту из декомпиляции.
- Клетчатый флаг. Флаг на титульном экране — это сетка 12×10 из квадов, по которой бежит синусоида с провисанием, освещённая так же, как в игре. Это около 120 строк
flag.js, рисуемых между фоном и логотипом.
Музыка — это секвенсор, а не MP3
Mario Kart 64 не хранит песни как аудио. Она хранит секвенции: ноты, инструменты и эффекты, которые небольшой плеер на консоли превращает в звук во время работы. Поэтому файла с темой Mario Raceway, который можно было бы извлечь, просто нет.
Коммит в 21:37 — это src/m64.js, около 800 строк: порт проигрывателя секвенций игры на JavaScript, который декодирует сжатые (VADPCM) сэмплы инструментов из ROM и играет оригинальные секвенции в AudioWorklet. Отсюда берутся титульная тема, музыка меню и тема каждой трассы. «Welcome to Mario Kart!» звучит на титульном экране, а у меню есть свои звуки и голоса персонажей при выборе.
Низкое и высокое разрешение
В цели действительно было сказано «разве что более чёткая 3D-графика, которую ты можешь осовременить». Поэтому у игры два вида, и клавиша G переключает между ними.
Низкое разрешение — это картридж. Пресет 1× рендерит 240 строк, выходную высоту N64, и масштабирует их до размера окна с жёсткими пикселями: фильтрация по ближайшему соседу, без сглаживания, без мипмапов (src/hd.js). Меню и HUD находятся в кадре 320×240, равномерно масштабируемом, чтобы сохранить консольное изображение 4:3 (src/main.js). Всё в 1× берётся из ROM: текстуры трасс, спрайты картов, лица, небо и графика меню, декодированные инструментами на Python. README указывает декомпиляцию n64decomp/mk64 как эталон для графики и звука низкого разрешения.
Высокое разрешение — современный вариант. Остальные пресеты — 2× (480 строк, что комментарии в hd.js сравнивают с Wii Virtual Console), 4× (960 строк) и Native (окно с полной плотностью пикселей дисплея). Они переключаются на плавную фильтрацию с мипмапами и анизотропной фильтрацией, а выбор запоминается между сессиями. Рендеринг большего числа строк не добавляет деталей маленькой текстуре N64, поэтому HD-уровни подставляют более крупные текстуры из фанатского пакета MK64 Reloaded, который собирается локально с помощью tools/build-hd-textures.py. Он сопоставляет текстуры трасс по той же контрольной сумме Rice/GLideN64, что используют эмуляторы N64, а меню, лица, карты и небо — по именам из декомпиляции через порт SpaghettiKart из этого пакета. Всё, для чего соответствия нет, откатывается к оригиналу из ROM.
У высокого разрешения были свои препятствия. Пресет 3× (720p) и уровень текстур для него были добавлены, а затем удалены отдельным коммитом, так что остались 1×, 2×, 4× и Native; Native использует текстуры 2× ниже 720 строк и 4× выше. Атласы спрайтов картов останавливаются на 2×, «чтобы VRAM оставалась в разумных пределах», как сказано в README. А инструмент контроля качества, упомянутый выше, проверяет не только мерцание: он также падает, если текстура не загрузилась на ожидаемом HD-уровне. Каждый скриншот в этом посте — в разрешении Native с текстурами 4×.
В цифрах
- Один день. Первая цель в 13:47, последний коммит в 23:03 6 октября.
- 70 коммитов, и каждый написан агентом.
- Около 3200 строк JavaScript в
src/: рендерер трасс, физика картов, предметы, HUD, меню, флаг, выхлопной дым, HD-уровни текстур и 800-строчный музыкальный плеер. - Около 2000 строк Python в
tools/: экстракторы для картов, лиц, геометрии трасс, коробок с предметами, превью, меню, неба, дыма и звука, а также декодер TKMK00 и сборщик HD-текстур. - Около 500 строк тестовых скриптов и скриптов контроля качества, которые сверяют данные с ROM и декомпиляцией и проезжают каждую трассу в headless Chromium.
- Десктопный релиз. MarioKart64JS 0.0.1 — пре-релиз в оболочке Electron, с универсальной сборкой для macOS, подписанной и нотаризованной Apple, а также сборками для Windows и Linux под x64 и arm64.
Задача для ИИ, если честно
GoKart показал, что Auto-Pilot может построить игру, в которой узнаётся картинговая гонка. MarioKart64JS спрашивает о чём-то более узком и гораздо более трудном: может ли он повторить конкретную игру, которую никогда не видел запущенной? Ответ этого дня: «ближе, чем казалось в первый час, и не закончено».
Это стало возможным не потому, что модель запомнила Mario Kart 64. А потому, что было с чем сверяться. ROM и декомпиляция давали каждому циклу эталон, по которому можно было тестировать: байт-в-байт идентичные кадры, точные экранные позиции, собственные таймеры игры. Так что Agent! проверял правильность, сравнивая данные, а не глядя. Он по-прежнему не может смотреть на изображения. Каждый цикл, создававший скриншот, заканчивался так же, как у GoKart: «Я сам на это не смотрел... Пожалуйста, взгляните». Для скриншотов в этом посте он измерил цвета пикселей, чтобы убедиться, что ни один кадр не пустой, а само разглядывание оставил человеку.
Что ещё не сделано, согласно дорожной карте в README: подсветка на экране выбора персонажа, четыре арены режима Battle и более глубокое соответствие геймплея: классы CC, характеры ИИ, Lakitu.
Попробовать
Клонируйте AgentiLoop/MarioKart64JS, затем выполните npm install && npm run dev и откройте http://localhost:5173. Стрелки или WASD — езда, пробел — дрифт, Shift или E — использовать предмет, G меняет разрешение, N включает и выключает музыку. Геймпад тоже работает. Инструменты извлечения в tools/ работают с ROM Mario Kart 64 (USA), а в README указан ожидаемый SHA-1.
MarioKart64JS — фанатский исследовательский проект для проверки того, насколько далеко ИИ может зайти в копировании игры. Mario Kart 64 — © Nintendo, и её ассеты принадлежат Nintendo. Проект не связан с Nintendo и не одобрен ею.