Зарождение идеи

Еще в бытность джунни, я наткнулся на милейшую интерактивную обучалку по селекторам CSS: CSS Diner. Кто не знает — обязательно посмотрите, эстетический кайф гарантирован. Прошел ее минут за 15 на одном дыхании и подумал: а почему бы не сделать что‑то подобное с аналогичной механикой «допиши правильное слово в пропуск в бойлерплейте», только по другой теме? Сходу на ум ничего не пришло, но идею я запомнил.

Три года спустя я решил изучить React, написав на нем какой‑нибудь пет‑проект (имхо, лучший способ изучить новую технологию/язык). Тут‑то я и вспомнил про CSS Diner. Осталось только найти подходящую тему, где эта механика бы зашла.

Примерно в то же время на работе я делал рефакторинг большого проекта на Vue и мне нужно было найти все использования компонента с определенными пропсами и заменить в них пропсы, но так, чтобы другие использования компонента остались без изменений. Мне было лень делать это вручную и к тому же это было бы не спортивно и задело бы мою честь разработчика, поэтому я придумал регулярку для поиска и замены нужных мне вхождений.

Та самая регулярка
/(<TextField\b(?:[\s\S&&[^</>]]*?))(v-bind="attrs.*")((?:[\s\S&&[^</>]]*?)(?:</|/>))/gi

И тут меня осенило: регулярки — идеальный материал для аналога CSS Diner:

  1. можно сделать уровни, где, чтобы пройти уровень, нужно ввести правильную регулярку, а в качестве бойлерплейта — код с методами регулярных выражений, вроде match(), replace()

  2. тема конечна и относительно небольшая: я прикинул, что основу по регуляркам можно разбить тем на 30

  3. по регуляркам еще нет игровых обучалок (а это, имхо, лучший способ изучить конкретную нишевую тему), есть только статьи и курсы

Шаг 1. Сеттинг и игровая механика

Я стал думать над сеттингом и механикой. Если вы еще не посмотрели CSS Diner, там игровое поле — это котацу на котором разложена еда: огурцы, яблоки, апельсины. Иногда еда лежат прямо на столе, а иногда — на бенто или на тарелках — на каждом уровне своя сервировка.

Сервировка стола кодируется в виде HTML, где каждому элементу на столе соответствует свой HTML‑элемент. Если на столе лежат бенто, тарелка с яблоком и просто яблоко, это будет закодировано так:

<div class="table">
  <bento />
  <plate>
    <apple />
  </plate>
  <apple />
</div>

Чтобы пройти уровень, вам нужно ввести такой CSS‑селектор, чтобы выбрать им на столе загаданный объект, например, третью по счету тарелку.

Пример уровня в CSS Diner
Пример уровня в CSS Diner

Если для CSS кодировка уровня — это HTML, то для регулярок, понятно, это должен быть текст. Если в CSS Diner ты с помощью CSS‑селектора выбирал нужный объект на столе, то регуляркой можно выбирать из массива строк те строки, которые содержат нужный паттерн.

Осталось придумать такой сеттинг, чтобы текстовое представление уровня было интересно с точки зрения изучения регулярных выражений:

  1. строки массива должны перекликаться между собой

  2. содержать повторяющиеся паттерны

  3. желательно — в разных частях слова, чтобы можно было их различать с помощью ^, $, .*

  4. желательно — с различием в один символ, чтобы можно было применить ?

  5. содержать цифры, чтобы можно было их отфильтровывать по \d и \D

Я вспомнил, что в названиях растений на английском есть подобная коллизия вокруг «розы»: rose, rosemary, primrose. А цифры можно обыграть как сорта культур. Я понял, что тематика растений — это именно то, что мне нужно.

У меня в голове выкристаллизовалась следующая механика и сеттинг: каждый уровень — это сад. Модель уровня — массив названий растений в саду. Цель уровня — полить, удобрить или срезать определенные растения в саду. Чтобы пройти уровень, нужно ввести регулярку, которая отфильтрует из массива названий растений нужные.

А для групп захвата и проверок я решил сделать представление сада не массивом названий растений, а одной строкой с названиями, разделенными табуляцией, в которой нужно сделать замену паттерна на заданный текст.

Шаг 2. Учебный план и дизайн уровней

Я накидал список тем, расположил их по возрастанию сложности и от частных к общим, и принялся придумывать для каждого уровня задания. ChatGPT мне помог подобрать названия растений, хотя пришлось его сильно помучить.

Как ChatGPT помогал мне придумать названия растений для игры

(Прим.: здесь я использовал технику прайминга: сначала даем ChatGPT простое абстрактное задание, не связанное с нашим основным заданием, но погружающее в контекст. А потом уже просим сделать что нужно. Говорят, такая техника дает хороший результат для творческих заданий)

Я: Привет. Ты — восторженный ученый‑ботаник, весь смысл жизни которого — растения. Ты горишь ботаникой. Вся твоя речь пронизана метафорами и отсылками к ботанике. Ты пишешь письмо своему коллеге о новом найденном тобой цветке из Южной Америки. (напиши мне такое письмо)

ChatGPT: Приветствую, уважаемый коллега и единомышленник в мире растительных чудес!

С радостью и волнением, подобным первому ростку, пробивающемуся сквозь землю весной, пишу тебе о моем последнем открытии. Представь себе, я обнаружил новый вид цветка в глубинах тропических лесов Южной Америки, месте, где природа еще хранит свои самые завораживающие тайны, подобно непроницаемому лиственному покрову.

Этот цветок — настоящее воплощение флористического совершенства. Он имеет удивительно насыщенный и яркий окрас, словно солнечный свет, играющий на каплях утренней росы. Лепестки его нежны и тонки, словно крылья бабочки, а узоры на них напоминают изящные узоры, которые пишет мороз по стеклу. Его аромат — сложная симфония запахов, будто он собрал в себе все самые прекрасные ароматы тропического леса.

Но самое удивительное — это его способность адаптироваться к различным условиям. Он словно маленький ботанический воин, который с грацией и силой противостоит испытаниям природы. Я убежден, что этот цветок откроет нам новые горизонты в понимании растительной адаптации и выживания.

Я уже приступил к детальному изучению этого удивительного растения и планирую опубликовать свои открытия в ближайшее время. Надеюсь, что при следующем нашем встречном рейсе через ботанические миры мы сможем вместе вдохновляться этим чудом природы.

С трепетом и уважением к каждому листку и корешку,
[Твоё Имя], страстный исследователь ботанических тайн.

(Прим.: далее я использую еще одну технику: прошу ChatGPT каждый раз перед ответом вспоминать свою роль — читал где‑то в Телеграмме, что это улучшает результат)

Я: Спасибо. Продолжай играть эту роль ботаника. Каждый раз перед ответом, вспоминай свою роль. Мне нужна твоя помощь, как первоклассного ботаника. Я делаю игру на внимательность, где игрокам нужно будет искать отличия среди похожих названий. Сеттинг игры — растения. Мне нужно придумать как можно больше максимально похожих друг на друга названий растений (это могут быть как таксономические названия на латинице, так и обиходные, на английском языке. Растения могут быть разных видов/семейств/быть вообще никак не связанными между собой: главное — схожесть написания, чем она больше, тем лучше), чтобы игра была как можно сложнее и интереснее. Примеры того, что я ищу (разумеется, вымышленные, абстрактные и совершенно бестолковые, потому как я к своему стыду не имею ни малейшего отношения к ботанике):

1. Rose Foo, Rose Foot, Rose Fool — если бы удалось придумать что‑то вроде такого — это было бы просто замечательно!

2. Rose Foo, Rosed Foo, Roser Foo — еще один пример, что было бы идеально!

3. Rose Foo, Rose Bar, Rose Kar — не так идеально, но вполне приемлемо

Дай мне пожалуйста топ-10 рядов похожих по написанию названий. В каждом ряду может быть как угодно много наименований, но не меньше двух — главное: без повторений и чтобы все были похожи друг на друга. И еще. Все наименования во всех рядах должны быть уникальны. Моя просьба понятна?


Полный диалог доступен здесь

Среди многочисленных галлюцинаций, мне удалось раскопать пару бриллиантов:

  1. Пара Rye и Rue отлично подошла на тему .

  2. Barberry и Bearberry наглядно показывают применение ?

  3. Heliopsis helianthoides — то что нужно для рекуррентных групп (про иланг‑иланг я сообразил уже гораздо позже. Странно, что мне его не предложил ChatGPT на мой запрос найти название цветка с повторяющейся последовательностью)

В совокупности с уже известными мне rose, rosemary, primrose, этого было более чем достаточно. К концу вечера у меня была готова таблица с садами для каждого уровня.

Таблица с уровнями для игры

Не знаю как оценить пользу от ИИ в этом конкретном примере в часах или деньгах, но я даже не знаю какие другие названия растений могли бы лучше подойти для иллюстрации этих тем.

Шаг 3. Генерация графики

Самую большую пользу от ИИ я ощутил, когда генерировал картинки растений в Midjourney. Всего за один вечер и $10 (стоимость платной подписки Midjourney) я сгенерировал картинки для всех используемых растений, а потом в фотошопе превратил их в png с прозрачным фоном (наверное, это тоже можно было сделать средствами нейронок, но показалось, что в моем случае вручную будет быстрее). Если бы не Midjourney, мне бы пришлось нанимать иллюстратора для отрисовки картинок и это заняло бы гораздо больше времени и денег.

Промпт для Midjourney v6 и его результаты

A very simple flat style image of a single rosemary plant with solid bright colors, presented in a minimalistic design. The rosemary plant should be viewed frontally with a slight angle with no additional details, set against a transparent background. no pot, stem from nowhere

И вот что получилось:

Для других растений я использовал этот же промпт, заменяя rosemary на нужное название

Не будет преувеличением оценка, что Midjourney сэкономил мне недели времени, тысячи рублей бюджета и сотни тысяч нервных клеток.

Шаг 4. Дизайн

Дизайн решает. Если бы у CSS Diner был дизайн в стиле сайтов *.narod.ru, думаю я бы о нем никогда не узнал. Поэтому я решил не экономить и заплатить хорошему дизайнеру, а не лепить самопал. Я разместил заказ на одной из самых известных фриланс‑бирж в России и в тот же вечер выбрал фрилансера с небольшим опытом, но хорошими отзывами. Договорились на 10 тыс. рублей и неделю на выполнение работы. Через неделю дизайнер предоставил мне макеты в фигме. Результат мне так понравился, что правок почти не было.

Шаг 5. MVP

Настало время запилить MVP. Я накидал дорожную карту. Получился такой список вех:

  1. пустое Next приложение

  2. есть модель данных

  3. готова верстка и дизайн‑система, но пока без функционала и роутинга

  4. есть роутинг, готов функционал: можно ввести ответ. Если ответ правильный — переход на следующий новый уровень, если нет — ошибка

  5. прогресс сохраняется

  6. есть все виды анимаций. В случае правильного ответа показывается анимация

  7. для всех уровней есть обучающие материалы

  8. украшательства: адаптив, подсветка при ховере по растению, подсказки и проч.

  9. деплой и автоматизация деплоя

Про создание пустого Next приложения командой create-next-app я рассказывать не буду, кратко расскажу про модель данных. Основные структуры данных — это енам названий растений PlantName, словарик картинок и названий для всех PlantName, описание игрового уровня — BaseLevelParams.

Основные структуры данных
//енам всех используемых растений
export enum PlantName {
 AppleGalaMust696 = "AppleGalaMust696",
 Barberry = "Barberry",
 Bearberry = "Bearberry",
 Blackberry = "Blackberry",
 ChristmasRose = "ChristmasRose",
 CornPioneer3751 = "CornPioneer3751",
 EchinaceaSunrise = "EchinaceaSunrise",
 HeliopsisHelianthoides = "HeliopsisHelianthoides",
 Primrose = "Primrose",
 Raspberry = "Raspberry",
 Roscoea = "Roscoea",
 Rice = "Rice",
 Rose = "Rose",
 Roseberry = "Roseberry",
 RoseDoubleDelight = "RoseDoubleDelight",
 RoseIceberg = "RoseIceberg",
 Rosemary = "Rosemary",
 RoseOfSharon = "RoseOfSharon",
 RoseQueenElizabeth = "RoseQueenElizabeth",
 Rue = "Rue",
 Rye = "Rye",
 SnowInSummer = "SnowInSummer",
 SnowOnTheMountain = "SnowOnTheMountain",
 Strawberry = "Strawberry",
 TomatoRomaVF1 = "TomatoRomaVF1",
 WheatNorin10 = "WheatNorin10",
}

//словарик картинок и названий для всех PlantName
export const PlantsDict: Record<PlantName, { img: string; label: string }> = {
 [PlantName.AppleGalaMust696]: {
   label: "Apple 'Gala Must 696'",
   img: Apple.src,
 },
 //.. и так для всех растений из PlantName
};

//обощенное описание уровня
export type BaseLevelParams = {
 //название разбираемого токена или просто заголовок уровня
 title: string;
 //токен
 titleToken: string;
 //краткий синопсис что делает токен
 titleDescription: string | FunctionComponent;
 //задание
 task: string | FunctionComponent;
 //краткий экскурс в тему
 description: string | FunctionComponent;
 //подсказки
 hint: FunctionComponent[];
 //возможное решение (на самом деле избыточные данные - можно и без них)
 possibleSolution: RegExp;
 //валидаторы, например, что не используется “|” для перечисления “правильных” цветов вместо написания правильной регулярки
 validators: Validator[];
 //по умолчанию, все уровни содержат валидатор на “|”. Для уровня с такой темой делаем исключение
 shouldUseEnumerationValidation?: boolean;
};


//описание уровня, где сад представлен массивом названий и нужно этот массив отфильтровать 
export type ArrayFilterMatchLevelParams = BaseLevelParams & {
 //сад
 garden: PlantName[];
 //подмножество сада, которое является правильным решением
 correctSolutionGarden: PlantName[];
};


//описание уровня, где в саду нужно заменять растения на другие
export type BaseReplacePlantLevelParams = BaseLevelParams & {
 //текст замены. фиксированный для данного уровня
 replacer: string;
 //описание что на что в саду нужно заменить
 gardenTransitions: GardenTransition[];
};


//описание трансформации растения при замене на другое
export type GardenTransition = { from: PlantName; to: PlantName };

На основании объектов с описанием уровней потом создаются инстансы классов для уровней, содержащие вспомогательные геттеры, методы, плейсхолдеры отображаемого кода. Я выделил следующие классы уровней по типу анимации и механики:

  1. CutPlantsByArrayFilterMatchLevel, FertilizePlantsByArrayFilterMatchLevel, WaterPlantsByArrayFilterMatchLevel — фильтрация массива названий растений, через Array.prototype.match(). Отфильтрованные растения обрезаются, удобряются или поливаются

  2. ReplacePlantsByArrayMapReplaceLevel — маппинг массива названий с использованием String.prototype.replace()

  3. ReplacePlantsByStringReplaceLevel — названия растений записаны не массивом, а одной строкой через табуляцию, и к этой строке применяется String.prototype.replace()

Не буду подробно рассказывать про рутину написания приложения на React, скажу лишь, что ChatGPT мне очень помог после Vue пересесть на React и NextJS. Я просто задавал вопросы нейронке по конкретным темам без какого либо промпта и этих ответов мне было достаточно, чтобы понять что к чему и написать мое приложение. После Vue очень непривычно, что в React приходится управлять реактивностью вручную. К счастью, для таких, как я, есть MobX (я про makeAutoObservable), preact‑signals и десятки других подобных библиотек.

Такая ассоциация у меня возникла от дизайна реактивности в React
Такая ассоциация у меня возникла от дизайна реактивности в React

Шаг 6. Анимации

Я писал игру еще до того, как стали известны Cursor и Windsurf, поэтому использовал good old ChatGPT для помощи с кодом анимации. Но сейчас я бы скорее всего использовал эти редакторы для этих задач — dev‑experience с ними лучше.

ChatGPT радикально упростил создание анимаций. Простую рабочую анимацию падающих капель воды из лейки он сгенерировал с первого раза

Анимация полива

Потом ChatGPT помог мне сделать более сложную анимацию сотни частиц удобрений выпадающих из мешка: написал рандомизацию формы, цвета и движения для сотен частиц на SCSS. Подсказал сделать движение частиц через css motion path — в моем случае это был единственный рабочий способ рандомизировать траекторию движения частиц. Позже, когда делал адаптив, столкнулся с тем, что в CSS нельзя параметризовать offset-path через переменные css. ChatGPT написал мне код для динамического перерасчета параметров рандомизации при изменениях размера экрана через JS.

Анимация удобрения

Я думаю, по меньшей мере пару дней я сэкономил благодаря ChatGPT.

Шаг 7. Учебные материалы

Самым трудозатратным было написать обучающие материалы по каждой из тем. Я из принципа не стал использовать ChatGPT и написал все сам: в этом я видел свое самовыражение и получил большое удовольствие от продумывания и написания каждого урока.

Шаг 8. Последние штрихи и деплой

Когда все уроки были написаны, я добавил пару мелких фич, причесал код и задеплоил на удаленную тачку. Подумал было бы круто автоматизировать деплой и перезапуск приложения на мерж в репу. ChatGPT с первого раза написал рабочий скрипт для github actions. Я ни разу раньше не писал Github actions, поэтому без ChatGPT на это ушло бы, думаю, 1–2 часа минимум, а реалистично — пару дней. А здесь за минуту и все работает.

Выводы

Я буду очень рад, если мой пример вдохновит тех, кто хотел бы написать свой пет‑проект, но по каким‑то причинам откладывает это. Если вы сомневаетесь стоит ли тратить время на пет‑проект, я скажу однозначно да: это отличный способ изучить новую технологию, пописать код в свое удовольствие, не говоря уже что пет‑проект — это отличное дополнение к резюме.

А еще написание пет‑проекта — это отличная возможность для разработчика побыть в роли менеджера: чтобы работа над пет‑проектом была эффективной, надо заранее продумать роадмэп, расписать основные вехи и прикинуть сколько времени займет достижение той или иной вехи. Мне, привыкшему быть винтиком в цепочке IT‑конвеера, который получает задачи сверху и выполняет их, это было немного непривычно.

С нейронками делать пет‑проекты стало проще в разы. Лучшие вектора использования нейронок (как для пет‑проектов, так и для промышленной разработки, если позволяет NDA и/или есть инхаус ИИ‑решения) я вижу так:

  1. изучение новых технологий

  2. генерация картинок и прочих ассетов

  3. настройка CI/CD

  4. генерация шаблонного кода

А с появлением Windsurf и Cursor, последние 2 пункта стало делать еще проще.

Тем не менее есть вещи, которые, я не доверю нейронкам и буду делать сам: продумывание архитектуры, модели данных, написание кода на новой технологии, которую я изучаю, написание текстов для людей, ну и вообще все то, в чем я вижу свое самовыражение. Например, умей я рисовать, думаю, сам отрисовал бы цветы для своей игры. При написании этой статьи, кстати, я ни разу не использовал ИИ.

А какие пет‑проекты вы хотели бы написать, но все откладываете?

?Игра: https://regexp-garden.madewith.fun

?Исходный код: https://github.com/avfirsov/regexp-garden

Комментарии (1)