игра «Гопник-2»
Приступ «гопникомании»
Постоянные читатели могут помнить, что я являюсь поклонником двух текстовых квестов «Гопник», написанных неизвестными авторами в нулевых. Я очень плохо отношусь к этой субкультуре, тем не менее эти квесты
Исходный код оригинальной игры, по всей видимости, канул в лету, но мне
Недавно в мой репозиторий пришёл некий активный гражданин и навёл там суеты, за что я ему благодарен. В частности, он подчеркнул
Слава нейросетям, всё это удалось довольно быстро исправить. В жизни я постоянно ворчу
Тем не менее, как мне кажется, с нейросетью я делал исправления куда быстрее, чем это вышло бы у меня без неё. Кстати, очень здорово выручают тесты — если попросить нейросеть написать тест, а потом добиться чтобы он заработал (или хотя бы не сломался), то участие человека требуется минимальное.
Зачесались руки
Марковские цепи
Сейчас время колоссального интереса к нейросетям, видимо поэтому меня не оставляет мысль вставить в игру
Но вставлять в игру, занимающую около мегабайта, любую, даже самую маленькую БЯМ (большую языковую модель) — странная идея. Размер возрастёт в десятки раз, а фана будет — чуть.
Поэтому я решил попробовать использовать марковские цепи. Марковская цепь — это, если очень упрощать, состояния и вероятности перехода между ними.
Засовываем в неё готовый осмысленный текст, цепь подсчитывает вероятности перехода между токенами (чаще всего это слова) и дальше её можно попросить продолжить произвольный текст на ту же тему.
У меня была идея скормить ей достаточное количество разговоров из книг «Слова пацана» и «Казанский феномен: миф и реальность», чтобы получать
Конечно, я посидел над этом идеей всего один вечер, но, кажется, качественного скачка не произойдёт, сколько не сиди. Удачные диалоги встречаются, но чаще всего видно, что модель мира там отсутствует полностью.
Зато, когда я скормил цепи тексты матерных загибов, получился довольно качественный генератор мата, правда с очень архаичным языком. Там, среди отборного мата, попадается упоминание небесного царя, архиереев и прочей религиозной атрибутики. Что неудивительно, конечно, — этим загибам не одна сотня лет.
Примеры приводить я не буду, мне
Не знаю, продолжать ли заниматься этой идеей или нет. Пока не решил. В игре есть несколько багов, которые тянутся ещё из оригинального кода, можно было бы их поправить вместо этого.
PVS-Studio и «Гопник-2»
Много читал об инструменте статического анализ
Нашлось несколько интересных ошибок. На скриншоте одна из них. Как видно, str_enemy_amount проверяется два раза, тогда как после первого условия там гарантировано ноль. Очевидно автор этого куска кода его скопипастил, но забыл во втором куске исправить enemy на hero.
Я бегло посмотрел все ошибки, которые он мне выдал, надо будет внимательно посмотреть каждую, но как будто бы всё по делу, ноль ложных срабатываний.
Накрыло «Гопником-2»
Пока играл, нашёл и поправил несколько незначительных багов, всё
«Гопник-2»: убрал printf
Закончил убирать функцию printf, сегодня вколотил последний гвоздь в крышку её гроба. Я
В процессе замены встретилось вот такое. Загадка, всё ли хорошо в следующем коде?
std::cout << fmt::format(" {}(+{})", main_hero->inv[i].name, main_hero->inv[i].armo)Мой компилятор считает, что всё, но сборка на «Гитхабе» падает с ошибкой «Binding packed field error».
Получается, я по неведомой причине не могу обратиться по ссылке на поле внутри упакованной структуры (fmt::format принимает параметры по ссылке).
Структуру я пакую, чтобы нивелировать различия на бинарном уровне между компиляторами, — так как я её сохраняю и восстанавливаю как есть. В общем, от упаковки не отказаться, пришлось присвоить значения промежуточным переменным:
const auto name = main_hero->inv[i].name;
const auto armo = main_hero->inv[i].armo;
std::cout << fmt::format(" {}(+{})", name, armo);Выглядит, как костыль, но самое главное — почему один компилятор требует промежуточные переменные, а другой умеет и без них?
Я бы понял, если бы «Гитхаб» собирал мне бинарники
«Гопник-2»: что нового?
Можно подумать, что я совсем забросил заниматься игрой «
Недавно появившиеся контрибьюторы добавили мне в проект библиотеку fmt, что упростило работу, —
Осталось примерно 260 замен, не так много, было около 400 или около того.
Ещё ребята постарались добавить мне тесты. Я ещё не смотрел, но планирую ознакомиться с тем как это делает. Так как на Си++ я раньше не писал, то совершенно без понятия как они тут выглядят, любопытно.
«Гопник-2»: контрибьюторы
Фантастика! У меня на проекте буквально за эти выходные появилось ещё два контрибьютора.
deadem перевёл проект на сборку cmake, принёс библиотеку форматирования fmt и приделал к нему CI «Гитхаба», теперь сборка идёт прямо там. kartaris убрал fmt в зависимости, принёс менеджер пакетов conan и дописал код для использования в нём fmt.
Это для меня новый интересный опыт — у меня не было возможности поработать на «Гитхабе» с контрибьюторами, да и само по себе приятно, что к проекту подключились новые люди и затащили в него достаточно сложные штуки.
Си++: форматированный вывод
Си++, похоже, язык для воспитания героев — создаём трудности на ровном месте и начинаем их преодолевать. В моём случае началось всё с разумной, как мне до сих пор кажется, идеи — заменить сишные безобразные строки char * на прекрасный класс std::string из Си++.
В процессе в комментариях мне написали, что вместо него надо использовать std::string_view, чтобы избежать копирования. Так я узнал, что за строки в Си++ отвечают не один класс, а четыре — есть ещё базовые классы, с приставкой base.
Заодно пришлось перейти на стандарт 2017 года, так как до строк с семантикой перемещения додумались только недавно.
Всё шло хорошо, пока я не дошёл до форматирования текста. В Си этим занимается функция printf, а Си++ есть метод std::format, но вот беда — он появился только в стандарте 2020 года и его ещё нет ни в одном из компиляторов, которые есть у меня под рукой.
Пошёл на поклон к ЧатГПТ, который выдал мне такую обёртку:
template<typename... Args>
std::string string_format(const std::string format, Args... args) {
int size_s = std::snprintf(nullptr, 0, format.data(), args ...) + 1; // Extra space for '\0'
if (size_s <= 0) {
throw std::runtime_error("Error during formatting.");
}
auto size = static_cast<size_t>( size_s );
std::unique_ptr<char[]> buf(new char[size]);
std::snprintf(buf.get(), size, format.data(), args ...);
return {buf.get(), buf.get() + size - 1}; // We don't want the '\0' inside
}Это так называемый «шаблонная функция». Насколько я пока понимаю принцип, компилятор смотрит с каким типами параметров вызывается эта функция, и делает их столько штук, сколько есть комбинаций вызовов.
Так как файлы с кодом друг друга не видят, чтобы шаблонная функция «знала» с какими параметрами её вызывают, её надо помещать в заголовочный файл (🤦🏻♂️), либо указать в коде все необходимые прототипы вручную (🤦🏻♂️🤦🏻♂️🤦🏻♂️).
И хотя я осознаю, что со своим уставом в чужой монастырь не лезут, первое мне кажется противоестественным, а второе — неудобным.
На этом этапе я выяснил, что мой компилятор под «Виндоуз» ничего не знает про std::string_view, а компилятор под «Мак» не может скомпилировать код выше вот с такой прекрасной ошибкой:
comm/../utils.hpp:30:60: error: cannot pass object of non-trivial type 'std::string' through variadic function; call will abort at runtime [-Wnon-pod-varargs]
int size_s = std::snprintf(nullptr, 0, format.c_str(), std::forward<Args>(args)...) + 1;Мне бы хотелось, чтобы код собирался под оба компилятора, поэтому я снова пошёл к ЧатГПТ и вернулся вот с таким прекрасным кодом:
template<typename... Args>
std::string string_format(const std::string& format, Args&&... args) {
auto size_s = std::snprintf(nullptr, 0, format.c_str(),
std::decay_t<Args>(args).c_str()...) + 1; // Extra space for '\0'
if (size_s <= 0) {
throw std::runtime_error("Error during formatting.");
}
auto size = static_cast<size_t>( size_s );
std::unique_ptr<char[]> buf(new char[size]);
std::snprintf(buf.get(), size, format.c_str(),
std::decay_t<Args>(args).c_str()...);
return {buf.get(), buf.get() + size - 1}; // We don't want the '\0' inside
}Тут происходит
comm/../utils.hpp:32:58: error: request for member 'c_str' in 'args#0', which is of non-class type 'std::decay_t<char*>' {aka 'char*'}
std::decay_t<Args>(args).c_str()...) + 1;К счастью, решилось это просто — надо было всего лишь убрать .c_str().
По пути там пришлось решить ещё пару забавных мелочей, но они недостаточно отложились в памяти, чтобы я их мог воспроизвести без опасения
Теперь я понимаю за что программистам на Си++ платят такие деньги — это доплата за нервную работу.
Бесплатный CLion?
Два дня до момента, когда моя среда разработки, где я мучаю Си++, превратится в тыкву. С преподавательской лицензией меня обломали, попробую другой путь — податься на бесплатную лицензию как бесплатный проект.
Казалось бы дело в шляпе, но там надо показать регулярные коммиты за три месяца, а у меня пока ещё и месяц не прошёл. Но я придумал план, надёжный, как швейцарские часы, — два месяца рефакторинга. Буду перетаскивать очевидные сишные части на Си++. Начал со строк — переписываю сишные указатели на нормальный std::string.
Придётся, видимо, пока взять в качестве редактора VSCode, вот только он не IDE. Ну а какой выбор? В процессе знакомлюсь с тем, что умеет Си++. Смотрите какая прелесть:
std::ostream &operator<<(std::ostream &os, const Colors &color) {
static const std::map <Colors, std::string> colorNames = {
{RESET, "39;49m"},
{BLUE, "01;34m"},
{GREEN, "01;32m"},
{CYAN, "01;36m"},
{RED, "01;31m"},
{MAGENTA, "01;35m"},
{YELLOW, "01;33m"},
{WHITE, "01;37m"},
{BLACK, "01;30m"},
};
os << "\033[" << colorNames.at(color);
return os;
}В Си это была большая функция с оператором switch, которая выводила на экран последовательности для переключения цвета, её надо было вызывать отдельно каждый раз. На Си++ всё удобнее:
std::cout << YELLOW << mess[2] << std::flush;Рраз и строка жёлтая. YELLOW тут у меня — значение перечисляемого типа Colors.
«Гопник-2»: демотивировался
Покупать мне её не хочется —
Дополнено: не дали:
Thank you for applying for the JetBrains Product Pack for Students.
We regret to inform you that we wouldn’t be able to provide a license due to EU and/or US export restrictions.
JetBrains as a company has to comply with all EU and US regulations and eliminate all possible risks coming out of it.
«Гопник-2»: переход на нормальный Си++11
Как я писал прежде, когда я адаптировал
Тут у нас в Татарстане выдались длинные выходные, — в праздник
Вызов _fileno у меня использовался для получения номера дескриптора входного потока, так что тут всё решилось просто, — для этого есть специальная константа STDIN_FILENO. А вот с strdup пришлось повозиться.
Сначала я, разумеется, сделал свою реализацию — там
Первым делом пошёл на поклон к «ЧатГПТ», спросил совета. Советы не заработали, но дали направление мысли.
Первая мысль была забахать макрос, который заменял бы собой strdup в проблемном компиляторе, а остальные не трогал. Тут надо сказать, что под «Виндой» далеко не во всех наборах утилит компиляторы не содержат функцию strdup, например в msys2 она есть.
Мне хотелось
Пришлось пошарить по исходникам. Оказалось, есть ряд макросов, которые присутствуют в msys2, но отсутствуют в msys. Из них я выбрал __MINGW64_VERSION_MAJOR:
#if defined(__MINGW32__) && !defined(__MINGW64_VERSION_MAJOR)
#define strdup(s) (__extension__({ const char* __s = (s); size_t __l = strlen(__s) + 1; char* __d = (char*)malloc(__l);\
__d ? (char*)memcpy(__d, __s, __l) : nullptr; }))
#endifТут используется расширение, которое позволяет писать многострочные макросы. Хотя этот вариант мне и нравится, но опять используется не нормальный Си++, а расширение компилятора.
Вторая мысль, на которую меня навела нейросеть — использовать слабые символы языка Си++. Это совершенно новая для меня штука, поэтому нужно небольшое пояснение. Прежде всего для меня же, чтобы я в будущем не забыл что это вообще такое.
Если упрощать, то символы — это экспортируемые именованные сущности. Те, что
Слабые символы используются для библиотек, чтобы пользователь мог заменить некоторые сущности, если они его не устраивают.
Если перевернуть ситуацию, и считать мой код библиотекой, а тот код, где должна быть определена strdup основным, то всё должно получиться.
«ЧатГПТ», к сожалению, помочь не смог, — сначала подсовывал нерабочий код, а потом зациклился в своих советах. Поэтому пришлось разбираться самостоятельно.
Всё оказалось не так уж и сложно. Сначала в заголовочном файле определяем прототип функции:
#ifdef __MINGW32__
char *strdup(const char *s1);
#endifЯ его спрятал под условие, чтобы мой редактор перестал ругаться на избыточное определение, но всё работает и без этого.
Потом в самом коде создаём слабый символ для strdup:
#ifdef __MINGW32__
#pragma weak my_strdup
char *strdup(const char *src) {
size_t len = strlen(src) + 1;
char *s = (char *) malloc(len);
return s == nullptr ? nullptr : (char *) memcpy(s, src, len);
}
#endifСлабый символ я определяю через директиву #pragma weak, у которой надо указать имя слабого символа, причём оно должно отличаться от имени функции. Я пока не до конца понял зачем это нужно делать, но без этого не работает.
Кстати, #pragma weak тоже расширение, но оно легко заменяется на атрибут weak, надо только будет изучить его синтаксис, я не успел этого сделать.
На имя my_strdup у меня ругается редактор — говорит, что слабый символ нигде не определён, поэтому я опять же засунул всё это в условную компиляцию.
В выходные, как всегда, есть куча дел, кроме хобби, поэтому до конца я разобраться не успел и буду рад, если
Дополнено: в итоге все ситуации я побороть не смог и откатился на самое простое решение со своей собственной реализацией strdup.
«Гопник-2»: новости
Может показаться, что после релиза
Как можно догадаться по этой заметке, это не так. За последнее время я исправил несколько незначительных багов и реализовал
Я долго не понимал как во внутреннем АПИ игры формируются
Шокер может вывалиться из одного из неигровых персонажей и немного оживляет бои, а то они очень уж скучные.
Думаю добавить в ларёк ещё пару предметов, а так же новую локацию. Так как пока не уверен стоит ли её добавлять, не буду писать какую. После всех добавлений выпущу следующий релиз.
В общем, игру потихоньку развиваю, хоть фантазия
«Гопник-2»: версия 1.24
У меня тут небольшая веха в игре
Там универсальный бинарник под МакОС и статически собранные файлы
«Гопник-2»: теперь «Андроид»
В полку не совсем обычных платформ, где я попробовал собрать
Решил попробовать скомпилируется ли. В принципе проблем почти не возникло. Всё необходимое поставил командой pkg install make clang git, после чего выписал проект и поправил путь до заголовочного файла — в «Андроиде» он находится в другом месте.
Правки в репозиторий внёс, теперь всё будет собираться ещё и под «Андроид». Проверять в будущем я не буду, вряд ли
«Гопник-2»: фан-клуб
Тем временем, у игры
Когда будет готово, надо будет это должным образом оформить, снабдить скриншотами и перенести в вики проекта.
В принципе уже почти всё готово,
Надо бы поторопиться, а то бесплатная лицензия на среду разработки кончится в начале мая, не так много времени осталось, дальше разрабатывать будет сложнее.
«Гопник-2»: разные mingw
Надо
Сильнее всего меня удивило в процессе, что поведение mingw, — инструментария для компиляции приложений под «Виндоуз», различается под разными операционными системами.
Поскольку мне не хотелось заморачиваться с установкой софта на виртуальную машину, я развернул mingw у себя под «Маком» и внёс правки, чтобы всё благополучно компилировалось именно на этой ОС.
Запустив получившееся на первой попавшейся виндовой виртуалке, я успокоился. Но оказалось этого недостаточно — под самой «Виндоуз» приложение не собиралось!
После этих правок игра начала собираться под «Виндой» тоже.
Недавно прочитал в одной из статей на Хабре, что «язык Си++ достаточно сложен, чтобы позволить нам писать на нём просто», как же это верно! Неожиданно много времени занимает борьба
«Гопник-2» на Haiku
К сожалению, компания, которая её писала, приказала долго жить, передала исходный код сообществу, которое до сих пор потихоньку пилит его уже под новым именем — Haiku.
А мне тут
Сразу у меня это сделать не получилось, но по ошибкам я догадался, что не хватает
Вот как выглядит рецепт компиляции и запуска:
pkgman install gcc make haiku_devel
git clone https://github.com/bolknote/gopnik2.git
cd gopnik2
make
./gop2Нужные мне библиотеки лежали, как оказалось, в пакете haiku_devel, сложно было догадаться, хорошо, что нашлись те, кто смог подсказать.
«Гопник-2»: UTF-8
Продолжаю писать про портирование
Я уже очень давно не работаю под этой операционной системой, поэтому некоторые обычные, возможно, для многих вещи для меня являются открытием. Когда я начинал процесс портирования игры под эту платформу, я был уверен, что столкнусь в трудностями совершенно в других частях кода. Одной из неожиданностей была плохая работа «Виндоуз» с кодировкой
Если вывод в консоль удалось победить сравнительно легко, то со вводом возникли проблемы — вызов SetConsoleCP(CP_UTF8), который должен был помочь, не помог. В интернете пишут, что он, по всей видимости, не реализован — просто не делает ничего.
Если с латинскими буквами проблем не возникает, они кодируются одним байтом, это ничем не отличается от того как программы до этого работали десятилетиями, а вот с русскими буквами беда — для них требуется два байта и в таком виде Виндоуз их не понимает.
Рекомендуемый специалистами способ — переключить консоль в режим ввода кодировки
#ifdef __MINGW32__
int wlen = 100;
int save = _setmode(_fileno(stdin), _O_U16TEXT);
wchar_t *wstr = (wchar_t *) malloc(wlen * sizeof(wchar_t));
fgetws(wstr, wlen, stdin);
int len = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, 0, 0, 0, 0);
user_name = (char *) malloc(len);
WideCharToMultiByte(CP_UTF8, 0, wstr, -1, user_name, len, 0, 0);
_setmode(_fileno(stdin), save);
free(wstr);
#endifМне, к счастью, ввод русских букв нужен только в одном месте — там где игрок вводит своё имя. Я убрал используемую там функцию fgets под условную компиляцию и вставил кусок, который выше. С таким исправлением всё заработало, как ожидается.
«Гопник-2»: буферизация
Как я писал уже сегодня, мне пришлось вчера выключить буферизацию вывода, так как текст визуально «застревал» на экране. Добавление fflush проблему решало, но не будешь же выставлять этот вызов везде, где есть вывод, это несколько сотен мест.
В общем, вчера я сделал попроще, а сегодня пришла в голову более удачная идея обернуть printf в макрос:
// включаем буферизацию вывода
setvbuf(stdout, nullptr, _IOFBF, 1000);
// делаем макрос со сбросом буфера
#define PRINTF(...) { printf (__VA_ARGS__); fflush(stdout); }Работает просто отлично, вывод текста стал выглядеть комфортно.
«Гопник-2»: адаптация под Windows
В итоге, вчерашняя мысль адаптировать
Несмотря на то, что «Чат» у меня последней, четвёртой версии, всё равно, даже на простых примерах он допускает совершенно детские ошибки, — не подключает нужные заголовочные файлы (мы программировали на Си), упускает важные части.
На скриншоте у меня как раз такой пример — он не скомпилируется
Вот как в итоге стал выглядеть этот кусок после нескольких уточняющих вопросов и гуглежа:
#ifdef __MINGW32__
HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);
DWORD mode = 0;
GetConsoleMode(hStdin, &mode);
if (echo) {
mode &= ~ENABLE_ECHO_INPUT;
} else {
mode |= ENABLE_ECHO_INPUT;
}
SetConsoleMode(hStdin, mode);
int ch = _getch();
if (ch == 0x03) // Ctrl+C
{
printf("Ctrl+C hit, exiting...\n");
gracefulexit();
}
return ch;
#endifКак бы то ни было, с помощью нейросетей
#ifdef __MINGW32__
SetConsoleCP(CP_UTF8);
SetConsoleOutputCP(CP_UTF8);
setvbuf(stdout, nullptr, _IONBF, 0);
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
DWORD mode = 0;
GetConsoleMode(handle, &mode);
SetConsoleMode(handle, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
#endifПришлось полностью выключить буферизацию, чтобы нормально отрабатывал printf с
Насколько я понял то, что написано в интернете, у консоли Виндоуз до сих пор большие проблемы с кодировкой
Из заметных багов — игра падает при попытке загрузки сохранения и совсем не работает ввод русских букв. Второе мне нужно только одном месте (где запрашивается ввод имени игрока), так что есть надежда это починить. Остальное как будто работает.