На выходных запилил игрулю, вариацию Сапёра.

Поиграть можно тут (с компа стрелочками удобнее, но на мобилах тоже работает), а исходники почитать — тут.

Про саму игру #

Идея пришла в голову не слишком случайно. Недавно поставил себе классического сапера на планшет и залипал в него. Но с “аккордеонами” (которые рекурсивно открывают соседние ячейки если все однозначно) игра превращается в тупейший кликер на скорость. Прям думать-думать надо раза 2-3 за игру на “Профессионале”, и обычно после этого приходится делать случайный выбор.

“Ездить” по полю было намеренным решением. Кликать скучно, а так себя можно представить роботом, который уволился из доставки и пошел в саперы. В голове у меня застряла прочная ассоциация с Bomberman, в который тоже недавно играл, — там тоже кто-то ходит и связано со взрывами, но общего тут только ходьба по большому счету.

Получившаяся игра чисто случайно (потому что консоль 160x160 пикселей) получилась с такими же параметрами как у “Любителя”, однако она тяжелее. Тут надо думать чаще, да еще и помнить, где мины стоят (отсутствие флажков тоже было намеренным). И следить за тем, чтобы случайно не уехать дальше, чем нужно: чаще всего я проигрываю как раз из-за этого.

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

Консоль и ее ограничения #

На платформу я наткнулся, когда что-то искал для Zig в рамках своего предыдущего приключения:

там есть еще и другие языки, чтобы на них пострадать

В целом, программировать под нее оказалось довольно просто. При этом она не идеальна (я нашел пару багов, придумал фичу и даже сделал мини-пуллреквест), но компенсирует это “ламповостью”.

И наличие ограничений тут сыграло на руку: я очень быстро выкинул несколько идей (некоторые, правда были неплохие), которые могли бы затянуть разработку на недельку, и смог быстро получить что-то рабочее.

Nim #

Nim я выбрал по принципу “что-то новое, но при этом хотя бы слышал” (оказывается, для этого баззворд даже есть). Про него до этого я знал только, что да, вот есть такой язык, похож на питон чем-то, но при этом системный, и вроде хвалят.

Писать на Nim было легко. Я опять поленился и не почитал даже простейшие туториалы (1, 2), но первую играбельную версию написал без проблем, просто используя те конструкции, которые казались логичными для питона/сей и при взгляде на код рядышком из шаблона. Углубиться в язык понадобилось только когда проверку решаемости добавлял.

С точки зрения синтаксиса — да, напоминает питон с его значимыми пробелами и некоторыми особенностями синтаксиса, и еще в чем-то есть сходство с F#. Есть непривычные вещи, например, конкатенация строк через & вместо + или перевод в строку с помощью доллара ($someInt). Забавно, что лямбды — это сахар, и для x => x + 1 надо буквально импортировать std/sugar. Немного расстроили структуры, там обязательно надо указывать имена полей при создании, Cell(x: p.x + 1, y: y) мне не очень понравилось и я в итоге оставил просто две переменных во многих местах. Switch не такой мощный, как хотелось бы (даже Kotlin’овский when поинтереснее), но это я по скаловскому скучаю. Впрочем, это все скорее вкусовщина.

Одна из крутых фич — все методы это по сути расширения. Т.е. x.pos(y) это сахар для pos(x, y). С точки зрения программиста на Cях со структурами, да и с точки зрения ФП-шника с ворохом DTO — очень удобно.

Еще крутая фишка — оптимизированные множества. Стандартный set — это битовый массив. Итераторы с yield — мое почтение, тоже очень удобно.

Есть и всякие плюшки для времени компиляции, например when или static: assert, но чтобы их по настоящему оценить, нужно видимо что-то мультиплатформенное делать.

Не понравилось, что объявление функции должно быть обязательно раньше его использования: не очень дружелюбно к косвенной рекурсии, да и организовать код в файле чуть-чуть тяжелее. Возможно, надо было разбивать код на модули, но я решил, что оно того не особо стоит: весь код занимает меньше 300 строк.

Прям неожиданно споткнулся об интервалы: они паскалевские/котлиновские, т.е. вместо привычного for i in 0..n надо писать for i in 0..<n. С одной стороны, более явно, чем “в устоявшемся” стиле, с другой — тяжело обнаружить классическую ошибку на единицу. В этом плане отстойно, что не было никакой проверки на выход за границы массива.

И еще один бесючий момент — жесткая сегрегация численных типов без неявной конверсии между ними, т.е. int, uint8, uint32 и т.п. Подобное было уже с Rust и Zig, и вроде в Nim чуть лучше, но все равно не нравится. Справедливости ради, один потенциальный баг может быть за счет этого был обнаружен.

С точки зрения инструментов, видимо, это модный тренд: делать менеджер пакетов, совмещенный с инструментом сборки. Да еще чтобы скрипты сборки были на этом языке (хотя по факту это мало отличается от Make с набором альясов для баш-команд).

В целом в Nim есть интересные фичи (но и сомнительные тоже), и ощущения после него остались приятные.

Разработка #

На все-про-все у меня ушло часов 10-11, и большую часть времени я потратил на установку рабочего окружения. Причем дважды: локально, чтобы не curl-sh (сесурити!) и в GitHub Actions (благословлен пусть будет act, без него было бы еще дольше).

Первую играбельную версию я получил всего через 2-3 часа после настройки окружения.

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

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

Наконец, подобрать более-менее пристойную картинку для логотипа тоже заняло время: найти AI-сервис генерации без регистрации и смс, выяснить, что с текстом все еще есть проблемы у старых моделей, а потом еще отмасштабировать картинку и с помощью ImageMagick снизить количество цветов до 4. Ну и поиграться с цветами, разумеется.

Из собственно алгоритмических вещей самой трудозатратной задачей было обеспечение того, что в игру можно выиграть. В ранних версиях этой проверки не было, и была возможна ситуация, когда пустое место в углу было перегорожено двумя минами. Я пытался придумать что-нибудь умное, чтобы влезть в “почти” константную память, но в итоге забил и оставил простой BFS. И была еще пара экспериментов, от которых я отказался.

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

Итого #

В целом у меня остались вполне позитивные впечатления от платформы и от Nim. Игрулей я доволен, и особенно я доволен тем, что очень быстро получилось что-то сделать и поиграть.