Журнал

Сапер для вымышленной консоли на 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-ое. Скинул админу, он заценил, но, увы, время было совсем неподходящее для таких развлекух, обещал, что позже постнет. “Позже” не наступило… Между тем оригинальный рейтинг уже не грузится, а мой еще работает.

После этого завирусилась шутка про говно.

По крайней мере я ее видел очень много где, и в недружественном интернете тоже.

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

Успех, чо :)

СсылкаКомментировать