Сапер для вымышленной консоли на Nim
На выходных запилил игрулю, вариацию Сапёра.
Поиграть можно тут (с компа стрелочками удобнее, но на мобилах тоже работает), а исходники почитать — тут.
Про саму игру
Идея пришла в голову не слишком случайно. Недавно поставил себе классического сапера на планшет и залипал в него. Но с “аккордеонами” (которые рекурсивно открывают соседние ячейки если все однозначно) игра превращается в тупейший кликер на скорость. Прям думать-думать надо раза 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. Игрулей я доволен, и особенно я доволен тем, что очень быстро получилось что-то сделать и поиграть.
Ограничения и предохранители
Зацепились языками с одним из коллег на тему удобства языка для разработки без IDE. Он заявил, что код на Kotlin невозможно читать без IDE. Вполне органично дискуссия перешла в плоскость того, полезны ли ограничения языка или нет.
Началось все с тезиса про то, что структура файлов в Java (1 файл на 1 класс, 1 папка часть пакета) это полезно и позволяет находить нужный класс быстрее. А вот в Kotlin можно как угодно делать. Поэтому ограничения — это хорошо, они приводят к порядку и простоте. В качестве дополнительных примеров были модификаторы видимости (C против Java), владение (C vs Rust) и т.п. Да и вообще, ограничения формируют/способствуют культуре разработки.
Когда я пытался аргументировать, что ограничение на структуру файлов — весьма дурацкое и особо не помогает, мой оппонент подумал, что я его троллю:) Так-то класс может быть в разных модулях, может быть вложенным и т.п., т.е. это не просто “иди по этому пути”, и хоть какой-то поиск придется рано или поздно использовать.
Весь спор пересказывать не буду, а перейду сразу к выводам.
Я понял, что есть 2 принципиально разных аспекта в языке: ограничения и предохранители. Предохранители — это обычно просто замечательно, они позволяют избегать тупых ошибок. Например, явная проверка на null в Kotlin или владение памятью в Rust — это предохранители. Ограничения — это противоположность выразительности, т.е. что-то, что мешает сделать программисту то, что он хочет. “Я ограничен технологиями своего времени”.
Соответственно, ограничение и предохранитель легко спутать друг с другом (или дебатировать, что хорошо и что плохо). И вот тут-то и зарыт корень наших разногласий: мой коллега искренне считал, что структура папок в Java — это предохранитель и это хорошо, а для меня это еще одно ограничение. В этом плане мне гораздо важнее разложить код по модулям, по ролям и/или по шаблону какой-нибудь трехбуквенной архитектуры. Обычно языку на это плевать, но в Java будет привет от com.company.name.package.folder
.
Мне нравятся выразительные языки. На мой взгляд, хреново и непонятно можно написать на любом языке. Да, в более выразительном языке возможностей может быть больше, но человеческая тупость вообще бесконечна. В свою очередь, выразительный язык предоставляет больше возможностей написать код хорошо. А предохранители должны этому способствовать.
И разумеется, опыт в более выразительных языках влияет на то, как разработчик пишет код. Однако говорить о том, что язык формирует культуру разработки — очень странно. Это как сказать, что ножницы формируют культуру парикмахерской, или молоток формирует культуру стройки. Если у вас инструмент формирует процессы и/или подходы — у меня для вас плохие новости…
Комменты телеграма на сайте и похвала роботам
На выходных удалил комментарии от телеги с сайта (которые comments.app). API и возможности администрирования с 2020 года совсем не изменились: например, до сих пор нельзя посмотреть все комменты на сайте. Да и не пользовался ими никто — ровно один комментарий был не от меня. Старые комменты спарсил краулером, написанным ChatGPT.
Нашел как подключить комментарии из канала. Повспоминал свои боли с Jekyll. С точки зрения самой телеги оказалось довольно нетривиально понять, как сделать так, чтобы была ссылка вела сразу на дискуссию с комментариями, а не тупо на пост. В итоге тупо вставил ссылку в конце поста, на компе работает, но на планшете — нет (не открывает комментарии, только пост). ¯\_(ツ)_/¯
Увы, нельзя комментировать старые посты, потому что для них нет соответствующего поста в группе обсуждений. Ну и фиг с ними, все равно их никто не читает :) Паравозиком приехала ссылка на комменты с Хабра (тоже не знаю зачем, даже я уже Хабр практически перестал читать).
Осталось дело за малым — добавить везде ID поста в телеге. Это около 250 файлов, и там не просто подряд числа. Тут очень хорошо повкалывал ChatGPT, прям аж похвалить его захотелось: всего за 3-4 итерации генерировал рабочие решения на питоне, причем мне даже особо не нужно было читать код.
В итоге все посты из телеги сохранил через экспорт канала (с доступом через API нахвался уже), потом сопоставил их с постами с сайта по заголовку. Часть файлов обновил руками — там заголовки не совпадали или были проблемы из-за кавычек. На все-про-все ушло минут 10, и это классно.
Немного напортачил с переносом строк. Хотел поправить башем, но ChatGPT внезапно отупел и его предложения не работали. Оказалось, что кончились токены на 4 версию. Т.е. если предыдущие версии я в основном ругал, то от четвертой в основном положительные впечатления остались.
Еще я сделал тупую опечатку, которую нашел не сразу. В итоге больше всего потратил на обработку крайних случаев, тупую опечатку и приколы с экранированием. Иронично, что первый пост, к которому можно оставить коммент — “Самые сложные проблемы в разработке” (ладно, для душнил есть еще пост про открытие комментариев). Но в целом результатом я доволен, наконец-то почувствовал продуктивность с ChatGPT. Сам подход напомнил скачивание архива — тоже из говна и палок, “лишь бы работало”.
Профдеформация и C
C/C++ (именно в таком сочетании) я в базовом варианте изучил за пару летних месяцев перед универом. Тогда для меня он был просто заменой паскалю, и уровень задач был соответствующий — всякая мелочевка для развлечения и разнообразные числодробилки. Универ со своими лабами не сильно что изменил (я получил автомат по программированию в те времена, когда по всем предметам надо было сдавать экзамены и не было балльной системы); Python, JavaScript и даже Java мимо пробегали, но всякие тесты булевой функции на монотонность проще писались на C/C++.
На первых двух работах мне даже платили за то, что я писал на C++, но я тогда был джуном и это все сейчас кажется несерьезным (хотя уже будучи зрелым специалистом написал на C++ модуль ядра для BSD, это был прикольный, но мимолетный опыт). Тем более что потом переключился на Python, оттуда на Scala и понеслось…
В общем, получается, что всерьез, профессионально, на Си я и не писал никогда. А вот в последние несколько месяцев я как раз этим и занимался в рамках научного проекта. И (вполне закономерно) оказалось, что процесс не сильно-то отличается от других языков.
Все стандартные паттерны в наличии, например:
- разделение интерфейса (
.h
) и реализации (.c
) - своего рода полиморфизм можно построить на структурах и указателях на функции
- разделение
api
иimpementation
в зависимостях (где включать заголовок — в.c
или.h
) - всякие билдеры, стратегии и фасады — это вообще легко
typedef
— one love ❤️- даже шаблоны можно сделать, правда макросами
- и т.п.
При этом возникает понимание многих конструкций, которые казались “лишними”: extern
, static
, макросы (увы, некоторые прикольные штуки только ими и получается делать), префиксы для функций из разных модулей (даже в не очень большом проекте словил коллизию имен, неймспейсов не хватает), #ifdef DEBUG
и отдельная сборка под valgrind
или санитайзеры (потому что без отладочного -g
особо и не отладишь утечки памяти, а еще valgrind
не знает всех инструкций из -march=native
и может даже врать про номера строк на -03
). Более того, -Wall
выдает все замечания по делу! Хотя inline
с его приколами все еще невнятный какой-то :/
В более высокоуровневых языках обычно многие вещи делаются гораздо проще (но не во всех конечно *выразительно смотрит на java*), да и “мыслишь” после них более абстрактно. Часто себя ловил на мысли, что вот тут лямбду надо бы, а их особо и нет (только указатели на функции), а вот тут хотелось бы иметь возможность тип менять (вместо void *
), а вот тут частичное применение функции прям зашло бы… Не хватает простых вещей типа Option
, а указатели уже не хочешь использовать, потому что дешевле структуру передать.
Увы, инструменты разработки — не сильная сторона Си. Makefile
еще можно потерпеть, autoconf
сотоварищи — просто жесть, пакетный менеджер — мимо, VS Code опять выбесил по какой-то фигне, а добил меня миллион настроек clang-format
, после которых я “обманываю” форматтер пустым комментарием, чтобы не совсем отвратно выдавал список аргументов функции. Впрочем, ничего нового (осторожно, по ссылке кринж). После космических технологий вроде IntelliJ Idea или Gradle — все очень грустно.
При этом язык все еще развивается. Например, весьма пригодились составные литералы:
return (some_struct_t){
.field1 = value1,
.field2 = .value2, // trailing comma FTW!
}
Сейчас есть стандарт c17
, а еще грядет c23
— и там есть много прикольных штук, про многие из которых я могу сказать: да, такая фича пригодилась бы! Даже лямбды маячат на далеком горизонте, но добавить их — непростая задача.
В общем, писать что-то на низкоуровневом языке достаточно интересно (если это не Zig :)). Это полезное упражнение, чтобы понять, как много делают всякие хорошие инструменты и библиотеки, да и собственный прогресс оценить. “Вернуться к истокам” тоже прикольно. Когда писал пост, откопал в папке со своей универской фигней вот такую хрень, которая датируется 2010 годом:
#include <stdio.h>
class foo
{
friend bool operator < ( bool left, const foo& right );
friend int operator ^ ( int left, const foo& right );
};
bool operator < ( bool left, const foo& right ) { return left; }
int operator ^ ( int left, const foo& right ) { return left; }
int main()
{
int O_o = 0, _ = 0, baka = 0; foo o_O, neko;
bool XD = O_o >_< o_O;
int nya = baka ^_^ neko;
//printf("%d ",nya);
printf("%d ",XD);
return 0;
}
Очевидно, все вышеизложенное хорошо так субъективизировано связанными воспоминаниями из тех времен :) Но даже с учетом этого впечатления останутся положительными.
Что бы там не пророчили, кажется, что Си пока рано умирать.
Мой профессиональный рост
Первые две свои работы я нашел “по знакомству” благодаря универу. В своем первом “настоящем” резюме я чисто ради кеков указал:
∏ρ؃uñçτØρ Øπτµç∑ принял от меня 6 постов, если это важно.
Большинство потенциальных работодателей на это не обращали внимания (кто ж читает эти резюме), некоторые не знали, что это вообще, пара человек удивились и похвалили. Но строчку все равно я оставил, только счетчик постов увеличивал.
Потом мою смешнявку админ профунктора не просто принял, но еще и репостнул себе в личный канал. Не зная, как совладать с этим, я жахнул на сайтик галерею со своими картиночками.
Потом еще сделал альтернативный рейтинг смешнявок, чтобы компенсировать свое падение в его рейтинге с 9 места на 26-ое. Скинул админу, он заценил, но, увы, время было совсем неподходящее для таких развлекух, обещал, что позже постнет. “Позже” не наступило… Между тем оригинальный рейтинг уже не грузится, а мой еще работает.
После этого завирусилась шутка про говно.
По крайней мере я ее видел очень много где, и в недружественном интернете тоже.
В последнем резюме я кеки уже не указывал: место надо экономить, да “и не солидно” же. Но через год работы мой мем используют для рекламы нашего платного продукта. На сайтике, я, разумеется, оставлю первую версию:
Успех, чо :)