Как пишут ботов для игр

Игровые боты. Начало

Время на прочтение
5 мин

Количество просмотров 82K

Что может быть интереснее процесса игры в игры? Правильно! Процесс наблюдения за тем, как играет в игры написанный тобой бот.

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

Введение

Боты для онлайн игр я бы грубо разделил на 3 разновидности по способам реализации:
1. Боты не использующие приложение игры. Имитирующие протокол обмена с сервером.
2. Боты работающие с процессом приложения игры. В случае с Web, работающие с окном браузера.
3. Боты работающие со скриншотом и имитирующие устройства ввода мышь и клавиатуру.

Первая разновидность скорее гипотетическая, т.к. протоколы, как правило, закрыты и не тривиальны.

Первая разновидность больше подходит для ботов с простыми и текстовыми протоколами. Если на сервер передаются бинарные данные, то предстоит разобраться с их структурой, что усложняет задачу.

Вторая разновидность более реальна и может быть реализована. Бот второго вида получает полезную информацию из памяти процесса игры. Недостаток — версии клиентов могут регулярно обновляться и тогда может потребоваться заново искать интересующие адреса памяти.

Мы рассмотрим третюю разновидность ботов, т.к. ИМХО они более привлекательны, хоть и не лишены недостатков.
А так же, такой подход более спортивный :-)

В этой статье я рассмотрю набор инструментов для самого простого бота для Windows.

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

Код

Для разработки приложений я буду использовать Qt Creator + Qt 5 либы (так мне привычнее) и раз бот для Виндовс то + windows.h (WinAPI).

Инклуды:

#include <windows.h>   // WinAPI
#include <iostream>    // std::cout
#include <unistd.h>    // sleep(), usleep()
#include <math.h>

Хидер бота:

// Индексы точек
enum {
    menu=0,
    elm_1,
    points_cnt
};

class MyBot
{
public:
    MyBot();
    void run();
    void move_to(int inx);
    void lclick_to(int inx);
    void rclick_to(int inx);
    void drag(int from_inx,int to_inx);
    POINT point[points_cnt];
};

Конструктор:

MyBot::MyBot() :
    // Массив координат органов управления (по которым мы будем кликать мышкой)
    point({
      {100,100}, // 0 - menu
      {130,130}, // 1 - elm_1
    })
{
}

Регистрация горячих кнопок для управления ботом:

    RegisterHotKey((HWND)Widget::winId(), 101, MOD_ALT, VK_F1);    // Запуск бота
    RegisterHotKey((HWND)Widget::winId(), 102, MOD_ALT, VK_F2);    // inx++
    RegisterHotKey((HWND)Widget::winId(), 103, MOD_ALT, VK_F3);    // Проверить точку inx
    RegisterHotKey((HWND)Widget::winId(), 104, MOD_ALT, VK_F4);    // Запомнить точку inx
    RegisterHotKey((HWND)Widget::winId(), 105, MOD_ALT, VK_F5);    // Вывести в консоль массив координат

Обработка событий нажатия кнопок управления ботом:

int inx=0;
MyBot bot;

bool Widget::nativeEvent(const QByteArray & eventType, void * message, long * result){
    Q_UNUSED(result);
    Q_UNUSED(eventType);
    MSG* msg = reinterpret_cast<MSG*>(message);
    if(msg->message!=WM_HOTKEY)return false;

    switch(msg->wParam){
    case 101: // Alt-F1 - запуск бота
        bot.run();
        return true;
    case 102: // Alt-F2 - inx++
        if(inx<points_cnt-1)inx++;
        return true;
    case 103: // Alt-F3 - Проверить точку inx
        bot.move_to(inx);
        return true;
    case 104: // Alt-F4 - Запомнить точку inx
        GetCursorPos(&bot.point[inx]);
        return true;
    case 105: // Alt-F5 - Вывести в консоль массив координат
        for(i=0;i<points_cnt;i++){
            std::cout << "{" << bot.point[i].x << "," << bot.point[i].y << "}, //" << i << std::endl;
        }
        return true;
    }
    return false;
}

Перемещение указателя мыши к нужной точке:
(выполнено не очень аккуратно, обещаю исправиться :-))

#define width 1920
#define height 1080

void MyBot::move_to(int inx){
    int x=point[id].x;
    int y=point[id].y;
    POINT pt;
    GetCursorPos(&pt);

    int from_x=pt.x;
    int from_y=pt.y;

    int to_x=x;
    int to_y=y;

    int dx=to_x-from_x;
    int dy=to_y-from_y;

    float fdx;
    float fdy;
    int loop_cnt;

    if(abs(dx)>abs(dy) && dx!=0){
        fdx=dx<0? -1.0 :1.0;
        fdy=(float)dy/abs(dx);
        loop_cnt=abs(dx);
    }
    else if(dy!=0){
        fdy=dy<0? -1.0 :1.0;
        fdx=(float)dx/abs(dy);
        loop_cnt=abs(dy);
    }
    else return;

    // двинуть за 1 секунду
    int time=1000000/loop_cnt;
    float fx=from_x;
    float fy=from_y;
    for(int i=0;i<loop_cnt;i++){
        fy+=fdy;
        fx+=fdx;
        int nx=(fx)*(65536 / width);
        int ny=(fy)*(65536 / height);
        mouse_event(MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE,nx,ny,0,0);
        usleep(time);
    }
    usleep(50000);
}

Клики:

void MyBot::lclick_to(int inx){
    move_to(inx);
    mouse_event(MOUSEEVENTF_LEFTDOWN,0,0,0,0);
    usleep(50000);
    mouse_event(MOUSEEVENTF_LEFTUP,0,0,0,0);
    usleep(100000);
}

void MyBot::rclick_to(int inx){
    move_to(inx);
    mouse_event(MOUSEEVENTF_RIGHTDOWN,0,0,0,0);
    usleep(50000);
    mouse_event(MOUSEEVENTF_RIGHTUP,0,0,0,0);
    usleep(100000);
}

Перетаскивание:

void MyBot::drag(int from_inx, to_inx){
    move_to(from_inx);
    usleep(50000);
    mouse_event(MOUSEEVENTF_LEFTDOWN,0,0,0,0);
    usleep(70000);
    move_to(to_inx);
    mouse_event(MOUSEEVENTF_LEFTUP,0,0,0,0);
    usleep(30000);
}

Работа бота:

void MyBot::run(){
    rclick_to(menu);  // кликнем правой кнопкой для вызова контекстного меню
    lclick_to(elm_1); // кликнем левой кнопкой по строке меню
}
Мануал юзера

Перед запуском бота горячей кнопкой Alt-F1, бот следует сначала настроить, определив верные координаты органов управления по которым бот будет кликать.
Для запоминания координат точки наводим указатель месту и жмём Alt-F4.
Для проверки корректности точки отводим указатель в сторону и жмём Alt-F3.
Для настройки следующей точки жмём Alt-F2.
Для сохранения верных координат жмём Alt-F5.

Подводные камни

Опыт показывает, что не стоит торопиться кликать по органам управления и двигать указатель. Часто игры притормаживают, при наведении мыши на кнопку срабатывает отрисовка подсветки и прочие неведанные процессы и торопливость приводит к несрабатыванию клика или захвата при перетягивании и т.п. и как следствие неработоспособность бота. При ручном управлении, люди обычно таких досадных мелочей даже не замечают, так как работает обратная связь через органы зрения. Решение — делать паузы после всех элементарных действий.

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

Qt Creator: qt-project.org/downloads
Исходный код проекта на гитхабе: github.com/rumaster/my_bot_v1

P.S. Не подумайте что я ярый противник онлайн игр, раз публикую исходники ботов. Я противник дискриминации ИИ (ботов) и за развитие онного. А ещё, игры — двигатель прогресса.

P.P.S. Говоря ИИ, я подразумеваю программу способную получать и обрабатывать (анализировать) информацию, планировать и выполнять действия в соответствии с целями и результатами анализа ситуации.

Игры бывают разные. Но рано или поздно любой геймер задает себе вопрос: «Как сделать бота для игры?». Почему так происходит? Потому что во многих играх:

  • есть масса рутинных действий, которые нужно выполнять человеку;

  • «качаться» до желанного уровня или скилла очень долго и не хочется тратить личное время, поэтому нужно, чтобы кто-то играл вместо самого геймера;

  • нужный скилл дается только за деньги или за большое количество игрового времени, деньги тратить не хочется и время тоже;

  • хочется «качать» сразу несколько игр подряд, а возможность играть есть только в одну игру;

  • и др.

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

Как сделать бота для игры 

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

  • для браузерной;

  • для игры в соцсети;

  • для мобильной игры;

  • для устанавливаемой десктопной игры.

Уже от этого будет зависеть выбор необходимых инструментов. Плюс есть разные виды ботов для игр. Для чего они нужны:

  • есть боты, созданные разработчиками самих игр;

  • есть боты-кликеры, которые выполняют в игре самые простые действия;

  • есть боты-повторители, которые выполняют одни и те же действия по заготовленному шаблону;

  • есть боты-имитаторы, которые призваны имитировать действия реальных игроков.

Последний вид — это самый сложный вид ботов, как правило, он входит в категорию «запрещенных» ботов для игр. Такие боты дают преимущества перед другими игроками, поэтому на них «охотятся» внутренние противоборствующие силы, которые нацелены на то, чтобы «банить» таких игроков и их аккаунты. Поэтому разработка таких ботов — это уже дело очень профессиональных программистов.

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

Итак, как сделать бота для простых игр? Попробуйте для этого использовать программы для создания ботов для игр:

  • Кибор. Есть визуальный редактор, можно создать бота для простой игры;

  • Zennoposter. Не нужно знать программирование для этого сервиса. При создании бота используется технология «перетаскивания» компонентов. Способен создать ботов для браузерных игр и игр в соцсетях. При желании можно сделать ботов и для других целей. Минус этого сервиса — не бесплатен, но есть бесплатный период в 14 дней.

  • Zbot. Это уже более продвинутая программа, которая способна создать бота даже для Counter Strike.

  • Также можно посмотреть еще POD-bot, YaPb и др.

Для более продвинутых «ботоделов» или тех, кто хочет таким стать, можно посоветовать специальные среды разработки для игровых ботов. Тут, конечно, нужно знать один из языков программирования:

  • C#;

  • Python;

  • Ruby;

  • Java;

  • JavaScript или др.

Конечно, с таким подходом вам будут открыты более широкие возможности при написании ботов для игр. Но и «попотеть» на старте придется, потому что если вы собираетесь сделать с нуля бота для игры, то вам придется как минимум выучить один язык программирования. А выученный язык программирования дает вам возможность состояться в IT-индустрии, если вы этого захотите. Потому что работы по программированию достаточно.

Две популярные среды разработки для игровых ботов:

  1. SikuliX. Для разработки бота при помощи этой среды нужно подучить языки Python или Ruby, при установке нужно будет выбрать.

  2. Nomad Bot. С данной средой не возникнет вопроса: «Как сделать бота для игры?», потому что данная среда «заточена» именно под создание ботов для игр и нацелена на то, чтобы экономить ваше время для разработки.

Итог

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

Подготовка

Этот туториал, и код в нем, требует установки нескольких дополнительных библиотек для Python. Они обеспечивают обертку Python’а в кусок низкоуровневого C-кода, который значительно упрощает создание и скорость исполнения.

Некоторые библиотеки существуют только под Windows. У них могут быть эквиваленты под Mac или linux, но мы не будем их рассматривать.

Вам нужно скачать и установить следующие библиотеки:

  • Python Imaging Library (PYL)
  • Numpy
  • PyWin

Все представленные библиотеки комплектуются установщиками. Запуск их автоматически установит модуль в директорию libsite-packages и, теоретически, добавит соответствующий pythonPath. Однако, на практике это происходит не всегда. Если Вы получите сообщение об ошибке после установки, добавьте их вручную в переменные Path.

Последний инструмент это графический редактор. Я предлагаю использовать Paint.Net как лучший из бесплатных, но подойдет любая программа с линейками и измерениями в пикеслях.

Мы будем использовать несколько игр в качестве примеров.

Введение

Это руководство написано с целью дать базовое понимание основы разработки ботов для браузерных игр. Подход, который мы собираемся дать, вероятно немного отличается от того, что многие ожидают услышать говоря о ботах. Вместо того, чтобы сделать программу, вставляющую код между клиентом и сервером (как боты для Quake или CS), наш бот будет находиться чисто снаружи. Мы будем опираться на методы Компьютерного зрения и вызовы Windows API для сбора необходимой информации и выполнения движений.

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

Когда вы привыкните к тому, что компьютер может видеть, начнете смотреть на игры по-другому. Хороший пример это поиск в играх-пазлах. Обычное решение основывается на ограничении скорости игрока, что заставляет принимать не оптимальные решения. Интересно (и довольно легко) «взломать» их скриптами движений которые не повторить человеку.

Эти боты могут быть также очень полезны для тестирования простых игр — в отличии от реальных игроков, боту не надоест играть один сценарий снова и снова.

Исходники примеров из курса, а также одного законченного бота можно найти здесь.

Шаг 1: Создание проекта

В папке с проектом создайте текстовый файл quickGrab, измените расширение на ‘py’ и откройте его в редакторе кода.

Шаг 2: Создаем приложение, которое делает скриншот экрана

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

Вставим в наш файл с проектом quickGrab.py следующий код:

ImageGrab
import os
import time
 
def screenGrab():
    box = ()
    im = ImageGrab.grab()
    im.save(os.getcwd() + '\full_snap__' + str(int(time.time())) + '.png', 'PNG')
 
def main():
    screenGrab()
 
if __name__ == '__main__':
    main()

Запустив этот код, вы получите скриншот экрана:

screenshot

Данный код забирает всю ширину и высоту области экрана и сохраняет в PNG файл в директорию проекта.

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

import ImageGrab
import os
import time

…называются ‘import statements’. Они говорят Pytjon’у какие модули загружать во время выполнения. Это дает доступ к методам этих модулей через синтаксис module.attribute .

Первый модуль Python Image Library мы установили ранее. Как следует из названия, он дает нам функциональность взаимодействия с экраном на которую ссылается бот.

Вторая строка импортирует модуль операционной системы (OS — operating system). Он дает возможность простой навигации по директориям в операционной системе. Это пригодится, когда мы начинаем размещать файлы в разных папках.

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

Следующие четыре строки определяют функцию screenGrab().

def screenGrab():
      box = ()
      im = ImageGrab.grab()
      im.save(os.getcwd() + '\full_snap__' + str(int(time.time())) + '.png', 'PNG')

Первая строка def screenGrab() определяет имя функции. Пустые скобки означают, что она не принимает аргументов.

Строка 2, box = () присваивает пустое значение переменной «box». Мы заполним это значение дальше.

Строка 3, im = ImageGrab.grab() создает полный скриншот экрана и возвращает RGB изображение в переменную im .

Строка 4, может быть немного сложнее если вы не очень хорошо знакомы с тем как работает Time module. Первая часть im.save( вызывает метод «save». Он принимает два аргумента. Первый это директория в которую нужно сохранить файл, а второй это формат файла.

Здесь мы устанавливаем директорию вызовом метода os.getcwd() . Функция получает текущую директорию в которой выполняется код и возвращает её как строку. Далее мы добавим «+». Сложение нужно использовать между каждым новым аргументом для соединения всех строк вместе.

Следующая часть '\full_snap__ дает нам простое описание в имени файла. Обратный слеш является экранирующим символом в Python, и мы добавили два, чтобы избежать отмены одного из символов.

Далее идет эта сложная конструкция: str(int(time.time())). Она использует встроенные функции Питона. Мы рассмотрим работу этого куска кода изнутри:

time.time() возвращает количество секунд с начала Эпохи, тип данных Float (число с плавающей точкой). Так как мы используем дату для именования файлов, мы не можем использовать десятичное число, поэтому мы обернем выражение в int(), чтобы конвертировать в целое число (Integer). Это делает нас ближе к решению, но Python не может соединить тип Integer с типом String, поэтому следующим шагом мы обернем все в функцию str(). Далее остается только добавить расширение как часть строки + '.png' и добавить вторым аргументом функции снова расширение: «PNG».

Последняя часть кода определяет функцию main(), которая вызывает функцию screenGrab(), когда исполняется.

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

def main():
      screenGrab()
   
  if __name__ == '__main__':
      main()

Шаг 3: Область видимости

Функция ImageGrab.grab() принимает один аргумент, который определяет область видимости. Это набор координат по шаблону (x,y,x,y), где

  1. Первая пара значение (x,y… определяет левый верхний угол рамки;
  2. Вторая пара …x,y) определяет правый нижний.

Это дает нам возможность скопировать только часть экрана, которая нам нужна.

Рассмотрим это на практике.

Для примера рассмотрим игру Sushi Go Round (Довольно увлекательная. Я Вас предупредил). Откройте игру в новой вкладке и сделайте скриншот использую существующий код screenGrab():

screenshot

Шаг 4: Задание координат

Пришло время задания координат для нашей области видимости.

Откройте скриншот в редакторе картинок.

Координаты (0,0) это всегда левый верхний угол изображения. Мы хотим заполнить X и Y таким образом, чтобы нашему новому скриншоту функция установила координаты (0,0) в крайний левый угол игровой области.

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

координаты экрана

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

Наведите курсор на первый пиксель игровой области и запишите координаты на линейках. Это будут первые два значения для нашей функции. У меня получились значения (305, 243).

Затем следуйте к нижнему краю и запишите вторую пару координат. У меня получилось (945, 723). Вместе эти пары дают область с координатами (305,243,945,723).

Давайте добавим координаты в код:

import ImageGrab
  import os
  import time
   
  def screenGrab():
      box = (305,243,945,723)
      im = ImageGrab.grab(box)
      im.save(os.getcwd() + '\full_snap__' + str(int(time.time())) +
  '.png', 'PNG')
   
  def main():
      screenGrab()
   
  if __name__ == '__main__':
      main()

На строке 6 мы обновили массив для хранения координат игровой области.

Сохраните и запустите код. Откройте новое сохраненное изображение и вы увидите следующее:

Отлично! Это идеальный снимок игровой области. Нам не всегда будет требоваться эта напряженная охота за координатами. После того, как мы узнаем о win32api, мы рассмотрим более быстрые методы для установки координат, когда нам нужна идеальная точность.

Шаг 5: Перспективное планирование для гибкости

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

Давайте создадим две новые переменные: x_pad и y_pad. В них будет храниться расстояние между игровой областью и остальным экраном. Это поможет легко портировать код с места на место, так как каждая новая координата будет задаваться относительно двух глобальных переменных, которые мы создадим. Чтобы настроить изменения экрана нужно сбросить эти две переменные.

Так как мы уже сделали измерения, установить отступы для нашей текущей системы достаточно просто. Мы собираемся установить отступы, чтобы хранить положение первого пикселя за пределами игровой площадки. От первой пары координат нашего кортежа вычесть по 1. Получается 304 и 242.

Давайте добавим это в наш код:

# Globals
# ------------------
 
x_pad = 304
y_pad = 242

Теперь, когда они установлены, мы скорректируем координаты игровой области относительно них.

def screenGrab():
    box = (x_pad+1, y_pad+1,945,723)
    im = ImageGrab.grab(box)
    im.save(os.getcwd() + '\full_snap__' + str(int(time.time())) + '.png', 'PNG')

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

def screenGrab():
    box = (x_pad+1, y_pad+1, x_pad+641, y_pad+481)
    im = ImageGrab.grab(box)
    im.save(os.getcwd() + '\full_snap__' + str(int(time.time())) + '.png', 'PNG')

Для координаты x значение стало 945 — 304 = 641, а для y стало 723 — 242 = 481.

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

Шаг 6: Создание документации

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

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

Для примера, я обычно добавляю подобный комментарий в начало моего кода:

"""
 
All coordinates assume a screen resolution of 1280x1024, and Chrome 
maximized with the Bookmarks Toolbar enabled.
Down key has been hit 4 times to center play area in browser.
x_pad = 156
y_pad = 345
Play area =  x_pad+1, y_pad+1, 796, 825
"""

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

Шаг 7: Делаем quickGrab.py удобным инструментом

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

Сохраните и закройте текущий проект.

Создайте копию проекта в этой же папке и переименуйте файл в code.py. Теперь добавление и редактирование всех изменений мы будем производить в code.py, а quickGrab.py оставим исключительно для скриншотов. Только добавим одно финальное изменение: изменим расширение на .pyw

Это расширение сообщает Питону, что нужно запускать скрипт без открытия консоли. Двойной клик по файлу и он быстро выполнится в фоне и сохранит скриншот в рабочую директорию.

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

Шаг 8: win32api — краткий обзор

Работать с win32api может быть немного сложно на начальном этапе. Это обертка в низкоуровневый Windows C, которых хорошо задокументирован здесь, но навигация похожа на лабиринт, так что пару раз придется пройти по кругу.

Если при выполнении Вы видите ошибку «ImportError: No module named win32api», значит не установлен этот модуль. Выполните в консоли команду pip install pypiwin32

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

win32api.mouse_event():

win32api.mouse_event(
    dwFlags,
    dx,
    dy,
    dwData  
    )

Первый параметр dwFlags определяет «действия» мыши. Такие как перемещение, клик, скроллинг и т.п. Следующий список показывает распространенные параметры, используемые для программирования движений.

  • win32con.MOUSEEVENTF_LEFTDOWN
  • win32con.MOUSEEVENTF_LEFTUP
  • win32con.MOUSEEVENTF_MIDDLEDOWN
  • win32con.MOUSEEVENTF_MIDDLEUP
  • win32con.MOUSEEVENTF_RIGHTDOWN
  • win32con.MOUSEEVENTF_RIGHTUP
  • win32con.MOUSEEVENTF_WHEEL

Имена говорят сами за себя. Если вы хотите выполнить виртуальный правый клик, нужно отправить параметр win32con.MOUSEEVENTF_RIGHTDOWN в dwFlags.

Следующие два параметра, dx и dy, описывают абсолютную позицию вдоль осей x и y. Пока мы будем использовать эти параметры для программирования движения мыши, они будут использовать систему координат отличную от той, которую мы использовали до этого. Мы зададим нули и будем опираться на другую часть API для движения мыши.

Четвертый параметр это dwData. Эта функция используется тогда и только тогда, когда dwFlags содержит MOUSEEVENTF_WHEEL. В других случаях она может быть опущена или установлена в 0. dwData скорость прокрутки колеса мыши.

Простой пример для закрепления

Если мы представим игру с переключением оружия как в Half-Life 2 (где оружие может быть выбрано вращением колеса) — мы можем использовать эту функцию для выбора оружия из списка:

def browseWeapons():
    weaponList = ['crowbar','gravity gun','pistol'...]
    for i in weaponList:    
        win32api.mouse_event(win32con.MOUSEEVENTF_MOUSEEVENTF_WHEEL,0,0,120)

Здесь мы хотим симулировать скроллинг колеса мыши для навигации по нашему теоретическому списку оружия, поэтому мы вносим ...MOUSEEVENTF_WHEEL в dwFlag. Не нужно указывать позиционирование dx и dy, оставим эти значения 0, и нам нужен один скрол вперед для каждого оружия в списке, поэтому устанавливаем значение 120 для dwData, что соответствует одному скролу мыши.

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

Шаг 9: Клики мыши

Мы переходим к созданию трех новых функций. Одна общая функция нажатия левой кнопки мыши, и два обработчика состояний нажатия и отпускания.

Откройте code.py в редакторе и добавьте следующее выражение к списку импортов

import win32api, win32con

Как и ранее, это дает нам доступ к содержимому модуля через синтаксис module.attribute

Далее создадим первую функцию клика мыши

def leftClick():
      win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN,0,0)
      time.sleep(.1)
      win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP,0,0)
      print "Click."          #completely optional. But nice for debugging purposes.

Напомню, что все, что мы делаем здесь это назначаем действие первому аргументу mouse_event. Мы не должны указывать никакую информацию о позиционировании, поэтому мы опускаем параметры координат (0,0), и мы не должны указывать дополнительную информацию, такую как dwData. Функция time.sleep(.1) говорит Питону приостановить выполнение на время указанное в скобках. Добавим это в наш код. Обычно это очень короткий промежуток времени. Без этого клик может получиться до того, как меню обновится.

Мы создали функцию для левого клика. Один раз нажать, один раз отпустить. Мы потратили много времени на нее, но давайте создадим еще две вариации. Это тоже самое, но теперь каждый шаг мы разобьем на отдельную функцию. Это можно использовать когда нам надо удерживать нажатой мышь в течение продолжительного времени (например, для перетаскивания предметов или стрельбы.)

def leftDown():
    win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN,0,0)
    time.sleep(.1)
    print 'left Down'
         
def leftUp():
    win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP,0,0)
    time.sleep(.1)
    print 'left release'

Шаг 10: Простые движения мышью

Все, что остается это движение мыши по экрану. Добавим следующие функции в файл code.py

def mousePos(cord):
    win32api.SetCursorPos((x_pad + cord[0], y_pad + cord[1])
     
def get_cords():
    x,y = win32api.GetCursorPos()
    x = x - x_pad
    y = y - y_pad
    print x,y

Эти две функции служат совершенно разным целям. Первая будет использоваться для задания движения в программе. Благодаря соглашению об именовании, тело функции делает именно то, что обозначает название SetCursorPos(). Вызов этой функции устанавливает координаты мыши по заданным (x,y). Обратите внимание, что мы добавили поправки x_pad и y_pad к координатам; это важно делать там, где координаты объявляются.

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

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

Шаг 11: Навигация в меню

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

Давайте начнем. Сохраните и запустите код в Python Shell. С тех пор как на прошлом шаге мы заменили тело функции main()на pass, вы должны увидеть пустое окно после запуска Shell.

Теперь прежде чем перейти к игровой части, нужно пройти 4 начальных меню.

  1. Кнопка «Play»
  2. Кнопка «continue»
  3. Пропустить обучение «skip»
  4. Кнопка «continue»

Мы должны получить координаты каждой кнопки и добавить их в новую функцию startGame(). Расположите на экране Shell так, чтобы была видна игровая область. Нужно поставить мышь на кнопку, координаты которой нужно получить и выполнить в Shell функцию get_cords(). Убедитесь, что активным окном является Shell. Мы получим в Shell’е координаты текущей позиции мыши. Повторите это для остальных трех окон.

Оставьте Shell открытым и настройте экран так, чтобы видеть IDLE редактор. Добавим функцию startGame() и заполним новыми координатами.

def startGame():
    #location of first menu
    mousePos((182, 225))
    leftClick()
    time.sleep(.1)
     
    #location of second menu
    mousePos((193, 410))
    leftClick()
    time.sleep(.1)
     
    #location of third menu
    mousePos((435, 470))
    leftClick()
    time.sleep(.1)
     
    #location of fourth menu
    mousePos((167, 403))
    leftClick()
    time.sleep(.1)

Теперь у нас есть компактная функция, которую можно вызывать на старте каждой игры. Она устанавливает курсор на каждую позицию в меню, которую мы заранее определили и кликает. time.sleep(.1) говорит Питону остановить выполнение на 1/10 секунды между каждым кликом, чтобы меню успевало обновляться между кликами. Сохраните и запустите код.

У меня, как у медленного человека, прохождение меню вручную занимает больше секунды, тогда как наш бот может сделать это в течение примерно 0,4 секунд. Совсем неплохо!

Шаг 12: Зададим координаты еды

Давайте повторим процесс для каждой кнопки.

меню с едой

С помощью get_cords(), соберите координаты еды из меню. Еще раз в Python Shell напишите get_cords(), наведите мышь на еду и выполните команду.

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

for i in range(6):
    time.sleep(1.2)
    print get_cords()

Нам нужно создать новый класс Cord, чтобы хранить в нем собранные координаты. Возможность вызова через Cord.f_rice дает большие преимущества, так как можно передававть координаты прямо в mousePos(). Как вариант, можно хранить всё в словарях, но я нахожу синтаксис классов более удобным.

class Cord:
     
    f_shrimp = (40,340)
    f_rice = (90, 340)
    f_nori = (40, 387)
    f_roe = (90, 387)
    f_salmon = (40, 440)
    f_unagi = (90, 440)

Мы будем хранить много наших координат в этом классе, там будет некоторое дублирование, поэтому добавив префикс ‘f_’ мы будем знать, что это ссылка на еду, а не, скажем, на заказ еды по телефону.

Продолжим добавлять координаты.

Шаг 13: Координаты пустых мест

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

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

"""
 
Plate cords:
 
    87, 208
    194, 208
    288, 208
    395, 208
    491, 208
    591, 208

Осталось всего несколько шагов до действительно интересных штук.

Шаг 14: Координаты телефона

Итак, это будет последняя часть координат. Здесь понадобиться намного больше действий, поэтому вы можете сделать это вручную функцией get_cords(), вместо цикла. Мы собираемся прокликать всё меню телефона, чтобы получить координаты каждого пункта.

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

Есть шесть меню, через которые нам надо пройти.

  1. Телефон

  2. Начальное меню

  3. Начинки

  4. Рис

  5. Доставка

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

class Cord:
     
    f_shrimp = (40,340)
    f_rice = (90, 340)
    f_nori = (40, 387)
    f_roe = (90, 387)
    f_salmon = (40, 440)
    f_unagi = (90, 440)
     
#-----------------------------------    
     
    phone = (574, 358)
 
    menu_toppings = (528, 273)
     
    t_shrimp = (495, 224)
    t_nori = (490, 287)
    t_roe = (574, 279)
    t_salmon = (494, 340)
    t_unagi = (576, 218)
    t_exit = (591, 342)
 
    menu_rice = (532, 293)
    buy_rice = (548, 281)
     
    delivery_norm = (490, 292)

Окей! Мы наконец собрали все необходимые координаты. Давайте создадим что-нибудь полезное!

Шаг 15: Убираем со стола

Мы используем координаты собранные ранее для создания функции clear_tables().

def clear_tables():
    mousePos((87, 208))
    leftClick()
 
    mousePos((194, 208))
    leftClick()
 
    mousePos((288, 208))
    leftClick()
 
    mousePos((395, 208))
    leftClick()
 
    mousePos((491, 208))
    leftClick()
 
    mousePos((591, 208))
    leftClick()
    time.sleep(1)

Как вы можете видеть, это выглядит более менее похоже на нашу прошлую функцию startGame(). С одним небольшим отличием: нет функции time.sleep() между кликами. Нам не нужно ждать обновления меню, поэтому не нужно ждать задержку между кликами.

Однако, у нас есть функция time.sleep() в самом конце. Хотя это и не обязательно, лучше добавить паузы в выполнение кода, чтобы была возможность вручную завершить цикл, если это потребуется. В противном случае скрипт будет менять позицию мыши снова и снова и вы не будете в состоянии переместить фокус на Shell, чтобы остановить сценарий. Это прикольно первые два или три раза, но быстро теряет свое очарование.

Шаг 16: Создание суши

Сначала нужно понять как сделать суши. Кликните на книгу рецептов, чтобы открыть инструкции. Все виды суши, встречающиеся в игре, могут быть найдены на страницах этой книги. Оставлю первые три ниже, остальные вы можете найти в книге по ходу игры.

'''
Recipes:
 
    onigiri
        2 rice, 1 nori
     
    caliroll:
        1 rice, 1 nori, 1 roe
         
    gunkan:
        1 rice, 1 nori, 2 roe
'''

Теперь давайте создадим функцию, которая будет принимать в виде аргумента тип суши и затем собирать требуемые ингредиенты для переданного значения.

def makeFood(food):
    if food == 'caliroll':
        print 'Making a caliroll'
        mousePos(Cord.f_rice)
        leftClick()
        time.sleep(.05)
        mousePos(Cord.f_nori)
        leftClick()
        time.sleep(.05)
        mousePos(Cord.f_roe)
        leftClick()
        time.sleep(.1)
        foldMat()
        time.sleep(1.5)
     
    elif food == 'onigiri':
        print 'Making a onigiri'
        mousePos(Cord.f_rice)
        leftClick()
        time.sleep(.05)
        mousePos(Cord.f_rice)
        leftClick()
        time.sleep(.05)
        mousePos(Cord.f_nori)
        leftClick()
        time.sleep(.1)
        foldMat()
        time.sleep(.05)
         
        time.sleep(1.5)
 
    elif food == 'gunkan':
        mousePos(Cord.f_rice)
        leftClick()
        time.sleep(.05)
        mousePos(Cord.f_nori)
        leftClick()
        time.sleep(.05)
        mousePos(Cord.f_roe)
        leftClick()
        time.sleep(.05)
        mousePos(Cord.f_roe)
        leftClick()
        time.sleep(.1)
        foldMat()
        time.sleep(1.5)

Эта функция такая же как и все остальные, но с одним небольшим отличием: мы не передаем координаты непосредственно, а вызываем их в качестве атрибутов класса Cord.

Функция foldMat() вызывается в конце каждого приготовления. Она кликает по циновке, чтобы завернуть суши, которое мы приготовили. Давайте зададим её:

def foldMat():
    mousePos((Cord.f_rice[0]+40,Cord.f_rice[1])) 
    leftClick()
    time.sleep(.1)

Давайте кратко пройдемся по функции mousePos(). Мы обращаемся к первому значению f_rice через добавление [0] в конце атрибута. Напомню, что это координата x. Для клика по циновке нам необходимо добавить небольшое смещение по оси х, и мы прибавим 40 к значению координаты x и передадим f_price[1] в y. Это сдвинет нашу позицию по x достаточно, чтобы активировать циновку.

Обратите внимание, что после вызова foldMat() у нас идет длинный time.sleep(). Циновка будет закручиваться и ингредиенты будут не доступны для клика, поэтому придется подождать конца анимации.

Шаг 17: Навигация в телефонном меню

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

def buyFood(food):
     
    mousePos(Cord.phone)
     
    mousePos(Cord.menu_toppings)
     
    mousePos(Cord.t_shrimp)
    mousePos(Cord.t_nori)
    mousePos(Cord.t_roe)
    mousePos(Cord.t_salmon)
    mousePos(Cord.t_unagi)
    mousePos(Cord.t_exit)
     
    mousePos(Cord.menu_rice)
    mousePos(Cord.buy_rice)
     
    mousePos(Cord.delivery_norm)

Краткое введение в компьютерное зрение

Сейчас в нашем распоряжении имеются очень интересные куски кода. Давайте рассмотрим как научить компьютер «видеть» события. Это очень увлекательная часть процесса.

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

Другая большая часть в написании бота это обучение игре. Понимание какие значения нужно отслеживать в игре, а какие можно игнорировать. Например, можно не отслеживать деньги в кассе. Это то, что в конечном счете не имеет отношения к боту. Все, что ему надо знать это достаточно ли еды, чтобы продолжать работать. Таким образом, вместо того, чтобы мониторить количество денег, он просто проверяет, может ли бот что-то купить независимо от цены, потому что в игре это вопрос лишь нескольких секунд. Поэтому если бот не может что-то купить, он просто ждет несколько секунд.

Это подводит нас к финальной точке. Это брутфорс против элегантного решения. Алгоритмы зрения требуют значительного процессорного времени. Проверка нескольких точек во многих разных областях игровой области могут сильно снижать производительность. Таким образом все сводится к вопросу «должен бот узнать о том что что-то случилось или нет?»

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

Шаг 18: Импорт библиотек Numpy и ImageOps

Добавим следующие выражения импорта

import ImageOps
from numpy import *

ImageOps это еще одна библиотека для работы с изображениями Python’а. Она используется для выполнения операций над изображениями (таких как перевод в черно-белый формат).

Я кратко поясню вторую строку для тех, кто близко не знаком с Питоном. Стандартное выражение импорта загружает пространство имен модуля (коллекцию имен переменных и функций). Таким образом, для доступа к элементам из области видимости мы используем синтаксис module.attribute. Однако, используя выражение from _ import мы наследуем имена в локальную область видимости. То есть синтаксис module.attribute больше не нужен. Это не верхний уровень, поэтому мы можем использовать их как встроенные функции Питона, как str() или list(). Импорт Numpy таким способом дает нам возможность просто вызвать array() вместо numpy.array().

Символ * означает импорт всего из модуля.

Шаг 19: Создаем компьютерное зрение

Первый метод сравнивает значения RGB у пикселей с ожидаемым значением. Этот метод отлично подходит для статичных объектов, таких как меню. Так как нужно иметь дело с конкретными пикселями, то метод не слишком надежен для движущихся объектов. Однако, это варьируется от случая к случаю. Иногда это отличная техника, а в другой раз приходится искать другой метод.

Запустите Sushi Go Round в браузере и начните новую игру. Откройте телефонное меню. Можете пока не обращать внимание на посетителей. Вы начинаете игру без денег, поэтому все пункты меню будут серыми как показано ниже. Это и будут те значения RGB, которые мы будем проверять.

В code.py перейдите к функции screenGrub(). Нужно внести следующие изменения:

def screenGrab():
    b1 = (x_pad + 1,y_pad+1,x_pad+640,y_pad+480)
    im = ImageGrab.grab(b1)
 
    ##im.save(os.getcwd() + '\Snap__' + str(int(time.time())) +'.png', 'PNG')
    return im

Мы сделали два небольших изменения. Во—первых, мы закомментировали строку, которая сохраняет скриншот. Во—вторых, на шестой строке мы возвращаем объект с изображением после выполнения функции.

Сохраните и запустите код.

Во время того как открыто телефонное меню и все пункты серые, выполните следующий код:

>>>im = screenGrab()
>>>

Это сохранит скриншот, который мы сделали с помощью функции screenGrub() в переменную im. Теперь мы можем вызвать функцию getpixel(xy) чтобы получить данные о заданных пикселях.

Теперь нам надо получить RGB значения для каждого серого элемента меню. Они нужны для сравнения со значениями, которые бот будет сравнивать с теми, которые он будет получать вызывая getpixel().

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

>>> im = screenGrab()
  >>> im.getpixel(Cord.t_nori)
  (109, 123, 127)
  >>> im.getpixel(Cord.t_roe)
  (127, 61, 0)
  >>> im.getpixel(Cord.t_salmon)
  (127, 71, 47)
  >>> im.getpixel(Cord.t_shrimp)
  (127, 102, 90)
  >>> im.getpixel(Cord.t_unagi)
  (94, 49, 8)
  >>> im = screenGrab()
  >>> im.getpixel(Cord.buy_rice)
  (127, 127, 127)
  >>>

Если мы добавим эти значения в функцию buyFood(), чтобы знать доступна покупка в данный момент или нет.

def buyFood(food):
     
    if food == 'rice':
        mousePos(Cord.phone)
        time.sleep(.1)
        leftClick()
        mousePos(Cord.menu_rice)
        time.sleep(.05)
        leftClick()
        s = screenGrab()
        if s.getpixel(Cord.buy_rice) != (109, 123, 127):
            print 'rice is available'
            mousePos(Cord.buy_rice)
            time.sleep(.1)
            leftClick()
            mousePos(Cord.delivery_norm)
            time.sleep(.1)
            leftClick()
            time.sleep(2.5)
        else:
            print 'rice is NOT available'
            mousePos(Cord.t_exit)
            leftClick()
            time.sleep(1)
            buyFood(food)
             
 
             
    if food == 'nori':
        mousePos(Cord.phone)
        time.sleep(.1)
        leftClick()
        mousePos(Cord.menu_toppings)
        time.sleep(.05)
        leftClick()
        s = screenGrab()
        print 'test'
        time.sleep(.1)
        if s.getpixel(Cord.t_nori) != (109, 123, 127):
            print 'nori is available'
            mousePos(Cord.t_nori)
            time.sleep(.1)
            leftClick()
            mousePos(Cord.delivery_norm)
            time.sleep(.1)
            leftClick()
            time.sleep(2.5)
        else:
            print 'nori is NOT available'
            mousePos(Cord.t_exit)
            leftClick()
            time.sleep(1)
            buyFood(food)
 
    if food == 'roe':
        mousePos(Cord.phone)
        time.sleep(.1)
        leftClick()
        mousePos(Cord.menu_toppings)
        time.sleep(.05)
        leftClick()
        s = screenGrab()
         
        time.sleep(.1)
        if s.getpixel(Cord.t_roe) != (127, 61, 0):
            print 'roe is available'
            mousePos(Cord.t_roe)
            time.sleep(.1)
            leftClick()
            mousePos(Cord.delivery_norm)
            time.sleep(.1)
            leftClick()
            time.sleep(2.5)
        else:
            print 'roe is NOT available'
            mousePos(Cord.t_exit)
            leftClick()
            time.sleep(1)
            buyFood(food)

Мы передаем имя ингредиента в функцию buyFood(). Серия выражений if/else получает переданный параметр и дает соответствующий ответ. Каждая ветка следует одинаковой логике, поэтому мы рассмотрим только одну.

if food == 'rice':
     mousePos(Cord.phone)
     time.sleep(.1)
     leftClick()
     mousePos(Cord.menu_rice)
     time.sleep(.05)
     leftClick()
     s = screenGrab()
     time.sleep(.1)

Первое, что мы должны сделать это кликнуть на телефон и открыть нужное меню. В этом случае меню с рисом.

s = screenGrab()
if s.getpixel(Cord.buy_rice) != (109, 123, 127):

Далее мы делаем скриншот области и вызываем getpixel(), чтобы получить RGB-значение пикселя у координат Cord.buy_rice. Мы сравниваем их с RGB-значением пикселя, полученного до этого на неактивном элементе. Если мы получаем в результате сравнения значение True, значит кнопка больше не серая и у нас достаточно денег для покупки. Соответственно, если получаем False, значит не можем себе это позволить.

print 'rice is available'
mousePos(Cord.buy_rice)
time.sleep(.1)
leftClick()
mousePos(Cord.delivery_norm)
time.sleep(.1)
leftClick()
time.sleep(2.5)

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

else:
    print 'rice is NOT available'
    mousePos(Cord.t_exit)
    leftClick()
    time.sleep(1)
    buyFood(food)

Наконец, если нам не хватает денег, то мы говорим боту закрыть меню, подождать секунду и повторить все сначала. Обычно это вопрос секунд, когда нам снова становится доступна покупка. Довольно просто добавить дополнительную логику, чтобы бот мог решить, нужно ли продолжать ждать или заняться в это время чем-то полезным и вернуться позже.

Шаг 20: Следим за ингредиентами

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

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

Количество каждого ингредиента остается постоянным на старте каждого уровня. Всегда начинаем с 10 обычных ингредиентов (рис, нори, икра), и по 5 дефицитных ингредиентов (креветки, лосось, угорь).

Начальные ингредиенты

Давайте добавим информацию о них в массив.

foodOnHand = {'shrimp':5,
              'rice':10,
              'nori':10,
              'roe':10,
              'salmon':5,
              'unagi':5}

Ключи массива содержат имена ингредиентов, по ним мы получаем доступ к значениям.

Шаг 21: Добавим отслеживание продуктов в код

Каждый раз, когда мы готовим, мы расходуем ингредиенты. И также пополняем их, когда делаем покупки. Давайте немного расширим нашу функцию makeFood()

def makeFood(food):
    if food == 'caliroll':
        print 'Making a caliroll'
        foodOnHand['rice'] -= 1
        foodOnHand['nori'] -= 1
        foodOnHand['roe'] -= 1 
        mousePos(Cord.f_rice)
        leftClick()
        time.sleep(.05)
        mousePos(Cord.f_nori)
        leftClick()
        time.sleep(.05)
        mousePos(Cord.f_roe)
        leftClick()
        time.sleep(.1)
        foldMat()
        time.sleep(1.5)
     
    elif food == 'onigiri':
        print 'Making a onigiri'
        foodOnHand['rice'] -= 2 
        foodOnHand['nori'] -= 1 
        mousePos(Cord.f_rice)
        leftClick()
        time.sleep(.05)
        mousePos(Cord.f_rice)
        leftClick()
        time.sleep(.05)
        mousePos(Cord.f_nori)
        leftClick()
        time.sleep(.1)
        foldMat()
        time.sleep(.05)
         
        time.sleep(1.5)
 
    elif food == 'gunkan':
        print 'Making a gunkan'
        foodOnHand['rice'] -= 1 
        foodOnHand['nori'] -= 1 
        foodOnHand['roe'] -= 2 
        mousePos(Cord.f_rice)
        leftClick()
        time.sleep(.05)
        mousePos(Cord.f_nori)
        leftClick()
        time.sleep(.05)
        mousePos(Cord.f_roe)
        leftClick()
        time.sleep(.05)
        mousePos(Cord.f_roe)
        leftClick()
        time.sleep(.1)
        foldMat()
        time.sleep(1.5)

Теперь каждый раз при приготовлении Суши, мы уменьшаем значения наших ингредиентов на соответствующие значения. Теперь дополним код в функции buyFood()

def buyFood(food):
     
    if food == 'rice':
        mousePos(Cord.phone)
        time.sleep(.1)
        leftClick()
        mousePos(Cord.menu_rice)
        time.sleep(.05)
        leftClick()
        s = screenGrab()
        print 'test'
        time.sleep(.1)
        if s.getpixel(Cord.buy_rice) != (127, 127, 127):
            print 'rice is available'
            mousePos(Cord.buy_rice)
            time.sleep(.1)
            leftClick()
            mousePos(Cord.delivery_norm)
            foodOnHand['rice'] += 10     
            time.sleep(.1)
            leftClick()
            time.sleep(2.5)
        else:
            print 'rice is NOT available'
            mousePos(Cord.t_exit)
            leftClick()
            time.sleep(1)
            buyFood(food)
             
    if food == 'nori':
        mousePos(Cord.phone)
        time.sleep(.1)
        leftClick()
        mousePos(Cord.menu_toppings)
        time.sleep(.05)
        leftClick()
        s = screenGrab()
        print 'test'
        time.sleep(.1)
        if s.getpixel(Cord.t_nori) != (33, 30, 11):
            print 'nori is available'
            mousePos(Cord.t_nori)
            time.sleep(.1)
            leftClick()
            mousePos(Cord.delivery_norm)
            foodOnHand['nori'] += 10         
            time.sleep(.1)
            leftClick()
            time.sleep(2.5)
        else:
            print 'nori is NOT available'
            mousePos(Cord.t_exit)
            leftClick()
            time.sleep(1)
            buyFood(food)
 
    if food == 'roe':
        mousePos(Cord.phone)
        time.sleep(.1)
        leftClick()
        mousePos(Cord.menu_toppings)
        time.sleep(.05)
        leftClick()
        s = screenGrab()
         
        time.sleep(.1)
        if s.getpixel(Cord.t_roe) != (127, 61, 0):
            print 'roe is available'
            mousePos(Cord.t_roe)
            time.sleep(.1)
            leftClick()
            mousePos(Cord.delivery_norm)
            foodOnHand['roe'] += 10                
            time.sleep(.1)
            leftClick()
            time.sleep(2.5)
        else:
            print 'roe is NOT available'
            mousePos(Cord.t_exit)
            leftClick()
            time.sleep(1)
            buyFood(food)

Шаг 22: Проверка запасов еды

Теперь когда функции makeFood() и buyFood() могут менять количество ингредиентов, нам нужно создать функцию, которая будет отслеживать что количество какого-то ингредиента стало ниже критического уровня.

def checkFood():
    for i, j in foodOnHand.items():
        if i == 'nori' or i == 'rice' or i == 'roe':
            if j <= 3:
                print '%s is low and needs to be replenished' % i
                buyFood(i)

Мы будем циклом обходить пары ключ:значение в массиве с запасами ингредиентов. Если нори, риса или икры останется меньше 4, вызывается функция buyFood(), параметром в которую передается имя ингредиента.

Шаг 23: Перевод RGB значений — Установка

Для того, чтобы двигаться дальше, мы должны получать информацию о том, какой тип суши запрашивает клиент. Сделать это с помощью функции getpixel() было бы достаточно тяжело, так как нам пришлось бы искать область с уникальным значением RGB для каждого вида суши для каждого посетителя. Кроме того, для каждого нового типа суши, вам придется вручную осмотреть его, чтобы увидеть, есть ли у него уникальный RGB, который не найден ни в одном из других типов суши. Это значит, что нам пришлось бы хранить 6 мест, по 8 типов суши. Это 48 уникальных координат.

Очевидно, нам нужен метод попроще.

Метод номер два: усреднение изображения. Этот способ работает с набором RGB-значений, вместо конкретного пикселя. Для каждого скриншота, переведенного в оттенки серого, и загруженного в массив, мы просуммируем все пиксели. Эта сумма обрабатывается также как значение RGB в методе getpixel().

Гибкость этого метода в том, что, когда он настроен, от нас больше ничего не требуется. По мере ввода новых суши, их значения RGB суммируются и выводятся для нашего использования. Нет необходимости искать конкретные координаты с помощью getpixel().

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

Найдите уже написанную функцию screenGrab() и скопируйте ее рядом. Переименуйте копию в grab(), и внесите следующие изменения:

def grab():
    box = (x_pad + 1,y_pad+1,x_pad+641,y_pad+481)
    im = ImageOps.grayscale(ImageGrab.grab(box))
    a = array(im.getcolors())
    a = a.sum()
    print a
    return a

На второй строке мы переводим скриншот в оттенки серого и записываем в переменную im. Такой скриншот позволяет работать с ним намного быстрее, так как вместо 3 значений Красного, Зеленого и Голубого, каждый пиксель имеет только одно значение от 0 до 255.

На третьей строчке мы создаем массив значений цветов использую PIL метод getcolors() и записываем в переменную im.

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

Шаг 24: Зададим области для скринов заказов

Начните новую игру и дождитесь когда все посетители рассядутся по своим местам. Сделайте скрин двойным кликом по quickGrab.py.

Области с заказами

Нам нужно задать ограничивающие области внутри каждого из этих спич-баблов (белые облака с рисунком суши). Приблизьте из в редакторе так, чтобы было видно пиксели:

Области с заказами

Для каждого спич-бабла мы должны быть уверены что верхний левый край начинается в одном и том же месте. Для этого отступим два пикселя от внутреннего края спич-бабла. Первый белый пиксель на второй ступеньке будет началом отсчета.

Области с заказами

Для создания пары координат, отсчитайте 63 по оси x и 16 по оси y. Это даст подобный прямоугольник:

Области с заказами

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

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

def get_seat_one():
    box = (45,427,45+63,427+16)
    im = ImageOps.grayscale(ImageGrab.grab(box))
    a = array(im.getcolors())
    a = a.sum()
    print a
    im.save(os.getcwd() + '\seat_one__' + str(int(time.time())) + '.png', 'PNG')    
    return a
 
def get_seat_two():
    box = (146,427,146+63,427+16)
    im = ImageOps.grayscale(ImageGrab.grab(box))
    a = array(im.getcolors())
    a = a.sum()
    print a
    im.save(os.getcwd() + '\seat_two__' + str(int(time.time())) + '.png', 'PNG')    
    return a
 
def get_seat_three():
    box = (247,427,247+63,427+16)
    im = ImageOps.grayscale(ImageGrab.grab(box))
    a = array(im.getcolors())
    a = a.sum()
    print a
    im.save(os.getcwd() + '\seat_three__' + str(int(time.time())) + '.png', 'PNG')    
    return a
 
def get_seat_four():
    box = (348,427,348+63,427+16)
    im = ImageOps.grayscale(ImageGrab.grab(box))
    a = array(im.getcolors())
    a = a.sum()
    print a
    im.save(os.getcwd() + '\seat_four__' + str(int(time.time())) + '.png', 'PNG')    
    return a
 
def get_seat_five():
    box = (449,427,449+63,427+16)
    im = ImageOps.grayscale(ImageGrab.grab(box))
    a = array(im.getcolors())
    a = a.sum()
    print a
    im.save(os.getcwd() + '\seat_five__' + str(int(time.time())) + '.png', 'PNG')    
    return a
 
def get_seat_six():
    box = (550,427,550+63,427+16)
    im = ImageOps.grayscale(ImageGrab.grab(box))
    a = array(im.getcolors())
    a = a.sum()
    print a
    im.save(os.getcwd() + '\seat_six__' + str(int(time.time())) + '.png', 'PNG')    
    return a
 
def get_all_seats():
    get_seat_one()
    get_seat_two()
    get_seat_three()
    get_seat_four()
    get_seat_five()
    get_seat_six()

Отлично! Много кода, но это только вариации функции ImageGrab.Grab. Теперь нужно перейти в игру и протестировать. Нужно убедиться, что не зависимо от того в каком спич-бабле находится суши, один и тот же заказ отображает одну и ту же сумму.

Шаг 25: Создаем массив с типами Суши

Как только Вы убедились, что каждый тип суши дает одинаковое значение суммы пикселей, запишите эти суммы в массив:

sushiTypes = {1893:'onigiri', 
    2537:'caliroll',
    1900:'gunkan',}

Здесь значение стоит на месте ключа потому, что по нему будет осуществляться поиск.

Шаг 26: Создаем массив мест без спич-баблов

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

Нужно начать игру и выполнить функцию get_all_seats() до того как придет первый посетитель. Полученные значения запишем в массив Blank.

class Blank:
    seat_1 = 8041
    seat_2 = 5908
    seat_3 = 10984
    seat_4 = 10304
    seat_5 = 6519
    seat_6 = 8963

Шаг 27: Соединяем все вместе

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

Основа логики будет следующая: Проверка мест > Проверка заказов > Если не хватает ингредиентов, то купить > почистить столы > Повторить сначала

def check_bubs():

  checkFood()
  s1 = get_seat_one()
  if s1 != Blank.seat_1:
      if sushiTypes.has_key(s1):
          print 'table 1 is occupied and needs %s' % sushiTypes[s1]
          makeFood(sushiTypes[s1])
      else:
          print 'sushi not found!n sushiType = %i' % s1

  else:
      print 'Table 1 unoccupied'

  clear_tables()
  checkFood()
  s2 = get_seat_two()
  if s2 != Blank.seat_2:
      if sushiTypes.has_key(s2):
          print 'table 2 is occupied and needs %s' % sushiTypes[s2]
          makeFood(sushiTypes[s2])
      else:
          print 'sushi not found!n sushiType = %i' % s2

  else:
      print 'Table 2 unoccupied'

  checkFood()
  s3 = get_seat_three()
  if s3 != Blank.seat_3:
      if sushiTypes.has_key(s3):
          print 'table 3 is occupied and needs %s' % sushiTypes[s3]
          makeFood(sushiTypes[s3])
      else:
          print 'sushi not found!n sushiType = %i' % s3

  else:
      print 'Table 3 unoccupied'

  checkFood()
  s4 = get_seat_four()
  if s4 != Blank.seat_4:
      if sushiTypes.has_key(s4):
          print 'table 4 is occupied and needs %s' % sushiTypes[s4]
          makeFood(sushiTypes[s4])
      else:
          print 'sushi not found!n sushiType = %i' % s4

  else:
      print 'Table 4 unoccupied'

  clear_tables()
  checkFood()
  s5 = get_seat_five()
  if s5 != Blank.seat_5:
      if sushiTypes.has_key(s5):
          print 'table 5 is occupied and needs %s' % sushiTypes[s5]
          makeFood(sushiTypes[s5])
      else:
          print 'sushi not found!n sushiType = %i' % s5

  else:
      print 'Table 5 unoccupied'

  checkFood()
  s6 = get_seat_six()
  if s6 != Blank.seat_6:
      if sushiTypes.has_key(s6):
          print 'table 1 is occupied and needs %s' % sushiTypes[s6]
          makeFood(sushiTypes[s6])
      else:
          print 'sushi not found!n sushiType = %i' % s6

  else:
      print 'Table 6 unoccupied'

  clear_tables()

Первое что мы делаем — это проверяем запасы. Далее делаем скрин позиции заказа первого посетителя и записываем в s1. После этого проверяем скрин на неравенство пустому месту. Если они не равны, значит у нас есть посетитель. Далее проверяем массив sushiTypes чтобы определить какой заказ сделал посетитель и передаем этот аргумент в функцию makeFood().

Clear_tables() выполняется через проверку каждых двух мест.

Теперь надо это зациклить.

Шаг 28: Главный цикл

Мы создаем цикл. Так как мы не задаем никакого условия выхода из цикла, то чтобы завершить игру, нужно в консоли нажать CTRL + C.

def main():
    startGame()
    while True:
        check_bubs()

Вот и все! Обновите страницу, дождитесь загрузки игры и запустите бота!

Бот немного неуклюжий и нуждается в доработках. Но это отличный скелет для того чтобы продолжить экспериментировать!

Факт 1. Вы можете использовать WinApi чтобы посылать нажатия клавиш и щелчки/движения мышки в определённых координатах.
Факт 2. Вы можете свободно копаться в оперативной памяти чужих программ (используя более низкоуровневые языки), вы можете слушать соединение игры с игровым сервером: в котором все ключевые данные (положение игрока и соперников например) как-раз и передают регулярно.

Так с простым ботом ничего сложного — перехватываете нужные данные, например положение деревьев и игрока в компьютерной игре. Ваш некий алгоритм определяет какие клавиши надо жать в текущий момент времени, куда двигать мышку. Выполняете, и дальше по кругу.

Некоторое время назад зарабатывал на ботах для браузерных игр. Так вот лично мои боты были расширениями браузеров написанные только на JS. Плюс расширения в этом деле в том, что есть DOM дерево: то есть очень просто достать исчерпывающую информацию о мире игры. Также интересно то, что можно «командовать» окном игры, вызывая из расширения любые скрипты в пространстве окна, а ещё в пространстве расширения можно подключать прямо из JS dll библиотеки, например WinApi .

Как сделать бота для игры?

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

Можно обратиться на сайт habrahabr, где есть подробное описание создания бота для игры.

И посмотрите обучающее видео.

Смотря для чего вам нужен бот. К примеру, в игре «Counter-Strike» во время игры можно добавлять ботов, то есть персонажей за команду, которые управляются не живыми игроками, а бегают сами по себе и выполняют ряд действий. При этом стоит отметить, что бот — это заскриптованный персонаж, которые выполняет команды при стечении обстоятельств и не более. То есть он видит противника — сразу начинает стрелять, при этом рандомно, процент попадания зависит от действий игрока и алгоритма.

Пишем бота для онлайн-игры на JavaScript с применением AOP


Если вы как и я любите онлайн игры, но не любите тратить на них сильно много времени, добро пожаловать под кат. Мы не будем обсуждать боты это хорошо или нет, а просто разберем как можно для конкретной онлайн игры сделать бота. Он будет не тупо клацать по кнопке по таймауту, а будет реагировать на события в браузере. Это мы сделаем с помощью Аспектно-ориентированного программирования (далее AOP). Для примера я выбрал полюбившуюся хабром игру Пернатск.

1. Готовим ингредиенты
  • Собствена сама игра. Я буду показывать на примере Пернатска
  • Браузер. У меня все стандартно — Chrome
  • Текстовый редактор или в чем вы будете редактировать JS код. Notepad++ подойдет
  • Аккаунт для тестов, который не жалко будет потерять в результате бана

Важно! Игра должна работать в браузере, а не в клиенте. Причем не на Flash, а на HTML+JavaScript.
На выходе у нас должно получиться расширение для Chrome, которое будет играть вместо нас.

2. Делаем расширение

О том как делается расширение я не буду подробно расписывать. На хабре об этом уже писали, например, тут.
Приведу лишь коды, нужных нам файлов.
В manifest.json

В строчке «matches»: [ «pernatsk.ru*» ] вам нужно будет указать адрес вашей игры.
Файл background.js я использую для случаев, когда хочу инджектить на сайте свой JS кода. Собственно код background.js:

Важно! Если вы не понимаете, что мы делаем в этой единственной функции, то делать бота вам пока рано. Почитайте основы JavaScript.
Вся работа у нас будет вестись в файле injected.js Его код пока такой:

Все эти файлы сохраняем в одной папке bot.

3. Первый пуск бота
4. Добавляем AOP

Для работы бота нам потребуются библиотеки. Мой любимый jQuery уже используется на Пернатске, поэтому добавлять его не будет.
Добавим плагин AOP for Jquery. По хорошему это стоило запаковать в само расширение в виде отдельного файла, но я ленив. Поэтому просто добавим код bin/aop.pack.js первой строкой в наш injected.js.
Проверим, что это работает изменив ai_on

Проверяем, что AOP нормально подключилось. В консоле разработчика теперь будет строчка «jQuery detected!» Сообщение будет только один раз, так как я отключаю совет после первого же срабатывания.
Важно! Прочитайте документацию AOP for Jquery, чтобы понять jQuery.aop.after и bot[0].unweave().

5. Зачем мы будем использовать AOP
6. Учим бота первой команде

В injected.js добавим такой код:

По этой команде наша бот-птичка будет лететь в Пернатске за шишками. Код слегка мудренный, так как в Пернатске есть небольшая защита от ботов.
Когда вы будете писать свои команды я рекомендую сначала опробовать их работоспособность в console, а уже потом переносить код в редактор.
Чтобы протестировать и проверить работу нашей команды запустим в косноле код commands.conessearch() Все работает.

7. Ищем событие на которое должен реагировать бот

Тут есть два метода первый — анализируем код игры. Долго 🙁
Второй метод — воспользоваться AOP, и после всех функций, который срабатывали вывести в лог их имя. Потом выбрать нужные.
Меняем ai_on()

У нас пойдет много-много функция. Там будет $ от jQuery или стандартная setTimeout.
Работать с этим очень не удобно изменим код еще раз.

Теперь у нас отражаются только те функции, которые еще не отображались. Их полный список мы храним в fnList.
После пары минут там будут такие варианты функции для прицепки [«clearInterval», «$», «setTimeout», «timerTick», «serverTimeUpdate», «getComputedStyle», «setInterval», «tutorialArr», «showQ», «showQc», «updateBirdData», «viz», «unviz», «weatherUpdate»]
Меняя target и регулярное выражение в method мы можем подобрать ту функцию, которая нам подойдет, чтобы к ней прицепиться. Для примера, я выбираю функцию weatherUpdate теперь каждый раз как будет меняться погода наша птичка будет лететь за шишками.

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

Многие браузерные и сетевые игры, игры в соц.сетях отличаются скучностью и однообразием «фарма». Игрок ставится перед выбором — «фармить» или заплатить. И так получается не только из-за желания разработчиков заработать.

Немного лирики

Проблема в балансе. Если добыть игровые ресурсы, развить своего персонажа — просто, то ценность такого развития не велика. Потому «ценный лут» выпадает из поверженных монстров редко, а каждый следующий шаг в развитии стоит намного дороже, чем предыдущий.

В определенный момент все сводится к тому, чтобы потратить много времени на добычу игровых ресурсов, которые потом можно будет обратить в какие то преимущества в сражениях против игроков (купив/добыв артефакты, прокачав персонажа, развив недостающие навыки и т.п.). Время, потраченное на «скучный фарм», занимает бòльшую часть игры.

Пишем ботов

В большинстве случаев фарм можно автоматизировать. Пусть компьютер, робот сам делает всю скучную работу.

Я не однократно писал роботов для игр.

Первая серьезная поделка была сделана, когда я ещё играл в Ultima Onlne — тогда был создан проект UO Shra pilot. Программа позволяла составлять скрипты для управления персонажем в UO, имитируя нажатия мыши и клавиатуры. С помощью этой программы, например, я записывал специальные скрипты, которые позволяли моему персонажу «летать» по лесу в Ultima Online и рубить лес, собирая бревна. Этот ресурс был довольно ходовым, легко продавался.

В Ogame требовалась другая задача — защитить флот от нападения высоразвитых противников. Разбитые флоты перерабатывались на ресурсы. За «небольшими» и «средними» флотами велась настоящая охота. Стоило уйти в оффлайн, и вы рисковали потерять все ваши корабли. Игроки прибегали к разным тактикам сохранения флота. Например, отправляли корабли в дальний многочасовой поход, где их нельзя было перехватить. Я же написал скрипт-няньку.

Нянька может работать как на локальном компьютере с выходом в интернет, так и на веб-сервере у провайдера, т.к. написана на PHP. Скрипт периодически запускаясь, проверял угрозы нападения на мои колонии. За 5-10 минут до момента атаки флоты поднимались с планеты, а потом возвращались назад, если угроза нападения миновала. Так удавалось сохранять и преумножать свои флоты, несмотря на происки врагов. 🙂

До тех пор, конечно, пока меня не забанили за мою находчивость.

Инструментарий для написания ботов

Я пробовал и сторонние инструментальные средства. Часть из них условно-бесплатна, т.е. в итоге потребует покупки. Другие — очень глючные и нестабильные. Некоторые применимы только для игр с определенной архитектрурой (к примеру только для браузерных HTML игр).

Чаще всего требуется выстроить логику бота в виде директив — найди картинку на экране, кликни/перетащи элемент, введи какие то значения в поля ввода, нажми кнопку и т.п.

Совсем недавно я нашел очень подходящий IDE для реализации подобной логики. Он разработан на JAVA, поэтому установлен может быть куда угодно — на MAC, WIN или UNIX.

siculiX

Рассказывать что то более подробно, чем изложено на www.sikulix.com — нет надобности. IDE имеет открытый код и распространяется бесплатно.

SikuliX использует синтаксис Phyton или Rubi (выбираете при установке), предоставляя для работы свои объекты и классы. Для людей, далеких от программирования, это прозвучит устрашающе. Но вы сами решаете на сколько глубоко залазить в эту кроличью нору. 🙂

Данная запись опубликована в 06.06.2016 19:14 и размещена в Наша жизнь — игра!, Программирование. Вы можете перейти в конец страницы и оставить ваш комментарий.

Мало букафф? Читайте есчо !

Основы программирования ботов на SikuliX

SikuliX имеет ряд особенностей, о которых я расскажу далее. Этот IDE разработан с целью автоматизации рутины, т.е. для создания роботов, в том числе и для игр. Скрипт не вмешивается в обмен данными по сети или работу игрового клиента. Он действует .

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

Однажды мы решили, что нам нужно сделать бота. Бот должен был давать логические задачи в групповом чате в Telegram «Развиваем логику», закреплять их на то время, пока её решают, не давать новую, пока ответ кого-то из участников не наберёт десять (потом снизили до пяти) плюсов, а также вывешивать топ наиболее успешных решателей задачек.

Идея не приходит одна

Задача довольно простая, подумали мы, особенно учитывая волну популярности ботов и количество материала в сети по этой теме. Однако материала не очень качественного: кто-то не использует композер, а кто-то даже пишет весь код в одном файле-обработчике, полагая, очевидно, будто количество файлов влияет на скорость работы кода.

Сначала бот должен был просто давать задачи, причём их можно было пропускать, постоянно вызывая одну и ту же команду. Это первая проблема, которую мы стали решать. Выход нашёлся почти сразу: сохранение состояния бота (а точнее, текущей команды) в базе. Другими словами, получая команду /get, бот даёт задачу и сохраняет команду в базе.

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

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

Ну и напоследок ещё одна проблема, которую мы с успехом решили. Некоторые задачи предполагали не точное решение, а наиболее подходящее. Как в таком случае должен поступить админ и чей ответ выбрать?

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

Какое решение быстрее наберёт пять плюсов, то и выбирается ботом как лучшее. Это хорошее решение не только с точки зрения справедливости, но и автоматизации: нам больше не придётся следить за чатом и читать все ответы.

Подготовка к созданию

Дело осталось за малым: написать бота. Мы ограничились Composer, библиотекой telegram-bot-sdk и symfony/dotenv для парсинга .env-файла. Весь код приводить не буду: он большой. Посмотреть на то, что получилось, можно по ссылке.

Composer — это стандарт при разработке на PHP. Он позволяет скачивать сторонние библиотеки на проект и предоставляет удобный механизм по автозагрузке классов. Вся работа с Composer происходит через консоль и в файле composer.json. Обычно он выглядит так:

// composer.json
{
«require»: {
«irazasyed/telegram-bot-sdk»: «3.*@dev»,
«symfony/dotenv»: «^4.2»
},
«autoload»: {
«psr-4»: {
«App\»: «src/»
}
}
}

Если вы разрабатываете не на фреймворке, то во множестве случаев создаёте composer.json самостоятельно и заполняете секцию autoload, которая загружает ваши классы по правилу psr-4, о котором можно найти много информации в интернете.

Далее вы выполняете команду composer install, и автозагрузка начинает работать. Также не забудьте про библиотеки, которые нужно установить, для этого выполните в терминале в папке с проектом следующие две команды:

composer require irazasyed/telegram-bot-sdk
composer require symfony/dotenv

И тогда ваш composer.json станет похож на тот, что я показывал выше.

Mr. Bot

Сразу же продемонстрирую готовую структуру проекта:

Файл app.php является точкой входа в наше приложение, на который мы вешаем веб-хук (это значит, что бот не будет постоянно опрашивать сервер на наличие обновлений; он их будет получать только тогда, когда они будут). Вот как он выглядит:

// app.php

<?php
use AppBotCommonChatHandler;
use AppBotPrivateChatHandler;
use AppHandler;
use AppStorageDB;
use TelegramBotApi;

require __DIR__ .’/vendor/autoload.php’;

$settings = require __DIR__ . ‘/config/settings.php’;

$api = new Api($settings[‘token’]);
$bot = new Handler($api);
$db = new DB($settings[‘db’]);
if ($bot->getChatType() === «private») {
(new PrivateChatHandler($bot, $db))->start();
} elseif ($bot->getChatType() === «supergroup») {
(new CommonChatHandler($bot, $db))->start();
}

Ничего необычного, создаём объекты всех нужных нам классов и делаем проверку на то, в каком чате мы находимся — приватном или супергруппе. Да, разделить код на достаточно независимые части является хорошей практикой. К тому же легче рефакторить и добавлять код, когда проблемы того потребуют.

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

// .env

DB_DSN=mysql:host=changeme;dbname=changeme
DB_USERNAME=changeme
DB_PASSWORD=changeme
BOT_ADMIN=changeme
TOKEN=changeme

Однако получить переменные окружения из .env можно только в том случае, если вы спарсите этот файл. Делается это крайне просто:

// config/settings.php

<?php

use SymfonyComponentDotenvDotenv;

$dotenv = new Dotenv();
$dotenv->loadEnv(__DIR__ . ‘/../.env’);

return [
‘db’ => [
‘dsn’ => getenv(‘DB_DSN’),
‘username’ => getenv(‘DB_USERNAME’),
‘password’ => getenv(‘DB_PASSWORD’)
],
‘token’ => getenv(‘TOKEN’)
];

Это тот самый файл, который мы включили в app.php. Там мы просто по ключу достаём нужные нам настройки:

$settings = require __DIR__ . ‘/config/settings.php’;
$settings[‘token’];
$settings[‘db’];

В папках Admin и User хранятся классы по работе как с приватным чатом, так и с супергруппой. Это не только удобно, как я говорил выше, но и даёт возможность отключить деятельность бота в одном из режимов, если мы соберёмся добавлять в него функциональность.

Больше томить вас кодом не буду, повторю только, что теперь он в свободном доступе.

Так мы решили сразу несколько проблем и написали достаточно неплохой, легко поддерживаемый код для бота, который по-прежнему в добром здравии и надёжно выполняет свои функции.

Боты для компьютерных игр

Это перевод книги «Practical Video Game Bots» на русский язык

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

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

Ссылки

  • Читать или скачать на Leanpub
  • Исходный код примеров
  • Связь с автором

Устаревший вариант книги в формате GitBook

  • Читать книгу онлайн на GitBook
  • Скачать в формате PDF
  • Скачать в формате Mobi
  • Скачать в формате ePub

Понравилась статья? Поделить с друзьями:
  • Как пишется яндекс на англ языке
  • Как пишут драйвера для устройств
  • Как пишут болгары
  • Как пишется яндекс музыка
  • Как пишут должностные инструкции