Предыстория.
Я не профессиональный it-разработчик. Пройдя два года назад онлайн курсы по программированию на java я, в свободное от основной профессиональной деятельности время, создаю так называемые pet-projects - небольшие телеграмм-боты, которыми, в том числе, активно пользуюсь сама.
Написав в 2023 году своего второго бота – картинную галерею (функционал которого был достаточно прост – выдавать пользователю по запросу примеры картин художников, экспонирующиеся в крупных российских музеях), во мне зародилась мысль трансформировать его в викторину, где пользователь должен угадать художника той или иной картины.
Полтора года глаза боялись, но в конце 2024 руки взялись-таки за его написание.
Техническое задание.
Функционал викторины классический:
- телеграмм-бот в случайном порядке должен присылать пользователю картины известных художников, которые сохранены в его базе,
- под картиной предлагаются кнопки с вариантами ответа,
- при выборе правильного ответа – присылается сообщение с результатом, названием картины и новая картина с вариантами ответов,
- если ответ неверный, пользователя об этом оповещают и предлагают либо выбрать другой вариант, либо нажать на соответствующую кнопку для получения нового вопроса-картины,
- если какая-то картина высылается повторно – варианты ответов должны выдаваться в иной последовательности, чем предлагались ранее,
- ведется статистика правильных ответов с возможностью ее обнуления,
- в перспективе – расширение тематики викторин.
Начало реализации.
Хранение вопросов и вариантов ответов к ним, работу с базой этих вопросов я реализовала через json:
1. Создала json-файл с массивом вопросов, где сам вопрос – это наименование файла с картиной (в формате .jpg, сами файлы хранятся в отдельной ресурсной папке), а варианты ответов на вопросы – отдельный массив.
Пример выдержки из json-файла:
2. Создала java-объект с полями, соответствующими полям объекта json-файла:
3. Используя библиотеку jackson прописала парсинг значений из json-файла для получения объектов:
Поскольку картин-вопросов у меня было 400, реализация выбора вопроса из коллекции вопросов рандомным способом мне представлялась достаточной. Для этого я написала метод:
Предложение вариантов ответа реализовано через InlineKeyboardMarkup:
Для получения значений полей объекта я использовала рефлексию. Чтобы реализовать вариативный порядок формирования вариантов ответов (в случае, если какая-то картина высылается повторно, чтобы варианты ответов выдавались в иной последовательности, чем предлагались ранее), использовала перемешивание элементов внутри массива:
Это то основное, что было написано довольно быстро.
После этого я прописала код класса-наследника TelegramLongPollingBot, где происходила обработка запросов пользователя, являющихся командами, текстовыми сообщениями или ответами Inline-клавиатуры. В этом же классе я проинициализировала получаемый объект и все его поля. Запустила бот и, на стадии моно-тестирования, все работало без нареканий: бот выдавал картинку, под ней - Inline-кнопки с фамилиями художников, при нажатии на которые происходила корректная обратная реакция – определялись верные и неверные ответы.
Возникшие трудности.
Первая трудность, которая возникла еще на стадии самотестирования бота, было то, что рандомный метод выбора вопроса из коллекции вопросов работал странно: из 400 объектов его словно замыкало на 10-20 и он навязчиво высылал картины в этом диапазоне, а порой выдавал одну картину 2 и более раз подряд.
Вторая трудность уже проявилась, когда я развернула бот на хостинге и предоставила его для использования широкой аудитории – бот работал со всеми пользователями как с одним. То есть, значения полей (правильный ответ, описание и пр.) были актуальными всегда для вопроса, который бот выслал последим независимо от того, что это за пользователь. Иными словами, получаю, например, я очередной вопрос от бота. Если до того, как я на него отвечу, какому-то другому пользователю бот тоже вышлет вопрос, то что бы я не отвечала – любой мой ответ уже будет неверным, т.к. в памяти бота правильный ответ привязан к тому вопросу, который он в принципе отправил последним, а не последним именно мне.
Изобретение собственного «велосипеда» с квадратными колесами.
Перебрав массу различных вариантов в пределах имеющихся знаний и информации в Интернет, я нашла следующее решение вышеуказанным трудностям.
Чтобы привязать бота к работе с запросами конкретного пользователя, при первом запуске программы каждым пользователем в ресурсной папке создается файл, в имени которого содержится id-пользователя и маркер «q» (определяющий, что это файл для вопросов). Этот же файл используется в дальнейшем для обеспечения уникальности присылаемых вопросов (без повторов, пока не будут отправлены все вопросы, имеющиеся в памяти бота).
Когда пользователь начинает викторину, в указанный файл записывается id-вопроса, отправленного ботом. В дальнейшем перед тем, как отправить пользователю новый вопрос, бот формирует список вопросов json-файла, сравнивает его со списком вопросов из файла пользователя и формирует список вопросов, id которых нет в файле пользователя. И вот уже из этого списка-разницы рандомно высылает пользователю новый вопрос, id которого тут же записывается в файл пользователя последней строкой.
Когда все вопросы из json-файла заданы, файл пользователя обнуляется и начинается все сначала.
Указанный «велосипед» с квадратными колесами обеспечил отправку вопросов без повторов в пределах имеющегося перечня и решил первую трудность.
Для решения второй трудности используется тот же файл с id пользователя и маркером «q» в названии. Для того, чтобы корректно обработать вызов Inline-кнопки, нажатой конкретным пользователем, необходимо верно идентифицировать, какой последний вопрос был ему задан. Для этого мы получаем последнюю строку с id-вопроса из файла пользователя и по извлеченному значению получаем вопрос из json-файла.
И уже у этого вопроса впоследствии определяем и получаем значение полей.
Прописав все эти методы для идентификации вопроса и его полей по каждому отдельно пользователю, в классе-наследнике TelegramLongPollingBot я создала метод отправки вопроса пользователю
и прописала обработку обновлений в методе onUpdateReceived () с вызовом метода отправки вопросов:
Ведение статистики правильных ответов пользователя я также реализовала через создание файла .txt, где в названии используется id пользователя и маркер («а», указывающий, что это файл с ответами). Если ответ правильный – в файл записывается цифра 1, если неправильный – срочная буква «w». При запросе статистики подсчитывается количество строк с цифрами и делится на общее количество строк в этом файле.
При необходимости, можно обнулить статистику, для чего прописала метод:
Итоги.
Вот такой получился «велосипед». Этот «велосипед» едет и пользователь даже не догадывается о его квадратных колесах.
Часто меня спрашивают – зачем я так подробно в своих статьях описываю те или иные процессы? Держала бы все при себе, пусть другие новички и дилетанты самостоятельно набивают шишки и мастерят свои поделки, авось бросят эти тщетные занятия и не будут покушаться на святая-святых - it-сферу. Но ведь я при написании своих pet-projects использую разъяснения альтруистов с различных ресурсов, так почему же не поделиться в ответ? – может быть кому-то мои варианты решений и пригодятся. А в "святая-святых", уверена, для каждого найдется место и своя ниша.
Поэтому надеюсь, этот разбор проекта, полный код которого выложен на GitHub, кому-нибудь поможет при разработке своих программ.
Если интересно, как фактически все отрабатывает, то милости прошу: игра "Викторина".
О.Блас