В ноябре Яндекс организовал интереснейшую игру по случаю 19-летия Кинопоиска. Выглядела она вот так:

Главная страница игры
Главная страница игры

Было представлено 6 игровых категорий: Кадры, Цитаты, Описания, Мемы, Вселенные и Нейропостеры. Во всех заданиях игроку предлагается определить к какому фильму/сериалу относится картинка/текст. Игроку необходимо удержать как можно более длинную серию правильных ответов. На каждую попытку дается 3 жизни и 10 секунд на каждый ответ.

Пример задания из категории "Кадры"
Пример задания из категории "Кадры"

После ответа игрока есть три сценария:

  1. Ответ был правильный. Плашка загорается зеленым цветом и через пару секунд появляется следующий вопрос.

  2. Ответ был неправильный. Плашка загорается красным цветом, появляется модальное окно, содержащее правильный ответ и кнопку "Продолжить".

  3. Ответ был неправильный и это была последняя попытка игрока. Плашка загорается красным цветом, появляется модальное окно, содержащее счет игрока, правильный ответ и кнопку "Играть снова".

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

И тут меня осенило! Ведь можно реализовать алгоритм, который будет "учиться" за тебя.

Решаем задачу

Дано: базовые знания Python, документация Selenium и по часу свободного времени каждый день после работы

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

Сценарий работы алгоритма был следующий:

Для поиска нужных элементов в коде страницы использовались Xpath-выражения. Вот пример кода, который ищет все кнопки начала игры (у каждого из эпизодов) и складывает их в массив:

xpath_expr = "//button[contains(text(),'Играть') or contains(text(),'Новый эпизод')]"
elements = WebDriverWait(self.driver, timeout=5).until(lambda d: d.find_elements(By.XPATH, xpath_expr))
print(f'Найдено {len(elements)} карточек с заданиями')

Для примера разберем игру в эпизод "Кадры". После того как игра началась, нам нужно проверить, нет ли ссылки на картинку в нашей базе ответов.

xpath_expr = "//img[contains(@class,'game__test-image-img')]"
task = WebDriverWait(self.driver, timeout=5).until(lambda d: d.find_element(By.XPATH, xpath_expr))
task_key = task.get_attribute("src")
answer = r.get(task_key).strip() if r.get(task_key) is not None else None

Если ответ есть в базе - отвечаем, если нет - пробуем нажать на первый вариант. Вариант можно было выбирать случайно или даже провести аналитику ответов и посмотреть какой встречается чаще, но для простоты остановимся на первом.

first_ans = list(answers_dict.items())[0]
self.driver.execute_script("arguments[0].click();", first_ans[1])
is_success = self.answer_is_success()

После того, как мы нажали на кнопку есть два сценария:

1) Ответ был правильный

Отлично! Запоминаем его

if is_success:
    answer: str = first_ans[0]
    r.mset({task_key: answer})

2) Ответ был неверный

Парсим окно, которое сообщает о том, что ответ был неправильный. В нем нас интересует правильный ответ, который игра нам любезно сообщает и кнопка "Продолжить" (в случае если у нас еще остались жизни) или кнопка "Играть снова" (в случае если жизней нет).

else:
  answer, resume_button = self.find_end_modal()
  r.mset({task_key: answer})
  self.driver.execute_script("arguments[0].click();", resume_button)

Вот и все! Простейшая задачка на алгоритмы, которую украсили особенности работы с Selenium и парсингом динамических страниц. Для ускорения обучения я кидал скрипт друзьям. Это превращало игру в своего рода "майнинг" ответов :)

Скрипт часто вылетал, появлялись непредвиденные краевые случаи, но в целом удалось добиться неплохих результатов!

Результаты

В начале статьи я говорил, что мои результаты не дотягивали до розыгрыша. Вот и они:

А теперь взгляем на успехи алгоритма.

3125 очков! И это с двумя недоступными на тот момент, эпизодами. Со временем счет увеличивался все медленнее и медленнее. Находить ответы становилось сложнее из-за того, что одному неправильному ответу могла предшествовать серия из 2000 правильных. К сожалению метрики я не отслеживал, но могу предположить, что количество ответов, которые мы находим за час игры соответствовало бы зависимости 1/x, где x - прошедшее время в часах.

Спустя несколько дней количество ответов на первые четыре эпизода было следующим:

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

Комментарии (0)