С детства я люблю компьютерные игры: хоть сейчас у меня и другие приоритеты, но иногда люблю зайти в старую добрую классику (недавно добил 100% прохождение GTA San Andreas, прошел классическую серию Serious Sam). Мой папа же — не особо любил подобные игры, ему больше нравился минимализм: шашки, тетрис и прочие 2d игры-головоломки. (До Зумы кстати не добрался, надо бы дать ему попробовать! ремарка)
Одна из таких игр — была незамысловатая игра жанра маджонг в сеттинге Покемонов: в ней нужно находить пары одинаковых карт, чтобы путь между ними был свободен и имел максимум 2 изменения траектории.
Задача усложняется лимитом по времени и дополнительными условиями на следующих уровнях: оставшиеся карты могли перемещаться, если по соседству есть свободный слот; это привносило в игру элемент стратегии, простое закликивание пар не подойдет, ведь нужно учитывать состояние доски после сдвига карт, иначе можно словить софт-лок и проиграть.
Сама игра выглядит так:
Довольно часто она выглядела так:
Сейчас, в силу обстоятельств и переездов, мои родители не пользуются каким-либо компьютером: у них есть телефоны и айпад; абсолютно все потребности закрываются ими. Папа всё так же поигрывает в головоломки, шашки на айпаде — благо выбора в App Store предостаточно. Но одной игры решительно не хватало: тех самых покемонов!
Маджонги в App Store имеются в достатке, но и правила у них были другие, они тянут деньги, показывают рекламу и в целом, не то.
Раньше я не знал, как можно было бы решить эту потребность, кроме как купить простенький ноутбук исключительно для этой игры. Но покупать ноутбук ради одной игры нецелесообразно, ибо когда он был у моих родителей, они всё равно делали выбор в пользу мобильных гаджетов.
И вот, во время моего недавнего приезда к родителям, я подумал, что можно было бы попробовать портировать эту игру и сделать веб-версию — без всяких сторов, прочих зависимостей: чистый html-файл с CSS и джаваскриптом, а сам код написать с помощью связки Cursor + Claude.
Спойлер — получилось! Конечно не всё, как я задумывал изначально, но конечным результатом я и главный игрок — мой папа, — довольны.
Но сейчас расскажу по порядку.
Вначале я пытался найти саму игру, оригинальный .exe-шник во плоти.
Оказывается по запросу pokemon exe в гугле сразу всплывают крипи-истории и картинки с покемонами; это конечно потешно, но это не то, что нужно.
Немного перебора и по запросу в Яндексе: "покемоны старая игра exe", удалось найти ссылку на Ответах Мейл Ру и топик на Rutracker (где кстати до сих пор 20+ сидов!) с названием "Madjong Pokemon". Там же, в комментариях лежит ссылка на Flash-версию игры на сайте Игро-утка.
Если бы в 2010-ом году, Стив Джобс публично не раскритиковал Flash и эта технология до сих пор поддерживалась бы в современных браузерах (она в целом поддерживается до сих пор, но на iPad отображается просто чёрный экран), то на этом бы мой квест завершился и я просто сохранил бы в закладки этот сайт и добавил его на главный экран iPad для быстрого доступа.
Но конечно, всё не так просто! Поэтому вооружившись гуглом, я начал искать декомпилятор .swf файлов, благо, что сам .swf файл порта игры с покемонами было достаточно легко скачать через DevTools браузера.
Выбор пал на https://github.com/jindrapetrik/jpexs-decompiler: 4.6k звезд, в фичах упоминается экспорт ActionScript, медиафайлов, есть версия под MacOS.
План был такой:
декомпилируем Flash .swf файл
достаём ActionScript с логикой игры
достаём ассеты
скармливаем это всё в Cursor и в режиме Composer'а говори
Забегая вперёд — так не получилось: декомпилятором получилось достать .svg файлы с картинками покемонов в формате base64, а сами экспортированные скрипты содержали в себе управление UI без самой логики игры. Был загадочный Frame 5: есть подозрения, что логика может быть спрятанна в нём, но сам декомпилятор безбожно зависал при попытке достать оттуда данные. При выборе опции "экспортировать всё", этот фрейм не попадал в экспорт.
Но зато удалось вытащить оригинальные картинки, которые декомпилятор может вытащить в привычном формате .svg/.png. Это мне пригодилось для дальнейшей разработки.
Дело было вечером перед моим полётом в другой город, поэтому мне было важно сделать MVP быстро за один вечер, показать её папе и вместе с ним разделить момент с игрой в неё. На часах тем временем было уже ближе к полуночи.
На самом деле, логика самой игры довольно простая — нужно сгенерировать таблицу/матрицу и потом находить между одинаковыми значениями этой матрицы путь, чтобы удалить пару элементов. Держа это в голове, я начал задавать простые промпты в Cursor — "создай игру, где есть доска с картами, игроку нужно удалить одинаковые пары карт со всей доски для победы".
Всё шло неплохо: Claude 3.5 Sonnet сгенерировал html, css, js файл — в ней была готова логика с рандомной генерацией доски, выделением карт, подгрузкой изображений, красота!
Единственное, что вызвало проблемы — это непосредственно алгоритм поиска пути между двумя картами. Иногда можно было выбрать карты, которые вообще не могли быть выбраны по правилам — между ними были препятствия или путь до них был изогнут больше 2-х раз; иногда соседние карты отказывались выбираться, хотя они были расположены диагонально друг к другу без препятствий.
Cursor + Claude в такие моменты начинает уходить в частные способы решения проблемы, aka брутфорс, ситуация напоминала картинку ниже:
В таких случаях можно попробовать:
создать новый чат, чтобы сбросить текущий контекст и попробовать уже с другим подходом — а-ля перебросить игровые кубики в надежде, что вам выпадет тот самый seed на стороне LLM-ки с нужным вам решением.
попробовать по-другому поставить изначальную задачу.
Я решил пойти по второму пути, задав вопрос в ChatGPT с примерами создания путей в матрице:
На выходе получилась симпатичная Python-функция с поиском в глубину (DFS):
def find_path(matrix):
from collections import deque
# Dimensions of the matrix
rows = len(matrix)
cols = len(matrix[0])
# Find coordinates of 1s (start and end points)
points = [(r, c) for r in range(rows) for c in range(cols) if matrix[r][c] == 1]
if len(points) != 2:
return "Invalid input: Need exactly two '1's"
start, end = points
# Directions: (row change, col change)
directions = [(-1, 0), (1, 0), (0, -1), (0, 1)] # up, down, left, right
# BFS with tracking of the path and direction changes
queue = deque([(start[0], start[1], -1, 0, [])]) # (row, col, last_direction, direction_changes, path)
while queue:
r, c, last_dir, dir_changes, path = queue.popleft()
# Add current position to the path
path = path + [(r, c)]
# If we reach the end point, mark the path with '*'
if (r, c) == end and dir_changes <= 2:
for pr, pc in path:
if matrix[pr][pc] == 0: # Mark only on zero cells
matrix[pr][pc] = '*'
return matrix
# Explore neighbors
for idx, (dr, dc) in enumerate(directions):
nr, nc = r + dr, c + dc
# Check if within bounds and on a passable cell (0 or end point)
if 0 <= nr < rows and 0 <= nc < cols and (matrix[nr][nc] == 0 or (nr, nc) == end):
# Calculate new direction changes
new_dir_changes = dir_changes + (1 if last_dir != -1 and last_dir != idx else 0)
# If changes are within limits, continue exploring
if new_dir_changes <= 2:
queue.append((nr, nc, idx, new_dir_changes, path))
# If no path found
return "No solution"
# Test cases
matrix1 = [
[0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 0, 2],
[0, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]
]
matrix2 = [
[0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]
]
matrix3 = [
[0, 0, 1, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]
]
matrix4 = [
[0, 2, 2, 2, 0, 0, 0],
[0, 1, 2, 1, 0, 0, 0],
[0, 2, 2, 2, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]
]
solutions = [find_path(matrix) for matrix in [matrix1, matrix2, matrix3, matrix4]]
solutions
Этот же скрипт можно передать в чат Cursor с просьбой использовать подход из Python-скрипта в .js файле и на моё удивление — проблема решилась с первого же сообщения в чат.
Легко было добавить таймер и счётчик баллов. Убедившись в том, что локально всё работает нормально, я выложил игру на Github Pages: https://gulivan.github.io/pokemon-mahjong/ (первая загрузка может занять 5-10 секунд)
Проверил iPad, зашел на задеплоенную страницу и вуаля, всё работало как ожидалось! Единственное, был косяк с масштабированием в версии для телефона — отображалось не всё поле, но в эту игру папа всё равно собирался играть с планшета.
Чтож, фиксируем, с идеи до реализации MVP — прошло 3.5 часа. Сам я не фронтендер, JS могу читать, но писать на нём мне пока тяжело, поэтому вручную у меня ушло бы куда больше времени.
Допиливаем игру
Времени на модификацию логики игры, замену ассетов с защищенных авторскими правами покемонов на сгенерированные нейросеткой персонажей — ушло больше чем 4 часа.
Оффтоп: потом я попробовал декомпилировать оригинальный .exe с помощью reverse-engineering фреймворка Ghidra, но так и не получилось. Попытка достать хотя бы Midi-файлы — тоже не удалась (их было видно в байт-коде, но экспортированный бинарный контент в формате .midi не воспроизвелся)
У игры, хоть она и работала, были следующие проблемы/пожелания:
покемоны классные, но заливать защищенные авторским правом картинки в репозиторий — значит подставляться под DMCA, хотелось бы исключить шансы, что за мной пошлют крепких японцев из Nintendo.
в оригинальных покемонах была прогрессия — после победы, начинался следующий уровень, где менялось поведение оставшихся карт; так игра становилась сложнее, но интереснее
не рисовался путь между двумя одинаковыми картами
хотелось бы больше кастомизации в плане сложности, размера игровой доски, настройки таймера
починить мобильную версию
Генерируем нейросетевых импортозамещенных Покемонов
Публично размещать игру с защищёнными ассетами нельзя, а терять прелести бесплатного хостинга от GitHub Pages не хотелось, поэтому в результате обсуждения с батей, было получено одобрение на замену покемонов сгенерированными картинками.
Всего протестил 3 нейросети:
Шедеврум от Яндекса
Локальный Stable Diffusion XL
Midjourney
Подход был следующий: просим сгенерировать промпт у ChatGPT, чтобы было похоже на покемонов, максимум экспрессии, мультяшный, похожий на аниме стиль.
Один из промптов для Шедеврума:
Слоненок стоит на задних лапках, подняв хобот и улыбаясь Белый фон. минималистично. Сделай похожего на покемона. Рисунок гуашью карандашами. Anime, Nickelodeon. Ghibli.
Результаты в целом неплохие, но:
нет консистентности (какие-то картинки похожи на 3д рендер, какие-то на классические иллюстрации, где-то слишком реалистично, где-то добавлялись посторонние объекты, иногда персонажи не помещались во весь рост)
Работать с Шедеврумом утомительно, потому что в бесплатной версии хоть и доступно до 70 генераций в день; но результат каждого промпта нужно ждать по 10-15 секунд, а на выходе вы получаете только одну картинку. Но какие-то отдельные картинки получались неплохими, пусть местами и слишком однообразными.
Далее на очереди был Stabble Diffusion XL, с рандомной анимешной моделью из CivitAI, но и тут сразу всплыли нюансы:
модельки слишком маленькие, чтобы обеспечить разнообразие
большие модельки не влезли в ноутбучную 3070 RTX
хоть и можно получить больше одной картинки за промпт, на рендер каждой уходило так же до 10-15 секунд
Примеры результатов:
По моему мнению, итоговые изображения достаточно хорошие для первой попавшейся модели, но получения действительно стоящего результата, нужно подтюнить модель под себя:
дообучить её на релевантных картинках,
развернуть инстанс удалённо у облачных провайдеров для большего VRAM
поэксперементировать с промптами
попробовать LoRA-чекпоинты
Потратив на бесплатные варианты пару вечеров, я решил опробовать MidJourney. Большой их плюс в том, что можно забить примерный промпт в поиске и посмотреть примеры результатов. Минус — поиск доступен только после оплаты подписки.
С промптами для MJ снова помог ChatGPT:
Create midjourney prompts to create sticker set based on pokemon like animals (but not directly copying them) on white background, vector illustration, cartoon stile. Name different animals with different accent color on them
Ответ ChatGPT:
Ответ
Electric Ferret
"Cartoon vector illustration of an electric-inspired ferret-like creature with a bright yellow accent, sparking with energy, sleek body, large curious eyes, slightly mischievous smile, small electric sparks around its fur, on a white background, sticker style."Fiery Fox
"Cute, cartoon fox-inspired creature with an orange-red accent, flaming tail, playful expression, small paws, pointy ears, and fire patterns on its fur, vector illustration, on a white background, ideal for a sticker set."Aqua Otter
"Adorable otter-like animal with a blue color accent, water droplets around its body, big round eyes, webbed paws, holding a bubble, soft fur, gentle smile, cartoon style on a white background, vector sticker format."Leafy Lizard
"Charming cartoon lizard with a green accent color, leafy textures on its scales, big friendly eyes, a plant-like tail, small feet, perched cutely, vector illustration on a white background, sticker-ready."Rocky Raccoon
"Cartoon-style raccoon with a gray and brown stone-textured accent, playful expression, large eyes, paws gripping a tiny rock, a tail with rocky ridges, vector art style, on a white background, designed as a sticker."Thunder Bat
"Cartoon bat creature with striking purple and yellow accents, small sharp teeth, tiny wings, wide eyes, and an electric aura around it, vector illustration on a white background, ready as a sticker."Floral Deer
"Graceful cartoon deer with pastel pink and white floral accents, gentle eyes, delicate flower petals along its antlers, calm expression, sleek vector art, white background, ideal for stickers."Frosty Penguin
"Chubby penguin-inspired creature with icy blue accents, tiny flippers, snowflake patterns on its belly, cheerful expression, smooth cartoon style on a white background, vector illustration for sticker use."Shadow Lynx
"Mysterious lynx-like animal with dark purple and black shadow accents, piercing yellow eyes, soft fur, slightly grumpy expression, vector cartoon style, on a white background, sticker format."Metallic Armadillo
"Cute cartoon armadillo with silver and dark gray metallic scales, tiny eyes, sturdy body, a tail that curls, little armored plates, vector illustration on a white background, designed as a sticker."
Результат тоже не всегда выходил консинстентным с разными животными, но пожалуй показатели можно улучшить, если поиграться с промтпами более детально: подобрать нужный --sref
, включить персонализацию (увы, она доступна только при годовой подписке).
Кстати, лично мне довольно тяжело единолично генерировать картинки: замыливается глаз, перестаешь понимать, подходит ли та или иная картинка, что ещё можно добавить в промпте; нужен соавтор, коим выступила моя любимая жена :D
После скачивания всех покемонов, было достаточно сжать все изображения до 512х512, сконвертировать в .jpg формат и положить их в папочку с игрой.
Отдельно в канвасе включил пару параметров для того, чтобы улучшения качества картинки:
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
В результате получаем игру с новыми ассетами!
Дело остаётся за малым:
реализовать прорисовку пути между одинаковыми картами
добавить меню настроек для игрока (сложность, размер борды, настройка таймера)
реализовать разные уровни (смещение оставшихся карт)
Звучит легко!
И тут началось веселье. Рефакторинг с Cursor.
Не буду описывать все детали, раздувая итак уже большой пост, но в процессе я сделал следующие выводы:
код лучше разбивать по модулям
для лучшей эффективности стоит отправлять запросы со всей кодовой базой
есть проблемы, если у вас одинаковые названия файлов в разных директориях
оказывается, напрямую импортированный .js файл в index.html не поддерживает импорты из модулей, нужно использовать
node httpserver
или аналогичные решения
файлы больше 1000 строк уже неохотно прожёвываются нейронкой:
при нажатии Apply, нейронка может перезатереть или сломать уже существующий функционал (например, мне нужно было улучшить отзывчивость управления, но Cursor вместе с новой функцией обработки нажатий, удалил функционал с проверкой на победу и добавлением бонусного времени)
местами есть вероятность, что нейронка напишет по краям "the rest of your code here" (Cursor для копипаста кода из Claude Sonnet использует собственную модель Cursor-mini, которая нет-нет, да ошибётся)
нейронка может не учесть уже существующие функции, написать что-то своё
дебажить .css опираясь на ваш визуальный фидбек в текстовом формате — она тоже не особо умеет; нейронка попробует разные параметры, но вы можете по итогу только потратить своё время.
в этом плане помогло открытие браузера и измение .css классов уже в DevTools вживую, чтобы посмотреть, где именно нужно внести изменения
по итогу, мобильная версия всё ещё далека от идеала (в ней могут некорректно фиксироваться точки нажатия, не работает зум, если вы зумитесь от канваса)
режим Composer в текущем формате — очень сырой, он не поможет вам порефакторить существующий код, зато сломать уже работающий — пожалуйста! Поэтому Git — must have, без него не выжить. Пожалуй сейчас режим Composer поможет только для построения базовой структуры проекта с первоначальными фичами, дальше уже только ручками.
Итоговый проект всё ещё не идеален, но достаточно играбелен и в нём есть почти весь необходимый функционал и даже чуть больше, чем было в оригинальной .exe игре из 2000-х. Главное — бате понравилось (говорит, что с различными уровнями играть в разы интереснее), а я сам получил прекрасный опыт построения MVP и последующей доработки продукта с использованием LLM/ImageGen.
А подписаться здесь (пишу про код, нейронки, data сферу): https://t.me/gulivan
upd (2024-11-18): удалил задублированный блок текста :)