Как написать бота для игры в браузере

Обновлено: 06.07.2024

Давно думаю попробовать написать несколько материалов, как легко в браузере писать ботов и не только. Не кармы ради, а плюсиков для. Шутка с долей шутки.

Данный материал не является пособием по программированию. И данный код ни в коем случае не считать эталонным! Использовать его надо с умом в любых интересах в познавательных целях, не во зло.

Если материал окажется интересным, то продолжу.

Пока писал пост, гадал: "забанит, не забанит, забанит, не забанит ли меня администрация за такой пост?"

Глава 0. Немного обо всём этом

Немного вводной, так скажем основы основ. Особенности описанного далее материала в том, что ботов (он же UserScript, гуглится легко что это такое) можно писать прямо в любимом браузере для любимого браузера. Преимуществ в этом много, начиная с удобства разработки и заканчивая наименьшей заметностью бота при меньших усилиях. В своё время я для одной игры написал бота. Разработчики хвалились, что они отслеживают ботов и банят. Но забанить меня в течение пары месяцев у них не получилось. Только когда я потерял интерес (как раз где-то через месяц) я оставил специально "дыру" и только тогда они начали подозревать неладное, но аргументов на забанивание всё равно не нашли. Но боты выступают не только в виде читерства, но и например для разгадки простых капч. В другой игре постоянно, по поводу и без надо было вводить капчу, простую. Тогда я написал разгадывателя капчи с 99.5% (примерно) попаданием. Но зря, через некоторое время капча была убрана разработчиками.

По сути эти боты - это просто кликеры (автокликеры), хотя можно сделать и не кликера, а фонового, но это, если и буду описывать, то в самом конце цикла статей.

Также, для удобства работы рекомендую пока что ознакомиться, а в будущем обязательно поставить Greasemonkey или Tampermonkey. Есть ещё Scriptish, но он мне не понравился. А также можно использовать UserScripts "как есть" в Хроме, но это чуть менее удобно.

Тут и далее материал будет писаться на основе браузера Firefox, ибо там есть очень хороший плагин для разработчиков FireBug и я к нему привык. Но в других браузерах есть всё тоже самое, свои инструменты для разработчика, просто чуть по другому выглядит и называется. Чтобы продолжить их надо открыть, для этого надо нажать F12. Если у вас не открываются они, например в Сафари, то ищем в гугле "название_вашего_браузеран открыть инструменты разработчика".

Далее под Хромом будут подразумеваться все браузеры основанные на хромоподобном движке webkit/blink, это Опера, Яндекс и другие.

Все названия элементов меню и прочего я буду писать примерные, обобщенные, а вы уже ищите что-то похожее.

Скрины, если и будут, то могут быть из разных браузеров, там где это нагляднее показывается.

Данный материал не рассказывает о кроссбраузерности. В материале используются только современные достижения для современных браузеров.

Глава 1. Функция click() и querySelector

В данной главе мы научимся пользоваться консолью, научимся искать и изменять элементы, ну и кликать.

Начнем с авторизации на Пикабу. Все свои эксперименты с авторизацией я провожу в Инкогнито режиме на тестовом аккаунте, но это не принципиально.

И так мы открыли Инструменты разработчика и там находим Консоль. В ней мы будем тестировать наш JS код.

Авторизация происходит следующим образом: вводится "логин" и "пароль" и нажимается кнопка "войти". Первым делом нам надо ввести данные. Но надо знать куда ввести, в какой элемент на странице, то есть найти элементы под названием input. Конечно же не любые, а именно для логина и пароля. Кто пользуется Firebug или Хром необходимо будет поставить курсор на этот input, нажать правую мышь и выбрать пункт "инспектировать элемент". В консоли откроется наш элемент. Если у вас нет подобного пункта, то в консоли должна быть кнопочка придется искать его по всему документу самим.

(на картинке ниже слева Хром, правее Firebug)

Говнокодим кликербота на JS на примере Пикабу. 1: click() и querySelector() Javascript, Нелегально, Быдлокодинг, Длиннопост

После того как выбрали его, необходимо найти пункт типа "скопировать селектор" или CSS. Вроде любой браузер должен копировать самый короткий селектор. Кроме Firebug, он копирует самый длинный, полный селектор.

Более подробно с этим можно ознакомиться по ссылке выше или в учебнике по CSS, это основы CSS.

Получается вот такие строки:

document.querySelector('button.b-button:nth-child(2)');

Выполняя каждую строку отдельно, в консоли мы увидим наши элементы.

Говнокодим кликербота на JS на примере Пикабу. 1: click() и querySelector() Javascript, Нелегально, Быдлокодинг, Длиннопост

Так просто не с каждым атрибутом, но сейчас не об этом.

Обязательно брать строки в кавычки всегда!

document.querySelector('button.b-button:nth-child(2)').click();

Выполняем эту строчку и у нас произойдёт отправка. Если выполнить все три строчки разом, то произойдёт сразу, в порядке переданных строк, заполнение и вход.

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

"Бонусом": тоже самое можно сделать с выставлением плюса или минуса комментарию. Выбираем наш элемент и кликаем:

Говнокодим кликербота на JS на примере Пикабу. 1: click() и querySelector() Javascript, Нелегально, Быдлокодинг, Длиннопост

На этом всё. Хотел написать кратко, думал уложусь в несколько абзацев, а получилось как всегда, Остапа было уже не остановить.

Эпилог поста

В планах примерно такой порядок материал:

2. Цикл (какой-то один или два, а не все).

3. Условия (if/else) и Интервалы (setTimeout/setInterval)

4. Массив, хранилища (наверное только localStorage) и прочее что понадобится. например JSON.

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

Предположим есть браузеры в виде клиентов и есть сервер. И общаются они между собой с помощью WebSocket.

Как правило, простой вариант координации в таком варианте, выглядит в виде пачки ифов и с стороны клиента, и с стороны сервера. Пример клиента:

А активность бота всегда должен продуцировать сервер и транслировать всем остальным. И вот на этом как раз основная дилема. Пока что приходит в голову идея что нужно прописать в каждую активность игроков то есть на каждый if (e.e == 'move') или на каждый if (e.e == 'shot') какое то действие бота.

Это наверно может выглядеть как то так:

Но дальше на этом фантазия заканчивается. Вопрос в том как лучше организовать перемещения бота и в архитектуре игры?

Возможно уже есть на других языках похожие библиотеки на Python или на PHP любом другом.


А может не перегружать код сервера еще и действиями бота. Может сделать бота отдельно в виде клиента, который коннектится к серверу почти как обычный пользователь. В принцепе тоже вариант, но я не совсем представляю как это реализовать. Потому что клиенты вроде как с браузера конектятся 1 браузер 1 клиент. А бот откуда возьмется? Ну нам же нужен совершенно нестандартный клиент, потому как ему не надо делать то, что делают остальные - т.е. отображать в браузере произошедшее в игре. Т.е. его можно написать на любом языке. Желательно что бы для этого языка была клиентская библиотека для WebSocket, хотя как я понимаю протокол там не сложный, реализовать не долго Ну и все же я не совсем понимаю как его реализовать в таком случае? Это должен быть еще один процесс запущенный на сервере и эмитирующий браузер? Или это должен делать браузер первого подключившегося клиента? да, процесс на сервере. эмуляцией браузера это можно назвать с большой натяжкой. браузер обычно делает много того, что боту совершенно не нужно. Да, он должен отвечать серверу по тому же протоколу, по которому работает обычный клиент. Ну и посылать запросы для выполнения своих действий как игрока

Лучше было бы, если бы ядро вашего приложения/игры не зависело так от вида транспорта

Схема получается такой

  • Игрок совершил действие
  • По websocket-ам данные передали на сервер
  • Сервер передал данные движку/ядру игре
  • Игра обработала данные, сделала необходимые обновления
  • Игра возвращает новое состояние себя серверу
  • Сервер отправляет всем обновленное состояние

Отсюда получается, что сервер - это лишь посредник между игрой и клиентом.

В примере это выглядит так:

Как это поможет с ботом?

При выполнении команды ботом, сообщайте серверу, что нужно разослать данные, можно добавть к объекту ws фу-нкции или кастомное событие, которое будет за это отвечать. Придется правда переписать логику игры, ведь теперь игроки будут храниться в объекте Game , зато у вас не будет проблем в дальнейшем, так как вы отделили транспортный модуль от игрового.

В этой статье, я объясню общий принцип создания ботов на Python, применив полученные знания, вы сможете создать бота который:

Создаем первого бота на Selenium.

Данный способ подойдет для любого сайта, однако, за все нужно платить. Selenium запускает браузер, отъедая огромный запас оперативной памяти. Используйте его только тогда, когда нужно выполнить JS код на странице.

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

Далее, установите веб-драйвер под браузер Firefox отсюда. Также, необходимо установить браузер Mozilla Firefox, если еще не установлен.

Теперь напишем простейшего бота. Для этого, напишите следующий python скрипт.

Код скрипта описан в комментариях.

Далее, переместите файл скрипта, в одну папку с веб-драйвером geckodriver.exe


И запустите python скрипт. У вас должен открыться браузер.


В адресной строке видна иконка робота, это значит, что браузером управляет программа.

Хорошо, бот создан, но он бесполезен. Единственное на что он способен, это заходить на сайт. Давайте добавим ему новых функций. Например, сделаем так, чтобы бот лайкал посты на сайте.

Бот лайкающий посты на сайте.

Последовательность действий у нас следующая.

Первый пункт мы уже сделали, перейдем ко второму.

Пройтись по каждому из постов.

На этом этапе, нужно понимать разметку HTML.

Зайдите на сайт, и нажмите кнопку F12.


У вас откроются инструменты разработчика. Изучив разметку, мы понимаем, что все посты находятся в теге article.

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


Данный селектор указывает:

  • На элемент с тегом a
  • который находится находится внутри тега h2 с классом entry-title
  • тот, в свою очередь, находится внутри тега header с классом entry-header
  • тег header находится внутри тега div с классом blog-entry-content
  • тот, находится в теге div
  • тег div находится внутри тега article

Теперь, дополним бота.

Разберем новую функцию.

Данная функция ищет элементы по css селектору. В результате своей работы, она возвращает массив элементов.

В-общем, мы из этого массива, достали первый элемент, и при помощи функции get_attribute(), получили значение атрибута href (ссылка на пост).

И вывели его на экран.

Запустите скрипт, в консоли должна появится ссылка на первый пост.


Если закинуть массив элементов в цикл, то получится извлечь ссылки на все посты.


Отлично, ссылки на все посты получены, осталось всем этим постам, поставить лайк.

Нажать кнопку лайк, если она не нажата

Сначала перекопируем наши ссылки в отдельный массив. Замените это:

Далее напишем код, отвечающий за нажатие кнопки лайк.

Разберем данные строки.

Данная строка ищет кнопку с помощью css_селектора, и получает строку с названиями классов нашей кнопки.


Кликаем по кнопке лайк.

Осталось закрыть браузер, делается это с помощью функции quit().

Бот завершен, запустите скрипт, и наслаждайтесь автоматизацией.

Делаем браузер невидимым

Бот работает и все-бы ничего, но своим окном бразуера, он перекрывает все остальные окна. К счастью, у Firefox есть headless режим, позволяющий пользоваться функциями бразура, не открывая окно браузера.

Добавьте следующий код перед инициализацией браузера:

Здесь, мы переопредили настройки браузера, осталось передать их, нашему браузеру.

Не так давно на Google+ появились игры. Прочитав топик об этом , я решил во что нибудь поиграть. Игра Diamond Dash - еще один пример популярной игры в Камушки . Через некоторое время игры программист побеждает геймера: однотипные действия нужно автоматизировать. И вот что из этого вышло…



*Примечание: «руками» даже опытному игроку сложно набрать больше 400к
После непродолжительного гугления было решено для решения использовать язык макросов AutoIt.
Под катом вы найдете краткое описание игры, один из способов распознавания поля, алгоритм определения точки нажатия, и некоторое количество оптимизаций. А так же ссылку на github-репозиторий скрипта.
UPD Добавлено видео работы скрипта.
Краткое описание игры

Игра представляет из себя простую «кликни-на-область-больше-трех-квадратиков-одного-цвета» головоломку.

Есть поле 9 на 10, заполненное квадратиками 5 цветов. У нас есть одна минута на то, чтобы набрать максимальное количество очков. При нажатии на область из 3 или более одноцветных клеток, она исчезает, то что над ней проваливается, а сверху падает недостающее. Количество начисленных очков зависит от размера области: чем она больше — тем больше очков.
Кроме того, если делать клики быстро, и почти безошибочно, поле вокруг загорается, а каждое удаление (в данном случае взрыв), захватывает соседние с удаляемой областью клетки.

И наконец последняя особенность: наверху есть шкала с алмазом в конце, которая заполняется по мере уничтожения клеток (и уменьшается при ошибочных кликах). Когда она заполняется, на поле в случайном месте появляется горящий алмаз. При клике на него время останавливается, а сверху поля падает огненный шар, уничтожая все клетки в строке и столбце, где находился камень.

Определение координат окна

Эта часть была добавлена в самую последнюю очередь, до этого координаты угла были жестко прописаны в коде. Используется функция из сторонней библиотеки ImageSearch для поиска сохраненного шаблона 10 на 10 пикселей. Судя по всему, фон слегка меняется от игры к игре, потому что не любой кусок подходил.
На форумах повсеместно не рекомендуют использование ImageSearch из-за долгого времени работы. Но так как нам нужно определить координаты только один раз в начале игры, провисаний по времени можно не опасаться.
Распознавание цветов и сохранение скриншотов

Для определения цвета пикселя по его координатам в AutoIt есть функция PixelGetColor. Но как показала практика, измерение всего 90 пикселей занимает полторы секунды, что, конечно, не очень хорошо. Но справедливости ради надо сказать, что бот написанный с использованием этой функции набирал 400-600 тысяч очков, а это больше чем может набрать человек, даже при большой сноровке.
На форумах был найден метод сохранения текущего состояния монитора в Bitmap, используя WinAPI. Кстати, этот битмап, при необходимости(например для дебага), можно сохранить в файл функцией _ScreenCapture. Далее смотрим цвета 90 пикселей, расположенных по решетке и по ним строим таблицу цветов квадратиков.

  1. Func _GetField ( ByRef $aiField ) ; получение массива цветов поля
  2. ; получение BitMap-снимка экрана с помощью WinAPI
  3. Local $hWnd = WinGetHandle ( "Игры Google+ - Google Chrome" )
  4. Local $Size = WinGetClientSize ( $hWnd )
  5. Local $hDC = _WinAPI_GetDC ( $hWnd )
  6. Local $hMemDC = _WinAPI_CreateCompatibleDC ( $hDC )
  7. Local $hBitmap = _WinAPI_CreateCompatibleBitmap ( $hDC , $Size [ 0 ] , $Size [ 1 ] )
  8. Local $hSv = _WinAPI_SelectObject ( $hMemDC , $hBitmap )
  9. _WinAPI_BitBlt ( $hMemDC , 0 , 0 , $Size [ 0 ] , $Size [ 1 ] , $hDC , 0 , 0 , $SRCCOPY )
  10. _WinAPI_SelectObject ( $hMemDC , $hSv )
  11. _WinAPI_DeleteDC ( $hMemDC )
  12. _WinAPI_ReleaseDC ( $hWnd , $hDC )
  13. Local $L = $Size [ 0 ] * $Size [ 1 ]
  14. Local $tBits = DllStructCreate ( 'dword[' & $L & ']' )
  15. _WinAPI_GetBitmapBits ( $hBitmap , 4 * $L , DllStructGetPtr ( $tBits ) )
  16. ; определение цветов клеток
  17. For $iCol = 0 To $iNumCols - 1
  18. For $iRow = $iNumRows - 1 to 0 Step - 1
  19. ; замер цвета квадратика
  20. $iX = $iCornerX + ( $iCol * 40 ) + $iDeltaX
  21. $iY = $iCornerY + ( $iRow * 40 ) + $iDeltaY
  22. $iPixelColor = Mod ( DllStructGetData ( $tBits , 1 , $iY * $Size [ 0 ] + $iX ) , 0x1000000 )
  23. $aiField [ $iRow ] [ $iCol ] = _GetCheckColor ( $iPixelColor )
  24. Next
  25. Next
  26. ; удаление данных для избежаня утечки памяти
  27. _WinAPI_DeleteObject ( $hBitmap )
  28. _WinAPI_DeleteObject ( $hMemDC )
  29. _WinAPI_DeleteObject ( $tBits )
  30. EndFunc




Тут стоит оговориться, почему замер происходит всего по 1 точке. Этот метод был испробован мной в первую очередь, и остался в финальной версии. Между этими двумя моментами было испробовано довольно большое количество альтернативных способов, среди которых были: замер 64 точек на каждый квадратик(решетка 8 на 8) и различные усреднения полученных значений, случайный выбор координат для замера, хранение истории нескольких последних замеров для лучшей точности… Но все они оказались менее точными или удобными, чем самый первый способ.
Возможно, что так как я весьма далек от темы распознавания изображений, я не знаю чего-то простого, способного помочь мне в этом вопросе. В таком случае буду рад любым предложениям. =)
Определение одноцветной области по таблице цветов

Ну, и, наконец, осталось найти подходящее место и кликнуть туда. Для этого обходим поле снизу вверх (потому что все падает вниз, а значит снизу менее пусто чем сверху), и проверяем одноцветные части. Я делал это с помощью алгоритма безрекурсивного поиска в глубину (DFS). Вкратце суть такова: кладем в стек стартовую клетку, а дальше, пока стек не пуст, достаем из него текущую клетку и обходим ее соседей, при совпадении цвета кладем в стек. Ну да что говорить, код понятней. =)

  1. Func _DfsAreaSize ( ByRef $aiField , $iStartX , $iStartY ) ; нерекурсивный алгоритм поиска размера одноцветной области
  2. ; методом поиска в глубину
  3. Local $aiResult [ $iNumCols * $iNumRows ] [ 2 ] ; список клеток входящих в область
  4. Local $iResultSize = 0
  5. Local $afMap [ $iNumRows ] [ $iNumCols ] ; флаги пройденности
  6. For $iRow = 0 to $iNumRows - 1
  7. For $iCol = 0 to $iNumCols - 1
  8. $afMap [ $iRow ] [ $iCol ] = False
  9. Next
  10. Next
  11. $afMap [ $iStartX ] [ $iStartY ] = True
  12. Local $aiStack [ $iNumRows * $iNumCols ] [ 2 ] ; активный стек
  13. Local $iStackSize = 1
  14. $aiStack [ 0 ] [ 0 ] = $iStartX
  15. $aiStack [ 0 ] [ 1 ] = $iStartY
  16. While $iStackSize > 0
  17. $iStackSize -= 1
  18. $iX = $aiStack [ $iStackSize ] [ 0 ]
  19. $iY = $aiStack [ $iStackSize ] [ 1 ]
  20. $aiResult [ $iResultSize ] [ 0 ] = $iX
  21. $aiResult [ $iResultSize ] [ 1 ] = $iY
  22. $iResultSize += 1
  23. For $iDirection = 0 to 3 ; перебор 4 рядомстоящих клеток
  24. Local $iNewX = $iX
  25. Local $iNewY = $iY
  26. Switch $iDirection
  27. Case 0
  28. $iNewY += 1
  29. Case 1
  30. $iNewY -= 1
  31. Case 2
  32. $iNewX += 1
  33. Case 3
  34. $iNewX -= 1
  35. EndSwitch
  36. If ( $iNewX >= 0 And $iNewX < $iNumRows And _
  37. $iNewY >= 0 And $iNewY < $iNumCols And _
  38. Not ( $afMap [ $iNewX ] [ $iNewY ] ) And $aiField [ $iNewX ] [ $iNewY ] = $aiField [ $iStartX ] [ $iStartY ] ) Then
  39. $afMap [ $iNewX ] [ $iNewY ] = True
  40. $aiStack [ $iStackSize ] [ 0 ] = $iNewX
  41. $aiStack [ $iStackSize ] [ 1 ] = $iNewY
  42. $iStackSize += 1
  43. EndIf
  44. Next
  45. WEnd
  46. Return $iResultSize
  47. EndFunc


Оптимизации


Написанного выше уже хватало для получения миллиона. Но мне хотелось больше. Самым большим недостатком вышеописанного алгоритма было, пожалуй, неумение определять алмазы. Поэтому под конец минуты поле выглядело примерно так:

А между тем, алмазы — очень полезная вещь, потому что пока падает огненный шар, таймер останавливается, а квадратики падают. А значит пробелы заполняются, и ошибок будет меньше.
Для определения алмазов для начала пришлось поиграть с координатами замеров, чтобы цветные клетки определялись корректно, а алмазы — не попадали ни под один из их цветов. После этого создаем массив $aiDiams размером 3 (будем проверять только нижние 3 строки, потому что все алмазы рано или поздно туда упадут) * ширину (в нашем случае — 10). При каждом замере просматриваем нижние 3 строки, и если ячейка определилась, то обнуляем соответствующее место в $aiDiams, иначе инкрементируем его. Таким образом для клеток с алмазиками значения будут велики. При накоплении некоего порога, кликаем.

  1. For $iRow = $iNumRows - 1 to $iNumRows - 3 Step - 1
  2. For $iCol = 0 to $iNumCols - 1
  3. If $aiField [ $iRow ] [ $iCol ] <> 0 Then
  4. $aiDiams [ $iRow ] [ $iCol ] = 0
  5. Else
  6. $aiDiams [ $iRow ] [ $iCol ] += 1
  7. If $aiDiams [ $iRow ] [ $iCol ] > 15 Then
  8. MouseClick ( "Left" , $iCornerX + 30 + ( $iCol * 40 ) , $iCornerY + 10 + ( $iRow * 40 ) , 1 , $iMouseSpeed )
  9. $aiDiams [ $iRow ] [ $iCol ] = 0
  10. Sleep ( 500 )
  11. Return 0
  12. EndIf
  13. EndIf
  14. Next
  15. Next

Оптимизация 2. Over Explosion

Защита от повторной ошибки, в эффективности которой я не уверен. Дело в том, что в самой игре при ошибке клетка становится серого цвета, а серый цвет алгоритмом определяется как неопределенный (тавтология получилась =) ). Но хуже эта проверка точно сделать не может, поэтому пусть живет.
Суть в том, что при каждом клике сохраняем область, по которой кликаем, и при следующем клике не трогаем ее.
Итог

Вместо постскриптума. Пара слов об AutoIt

Читайте также: