Как написать игрового бота на python для web

Подготовка

Этот туториал, и код в нем, требует установки нескольких дополнительных библиотек для 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()

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

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

В этом уроке мы рассмотрим все тонкости создания игрового бота на основе Computer Vision на Python, который сможет играть в популярную флеш-игру Sushi Go Round . Вы можете использовать методы, описанные в этом руководстве, для создания ботов для автоматического тестирования ваших собственных веб-игр.


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

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


Предпосылки

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

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

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

  • Библиотека изображений Python
  • Numpy
  • PyWin
  • У всего вышеперечисленного есть самостоятельные установщики; Запустив их, вы автоматически установите модули в каталог libsite-packages и, теоретически, настроите ваш pythonPath соответствующим образом. Однако на практике это не всегда происходит. Если вы начнете получать какие-либо сообщения об Import Error после установки, вам, вероятно, потребуется вручную настроить переменные среды. Более подробную информацию о настройке переменных пути можно найти здесь .

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

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

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

Игровые шаблоны на Envato Market

Игровые шаблоны на Envato Market

Вступление

Это руководство написано, чтобы дать базовое введение в процесс создания ботов, которые играют в браузерные игры. Подход, который мы собираемся предпринять, вероятно, немного отличается от того, что большинство ожидает, когда они думают о боте. Вместо того, чтобы создавать программу, которая находится между клиентом и сервером, внедряющим код (например, бот Quake или C / S), наш бот будет сидеть «снаружи». Мы будем полагаться на методы Computer Vision-esque и вызовы API Windows для сбора необходимой информации и создания движений.

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

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

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

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

Веселиться!


Шаг 1: Создайте новый проект Python

В новой папке щелкните правой кнопкой мыши и выберите « New > Text Document .

Python_Snapshot_of_entire_screen_area

После этого переименуйте файл из «New Text Document» в «quickGrab.py» (без кавычек) и подтвердите, что вы хотите изменить расширение имени файла.

Python_Snapshot_of_entire_screen_area

Наконец, щелкните правой кнопкой мыши на нашем вновь созданном файле и выберите «Редактировать с IDLE» в контекстном меню, чтобы запустить редактор

Python_Snapshot_of_entire_screen_area


Шаг 2: Настройка первого захвата экрана

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

В quickgrab.py введите следующий код:

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

import 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()

Запуск этой программы должен дать вам полный снимок области экрана:

Python_Snapshot_of_entire_screen_area

Текущий код захватывает всю ширину и высоту области экрана и сохраняет ее как PNG в вашем текущем рабочем каталоге.

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

Первые три строки:

1

2

3

import ImageGrab

import os

import time

… метко названы «операторы импорта». Они говорят Python загружать перечисленные модули во время выполнения. Это дает нам доступ к их методам через синтаксис module.attribute .

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

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

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

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

1

2

3

4

5

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 . Первая часть im.save( вызывает метод «save» из класса Image. Он ожидает два аргумента. Первый – это место, в котором нужно сохранить файл, а второй – формат файла.

Здесь мы устанавливаем местоположение, сначала вызывая os.getcwd() . Это получает текущий каталог, из которого выполняется код, и возвращает его в виде строки. Затем мы добавим + . Это будет использоваться между каждым новым аргументом для объединения всех строк вместе.

Следующая часть '\full_snap__ дает нашему файлу простое описательное имя. (Поскольку в Python обратная косая черта является escape-символом, мы должны добавить два из них, чтобы не пропустить одну из наших букв).

Далее идет волосатый бит: str(int(time.time())) . Это использует преимущества встроенных в Python методов Type. Мы объясним эту часть, работая изнутри:

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

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

И здесь, в конце, соглашение Python, которое проверяет, является ли скрипт верхнего уровня, и если да, позволяет ему работать. В переводе это просто означает, что он выполняет main() если он запускается сам по себе. В противном случае – если, например, он загружен как модуль другим скриптом Python – он предоставляет только свои методы вместо выполнения своего кода.

1

2

3

4

5

def main():

    screenGrab()

if __name__ == ‘__main__’:

    main()


Шаг 3: Ограничительная рамка

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

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

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

Давайте применим это на практике.

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

Python_Snapshot_of_sushi_game_full_screen

Снимок всей области экрана.


Шаг 4: Получение координат

Теперь пришло время начать добывать некоторые координаты для нашей ограничительной рамки.

Откройте свой последний снимок в редакторе изображений.

Python_Snapshot_of_sushi_game_full_screen

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

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

looking_at_xy

Если это еще не сделано, включите отображение линейки в редакторе и увеличивайте верхний угол игровой зоны, пока не увидите детали в пикселях:

looking_at_x_y

Наведите курсор на первый пиксель игровой площадки и проверьте координаты, отображаемые на линейке. Это будут первые два значения нашего кортежа Box. На моей конкретной машине эти значения 157, 162 .

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

looking_at_x_y

Это показывает координаты 796 и 641. Объединение их с нашей предыдущей парой дает коробку с координатами (157,162,796,641) .

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

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

import ImageGrab

import os

import time

def screenGrab():

    box = (157,346,796,825)

    im = ImageGrab.grab(box)

    im.save(os.getcwd() + ‘\full_snap__’ + str(int(time.time())) +

‘.png’, ‘PNG’)

def main():

    screenGrab()

if __name__ == ‘__main__’:

    main()

В строке 6 мы обновили кортеж, чтобы он содержал координаты игровой зоны.

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

play_area_snapshotpng

Успех! Идеальный захват игровой площадки. Нам не всегда нужно заниматься интенсивной охотой за координатами. Как только мы войдем в win32api, мы рассмотрим несколько более быстрых методов для установки координат, когда нам не нужна идеальная точность пикселей.


Шаг 5. Планирование гибкости

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

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

Поскольку мы уже выполнили измерения, установка пэдов для нашей нынешней системы очень проста. Мы собираемся установить пэды для хранения местоположения первого пикселя за пределами игровой зоны. Из первой пары координат x, y в нашем кортеже вычтите 1 из каждого значения. Таким образом, 157 становится 156 , а 346 становится 345 .

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

1

2

3

4

5

# Globals

# ——————

x_pad = 156

y_pad = 345

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

1

2

3

4

5

def screenGrab():

    box = (x_pad+1, y_pad+1, 796, 825)

    im = ImageGrab.grab()

    im.save(os.getcwd() + ‘\full_snap__’ + str(int(time.time())) +

‘.png’, ‘PNG’)

Для второй пары мы сначала вычтем значения площадок (156 и 345) из координат (796, 825), а затем используем эти значения в том же формате Pad + Value .

1

2

3

4

5

def screenGrab():

    box = (x_pad+1, y_pad+1, x_pad+640, y_pad+479)

    im = ImageGrab.grab()

    im.save(os.getcwd() + ‘\full_snap__’ + str(int(time.time())) +

‘.png’, ‘PNG’)

Здесь координата x становится 640 (769-156), а y становится 480 (825-345)

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


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

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

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

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

1

2

3

4

5

6

7

8

9

“””

All coordinates assume a screen resolution of 1280×1024, 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

“””

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


Шаг 7: Превращение quickGrab.py в полезный инструмент

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

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

В вашей папке щелкните правой кнопкой мыши quickGrab.py и выберите «Копировать» из меню.

play_area_snapshotpng

Теперь щелкните правой кнопкой мыши и выберите «вставить» из меню

play_area_snapshotpng

Выберите скопированный файл и переименуйте его в «code.py»

play_area_snapshotpng

Отныне все новые дополнения и изменения кода будут вноситься в code.py. quickGrab.py теперь будет функционировать исключительно как инструмент для создания снимков. Нам просто нужно сделать одно окончательное изменение:

Измените расширение файла с .py на .pyw и подтвердите изменения.

play_area_snapshotpng

Это расширение указывает Python запускать скрипт без запуска консоли. Таким образом, quickGrab.pyw соответствует своему названию. Дважды щелкните файл, и он тихо выполнит свой код в фоновом режиме и сохранит снимок в рабочий каталог.

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


Шаг 8: Win32api – краткий обзор

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

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

win32api.mouse_event() :

1

2

3

4

5

6

win32api.mouse_event(

   dwFlags,

   dx,

   dy,

   dwData

   )

Первый параметр dwFlags определяет «действие» мыши. Он контролирует такие вещи, как движение, щелчок, прокрутка и т. Д.

В следующем списке показаны наиболее распространенные параметры, используемые при движении сценариев.

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 параметру dwFlags .

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

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

Быстрый пример, чтобы укрепить эти методы:

Если мы представим игру с системой выбора оружия, похожую на Half-Life 2, в которой оружие можно выбрать, вращая колесико мыши, мы предложим следующую функцию для просмотра списка оружия:

1

2

3

4

def browseWeapons():

   weaponList = [‘crowbar’,’gravity gun’,’pistol’…]

   for i in weaponList:

       win32api.mouse_event(win32con.MOUSEEVENTF_MOUSEEVENTF_WHEEL,0,0,120)

Здесь мы хотим смоделировать прокрутку колесика мыши для навигации по списку теоретического оружия, поэтому мы передали ...MOUSEEVENTF_WHEEL действие “MOUSEEVENTF_WHEEL” dwFlag. Нам не нужны dx или dy , позиционные данные, поэтому мы оставили их равными нулю, и мы хотели прокрутить один щелчок в направлении вперед для каждого «оружия» в списке, поэтому мы передали целое число 120 в dwData (каждый щелчок колеса равен 120).

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


Шаг 5: щелчок мышью

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

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

1

import win32api, win32con

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

Далее мы сделаем нашу первую функцию щелчка мышью.

1

2

3

4

5

def leftClick():

   win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN,0,0)

   time.sleep(.1)

   win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP,0,0)

   print “Click.”

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

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

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

1

2

3

4

5

6

7

8

9

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’


Шаг 9: Основное движение мыши

С помощью щелчка мышью все, что осталось, – это перемещать мышь по экрану.

Добавьте следующие функции в code.py :

1

2

3

4

5

6

7

8

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 и y ; важно делать это везде, где называется координата.

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

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


Шаг 10: навигация по игровым меню

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

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

play_area_snapshotpng

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

  1. Начальная кнопка воспроизведения

    play_buttonpng

  2. Кнопка «Продолжить» на iPhone

  3. Учебник “Пропустить”, кнопка

  4. Сегодняшняя цель “Продолжить” кнопка

    PNG

Нам нужно получить координаты для каждого из них и добавить их в новую функцию startGame() . Расположите оболочку IDLE так, чтобы вы могли видеть ее и игровую зону. Введите get_cords() но пока не нажимайте return; наведите курсор мыши на кнопку, для которой вам нужны координаты. Не нажимайте пока, потому что мы хотим, чтобы фокус оставался в оболочке. Наведите указатель мыши на элемент меню и нажмите клавишу возврата. Это захватит текущее местоположение мыши и выведет на консоль кортеж, содержащий значения x,y . Повторите это для оставшихся трех меню.

Оставьте оболочку открытой и расположите ее так, чтобы вы могли видеть ее так же, как и редактор IDLE. Теперь мы собираемся добавить нашу startGame() и заполнить ее вновь полученными координатами.

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

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) говорит Python прекратить выполнение на 1/10 секунды между каждым кликом, что дает меню достаточно времени для обновления между ними.

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

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


Шаг 11: Получение координат еды

Теперь мы собираемся повторить один и тот же процесс для каждой из этих кнопок:

play_buttonpng

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

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

Вот цикл for в действии:

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

1

2

3

4

5

6

7

8

class Cord:

   f_shrimp = (54,700)

   f_rice = (119 701)

   f_nori = (63 745)

   f_roe = (111 749)

   f_salmon = (54 815)

   f_unagi = (111 812)

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

Мы вернемся к ним чуть позже. Есть еще немного координатной охоты!


Шаг 12: Получение координат пустой тарелки

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

play_buttonpng

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

01

02

03

04

05

06

07

08

09

10

11

“””

Plate cords:

    108, 573

    212, 574

    311, 573

    412, 574

    516, 575

    618, 573

“””

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


Шаг 13: Получение телефонных координат

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

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

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

Нам нужно пройти через шесть меню:

  1. Телефон

    play_buttonpng

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

  3. Начинка

  4. Рис

    PNG

  5. Перевозка

    PNG

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

Получение координат:

Мы собираемся добавить все это в наш класс Cord. Мы будем использовать префикс « t_ » для обозначения того, что типы продуктов являются пунктами меню телефона> начинки.

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

class Cord:

    f_shrimp = (54,700)

    f_rice = (119 701)

    f_nori = (63 745)

    f_roe = (111 749)

    f_salmon = (54 815)

    f_unagi = (111 812)

#———————————–

    phone = (601, 730)

    menu_toppings = (567, 638)

    t_shrimp = (509, 581)

    t_nori = (507, 645)

    t_roe = (592, 644)

    t_salmon = (510, 699)

    t_unagi = (597, 585)

    t_exit = (614, 702)

    menu_rice = (551, 662)

    buy_rice = 564, 647

    delivery_norm = (510, 664)

Хорошо! Мы наконец-то добыли все нужные нам значения координат. Итак, давайте начнем делать что-то полезное!


Шаг 14: Очистка таблиц

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

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

def clear_tables():

   mousePos((108, 573))

   leftClick()

   mousePos((212, 574))

   leftClick()

   mousePos((311, 573))

   leftClick()

   mousePos((412, 574))

   leftClick()

   mousePos((516, 575))

   leftClick()

   mousePos((618, 573))

   leftClick()

   time.sleep(1)

Как видите, это выглядит более или менее точно так же, как наша ранняя startGame() . Несколько небольших отличий:

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

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

Так что не забудьте добавить несколько надежных пауз в ваших собственных ботов!


Шаг 15: Приготовление суши

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

play_buttonpng

01

02

03

04

05

06

07

08

09

10

11

12

”’

Recipes:

    onigiri

        2 rice, 1 nori

    caliroll:

        1 rice, 1 nori, 1 roe

    gunkan:

        1 rice, 1 nori, 2 roe

”’

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

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

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() вызывается в конце каждого процесса приготовления суши. Это щелкает мат, чтобы катить суши, которые мы только что собрали. Давайте определим эту функцию сейчас:

1

2

3

4

def foldMat():

   mousePos((Cord.f_rice[0]+40,Cord.f_rice[1]))

   leftClick()

   time.sleep(.1)

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

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


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

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

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

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)

Вот именно для этого шага. Мы сделаем больше с этим позже.


Краткое введение: сделать компьютер видимым

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

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

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

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

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


Шаг 17: Импорт Numpy и ImageOps

Добавьте следующее в ваш список операторов импорта.

1

2

import ImageOps

from numpy import *

ImageOps – это еще один модуль PIL. Он используется для выполнения операций (например, в градациях серого) над изображением.

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

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


Шаг 18: Заставить компьютер видеть

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

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

play_buttonpng

В code.py, выделите вашу screenGrab()функцию. Мы собираемся внести следующие изменения:

1

2

3

4

5

6

def screenGrab():

   b1 = (x_pad + 1,y_pad+1,x_pad+640,y_pad+480)

   im = ImageGrab.grab()

   return im

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

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

С открытым меню Toppings и серым цветом все элементы запустите следующий код:

Это назначает снимок, который мы берем screenGrab()на экземпляр im. Здесь мы можем вызвать getpixel(xy)метод, чтобы получить данные о конкретных пикселях.

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

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

Вывод нашей интерактивной сессии:

01

02

03

04

05

06

07

08

09

10

11

12

13

14

>>> im = screenGrab()

>>> im.getpixel(Cord.t_nori)

(33, 30, 11)

>>> 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.getpixel(Cord.buy_rice)

(127, 127, 127)

>>>

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

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

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) != (127, 127, 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) != (33, 30, 11):

           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 / elif используется для перехвата переданного параметра и соответствующего ответа. Каждый форк следует той же логике, поэтому мы рассмотрим только первый.

1

2

3

4

5

6

7

8

9

if food == 'rice':

       mousePos(Cord.phone)

       time.sleep(.1)

       leftClick()

       mousePos(Cord.menu_rice)

       time.sleep(.05)

       leftClick()

       s = screenGrab()

       time.sleep(.1)

Первое, что мы делаем после ifразветвления, это нажимаем на телефон и открываем соответствующий пункт меню – в данном случае меню Риса.

1

2

s = screenGrab()

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

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

1

2

3

4

5

6

7

8

print 'rice is available'

mousePos(Cord.buy_rice)

time.sleep(.1)

leftClick()

mousePos(Cord.delivery_norm)

time.sleep(.1)

leftClick()

time.sleep(2.5)

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

1

2

3

4

5

6

else:

            print 'rice is NOT available'

            mousePos(Cord.t_exit)

            leftClick()

            time.sleep(1)

            buyFood(food)

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


Шаг 19: Отслеживание ингредиентов

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

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

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

play_buttonpng

Давайте добавим эту информацию в словарь.

1

2

3

4

5

6

foodOnHand = {'shrimp':5,

             'rice':10,

             'nori':10,

             'roe':10,

             'salmon':5,

             'unagi':5}

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


Шаг 20: добавление отслеживания в код

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

Давайте начнем с расширения makeFood()функции

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

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)

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

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

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)

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


Шаг 21: Проверка еды на руках

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

1

2

3

4

5

6

def checkFood():

   for i, j in foodOnHand.items():

       if i == 'nori' or i == 'rice' or i == 'roe':

           if j <= 4:

               print '%s is low and needs to be replenished' % i

               buyFood(i)

Здесь мы настраиваем forцикл, чтобы перебирать пары ключ и значение нашего foodOnHandсловаря. Для каждого значения проверяется, соответствует ли имя одному из необходимых нам ингредиентов; если это так, то он проверяет, является ли его значение меньше или равно 3; и, наконец, при условии, что оно меньше 3, он вызывает buyFood()тип ингредиента в качестве параметра.

Давайте проверим это немного.

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


Шаг 22: Обход значений RGB – Настройка

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

Итак, в заключение, нам нужен лучший метод.

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

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

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

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

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

def screenGrab():

    box = (x_pad + 1,y_pad+1,x_pad+640,y_pad+480)

    im = ImageGrab.grab(box)

    return im

def grab():

    box = (x_pad + 1,y_pad+1,x_pad+640,y_pad+480)

    im = ImageOps.grayscale(ImageGrab.grab(box))

    a = array(im.getcolors())

    a = a.sum()

    print a

    return a

Строка 2: мы берем скриншот, как и раньше, но теперь мы конвертируем его в градации серого, прежде чем назначить его экземпляру im. Преобразование в оттенки серого значительно ускоряет прохождение всех значений цвета; вместо того, чтобы каждый пиксель имел значение Red, Green и Blue, он имеет только одно значение в диапазоне 0-255.

Строка 3: мы создаем массив значений цвета изображения с помощью метода PIL getcolors()и присваиваем их переменнойa

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


Шаг 23: Установка новых ограничивающих рамок

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

play_buttonpng

Нам нужно установить ограничивающие рамки внутри каждого из этих пузырей.

Увеличьте изображение, чтобы увидеть мелкие детали пикселей

play_buttonpng

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

play_buttonpng

Чтобы получить нижнюю пару, добавьте 63 к позиции x, а 16 к y. Это даст вам коробку, похожую на приведенную ниже:

play_buttonpng

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

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

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

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. Оттуда мы конвертируем в массив значений RGB и выводим сумму на экран.

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


Шаг 24: Создайте словарь типов суши

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

1

2

3

sushiTypes = {2670:'onigiri',

             3143:'caliroll',

             2677:'gunkan',}

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


Шаг 25. Создайте класс без пузырьков

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

Запустите новую игру и быстро бегите, get_all_seats()прежде чем кто-либо сможет появиться. Числа, которые он печатает, мы поместим в класс с именем Blank. Как и раньше, вы можете использовать словарь, если хотите.

1

2

3

4

5

6

7

class Blank:

   seat_1 = 8119

   seat_2 = 5986

   seat_3 = 11598

   seat_4 = 10532

   seat_5 = 6782

   seat_6 = 9041

Мы почти у цели! Один последний шаг, и у нас будет простой, работающий бот!


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

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

Основной поток будет следующим: проверить места> если клиент, сделать заказ> проверить еду> если низкий, купить еду> очистить столы> повторить .

Это длинный; Давайте начнем.

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

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. После этого мы проверяем, чтобы s1оно НЕ равнялось Blank.seat_1. Если это не так , у нас есть клиент. Мы проверяем наш sushiTypesсловарь, чтобы увидеть, что он имеет такую ​​же сумму, как и наш s1. Если это так, мы вызываем makeFood()и передаем в sushiTypeкачестве аргумента.

Clear_tables() называется каждые два места.

Остался только один последний кусок: настройка петли.


Шаг 27: Основной цикл

Мы собираемся создать очень простой цикл while, чтобы играть в игру. Мы не делали какой-либо механизм прерывания, поэтому чтобы остановить выполнение, нажмите на оболочку и нажмите Ctrl + C, чтобы отправить прерывание клавиатуры.

1

2

3

4

def main():

   startGame()

   while True:

       check_bubs()

Вот и все! Обновите страницу, загрузите игру и освободите своего бота!

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

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


Вывод

Теперь у вас есть все инструменты, необходимые для создания ваших собственных простых ботов. Методы, которые мы использовали в этом руководстве, довольно примитивны в мире Computer Vision, но, тем не менее, с достаточной настойчивостью вы можете создавать с ними много интересных вещей – даже за пределами игровых ботов. Например, мы запускаем несколько сценариев, основанных на этих методах, для автоматизации повторяющихся задач программного обеспечения по всему офису. Довольно приятно удалить задачу человека всего несколькими строками кода.

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

Выучить питон

Изучите Python с нашим полным руководством по питону, независимо от того, начинаете ли вы или начинающий программист, ищущий новые навыки.

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

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

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

image

Ищем цель

Идем на тематический сайт miniclip.com и ищем цель. Выбор пал на цветовую головоломку Coloruid 2 раздела Puzzles, в которой нам необходимо заполнить круглое игровое поле одним цветом за заданное количество ходов.

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

image

Подготовка

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

Игра находится тут
GitHub бота тут

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

  • opencv-python
  • Pillow
  • selenium

Бот написан и протестирован для версии Python 3.8 на Ubuntu 20.04.1. Устанавливаем необходимые модули в ваше виртуальное окружение или через pip install. Дополнительно для работы Selenium нам понадобится geckodriver для FireFox, скачать можно тут github.com/mozilla/geckodriver/releases

Управление браузером

Мы имеем дело с онлайн-игрой, поэтому для начала организуем взаимодействие с браузером. Для этой цели будем использовать Selenium, который предоставит нам API для управления FireFox. Изучаем код страницы игры. Пазл представляет из себя canvas, которая в свою очередь располагается в iframe.

Ожидаем загрузки фрейма с id = iframe-game и переключаем контекст драйвера на него. Затем ждем canvas. Она единственная во фрейме и доступна по XPath /html/body/canvas.

wait(self.__driver, 20).until(EC.frame_to_be_available_and_switch_to_it((By.ID, "iframe-game")))
self.__canvas = wait(self.__driver, 20).until(EC.visibility_of_element_located((By.XPATH, "/html/body/canvas")))

Далее наша канва будет доступна через свойство self.__canvas. Вся логика работы с браузером сводится к получению скриншота canvas и клику по ней в заданной координате.

Полный код Browser.py:

from selenium import webdriver
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait as wait
from selenium.webdriver.common.by import By

class Browser:
    def __init__(self, game_url):
        self.__driver = webdriver.Firefox()
        self.__driver.get(game_url)
        wait(self.__driver, 20).until(EC.frame_to_be_available_and_switch_to_it((By.ID, "iframe-game")))
        self.__canvas = wait(self.__driver, 20).until(EC.visibility_of_element_located((By.XPATH, "/html/body/canvas")))

    def screenshot(self):
        return self.__canvas.screenshot_as_png

    def quit(self):
        self.__driver.quit()

    def click(self, click_point):
        action = webdriver.common.action_chains.ActionChains(self.__driver)
        action.move_to_element_with_offset(self.__canvas, click_point[0], click_point[1]).click().perform()

Состояния игры

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

  • Приветственный экран
  • Экран выбора уровня
  • Выбор цвета на обучающем уровне
  • Выбор области на обучающем уровне
  • Выбор цвета
  • Выбор области
  • Результат хода

class Robot:
    STATE_START = 0x01
    STATE_SELECT_LEVEL = 0x02
    STATE_TRAINING_SELECT_COLOR = 0x03
    STATE_TRAINING_SELECT_AREA = 0x04
    STATE_GAME_SELECT_COLOR = 0x05
    STATE_GAME_SELECT_AREA = 0x06
    STATE_GAME_RESULT = 0x07

    def __init__(self):
        self.states = {
            self.STATE_START: self.state_start,
            self.STATE_SELECT_LEVEL: self.state_select_level,
            self.STATE_TRAINING_SELECT_COLOR: self.state_training_select_color,
            self.STATE_TRAINING_SELECT_AREA: self.state_training_select_area,
            self.STATE_GAME_RESULT: self.state_game_result,
            self.STATE_GAME_SELECT_COLOR: self.state_game_select_color,
            self.STATE_GAME_SELECT_AREA: self.state_game_select_area,
        }

Для большей стабильности бота будем проверять, успешно ли произошла смена игрового состояния. Если self.state_next_success_condition не вернет True за время self.state_timeout — продолжаем обрабатывать текущее состояние, иначе переключаемся на self.state_next. Также переведем скриншот, полученный от Selenium, в понятный для OpenCV формат.


import time
import cv2
import numpy
from PIL import Image
from io import BytesIO

class Robot:

    def __init__(self):

	# …

	self.screenshot = []
        self.state_next_success_condition = None  
        self.state_start_time = 0  
        self.state_timeout = 0 
        self.state_current = 0 
        self.state_next = 0  

    def run(self, screenshot):
        self.screenshot = cv2.cvtColor(numpy.array(Image.open(BytesIO(screenshot))), cv2.COLOR_BGR2RGB)
        if self.state_current != self.state_next:
            if self.state_next_success_condition():
                self.set_state_current()
            elif time.time() - self.state_start_time >= self.state_timeout
                    self.state_next = self.state_current
            return False
        else:
            try:
                return self.states[self.state_current]()
            except KeyError:
                self.__del__()

    def set_state_current(self):
        self.state_current = self.state_next

    def set_state_next(self, state_next, state_next_success_condition, state_timeout):
        self.state_next_success_condition = state_next_success_condition
        self.state_start_time = time.time()
        self.state_timeout = state_timeout
        self.state_next = state_next

Реализуем проверку в методах обработки состояний. Ждем кнопку Play на стартовом экране и кликаем по ней. Если в течении 10 секунд мы не получили экран выбора уровней, возвращаемся к предыдущему этапу self.STATE_START, иначе переходим к обработке self.STATE_SELECT_LEVEL.


# …

class Robot:
   DEFAULT_STATE_TIMEOUT = 10
   
   # …
 
   def state_start(self):
        # пытаемся получить координату кнопки Play
        # …

        if button_play is False:
            return False
        self.set_state_next(self.STATE_SELECT_LEVEL, self.state_select_level_condition, self.DEFAULT_STATE_TIMEOUT)
        return button_play

    def state_select_level_condition(self):
        # содержит ли скриншот выбор уровней
	# …

Зрение бота

Пороговая обработка изображения

Определим цвета, которые используются в игре. Это 5 игровых цветов и цвет курсора на учебном уровне. COLOR_ALL будем использовать, если нужно найти все объекты, независимо от цвета. Для начала этот случай мы и рассмотрим.

    COLOR_BLUE = 0x01  
    COLOR_ORANGE = 0x02
    COLOR_RED = 0x03
    COLOR_GREEN = 0x04
    COLOR_YELLOW = 0x05
    COLOR_WHITE = 0x06
    COLOR_ALL = 0x07

Для поиска объекта в первую очередь необходимо упростить изображение. Для примера возьмем символ «0» и применим к нему пороговую обработку, то есть отделим объект от фона. На этом этапе нам не важно, какого цвета символ. Для начала переведем изображение в черно-белое, сделав его 1-канальным. В этом нам поможет функция cv2.cvtColor со вторым аргументом cv2.COLOR_BGR2GRAY, который отвечает за перевод в градации серого. Далее производим пороговую обработку при помощи cv2.threshold. Все пиксели изображения ниже определенного порога устанавливаются в 0, все, что выше, в 255. За значение порога отвечает второй аргумент функции cv2.threshold. В нашем случае там может стоят любое число, так как мы используем cv2.THRESH_OTSU и функция сама определит оптимальный порог по методу Оцу на основе гистограммы изображения.

image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
_, thresh = cv2.threshold(image, 0, 255, cv2.THRESH_OTSU)

image

Цветовая сегментация

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

image

По умолчанию, все изображения OpenCV хранит в формате BGR. Для цветовой сегментации больше подходит HSV (Hue, Saturation, Value — тон, насыщенность, значение). Ее преимущество перед RGB заключается в том, что HSV отделяет цвет от его насыщенности и яркости. Цветовой тон кодируется одним каналом Hue. Возьмем для примера салатовый прямоугольник и будем постепенно уменьшать его яркость.

image

В отличии от RGB, в HSV данное преобразование выглядит интуитивно — мы просто уменьшаем значение канала Value или Brightness. Тут стоит обратить внимание на то, что в эталонной модели шкала оттенков Hue варьируется в диапазоне 0-360°. Наш салатовый цвет соответствует 90°. Для того, чтобы уместить это значение в 8 битный канал, его следует разделить на 2.
Сегментация цветов работает с диапазонами, а не с одним цветом. Определить диапазон можно опытным путем, но проще написать небольшой скрипт.

import cv2
import numpy as numpy

image_path = "tests_data/SELECT_LEVEL.png"
hsv_max_upper = 0, 0, 0
hsv_min_lower = 255, 255, 255


def bite_range(value):
    value = 255 if value > 255 else value
    return 0 if value < 0 else value


def pick_color(event, x, y, flags, param):
    if event == cv2.EVENT_LBUTTONDOWN:
        global hsv_max_upper
        global hsv_min_lower
        global image_hsv
        hsv_pixel = image_hsv[y, x]
        hsv_max_upper = bite_range(max(hsv_max_upper[0], hsv_pixel[0]) + 1), 
                        bite_range(max(hsv_max_upper[1], hsv_pixel[1]) + 1), 
                        bite_range(max(hsv_max_upper[2], hsv_pixel[2]) + 1)
        hsv_min_lower = bite_range(min(hsv_min_lower[0], hsv_pixel[0]) - 1), 
                        bite_range(min(hsv_min_lower[1], hsv_pixel[1]) - 1), 
                        bite_range(min(hsv_min_lower[2], hsv_pixel[2]) - 1)
        print('HSV range: ', (hsv_min_lower, hsv_max_upper))
        hsv_mask = cv2.inRange(image_hsv, numpy.array(hsv_min_lower), numpy.array(hsv_max_upper))
        cv2.imshow("HSV Mask", hsv_mask)


image = cv2.imread(image_path)
cv2.namedWindow('Original')
cv2.setMouseCallback('Original', pick_color)
image_hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
cv2.imshow("Original", image)
cv2.waitKey(0)
cv2.destroyAllWindows()

Запустим его с нашим скриншотом.

image

Кликаем по красному цвету и смотрим на полученную маску. Если вывод нас не устраивает — выбираем оттенкам красного, увеличивая диапазон и площадь маски. Работа скрипта основана на функции cv2.inRange, которая работает как цветовой фильтр и возвращает пороговое изображение для заданного цветового диапазона.
Остановимся на следующих диапазонах:


    COLOR_HSV_RANGE = {
   COLOR_BLUE: ((112, 151, 216), (128, 167, 255)),
   COLOR_ORANGE: ((8, 251, 93), (14, 255, 255)),
   COLOR_RED: ((167, 252, 223), (171, 255, 255)),
   COLOR_GREEN: ((71, 251, 98), (77, 255, 211)),
   COLOR_YELLOW: ((27, 252, 51), (33, 255, 211)),
   COLOR_WHITE: ((0, 0, 159), (7, 7, 255)),
}

Поиск контуров

Вернемся к нашему экрану выбора уровней. Применим цветовой фильтр красного диапазона, который мы только что определили, и передадим найденный порог в cv2.findContours. Функция найдет нам контуры красных элементов. Укажем вторым аргументом cv2.RETR_EXTERNAL — нам нужны только внешние контуры, и третьим cv2.CHAIN_APPROX_SIMPLE — нас интересуют прямые контуры, экономим память и храним только их вершины.

thresh = cv2.inRange(image, self.COLOR_HSV_RANGE[self.COLOR_RED][0], self.COLOR_HSV_RANGE[self.COLOR_RED][1])
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE

image

Удаление шума

Полученные контуры содержат много шума от фона. Чтобы убрать его воспользуемся свойством наших цифр. Они состоят из прямоугольников, которые параллельны осям координат. Перебираем все контуры и вписываем каждый в минимальный прямоугольник при помощи cv2.minAreaRect. Прямоугольник определяется 4 точками. Если наш прямоугольник параллелен осям, то одна из координат для каждой пары точек должны совпадать. Значит у нас будет максимум 4 уникальных значения, если представить координаты прямоугольника как одномерный массив. Дополнительно отфильтруем слишком длинные прямоугольники, где соотношение сторон больше, чем 3 к 1. Для этого найдем их ширину и длину при помощи cv2.boundingRect.


squares = []
        for cnt in contours:
            rect = cv2.minAreaRect(cnt)
            square = cv2.boxPoints(rect)
            square = numpy.int0(square)
            (_, _, w, h) = cv2.boundingRect(square)
            a = max(w, h)
            b = min(w, h)
            if numpy.unique(square).shape[0] <= 4 and a <= b * 3:
                squares.append(numpy.array([[square[0]], [square[1]], [square[2]], [square[3]]]))

image

Объединение контуров

Уже лучше. Теперь нам нужно объединить найденные прямоугольники в общий контур символов. Нам понадобится промежуточное изображение. Создадим его при помощи numpy.zeros_like. Функция создает копию матрицы image с сохранением ее формы и размера, затем заполняет ее нулями. Другими словами, мы получили копию нашего оригинального изображения, залитую черным фоном. Переводим его в 1-канальное и наносим найденные контуры при помощи cv2.drawContours, заполнив их белым цветом. Получаем бинарный порог, к которому можно применить cv2.dilate. Функция расширяет белую область, соединяя отдельные прямоугольники, расстояние между которыми в пределах 5 пикселей. Еще раз вызываем cv2.findContours и получаем контуры красных цифр.


        image_zero = numpy.zeros_like(image)
        image_zero = cv2.cvtColor(image_zero, cv2.COLOR_BGR2RGB)
        cv2.drawContours(image_zero, contours_of_squares, -1, (255, 255, 255), -1)
	  _, thresh = cv2.threshold(image_zero, 0, 255, cv2.THRESH_OTSU)
	  kernel = numpy.ones((5, 5), numpy.uint8)
        thresh = cv2.dilate(thresh, kernel, iterations=1)	
        dilate_contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

image

Оставшийся шум отфильтруем по площади контуров при помощи cv2.contourArea. Убираем все, что занимает меньше 500 пикселей².

digit_contours = [cnt for cnt in digit_contours if cv2.contourArea(cnt) > 500]

image

Вот теперь отлично. Реализуем все вышеописанное в нашем классе Robot.


# ...

class Robot:
     
    # ...
    
    def get_dilate_contours(self, image, color_inx, distance):
        thresh = self.get_color_thresh(image, color_inx)
        if thresh is False:
            return []
        kernel = numpy.ones((distance, distance), numpy.uint8)
        thresh = cv2.dilate(thresh, kernel, iterations=1)
        contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        return contours

    def get_color_thresh(self, image, color_inx):
        if color_inx == self.COLOR_ALL:
            image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
            _, thresh = cv2.threshold(image, 0, 255, cv2.THRESH_OTSU)
        else:
            image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
            thresh = cv2.inRange(image, self.COLOR_HSV_RANGE[color_inx][0], self.COLOR_HSV_RANGE[color_inx][1])
        return thresh
			
	def filter_contours_of_rectangles(self, contours):
        squares = []
        for cnt in contours:
            rect = cv2.minAreaRect(cnt)
            square = cv2.boxPoints(rect)
            square = numpy.int0(square)
            (_, _, w, h) = cv2.boundingRect(square)
            a = max(w, h)
            b = min(w, h)
            if numpy.unique(square).shape[0] <= 4 and a <= b * 3:
                squares.append(numpy.array([[square[0]], [square[1]], [square[2]], [square[3]]]))
        return squares

    def get_contours_of_squares(self, image, color_inx, square_inx):
        thresh = self.get_color_thresh(image, color_inx)
        if thresh is False:
            return False
        contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        contours_of_squares = self.filter_contours_of_rectangles(contours)
        if len(contours_of_squares) < 1:
            return False
        image_zero = numpy.zeros_like(image)
        image_zero = cv2.cvtColor(image_zero, cv2.COLOR_BGR2RGB)
        cv2.drawContours(image_zero, contours_of_squares, -1, (255, 255, 255), -1)
        dilate_contours = self.get_dilate_contours(image_zero, self.COLOR_ALL, 5)
        dilate_contours = [cnt for cnt in dilate_contours if cv2.contourArea(cnt) > 500]
        if len(dilate_contours) < 1:
            return False
        else:
            return dilate_contours

Распознание цифр

Добавим возможность распознания цифр. Зачем нам это нужно?

Потому что мы можем

. Данная возможность не является обязательной для работы бота и при желании ее можно смело вырезать. Но так как мы обучаемся, добавим ее для подсчета набранных очков и для понимания бота, на каком он шаге на уровне. Зная завершающий ход уровня, бот будет искать кнопку перехода на следующий или повтор текущего. Иначе пришлось бы осуществлять их поиск после каждого хода. Откажемся от использования Tesseract и реализуем все средствами OpenCV. Распознание цифр будет построено на сравнении hu моментов, что позволит нам сканировать символы в разном масштабе. Это важно, так как в интерфейсе игры есть разные размеры шрифта. Текущий, где мы выбираем уровень, определим SQUARE_BIG_SYMBOL: 9, где 9 — средняя сторона квадрата в пикселях, из которых состоит цифра. Кадрируем изображения цифр и сохраним их в папке data. В словаре self.dilate_contours_bi_data у нас содержатся эталоны контуров, с которым будет происходить сравнение. Индексом будет название файла без расширения (например «digit_0»).

# …

class Robot:

    # ...

    SQUARE_BIG_SYMBOL = 0x01

    SQUARE_SIZES = {
        SQUARE_BIG_SYMBOL: 9,  
    }

    IMAGE_DATA_PATH = "data/" 

    def __init__(self):

        # ...

        self.dilate_contours_bi_data = {} 
        for image_file in os.listdir(self.IMAGE_DATA_PATH):
            image = cv2.imread(self.IMAGE_DATA_PATH + image_file)
            contour_inx = os.path.splitext(image_file)[0]
            color_inx = self.COLOR_RED
            dilate_contours = self.get_dilate_contours_by_square_inx(image, color_inx, self.SQUARE_BIG_SYMBOL)
            self.dilate_contours_bi_data[contour_inx] = dilate_contours[0]

    def get_dilate_contours_by_square_inx(self, image, color_inx, square_inx):
        distance = math.ceil(self.SQUARE_SIZES[square_inx] / 2)
        return self.get_dilate_contours(image, color_inx, distance)

В OpenCV для сравнения контуров на основе Hu моментов используется функция cv2.matchShapes. Она скрывает от нас детали реализации, принимая на вход два контура и возвращает результат сравнения в виде числа. Чем оно меньше, тем более схожими являются контуры.

cv2.matchShapes(dilate_contour, self.dilate_contours_bi_data['digit_' + str(digit)], cv2.CONTOURS_MATCH_I1, 0)

Сравниваем текущий контур digit_contour со всеми эталонами и находим минимальное значение cv2.matchShapes. Если минимальное значение меньше 0.15, цифра считается распознанной. Порог минимального значения найден опытным путем. Также объединим близко расположенные символы в одно число.

# …

class Robot:

    # …

    def scan_digits(self, image, color_inx, square_inx):
        result = []
        contours_of_squares = self.get_contours_of_squares(image, color_inx, square_inx)
        before_digit_x, before_digit_y = (-100, -100)
        if contours_of_squares is False:
            return result
        for contour_of_square in reversed(contours_of_squares):
            crop_image = self.crop_image_by_contour(image, contour_of_square)
            dilate_contours = self.get_dilate_contours_by_square_inx(crop_image, self.COLOR_ALL, square_inx)
            if (len(dilate_contours) < 1):
                continue
            dilate_contour = dilate_contours[0]
            match_shapes = {}
            for digit in range(0, 10):
                match_shapes[digit] = cv2.matchShapes(dilate_contour, self.dilate_contours_bi_data['digit_' + str(digit)], cv2.CONTOURS_MATCH_I1, 0)
            min_match_shape = min(match_shapes.items(), key=lambda x: x[1])
            if len(min_match_shape) > 0 and (min_match_shape[1] < self.MAX_MATCH_SHAPES_DIGITS):
                digit = min_match_shape[0]
                rect = cv2.minAreaRect(contour_of_square)
                box = cv2.boxPoints(rect)
                box = numpy.int0(box)
                (digit_x, digit_y, digit_w, digit_h) = cv2.boundingRect(box)
                if abs(digit_y - before_digit_y) < digit_y * 0.3 and abs(
                        digit_x - before_digit_x) < digit_w + digit_w * 0.5:
                    result[len(result) - 1][0] = int(str(result[len(result) - 1][0]) + str(digit))
                else:
                    result.append([digit, self.get_contour_centroid(contour_of_square)])
                before_digit_x, before_digit_y = digit_x + (digit_w / 2), digit_y
        return result

На выходе метод self.scan_digits выдаст массив, содержащий распознанную цифру и координату клика по ней. Точкой клика будет центроид ее контура.

# …

class Robot:

    # …

def get_contour_centroid(self, contour):
        moments = cv2.moments(contour)
        return int(moments["m10"] / moments["m00"]), int(moments["m01"] / moments["m00"])

Радуемся полученной распознавалке цифр, но не долго. Hu моменты помимо масштаба инвариантны также к повороту и зеркальности. Следовательно бот будет путать цифры 6 и 9 / 2 и 5. Добавим дополнительную проверку этих символов по вершинам. 6 и 9 будем отличать по правой верхней точке. Если она ниже горизонтального центра, значит это 6 и 9 для обратного. Для пары 2 и 5 проверяем, лежит ли верхняя правая точка на правой границе символа.

if digit == 6 or digit == 9:
    extreme_bottom_point = digit_contour[digit_contour[:, :, 1].argmax()].flatten()
    x_points = digit_contour[:, :, 0].flatten()
    extreme_right_points_args = numpy.argwhere(x_points == numpy.amax(x_points))
    extreme_right_points = digit_contour[extreme_right_points_args]
    extreme_top_right_point = extreme_right_points[extreme_right_points[:, :, :, 1].argmin()].flatten()
    if extreme_top_right_point[1] > round(extreme_bottom_point[1] / 2):
        digit = 6
    else:
        digit = 9
if digit == 2 or digit == 5:
    extreme_right_point = digit_contour[digit_contour[:, :, 0].argmax()].flatten()
    y_points = digit_contour[:, :, 1].flatten()
    extreme_top_points_args = numpy.argwhere(y_points == numpy.amin(y_points))
    extreme_top_points = digit_contour[extreme_top_points_args]
    extreme_top_right_point = extreme_top_points[extreme_top_points[:, :, :, 0].argmax()].flatten()
    if abs(extreme_right_point[0] - extreme_top_right_point[0]) > 0.05 * extreme_right_point[0]:
        digit = 2
    else:
        digit = 5

image

image

Анализируем игровое поле

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

Представим игровое поле как сеть. Каждая область цвета будет узлом, который связан с граничащими рядом соседями. Создадим класс self.ColorArea, который будет описывать область цвета/узел.

class ColorArea: 
        def __init__(self, color_inx, click_point, contour):
            self.color_inx = color_inx  # индекс цвета
            self.click_point = click_point  # клик поинт области
            self.contour = contour  # контур области
            self.neighbors = []  # индексы соседей

Определим список узлов self.color_areas и список того, как часто встречается цвет на игровом поле self.color_areas_color_count. Кадрируем игровое поле из скриншота канвы.

image[pt1[1]:pt2[1], pt1[0]:pt2[0]]

Где pt1, pt2 – крайние точки кадра. Перебираем все цвета игры и применяем к каждому метод self.get_dilate_contours. Нахождение контура узла аналогично тому, как мы искали общий контур символов, с тем отличием, что на игровом поле отсутствуют шумы. Форма узлов может быть вогнутой или иметь отверстие, поэтому центроид будет выпадать за пределы фигуры и не подходит в качестве координата для клика. Для этого найдем экстремальную верхнюю точку и опустимся на 20 пикселей. Способ не универсальный, но в нашем случае рабочий.

        self.color_areas = []
        self.color_areas_color_count = [0] * self.SELECT_COLOR_COUNT
        image = self.crop_image_by_rectangle(self.screenshot, numpy.array(self.GAME_MAIN_AREA))
        for color_inx in range(1, self.SELECT_COLOR_COUNT + 1):
            dilate_contours = self.get_dilate_contours(image, color_inx, 10)
            for dilate_contour in dilate_contours:
                click_point = tuple(
                    dilate_contour[dilate_contour[:, :, 1].argmin()].flatten() + [0, int(self.CLICK_AREA)])
                self.color_areas_color_count[color_inx - 1] += 1
                color_area = self.ColorArea(color_inx, click_point, dilate_contour)
                self.color_areas.append(color_area)

image

Связываем области

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

        blank_image = numpy.zeros_like(image)
        blank_image = cv2.cvtColor(blank_image, cv2.COLOR_BGR2GRAY)
        for color_area_inx_1 in range(0, len(self.color_areas)):
            for color_area_inx_2 in range(color_area_inx_1 + 1, len(self.color_areas)):
                color_area_1 = self.color_areas[color_area_inx_1]
                color_area_2 = self.color_areas[color_area_inx_2]
                if color_area_1.color_inx == color_area_2.color_inx:
                    continue
                common_image = cv2.drawContours(blank_image.copy(), [color_area_1.contour, color_area_2.contour], -1, (255, 255, 255), cv2.FILLED)
                kernel = numpy.ones((15, 15), numpy.uint8)
                common_image = cv2.dilate(common_image, kernel, iterations=1)
                common_contour, _ = cv2.findContours(common_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
                if len(common_contour) == 1:
self.color_areas[color_area_inx_1].neighbors.append(color_area_inx_2)
self.color_areas[color_area_inx_2].neighbors.append(color_area_inx_1)

image

Ищем оптимальный ход

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

Варианты ходов = Количество узлов * Количество цветов — 1

Для предыдущего игрового поля у нас есть 7*(5-1) = 28 вариантов. Их немного, поэтому мы можем перебрать все ходы и выбрать оптимальный. Определим варианты как матрицу
select_color_weights, в которой строкой будет индекс узла, столбцом индекс цвета и ячейкой вес хода. Нам нужно уменьшить количество узлов до одного, поэтому отдадим приоритет областям, цвет которых уникален на игровом поле и которые исчезнут после хода на них. Дадим +10 к весу ко все строке узла с уникальным цветом. Как часто встречается цвет на игровом поле, мы ранее собрали в self.color_areas_color_count

if self.color_areas_color_count[color_area.color_inx - 1] == 1:
   select_color_weight = [x + 10 for x in select_color_weight]

Далее рассмотрим цвета соседних областей. Если у узла есть соседи цвета color_inx, и их количество равно общему количеству данного цвета на игровом поле, назначим +10 к весу ячейки. Это также уберет цвет color_inx с поля.

for color_inx in range(0, len(select_color_weight)):
   color_count = select_color_weight[color_inx]
   if color_count != 0 and self.color_areas_color_count[color_inx] == color_count:
      select_color_weight[color_inx] += 10

Дадим +1 к весу ячейки за каждого соседа одного цвета. То есть если у нас есть 3 красных соседа, красная ячейка получит +3 к весу.

for select_color_weight_inx in color_area.neighbors:
   neighbor_color_area = self.color_areas[select_color_weight_inx]
   select_color_weight[neighbor_color_area.color_inx - 1] += 1

После сбора всех весов, найдем ход с максимальным весом. Определим к какому узлу и к какому цвету он относится.


max_index = select_color_weights.argmax()
self.color_area_inx_next = max_index // self.SELECT_COLOR_COUNT
select_color_next = (max_index % self.SELECT_COLOR_COUNT) + 1
self.set_select_color_next(select_color_next)

Полный код для определения оптимального хода.

# …

class Robot:

    # …

def scan_color_areas(self):
        self.color_areas = []
        self.color_areas_color_count = [0] * self.SELECT_COLOR_COUNT
        image = self.crop_image_by_rectangle(self.screenshot, numpy.array(self.GAME_MAIN_AREA))
        for color_inx in range(1, self.SELECT_COLOR_COUNT + 1):
            dilate_contours = self.get_dilate_contours(image, color_inx, 10)
            for dilate_contour in dilate_contours:
                click_point = tuple(
                    dilate_contour[dilate_contour[:, :, 1].argmin()].flatten() + [0, int(self.CLICK_AREA)])
                self.color_areas_color_count[color_inx - 1] += 1
                color_area = self.ColorArea(color_inx, click_point, dilate_contour, [0] * self.SELECT_COLOR_COUNT)
                self.color_areas.append(color_area)
        blank_image = numpy.zeros_like(image)
        blank_image = cv2.cvtColor(blank_image, cv2.COLOR_BGR2GRAY)
        for color_area_inx_1 in range(0, len(self.color_areas)):
            for color_area_inx_2 in range(color_area_inx_1 + 1, len(self.color_areas)):
                color_area_1 = self.color_areas[color_area_inx_1]
                color_area_2 = self.color_areas[color_area_inx_2]
                if color_area_1.color_inx == color_area_2.color_inx:
                    continue
                common_image = cv2.drawContours(blank_image.copy(), [color_area_1.contour, color_area_2.contour],
                                                -1, (255, 255, 255), cv2.FILLED)
                kernel = numpy.ones((15, 15), numpy.uint8)
                common_image = cv2.dilate(common_image, kernel, iterations=1)
                common_contour, _ = cv2.findContours(common_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
                if len(common_contour) == 1:
                    self.color_areas[color_area_inx_1].neighbors.append(color_area_inx_2)
                    self.color_areas[color_area_inx_2].neighbors.append(color_area_inx_1)

    def analysis_color_areas(self):
        select_color_weights = []
        for color_area_inx in range(0, len(self.color_areas)):
            color_area = self.color_areas[color_area_inx]
            select_color_weight = numpy.array([0] * self.SELECT_COLOR_COUNT)
            for select_color_weight_inx in color_area.neighbors:
                neighbor_color_area = self.color_areas[select_color_weight_inx]
                select_color_weight[neighbor_color_area.color_inx - 1] += 1
            for color_inx in range(0, len(select_color_weight)):
                color_count = select_color_weight[color_inx]
                if color_count != 0 and self.color_areas_color_count[color_inx] == color_count:
                    select_color_weight[color_inx] += 10
            if self.color_areas_color_count[color_area.color_inx - 1] == 1:
                select_color_weight = [x + 10 for x in select_color_weight]
            color_area.set_select_color_weights(select_color_weight)
            select_color_weights.append(select_color_weight)
        select_color_weights = numpy.array(select_color_weights)
        max_index = select_color_weights.argmax()
        self.color_area_inx_next = max_index // self.SELECT_COLOR_COUNT
        select_color_next = (max_index % self.SELECT_COLOR_COUNT) + 1
        self.set_select_color_next(select_color_next)

Добавим возможность перехода между уровнями и радуемся результату. Бот работает стабильно и проходит игру за одну сессию.

Вывод

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

In this tutorial we’ll explore the ins and outs of building a Computer Vision-based game bot in Python, which will be able to to play the popular Flash game Sushi Go Round. You can use the techniques taught in this tutorial to create bots for automatically testing your own web games.


Final Result Preview

Let’s take a look at the final result we will be working towards:


Prerequisites

This tutorial, and all the code within it, requires that a few additional Python libraries be installed. They provide a nice Python wrapping to a bunch of low-level C code which greatly eases the process and speed of bot scripting.

Some of the code and libraries are Windows-specific. There may be Mac or Linux equivalents, but we won’t be covering them in this tutorial.

You’ll need to download and install the following libraries:

  • The Python Imaging Library
  • Numpy
  • PyWin
  • All of the above have self installers; Running them will automatically install the modules into your libsite-packages directory and, in theory, adjust your pythonPath accordingly. However in practice this doesn’t always happen. Should you begin receiving any Import Error messages after installation, you’ll probably need to manually adjust your Environment Variables. More information on adjusting Path Variables may be found here.

The final tool we’ll need is a decent paint program. I suggest Paint.NET as an excellent free option, but any program with rulers that display their measurements in pixels can be used.

We’ll use a few games as examples along the way.

By the way, if you want to take a shortcut, you can find plenty of browser-based game templates to work from on Envato Market.

Game templates on Envato MarketGame templates on Envato MarketGame templates on Envato Market

Game templates on Envato Market

Introduction

This tutorial is written to gave a basic introduction to the process of building bots that play browser-based games. The approach we’re going to take is likely slightly different than what most would expect when they think of a bot. Rather than making a program that sits between the client and server injecting code (like a Quake or C/S bot), our bot will sit purely on the ‘outside’. We’ll rely on Computer Vision-esque techniques and Windows API calls to gather needed information and generate movement.

With this approach we lose a bit of refined detail and control, but make up for it in shortened dev time and ease of use. Automating a specific game function can be done in a few short lines of code, and a full-fledged, start-to-finish bot (for a simple game) can be cranked out in a few hours.

The joys of this fast approach are such that once you get familiar with what the computer can easily ‘see’, you’ll begin to view games slightly differently. A good example is found in puzzle games. A common construct involves exploiting human speed limitations to force you into a less than optimal solution. It’s fun (and pretty easy) to ‘break’ these games by scripting in movements that could never be accomplished by a human.

These bots are also very useful for testing simple games — unlike a human, a bot won’t get bored playing the same scenario over and over again.

Source code for all of the tutorial examples, as well as for one of the completed example bots, may be found here.

Have fun!


Step 1: Create a New Python Project

In a new folder, right-click and select New > Text Document.

Python_Snapshot_of_entire_screen_areaPython_Snapshot_of_entire_screen_areaPython_Snapshot_of_entire_screen_area

Once made, rename the file from ‘New Text Document’ to ‘quickGrab.py’ (without the quotes) and confirm that you want to change the file name extension.

Python_Snapshot_of_entire_screen_areaPython_Snapshot_of_entire_screen_areaPython_Snapshot_of_entire_screen_area

Finally, right-click on our newly created file and select «Edit with IDLE» from the context menu to launch the editor

Python_Snapshot_of_entire_screen_areaPython_Snapshot_of_entire_screen_areaPython_Snapshot_of_entire_screen_area


Step 2: Setting Up Your First Screen Grab

We’ll begin work on our bot by exploring the basic screen grab function. Once up and running, we’ll step through it line by line, as this function (and its many iterations) will serve as the backbone of our code.

In quickgrab.py enter the following code:

1
import ImageGrab
2
import os
3
import time
4

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

11
def main():
12
    screenGrab()
13

14
if __name__ == '__main__':
15
    main()

Running this program should give you a full snapshot of the screen area:

Python_Snapshot_of_entire_screen_areaPython_Snapshot_of_entire_screen_areaPython_Snapshot_of_entire_screen_area

The current code grabs the full width and height of your screen area and stores it as a PNG in your current working directory.

Now let’s step through the code to see exactly how it works.

The first three lines:

1
import ImageGrab
2
import os
3
import time

…are the aptly named ‘import statements’. These tell Python to load in the listed modules at runtime. This gives us access to their methods via the module.attribute syntax.

The first module is part of the Python Image Library we installed earlier. As its name suggests, it gives us the basic screen gabbing functionality our bot will rely on.

The second line imports the OS (Operating System) Module. This gives us the ability to easily navigate around our operating system’s directories. It’ll come in handy once we begin organizing assets into different folders.

This final import is the built-in Time module. Well use this mostly for stamping the current time onto snapshots, but it can be very useful as a timer for bots that need events triggered over a given number of seconds.

The next four lines make up the heart of our screenGrab() function.

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

The first line def screenGrab() defines the name of our function. The empty parentheses mean it expects no arguments.

Line 2, box=() assigns an empty tuple to a variable named «box». We’ll fill this with arguments in the next step.

Line 3, im = ImageGrab.grab() creates a full snapshot of your screen and returns an RGB image to the instance im

Line 4 can be a little tricky if you’re unfamiliar with how the Time module works. The first part im.save( calls the «save» method from the Image class. It expects two arguments. The first is the location in which to save the file, and the second is the file format.

Here we set the location by first calling os.getcwd(). This gets the current directory the code is being run from and returns it as a string. We next add a +. This will be used in between each new argument to concatenate all of the strings together.

The next piece '\full_snap__ give our file a simple descriptive name. (Because the backslash is an escape character in Python, we have to add two of them to avoid cancelling out one of our letters).

Next is the hairy bit: str(int(time.time())) . This takes advantage of Python’s built-in Type methods. We’ll explain this piece by working from the inside out:

time.time() returns the number of seconds since Epoch, which is given as a type Float. Since we’re creating a file name we can’t have the decimal in there, so we first convert it to an integer by wrapping it in int(). This gets us close, but Python can’t concatenate type Int with type String, so the last step is to wrap everything in the str() function to give us a nice usable timestamp for the file name. From here, all that remains is adding the extension as part of the string: + '.png' and passing the second argument which is again the extension’s type: "PNG".

The last part of our code defines the function main(), and tells it to call the screenGrab() function whenever it’s run.

And here, at the end, is a Python convention that checks whether the script is top level, and if so allows it to run. Translated, it simply means that that it only executes main() if it is run by itself. Otherwise — if, for instance, it is loaded as a module by a different Python script — it only supplies its methods instead of executing its code.

1
def main():
2
    screenGrab()
3

4
if __name__ == '__main__':
5
    main()

Step 3: The Bounding Box

The ImageGrab.grab() function accepts one argument which defines a bounding box. This is a tuple of coordinates following the pattern of (x,y,x,y) where,

  1. The first pair of values (x,y.. defines the top left corner of the box
  2. The second pair ..x,y) defines the bottom right.

Combining these allows us to only copy the part of the screen we need.

Let’s put this into practice.

For this example, we’re going to use a game called Sushi Go Round. (Quite addicting. You’ve been warned.) Open the game in a new tab and take a snapshot using our existing screenGrab() code:

Python_Snapshot_of_sushi_game_full_screenPython_Snapshot_of_sushi_game_full_screenPython_Snapshot_of_sushi_game_full_screen

A snapshot of the full screen area.


Step 4: Getting Coordinates

Now it’s time to start mining some coordinates for our bounding box.

Open up your most recent snapshot in an image editor.

Python_Snapshot_of_sushi_game_full_screenPython_Snapshot_of_sushi_game_full_screenPython_Snapshot_of_sushi_game_full_screen

The (0,0) position is always located at the top left corner of the image. We want to pad the x and y coordinates so that our new snapshot function sets (0,0) to the leftmost corner of the game’s play area.

The reasons for this are two-fold. First, it makes finding in-game coordinates much easier when we only need to adjust values in relation to the play area versus the entire area of your screen resolution. Second, grabbing a smaller portion of the screen reduces the processing overhead required. Full screen grabs produce quite a bit of data, which can make it tough to traverse it multiple times per second.

looking_at_xylooking_at_xylooking_at_xy

If not done already, enable the ruler display in your editor and zoom in on the top corner of the play area until you can see the pixels in detail:

looking_at_x_ylooking_at_x_ylooking_at_x_y

Hover your cursor over the first pixel of the play area and check the coordinates displayed on the ruler. These will be the first two values of our Box tuple. On my specific machine these values are 157, 162.

Navigate to the lower edge of the play area to get the bottom pair of coordinates.

looking_at_x_ylooking_at_x_ylooking_at_x_y

This shows coordinates of 796 and 641. Combining these with our previous pair gives a box with the coordinates of (157,162,796,641).

Let’s add this to our code.

1
import ImageGrab
2
import os
3
import time
4

5
def screenGrab():
6
    box = (157,346,796,825)
7
    im = ImageGrab.grab(box)
8
    im.save(os.getcwd() + '\full_snap__' + str(int(time.time())) + 
9
'.png', 'PNG')
10

11
def main():
12
    screenGrab()
13

14
if __name__ == '__main__':
15
    main()

In line 6 we’ve updated the tuple to hold the coordinates of the play area.

Save and run the code. Open up the newly saved image and you should see:

play_area_snapshotpngplay_area_snapshotpngplay_area_snapshotpng

Success! A perfect grab of the play area. We won’t always need to do this kind of intensive hunt for coordinates. Once we get into the win32api we’ll go over some faster methods for setting coordinates when we don’t need pixel perfect accuracy.


Step 5: Planning Ahead for Flexibility

As it stands now, we’ve hard-coded the coordinates in relation to our current setup, assuming our browser, and our resolution. It’s generally a bad idea to hard-code coordinates in this way. If, for instance, we want to run the code on a different computer — or say, a new ad on the website shifts the position of the play area slightly — we would have to manually and painstakingly fix all of our coordinate calls.

So we’re going to create two new variables: x_pad and y_pad. These will be used to store the relationship between the game area and the rest of the screen. This will make it very easy to port the code from place to place since every new coordinate will be relative to the two global variables we’re going to create, and to adjust for changes in screen area, all that’s required is to reset these two variables.

Since we’ve already done the measurements, setting the pads for our current system is very straightforward. We’re going to set the pads to store the location of the first pixel outside of the play area. From the first pair of x,y coordinates in our box tuple, subtract a 1 from each value. So 157 becomes 156, and 346 becomes 345.

Let’s add this to our code.

1
# Globals

2
# ------------------

3

4
x_pad = 156
5
y_pad = 345

Now that these are set, we’ll begin to adjust the box tuple to be in relation to these values.

1
def screenGrab():
2
    box = (x_pad+1, y_pad+1, 796, 825)
3
    im = ImageGrab.grab()
4
    im.save(os.getcwd() + '\full_snap__' + str(int(time.time())) + 
5
'.png', 'PNG')

For the second pair, we’re going to first subtract the values of the pads (156 and 345) from the coordinates (796, 825), and then use those values in the same Pad + Value format.

1
def screenGrab():
2
    box = (x_pad+1, y_pad+1, x_pad+640, y_pad+479)
3
    im = ImageGrab.grab()
4
    im.save(os.getcwd() + '\full_snap__' + str(int(time.time())) + 
5
'.png', 'PNG')

Here the x coordinate becomes 640 (769-156), and the y becomes 480 (825-345)

It may seem a little redundant at first, but doing this extra step ensures easy maintenance in the future.


Step 6: Creating a Docstring

Before we go any further, we’re going to create a docstring at the top of our project. Since most of our code will be based around specific screen coordinates and relationships to coordinates, it’s important to know the circumstances under which everything will line up correctly. For instance, things such as current resolution, browser, toolbars enabled (since they change the browser area), and any adjustments needed to center the play area on screen, all affect the relative position of the coordinates. Having all of this documented greatly helps the troubleshooting process when running your code across multiple browsers and computers.

One last thing to be aware of is the ever-changing ad space on popular gaming sites. If all of your grab calls suddenly stop behaving as expected, a new add slightly shifting things on screen is a good bet.

As an example, I usually have the following comments at the top of my Python code:

1
"""

2


3
All coordinates assume a screen resolution of 1280x1024, and Chrome 

4
maximized with the Bookmarks Toolbar enabled.

5
Down key has been hit 4 times to center play area in browser.

6
x_pad = 156

7
y_pad = 345

8
Play area =  x_pad+1, y_pad+1, 796, 825

9
"""

Dropping all of this information at the beginning of your Python file makes it quick and easy to double check all of your settings and screen alignment without having to pore over your code trying to remember where you stored that one specific x-coordinate.


Step 7: Turning quickGrab.py Into a Useful Tool

We’re going to fork our project at this point, creating two files: one to hold all of our bot’s code, and the other to act as a general screen shot utility. We’re going to be taking a lot of screen shots as we hunt for coordinates, so having a separate module ready to go will make things a lot speedier.

Save and close our current project.

In your folder, right-click on quickGrab.py and select ‘copy’ from the menu.

play_area_snapshotpngplay_area_snapshotpngplay_area_snapshotpng

Now right-click and select ‘paste’ from the menu

play_area_snapshotpngplay_area_snapshotpngplay_area_snapshotpng

Select the copied file and rename it to ‘code.py’

play_area_snapshotpngplay_area_snapshotpngplay_area_snapshotpng

From now on all new code additions and changes will be made in code.py. quickGrab.py will now function purely as a snapshot tool. We just need to make one final modification:

Change the file extension from .py, to .pyw and confirm the changes.

play_area_snapshotpngplay_area_snapshotpngplay_area_snapshotpng

This extension tells Python to run the script without launching the console. So now, quickGrab.pyw lives up to its name. Double click on the file and it will quietly execute its code in the background and save a snapshot to your working directory.

Keep the game open in the background (be sure to mute it before the looped music drives you to madness); we’ll return to it shortly. We have a few more concepts/tools to introduce before we get into controlling things on-screen.


Step 8: Win32api — A Brief Overview

Working with the win32api can be a little daunting initially. It wraps the low-level Windows C code — which is thankfully very well documented here, but a little like a labyrinth to navigate through your first couple of go-arounds.

Before we start scripting any useful actions, we’re going to take a close look at some of the API functions upon which we’ll be relying. Once we have a clear understanding of each parameter it will be easy to adjust them to serve whatever purposes we need in-game.

The win32api.mouse_event():

1
win32api.mouse_event(
2
	dwFlags,
3
	dx,
4
	dy,
5
	dwData	
6
	)

The first parameter dwFlags defines the «action» of the mouse. It controls things such as movement, clicking, scrolling, etc..

The following list shows the most common parameters used while scripting movement.

dwFlags:

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

Each name is self explanatory. If you wanted to send a virtual right-click, you would pass win32con.MOUSEEVENTF_RIGHTDOWN to the dwFlags parameter.

The next two parameters, dx and dy, describe the mouse’s absolute position along the x and y axis. While we could use these parameters for scripting mouse movement, they use a coordinate system different than the one we’ve been using. So, we’ll leave them set to zero and rely on a different part of the API for our mouse moving needs.

The fourth parameter is dwData . This function is used if (and only if) dwFlags contains MOUSEEVENTF_WHEEL. Otherwise is can be omitted or set to zero. dwData specifies the amount of movement on your mouse’s scroll wheel.

A quick example to solidify these techniques:

If we imagine a game with a weapon selection system similar to Half-Life 2 — where weapons can be selected by rotating the mouse wheel — we would come up with the following function to sift through the weapons list:

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

Here we want to simulate scrolling the mouse wheel to navigate our theoretical weapon listing, so we passed the ...MOUSEEVENTF_WHEEL ‘action’ to the dwFlag. We don’t need dx or dy, positional data, so we left those set to zero, and we wanted to scroll one click in the forward direction for each ‘weapon’ in the list, so we passed the integer 120 to dwData (each wheel click equals 120).

As you can see, working with mouse_event is simply a matter of plugging the right arguments into the right spot. Let’s now move onto some more usable functions


Step 5: Basic Mouse Clicking

We’re going to make three new functions. One general left-click function, and two that handle the specific down and up states.

Open code.py with IDLE and add the following to our list of import statements:

1
import win32api, win32con

As before, this gives us access to module’s contents via the module.attribute syntax.

Next we’ll make our first mouse click function.

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

Recall that all we’re doing here is assigning an ‘action’ to the first argument of mouse_event. We don’t need to pass any positional information, so we’re leaving the coordinate parameters at (0,0), and we don’t need to send any additional info, so dwData is being omitted. The time.sleep(.1) function tells Python to halt execution for the time specified in parentheses. We’ll add these through out our code, usually for very short amount of times. Without these, the ‘clicking’ can get ahead of itself and fire before menus have a chance to update.

So what we’ve made here is a general left-click. One press, one release. We’ll spend most of our time with this one, but we’re going to make two more variations.

The next two are the exact same thing, but now each step is split into its own function. These will be used when we need to hold down the mouse for a length of time (for dragging, shooting, etc..).

1
def leftDown():
2
    win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN,0,0)
3
    time.sleep(.1)
4
    print 'left Down'
5
		
6
def leftUp():
7
    win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP,0,0)
8
    time.sleep(.1)
9
    print 'left release'

Step 9: Basic Mouse Movement

With clicking out of the way all that’s left is moving the mouse around on screen.

Add the following functions to code.py:

1
def mousePos(cord):
2
	win32api.SetCursorPos((x_pad + cord[0], y_pad + cord[1])
3
	
4
def get_cords():
5
	x,y = win32api.GetCursorPos()
6
	x = x - x_pad
7
	y = y - y_pad
8
	print x,y

These two functions serve distinctly different purposes. The first will be used for scripting movement in the program. Thanks to excellent naming conventions, the body of the function does exactly as SetCursorPos() implies. Calling this function sets the mouse to the coordinates passed to it as an x,y tuple. Notice that we’ve added in our x and y pads; it’s important to do this anywhere a coordinate is called.

The second is a simple tool that we’ll use while running Python interactively. It prints to the console the current position of the mouse as an x,y tuple. This greatly speeds up the process of navigating through menus without having to take a snapshot and break out a ruler. We won’t always be able to use it, as some mouse activities will need to be pixel-specific, but when we can, it’s a fantastic time saver.

In the next step we’ll put some of these new techniques to use and start navigating in-game menus. But before we do, delete the current contents of main() in code.py and replace it with pass. We’re going to be working with the interactive prompt for the next step, so we won’t be needing the screenGrab() function.


Step 10: Navigating Game Menus

In this, and the next few steps, we’re going to attempt to gather as many event coordinates as we can using our get_cords() method. Using it we’ll be able to quickly build up the code for things like navigating menus, clearing tables, and making food. Once we have these set, it will just be a matter of hooking them into the bot’s logic.

Let’s get started. Save and run your code to bring up the Python shell. Since we replaced the body of main() with pass in the last step, you should see a blank shell upon running.

play_area_snapshotpngplay_area_snapshotpngplay_area_snapshotpng

Now, before we even get to the playable part of the game there are four initial menus we need to get through. They are as follows:

  1. Initial «play» button

    play_buttonpng

  2. iPhone «continue» button

  3. Tutorial «Skip» button

  4. Today’s goal «Continue» button

    pngpngpng

We’ll need to get the coordinates for each of these and the add them to a new function called startGame(). Position the IDLE shell so you can see both it and the play area. Type in the get_cords() function but don’t press return yet; move your mouse over the button for which you need coordinates. Be sure not to click yet, as we want focus to remain in the shell. Hover your mouse over the menu item and now press the return key. This will grab the current location of the mouse and print to the console a tuple containing the x,y values. Repeat this for the remaining three menus.

Leave the shell open and arrange it so you can see it as well as the IDLE editor. We’re now going to now add our startGame() function and fill it with our newly acquired coordinates.

1
def startGame():
2
	#location of first menu

3
	mousePos((182, 225))
4
    leftClick()
5
    time.sleep(.1)
6
	
7
	#location of second menu

8
    mousePos((193, 410))
9
    leftClick()
10
    time.sleep(.1)
11
	
12
	#location of third menu

13
    mousePos((435, 470))
14
    leftClick()
15
    time.sleep(.1)
16
	
17
	#location of fourth menu

18
    mousePos((167, 403))
19
    leftClick()
20
    time.sleep(.1)

We now have a nice compact function to call at the start of each game. It sets the cursor position to each of the menu locations we previously defined, and then tells the mouse to click. time.sleep(.1) tells Python to halt execution for 1/10 of a second between each click, which gives the menus enough time to update in between.

Save and run your code and you should see a result similar to this:

As a feeble human it takes me slightly longer than a second to navigate all of the menus by hand, but our bot can now do it in about .4 seconds. Not bad at all!


Step 11: Getting Food Coordinates

Now we’re going to repeat the same process for each of these buttons:

play_buttonpng

Once again, in the Python shell, type in get_cords(), hover you mouse over the food box you need, and press the Enter key to execute the command.

As an option to further speed things along, if you have a second monitor, or are able to arrange the python shell in a way that you can see it as well as the game area, rather than typing in and running get_cords() each time we need it, we can set up a simple for loop. Use a time.sleep() method to halt execution just long enough for you to move the mouse to the next location needing coordinates.

Here’s the for loop in action:

We’re going to create a new class called Cord and use it to store all of the coordinate values we gather. Being able to call Cord.f_rice offers a huge readability advantage over passing the coordinates directly to mousePos(). As an option, you could also store everything in a dictionary, but I find the class syntax more enjoyable.

1
class Cord:
2
	
3
	f_shrimp = (54,700)
4
	f_rice = (119 701)
5
	f_nori = (63 745)
6
	f_roe = (111 749)
7
	f_salmon = (54 815)
8
	f_unagi = (111 812)

We’re going to store a lot of our coordinates in this class, and there will be some overlap, so adding the ‘f_‘ prefix lets us know that we referring to the food locations, rather than, say, a location in the phone menu.

We’ll return to these in a bit. There is a bit more coordinate hunting to do!


Step 12: Getting Empty Plate Coordinates

Each time a customer finishes eating, they leave behind a plate that needs to be clicked on to be removed. So we need to get the location of the empty plates as well.

play_buttonpngplay_buttonpngplay_buttonpng

I’ve noted their position with a giant red ‘X’. Repeat the same pattern as in the last two steps to get their coordinates. Store them in the comment string for now.

1
"""

2


3
Plate cords:

4


5
    108, 573

6
    212, 574

7
    311, 573

8
    412, 574

9
    516, 575

10
    618, 573

11
"""

We’re getting close. Only a few more steps of preliminary setup before we get into the really fun stuff.


Step 13: Getting Phone Coordinates

Ok, this will be the final set of coordinates we have to mine in this specific manner.

This one has a lot more to keep track of so you may want to do it by manually calling the get_cords() function rather than the previously used for loop method. Either way, we’re going to go through all of the phone menus to get the coordinates for each item.

This one is a bit more involved as to reach one of the purchase screens we need, you need to have enough money to actually purchase something. So you’ll need to make a few pieces of sushi before you go about the business of coordinate hunting. At the most, you’ll have to make two sushi rolls, I believe. That will get you enough to buy some rice, which will get us to the screen we need.

There are six menus we have to get through:

  1. The Phone

    play_buttonpng

  2. Initial Menu

  3. Toppings

  4. Rice

    png

  5. Shipping

    png

We need to get coordinates for everything but Sake (although you can if you want. I found the bot worked fine without it. I was willing to sacrifice the occasional bad in-game review for not having to code in the logic.)

Getting the coordinates:

We’re going to add all of these to our Cord class. We’ll use the prefix ‘t_‘ to denote that food types are phone>toppings menu items.

1
class Cord:
2
	
3
	f_shrimp = (54,700)
4
	f_rice = (119 701)
5
	f_nori = (63 745)
6
	f_roe = (111 749)
7
	f_salmon = (54 815)
8
	f_unagi = (111 812)
9
    
10
#-----------------------------------	

11
	
12
	phone = (601, 730)
13

14
    menu_toppings = (567, 638)
15
    
16
    t_shrimp = (509, 581)
17
    t_nori = (507, 645)
18
    t_roe = (592, 644)
19
    t_salmon = (510, 699)
20
    t_unagi = (597, 585)
21
    t_exit = (614, 702)
22

23
    menu_rice = (551, 662)
24
    buy_rice = 564, 647
25
    
26
    delivery_norm = (510, 664)

Alright! We’ve finally mined all the coordinate values we need. So let’s start making something useful!


Step 14: Clearing Tables

We’re going to take our previously recorded coordinates and use them to fill a function called clear_tables().

1
def clear_tables():
2
    mousePos((108, 573))
3
    leftClick()
4

5
    mousePos((212, 574))
6
    leftClick()
7

8
    mousePos((311, 573))
9
    leftClick()
10

11
    mousePos((412, 574))
12
    leftClick()
13

14
    mousePos((516, 575))
15
    leftClick()
16

17
    mousePos((618, 573))
18
    leftClick()
19
    time.sleep(1)

As you can see, this looks more or less exactly like our earlier startGame() function. A few small differences:

We have no time.sleep() functions in between the different click events. We don’t have to wait for any menus to update, so we don’t have to throttle our click speeds.

We do, however, have one long time.sleep() at the very end. While not strictly required, it is nice to add these occasional pauses in execution to our code, something just long enough to give us time to manually break out of the bot’s main loop if necessary (which we’ll get to). Otherwise, the thing will continue to steal your mouse position over and over, and you won’t be able to shift focus to the shell long enough to stop the script — which can funny the first two or three times as you struggle against a mouse, but it quickly loses its charm.

So be sure to add in some reliable pauses in your own bots!


Step 15: Making Sushi

The first thing we need to do is learn how to make the sushi. Click the recipe book to open the instruction manual. All sushi types encountered throughout the game will be found within its pages. I’ll note the first three below, but I leave it to you to catalog the rest.

play_buttonpngplay_buttonpngplay_buttonpng

1
'''

2
Recipes:

3


4
	onigiri

5
		2 rice, 1 nori

6
	

7
	caliroll:

8
		1 rice, 1 nori, 1 roe

9
		

10
	gunkan:

11
		1 rice, 1 nori, 2 roe

12
'''

Now we’re going to set up a function that will accept an argument for «sushi type» and then assemble the proper ingredients based on the passed value.

1
def makeFood(food):
2
    if food == 'caliroll':
3
        print 'Making a caliroll'
4
        mousePos(Cord.f_rice)
5
        leftClick()
6
        time.sleep(.05)
7
        mousePos(Cord.f_nori)
8
        leftClick()
9
        time.sleep(.05)
10
        mousePos(Cord.f_roe)
11
        leftClick()
12
        time.sleep(.1)
13
        foldMat()
14
        time.sleep(1.5)
15
	
16
    elif food == 'onigiri':
17
        print 'Making a onigiri'
18
        mousePos(Cord.f_rice)
19
        leftClick()
20
        time.sleep(.05)
21
        mousePos(Cord.f_rice)
22
        leftClick()
23
        time.sleep(.05)
24
        mousePos(Cord.f_nori)
25
        leftClick()
26
        time.sleep(.1)
27
        foldMat()
28
        time.sleep(.05)
29
        
30
        time.sleep(1.5)
31

32
    elif food == 'gunkan':
33
        mousePos(Cord.f_rice)
34
        leftClick()
35
        time.sleep(.05)
36
        mousePos(Cord.f_nori)
37
        leftClick()
38
        time.sleep(.05)
39
        mousePos(Cord.f_roe)
40
        leftClick()
41
        time.sleep(.05)
42
        mousePos(Cord.f_roe)
43
        leftClick()
44
        time.sleep(.1)
45
        foldMat()
46
        time.sleep(1.5)

This functions just as all the others but with one small change: rather than passing the coordinates directly, we’re calling them as attributes from our Cord class.

The function foldMat() is called at the end of each sushi making process. This clicks the mat to roll the sushi we just assembled. Let’s define that function now:

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

Let’s briefly walk though this mousePos() call as it’s a bit cobbled together. We access the first value of the f_rice tuple by adding [0] on the end of the attribute. Recall that this is our x value. To click on the mat we only need to adjust our x values by a handful of pixels, so we add 40 to the current x coordinate, and the then pass f_rice[1] to the y. This shifts our x position just enough to the right to allow us to trigger the mat.

Notice that after the foldMat() call we have a long time.sleep(). The Mat takes quite a while to roll, and food items can’t be clicked while their animations are running, so you just have to wait.


Step 16: Navigating the Phone Menu

In this step we’ll set all of the mousePos() to point to the appropriate menu items, but we’ll leave it there for now. This is part of the program that will be wrapped in and controlled by the bot’s logic. We’ll revisit this function after getting a few new techniques under our belt.

1
def buyFood(food):
2
	
3
	mousePos(Cord.phone)
4
	
5
	mousePos(Cord.menu_toppings)
6
	
7
	
8
	mousePos(Cord.t_shrimp)
9
	mousePos(Cord.t_nori)
10
	mousePos(Cord.t_roe)
11
	mousePos(Cord.t_salmon)
12
	mousePos(Cord.t_unagi)
13
	mousePos(Cord.t_exit)
14
	
15
	mousePos(Cord.menu_rice)
16
	mousePos(Cord.buy_rice)
17
	
18
	mousePos(Cord.delivery_norm)

That’s it for this step. We’ll do more with this later.


Brief Intro: Making the Computer See

We’re now getting to the very interesting bits. We’re going to start looking at how to make the computer ‘see’ on-screen events. This is a very exciting part of the process, and one that’s easy to get wrapped up thinking about.

Another neat part of bot building is that eventually the bot can provide us, the programmers, with enough information that further vision work is not required. For instance, in the case of the Sushi bot, once we get the first level running, the bot is spitting out accurate enough data about what’s happening on screen that all we have to do from that point on is take that data it’s «seeing» and simply tell it how to react to it.

Another large part of bot building is learning the game, knowing what values you need to keep track of versus which you can ignore. For instance, we’ll make no effort to track cash on hand. It’s just something that ended up being irrelevant to the bot. All it needs to know is if it has enough food to continue working. So rather than keeping tabs on the total money, it simply checks to see if it can afford something, regardless of price, because as it works out in the game, it’s only a matter of a few seconds before you can afford to replenish something. So if it can’t afford it now, it just tries again in a few seconds.

Which brings me to my final point. That of the brute force method versus the elegant one. Vision algorithms take valuable processing time. Checking multiple points in many different regions of the play area can quickly eat away your bot performance, so it comes down to a question of «does the bot need to know whether _______ has happened or not?».

As an example, a customer in the Sushi game could be thought of as having four states: not present, waiting, eating, and finished eating. When finished, they leave a flashing empty plate behind. I could expend the processing power on checking all plate locations by snapping all six plate locations and then checking against an expected value (which is prone to failure since the plates flash on and off, making a false negative a big possibility), or… I could just brute force my way through by clicking each plate location every few seconds. In practice this is every bit as effective as the ‘elegant’ solution of letting the bot determine the state of the customer. Clicking six locations takes a fraction of a second where as grabbing and processing six different images is comparatively slow. We can use the time we saved on other more important image processing tasks.


Step 17: Importing Numpy and ImageOps

Add the following to your list of import statements.

1
import ImageOps
2
from numpy import *

ImageOps is another PIL module. It is used to perform operations (such as grayscaling) on an Image.

I’ll briefly explain the second for those who aren’t familiar with Python. Our standard import statements loads the module’s namespace (a collection of variable names and functions). So, to access items in a module’s scope, we have to employ the module.attribute sytax. However, by using a from ___ import statement we inherit the names into our local scope. Meaning, the module.attribute syntax is no longer needed. They are not top level, so we use them as we would any other Python built-in function, like str() or list(). By importing Numpy in this manner, it allows us to simply call array(), instead of numpy.array().

The wildcard * means import everything from the module.


Step 18: Making the Computer See

The first method we’ll explore is that of checking a specific RGB value of a pixel against an expected value. This method is good for static things such as menus. Since it deals with specific pixels, it’s usually a little too fragile for moving objects. however, its varies from case to case. Sometimes it’s the perfect technique, other time you’ll have to sort out a different method.

Open Sushi Go Round in your browser and start a new game. Ignore your customers and open the phone menu. You start off with no money in the bank, so everything should be greyed out as below. These will be the RGB values we’ll check.

play_buttonpngplay_buttonpngplay_buttonpng

In code.py, scroll to your screenGrab() function. We’re going to make the following changes:

1
def screenGrab():
2
    b1 = (x_pad + 1,y_pad+1,x_pad+640,y_pad+480)
3
    im = ImageGrab.grab()
4

5
    ##im.save(os.getcwd() + '\Snap__' + str(int(time.time())) +'.png', 'PNG')

6
    return im

We’ve made two small changes. In line 5 we commented out our save statement. In line 6 we now return the Image object for use outside of the function.

Save and run the code. We’re going to do some more interactive work.

With the Toppings menu open and all items greyed out, run the following code:

1
>>>im = screenGrab()
2
>>>

This assigns the snap shot we take in screenGrab() to the instance im. For here, we can call the getpixel(xy) method to grab specific pixel data.

Now we need to get RGB values for each of the greyed out items. These will make up our ‘expected value’ that the bot will test against when it makes its own getpixel() calls.

We already have the coordinates we need from the previous steps, so all we have to do is pass them as arguments to getpixel() and note the output.

Output from our interactive session:

1
>>> im = screenGrab()
2
>>> im.getpixel(Cord.t_nori)
3
(33, 30, 11)
4
>>> im.getpixel(Cord.t_roe)
5
(127, 61, 0)
6
>>> im.getpixel(Cord.t_salmon)
7
(127, 71, 47)
8
>>> im.getpixel(Cord.t_shrimp)
9
(127, 102, 90)
10
>>> im.getpixel(Cord.t_unagi)
11
(94, 49, 8)
12
>>> im.getpixel(Cord.buy_rice)
13
(127, 127, 127)
14
>>>

We need to add these values to our buyFood() function in way that allows it to know whether or not something is available.

1
def buyFood(food):
2
    
3
    if food == 'rice':
4
        mousePos(Cord.phone)
5
        time.sleep(.1)
6
        leftClick()
7
        mousePos(Cord.menu_rice)
8
        time.sleep(.05)
9
        leftClick()
10
        s = screenGrab()
11
        if s.getpixel(Cord.buy_rice) != (127, 127, 127):
12
            print 'rice is available'
13
            mousePos(Cord.buy_rice)
14
            time.sleep(.1)
15
            leftClick()
16
            mousePos(Cord.delivery_norm)
17
            time.sleep(.1)
18
            leftClick()
19
            time.sleep(2.5)
20
        else:
21
            print 'rice is NOT available'
22
            mousePos(Cord.t_exit)
23
            leftClick()
24
            time.sleep(1)
25
            buyFood(food)
26
            
27

28
            
29
    if food == 'nori':
30
        mousePos(Cord.phone)
31
        time.sleep(.1)
32
        leftClick()
33
        mousePos(Cord.menu_toppings)
34
        time.sleep(.05)
35
        leftClick()
36
        s = screenGrab()
37
        print 'test'
38
        time.sleep(.1)
39
        if s.getpixel(Cord.t_nori) != (33, 30, 11):
40
            print 'nori is available'
41
            mousePos(Cord.t_nori)
42
            time.sleep(.1)
43
            leftClick()
44
            mousePos(Cord.delivery_norm)
45
            time.sleep(.1)
46
            leftClick()
47
            time.sleep(2.5)
48
        else:
49
            print 'nori is NOT available'
50
            mousePos(Cord.t_exit)
51
            leftClick()
52
            time.sleep(1)
53
            buyFood(food)
54

55
    if food == 'roe':
56
        mousePos(Cord.phone)
57
        time.sleep(.1)
58
        leftClick()
59
        mousePos(Cord.menu_toppings)
60
        time.sleep(.05)
61
        leftClick()
62
        s = screenGrab()
63
        
64
        time.sleep(.1)
65
        if s.getpixel(Cord.t_roe) != (127, 61, 0):
66
            print 'roe is available'
67
            mousePos(Cord.t_roe)
68
            time.sleep(.1)
69
            leftClick()
70
            mousePos(Cord.delivery_norm)
71
            time.sleep(.1)
72
            leftClick()
73
            time.sleep(2.5)
74
        else:
75
            print 'roe is NOT available'
76
            mousePos(Cord.t_exit)
77
            leftClick()
78
            time.sleep(1)
79
            buyFood(food)

Here we pass a ingredient name to the buyFood() function. A series of if/elif statements is used to catch the passed parameter and respond accordingly. Each fork follows the exact same logic, so we’ll just explore the first one.

1
 
2
 if food == 'rice':
3
        mousePos(Cord.phone)
4
        time.sleep(.1)
5
        leftClick()
6
        mousePos(Cord.menu_rice)
7
        time.sleep(.05)
8
        leftClick()
9
        s = screenGrab()
10
        time.sleep(.1)

The first thing we do after the if fork is click on the phone and open up the proper menu item — in this case the Rice menu.

1
 
2
 s = screenGrab()
3
 if s.getpixel(Cord.buy_rice) != (127, 127, 127):

Next we take a quick snapshot of the screen area and call getpixel() to get an RGB value for the pixel at the coordinates of Cord.buy_rice. We then test this against our previously established RGB value for when the item is greyed out. If it evaluates to True, we know that the item is not longer greyed out, and we have enough money to buy it. Consequently, if it evaluated to False, we can’t afford it.

1
 
2
print 'rice is available'
3
mousePos(Cord.buy_rice)
4
time.sleep(.1)
5
leftClick()
6
mousePos(Cord.delivery_norm)
7
time.sleep(.1)
8
leftClick()
9
time.sleep(2.5)

Providing we can afford the ingredient, we simply navigate through the remaining boxes required to purchase the food.

1
 
2
else:
3
            print 'rice is NOT available'
4
            mousePos(Cord.t_exit)
5
            leftClick()
6
            time.sleep(1)
7
            buyFood(food)

Finally, if we cannot afford the food, we tell Python to close the menu, wait one second, and then try the process again. It is usually only a matter of seconds between being able to afford something versus not being able to afford something. We won’t do it in this tutorial, but it is fairly straightforward to add additional logic to this function to let the bot decide whether it needs to continue waiting until it can afford something, or if it’s free to do other tasks and return at a later time.


Step 19: Keeping Track of Ingredients

All right, now we’re going to slowly, little by little, start replacing areas where we, the external entity, provide input and decision making with logic that can run by itself.

We need to device a way of keeping track of how many ingredients we currently have on hand. We could do this by pinging the screen in certain areas, or by averaging each ingredient box (we’ll get to this technique later), but by far, the simplest and fastest method is to just store all of the on hand items in a dictionary.

The amount of each ingredient stays constant throughout each level. You will always begin with 10 of the ‘common’ items (rice, nori, roe), and 5 of the ‘premium’ items (shrimp, salmon, unagi).

play_buttonpng

Let’s add this information to a dictionary.

1
foodOnHand = {'shrimp':5,
2
              'rice':10,
3
              'nori':10,
4
              'roe':10,
5
              'salmon':5,
6
              'unagi':5}

Our dictionary keys hold the name of the ingredient, and we’ll be able to get current amount by exploring the values.


Step 20: Adding Tracking to Code

Now that we have our dictionary of values. Let’s work it into the code. Every time we make something, we’ll subtract the ingredients used. Every time we shop, we’ll add them back in.

Let’s begin by expanding the makeFood() function

1
def makeFood(food):
2
    if food == 'caliroll':
3
        print 'Making a caliroll'
4
        foodOnHand['rice'] -= 1 
5
        foodOnHand['nori'] -= 1 
6
        foodOnHand['roe'] -= 1  
7
        mousePos(Cord.f_rice)
8
        leftClick()
9
        time.sleep(.05)
10
        mousePos(Cord.f_nori)
11
        leftClick()
12
        time.sleep(.05)
13
        mousePos(Cord.f_roe)
14
        leftClick()
15
        time.sleep(.1)
16
        foldMat()
17
        time.sleep(1.5)
18
	
19
    elif food == 'onigiri':
20
        print 'Making a onigiri'
21
        foodOnHand['rice'] -= 2  
22
        foodOnHand['nori'] -= 1  
23
        mousePos(Cord.f_rice)
24
        leftClick()
25
        time.sleep(.05)
26
        mousePos(Cord.f_rice)
27
        leftClick()
28
        time.sleep(.05)
29
        mousePos(Cord.f_nori)
30
        leftClick()
31
        time.sleep(.1)
32
        foldMat()
33
        time.sleep(.05)
34
        
35
        time.sleep(1.5)
36

37
    elif food == 'gunkan':
38
        print 'Making a gunkan'
39
        foodOnHand['rice'] -= 1  
40
        foodOnHand['nori'] -= 1  
41
        foodOnHand['roe'] -= 2  
42
        mousePos(Cord.f_rice)
43
        leftClick()
44
        time.sleep(.05)
45
        mousePos(Cord.f_nori)
46
        leftClick()
47
        time.sleep(.05)
48
        mousePos(Cord.f_roe)
49
        leftClick()
50
        time.sleep(.05)
51
        mousePos(Cord.f_roe)
52
        leftClick()
53
        time.sleep(.1)
54
        foldMat()
55
        time.sleep(1.5)

Now each time we make a piece of Sushi, we reduce the values in our foodOnHand dictionary by the appropriate amount. Next we’ll adjust buyFood() to add values.

1
def buyFood(food):
2
    
3
    if food == 'rice':
4
        mousePos(Cord.phone)
5
        time.sleep(.1)
6
        leftClick()
7
        mousePos(Cord.menu_rice)
8
        time.sleep(.05)
9
        leftClick()
10
        s = screenGrab()
11
        print 'test'
12
        time.sleep(.1)
13
        if s.getpixel(Cord.buy_rice) != (127, 127, 127):
14
            print 'rice is available'
15
            mousePos(Cord.buy_rice)
16
            time.sleep(.1)
17
            leftClick()
18
            mousePos(Cord.delivery_norm)
19
            foodOnHand['rice'] += 10      
20
            time.sleep(.1)
21
            leftClick()
22
            time.sleep(2.5)
23
        else:
24
            print 'rice is NOT available'
25
            mousePos(Cord.t_exit)
26
            leftClick()
27
            time.sleep(1)
28
            buyFood(food)
29
            
30
    if food == 'nori':
31
        mousePos(Cord.phone)
32
        time.sleep(.1)
33
        leftClick()
34
        mousePos(Cord.menu_toppings)
35
        time.sleep(.05)
36
        leftClick()
37
        s = screenGrab()
38
        print 'test'
39
        time.sleep(.1)
40
        if s.getpixel(Cord.t_nori) != (33, 30, 11):
41
            print 'nori is available'
42
            mousePos(Cord.t_nori)
43
            time.sleep(.1)
44
            leftClick()
45
            mousePos(Cord.delivery_norm)
46
            foodOnHand['nori'] += 10          
47
            time.sleep(.1)
48
            leftClick()
49
            time.sleep(2.5)
50
        else:
51
            print 'nori is NOT available'
52
            mousePos(Cord.t_exit)
53
            leftClick()
54
            time.sleep(1)
55
            buyFood(food)
56

57
    if food == 'roe':
58
        mousePos(Cord.phone)
59
        time.sleep(.1)
60
        leftClick()
61
        mousePos(Cord.menu_toppings)
62
        time.sleep(.05)
63
        leftClick()
64
        s = screenGrab()
65
        
66
        time.sleep(.1)
67
        if s.getpixel(Cord.t_roe) != (127, 61, 0):
68
            print 'roe is available'
69
            mousePos(Cord.t_roe)
70
            time.sleep(.1)
71
            leftClick()
72
            mousePos(Cord.delivery_norm)
73
            foodOnHand['roe'] += 10                 
74
            time.sleep(.1)
75
            leftClick()
76
            time.sleep(2.5)
77
        else:
78
            print 'roe is NOT available'
79
            mousePos(Cord.t_exit)
80
            leftClick()
81
            time.sleep(1)
82
            buyFood(food)

Now each time an ingredient is purchased, we add the quantity to the appropriate dictionary value.


Step 21: Checking Food on Hand

Now that we have our makeFood() and buyFood() functions set up to modify the foodOnHand dictionary, we need to create a new function to monitor all the changes and check whether an ingredient has fallen below a certain threshold.

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

Here we set up a for loop to iterate through the key and value pairs of our foodOnHand dictionary. For each value, it checks whether the name equals one of the ingredients we need; if so, it then checks to see if its value is less than or equal to 3; and finally, providing it is less than 3, it calls buyFood() with the ingredient type as the parameter.

Let’s test this out a bit.

Everything seems to be working fairly well, so let’s move on to some more image recognition tasks.


Step 22: Traversing RGB Values — Setup

To go any further with our bot, we need to gather information about which sushi type is in which customer’s bubble. Doing this with the getpixel() method would be very painstaking as you would need to find an area in each thought bubble that has a unique RGB value not shared by any other sushi type/thought bubble. Given the pixel style art, which by its very nature has a limited color palette, you would have to fight tons of color overlap in the sushi types. Furthermore, for each new sushi type introduced through out the game, you would have to manually inspect it to see if it has a unique RGB not found in any of the other sushi types. Once found, it would certainly be at a different coordinate than the others so that means storing ever more coordinate values — 8 sushi types per bubble times 6 seat locations means 48 unique needed coordinates!

So, in summary, we need a better method.

Enter method two: Image summing/averaging. This version works off of a list of RGB values instead of one specific pixel. For each snapshot we take, the image is grayscaled, loaded into an array, and then summed. This sum is treated the same as the RGB value in the getpixel() method. We will use it to test and compare multiple images.

The flexibility of this method is such that once it is set up, in the case of our sushi bot, not more work is required on our part. As new sushi types are introduced their unique RGB values are summed and printed to the screen for our use. There’s no need to chase down any more specific coordinates like with getpixel().

That said, there is still a bit of setup required for this technique. We’ll need to create a few new bounding boxes so we process just the area of the screen we need rather than the entire play area.

Let get started. Navigate to your screenGrab() function and make a second copy of it. Rename the copy to grab() and make the following changes:

1
def screenGrab():
2
    box = (x_pad + 1,y_pad+1,x_pad+640,y_pad+480)
3
    im = ImageGrab.grab(box)
4

5
    ##im.save(os.getcwd() + '\Snap__' + str(int(time.time())) + '.png', 'PNG')

6
    return im
7
	
8
	
9
def grab():
10
    box = (x_pad + 1,y_pad+1,x_pad+640,y_pad+480)
11
    im = ImageOps.grayscale(ImageGrab.grab(box))
12
    a = array(im.getcolors())
13
    a = a.sum()
14
    print a
15
    return a

Line 2: We’re taking a screengrab just as we have before, but now we’re converting it to grayscale before we assign it to the instance im. Converting to grayscale makes traversing all of the color values much faster; instead of each pixel having a Red, Green, and Blue value, it only has one value ranging from 0-255.

Line 3: We create an array of the image’s color values using the PIL method getcolors() and assign them to the variable a

Line 4: We sum all the values of the array and print them to the screen. These are the numbers we’ll use when we compare two images.


Step 23: Setting New Bounding Boxes

Start a new game and wait for all of the customers to fill up. Double click on quickGrab.py to take a snapshot of the play area.

play_buttonpngplay_buttonpngplay_buttonpng

We’ll need to set bounding boxes inside of each of those bubbles.

Zoom in till you can see the fine detail of the pixels

play_buttonpngplay_buttonpngplay_buttonpng

For each bubble, we need to make sure the top left of our bounding box starts in the same location. To do so, count up two ‘edges’ from the inner left of the bubble. We want the white pixel at the second ‘edge’ to mark our first x,y location.

play_buttonpngplay_buttonpngplay_buttonpng

To get the bottom pair, add 63 to the x position, and 16 to the y. This will give you a box similar to the one below:

play_buttonpngplay_buttonpngplay_buttonpng

Don’t worry that we’re not getting the entire picture of the Sushi type. Since we’re summing all of the values, even a small change in one pixel will change the total and let us know something new is on screen.

We’re going to create six new functions, each a specialized version of our general grab() one, and fill their bounding arguments with the coordinates of all the bubbles. Once those are made, we’ll make a simple function to call everything at once, just for testing purposes.

1
def get_seat_one():
2
    box = (45,427,45+63,427+16)
3
    im = ImageOps.grayscale(ImageGrab.grab(box))
4
    a = array(im.getcolors())
5
    a = a.sum()
6
    print a
7
    im.save(os.getcwd() + '\seat_one__' + str(int(time.time())) + '.png', 'PNG')    
8
    return a
9

10
def get_seat_two():
11
    box = (146,427,146+63,427+16)
12
    im = ImageOps.grayscale(ImageGrab.grab(box))
13
    a = array(im.getcolors())
14
    a = a.sum()
15
    print a
16
    im.save(os.getcwd() + '\seat_two__' + str(int(time.time())) + '.png', 'PNG')    
17
    return a
18

19
def get_seat_three():
20
    box = (247,427,247+63,427+16)
21
    im = ImageOps.grayscale(ImageGrab.grab(box))
22
    a = array(im.getcolors())
23
    a = a.sum()
24
    print a
25
    im.save(os.getcwd() + '\seat_three__' + str(int(time.time())) + '.png', 'PNG')    
26
    return a
27

28
def get_seat_four():
29
    box = (348,427,348+63,427+16)
30
    im = ImageOps.grayscale(ImageGrab.grab(box))
31
    a = array(im.getcolors())
32
    a = a.sum()
33
    print a
34
    im.save(os.getcwd() + '\seat_four__' + str(int(time.time())) + '.png', 'PNG')    
35
    return a
36

37
def get_seat_five():
38
    box = (449,427,449+63,427+16)
39
    im = ImageOps.grayscale(ImageGrab.grab(box))
40
    a = array(im.getcolors())
41
    a = a.sum()
42
    print a
43
    im.save(os.getcwd() + '\seat_five__' + str(int(time.time())) + '.png', 'PNG')    
44
    return a
45

46
def get_seat_six():
47
    box = (550,427,550+63,427+16)
48
    im = ImageOps.grayscale(ImageGrab.grab(box))
49
    a = array(im.getcolors())
50
    a = a.sum()
51
    print a
52
    im.save(os.getcwd() + '\seat_six__' + str(int(time.time())) + '.png', 'PNG')    
53
    return a
54

55
def get_all_seats():
56
    get_seat_one()
57
    get_seat_two()
58
    get_seat_three()
59
    get_seat_four()
60
    get_seat_five()
61
    get_seat_six()

Okay! Lots of code, but it’s all just specialised versions of previously defined functions. Each defines a bounding box, and passes it to ImageGrab.Grab. From there, we convert to an array of RGB values and print the sum to the screen.

Go ahead and run this a few times while playing the game. Be sure to verify that every sushi type, regardless of which bubble it’s in, displays the same sum each time.


Step 24: Create a Sushi Types Dictionary

Once you’ve verified that each of the sushi types is always displaying the same value, record their sums into a dictionary as follows:

1
sushiTypes = {2670:'onigiri', 
2
              3143:'caliroll',
3
              2677:'gunkan',}

Having the numbers as the key and the strings as the values will make it easy to shuffle things from function to function without loosing track of everything.


Step 25: Create a No Bubble Class

The final step in our bubble gathering is getting the sums for when there are no bubbles present. We’ll use these to check when customers have come and gone.

Start a new game and quickly run get_all_seats() before anyone has a chance to show up. The numbers it prints out we’ll place into a class called Blank. As before, you could use a dictionary if you prefer.

1
class Blank:
2
    seat_1 = 8119
3
    seat_2 = 5986
4
    seat_3 = 11598 
5
    seat_4 = 10532
6
    seat_5 = 6782
7
    seat_6 = 9041

We’re almost there now! One final step and we’ll have a simple, working bot!


Step 26: Putting It All Together

Time to finally hand off control to our bot. We’ll script in the basic logic that will let it respond to customers, make their orders, and replenish its ingredients when the begin to run low.

The basic flow will follow this: Check seats > if customer, make order > check food > if low, buy food > clear tables > repeat.

This is a long one; let’s get started.

1
def check_bubs():
2

3
    checkFood()
4
    s1 = get_seat_one()
5
    if s1 != Blank.seat_1:
6
        if sushiTypes.has_key(s1):
7
            print 'table 1 is occupied and needs %s' % sushiTypes[s1]
8
            makeFood(sushiTypes[s1])
9
        else:
10
            print 'sushi not found!n sushiType = %i' % s1
11

12
    else:
13
        print 'Table 1 unoccupied'
14

15
    clear_tables()
16
    checkFood()
17
    s2 = get_seat_two()
18
    if s2 != Blank.seat_2:
19
        if sushiTypes.has_key(s2):
20
            print 'table 2 is occupied and needs %s' % sushiTypes[s2]
21
            makeFood(sushiTypes[s2])
22
        else:
23
            print 'sushi not found!n sushiType = %i' % s2
24

25
    else:
26
        print 'Table 2 unoccupied'
27

28
    checkFood()
29
    s3 = get_seat_three()
30
    if s3 != Blank.seat_3:
31
        if sushiTypes.has_key(s3):
32
            print 'table 3 is occupied and needs %s' % sushiTypes[s3]
33
            makeFood(sushiTypes[s3])
34
        else:
35
            print 'sushi not found!n sushiType = %i' % s3
36

37
    else:
38
        print 'Table 3 unoccupied'
39

40
    checkFood()
41
    s4 = get_seat_four()
42
    if s4 != Blank.seat_4:
43
        if sushiTypes.has_key(s4):
44
            print 'table 4 is occupied and needs %s' % sushiTypes[s4]
45
            makeFood(sushiTypes[s4])
46
        else:
47
            print 'sushi not found!n sushiType = %i' % s4
48

49
    else:
50
        print 'Table 4 unoccupied'
51

52
    clear_tables()
53
    checkFood()
54
    s5 = get_seat_five()
55
    if s5 != Blank.seat_5:
56
        if sushiTypes.has_key(s5):
57
            print 'table 5 is occupied and needs %s' % sushiTypes[s5]
58
            makeFood(sushiTypes[s5])
59
        else:
60
            print 'sushi not found!n sushiType = %i' % s5
61

62
    else:
63
        print 'Table 5 unoccupied'
64

65
    checkFood()
66
    s6 = get_seat_six()
67
    if s6 != Blank.seat_6:
68
        if sushiTypes.has_key(s6):
69
            print 'table 1 is occupied and needs %s' % sushiTypes[s6]
70
            makeFood(sushiTypes[s6])
71
        else:
72
            print 'sushi not found!n sushiType = %i' % s6
73

74
    else:
75
        print 'Table 6 unoccupied'
76

77
    clear_tables()

The very first thing we do is check food on hand. from there, we take a snapshot of position one and assign the sum to s1. After that we check to see that s1 does NOT equal Blank.seat_1. If it doesn’t, we have a customer. We check our sushiTypes dictionary to see it has a sum the same as our s1. If it does, we then call makeFood() and pass the sushiType as an argument.

Clear_tables() is called every two seats.

Only one final piece remaining: setting up the loop.


Step 27: Main Loop

We’re going to set up a very simple while loop to play the game. We didn’t make any sort of break mechanism, so to stop execution, click in the shell and hit Ctrl+C to send a keyboard interrupt.

1
def main():
2
	startGame()
3
    while True:
4
        check_bubs()

And that’s it! Refresh the page, load the game, and set your bot loose!

So, it’s a bit clunky and in need of refinement, but it stands as a decent skeleton for you to iterate upon.

A more complete version of the bot can be found here. It has several fixes such as keeping track of what’s being made, not getting stuck in the phone menus, and other general optimizations.


Conclusion

You now have all of the tools you need to go about building your own simple bots. The techniques we used in this tutorial are quite primitive in the world of Computer Vision, but still, with enough persistence, you can create many cool things with them — even outside the realm of game bots. We, for instance, run several scripts based on these techniques to automate repetitive software tasks around the office. It’s pretty satisfying to remove a human task with just a few lines of code.

Thanks for reading, and if you have any issues or comments, be sure to leave a note below. Good luck, have fun.

Learn Python

Learn Python with our complete python tutorial guide, whether you’re just getting started or you’re a seasoned coder looking to learn new skills.

Chris Kiehl

Chris Kiehl is an editor for an Atlanta based media company. When not null testing audio gear for fun, he can be found tinkering in Pro Tools or building bots. He greatly enjoys making machines do people things.

Foreword

How can you have fun on New Year’s holidays? Play computer games? No! It is better to write a bot that will do this for you, and most go to sculpt a snowman and drink mulled wine.

Once in school, I was fascinated by one of the popular MMORPGs – Lineage 2. In the game, you can join clans, groups, make friends and fight with rivals, but in general, the game is filled with monotonous actions: doing quests and farming (gathering resources, gaining experience).

  • As a result, I decided that the bot should solve one task: farm. For control, emulated mouse clicks and keystrokes of the keyboard will be used, and for computer orientation, Python will be used for orientation in space.

In general, creating a bot for L2 is not a new thing and there are quite a few ready-made ones. They are divided into 2 main groups: those that are implemented in the client’s work and clickers.

The first – this is a hard cheat, in terms of the game to use them too unsporting. The second option is more interesting, given that it can be applied with some modifications to any other game, and the implementation will be more interesting. Those clickers that I found, for various reasons, did not work or worked unstably.

Notes: All information here is only for cognitive purposes. Especially for game developers to help them better deal with bots.

Working with the window

Everything is simple. We will work with screenshots from the window with the game.
To do this, we define the coordinates of the window. With the window, we work with the win32gui module. The required window is defined by the title – “Lineage 2”.

Code of methods for obtaining the position of the window

def get_window_info():
    # set window info
    window_info = {}
    win32gui.EnumWindows(set_window_coordinates, window_info)
    return window_info

# EnumWindows handler
# sets L2 window coordinates
def set_window_coordinates(hwnd, window_info):
    if win32gui.IsWindowVisible(hwnd):
        if WINDOW_SUBSTRING in win32gui.GetWindowText(hwnd):
            rect = win32gui.GetWindowRect(hwnd)
            x = rect[0]
            y = rect[1]
            w = rect[2] - x
            h = rect[3] - y
            window_info['x'] = x
            window_info['y'] = y
            window_info['width'] = w
            window_info['height'] = h
            window_info['name'] = win32gui.GetWindowText(hwnd)
            win32gui.SetForegroundWindow(hwnd)

Get the picture of the desired window using ImageGrab:

def get_screen(x1, y1, x2, y2):
    box = (x1 + 8, y1 + 30, x2 - 8, y2)
    screen = ImageGrab.grab(box)
    img = array(screen.getdata(), dtype=uint8).reshape((screen.size[1], screen.size[0], 3))
    return img

Now we will work with the content.

Search for monsters

What is the most interesting is that those implementations that I found are not that accurate like a wanted? For example, in one popular and even paid one it is done through a game macro. Which means that the “player” has to write all the time for each type of monster in a macro something like that “/ target Monster Name Bla Bla”.

In our case, we follow this logic: first of all, we find all the texts of white color on the screen. White text can be not only the name of the monster but also the name of the character, the name of the NPC or other players. Therefore, you must point the cursor at the object and if a highlight appears with the desired pattern, then you can attack the target.

Here is the original picture from which we will work:

Bot in Python For Online Games 1

Let’s blacken my name, so as not to interfere and translate the picture into black and white. The original image in RGB – each pixel is an array of three values from 0 to 255 when b / w is one value. So we will significantly reduce the amount of data:

img[210:230, 350:440] = (0, 0, 0)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

Bot in Python For Online Games 2

Let’s find all objects of white color (this is white text with the names of monsters)

ret, threshold1 = cv2.threshold(gray, 252, 255, cv2.THRESH_BINARY)

Bot in Python For Online Games 6

Morphological transformations:

  • We will filter by the rectangle in size 50×5. This rectangle came up best.
  • We remove noise inside rectangles with the text (as a matter of fact we paint all between letters white).
  • Once again, remove the noise, blurring and stretching using a filter.
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (50, 5))
closed = cv2.morphologyEx(threshold1, cv2.MORPH_CLOSE, kernel)
closed = cv2.erode(closed, kernel, iterations=1)
closed = cv2.dilate(closed, kernel, iterations=1)

Bot in Python For Online Games 4

Find the middle of the resulting spots

(_, centers, hierarchy) = cv2.findContours(closed, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

It works, but it can be done more fun (for example, for monsters whose names are not visible, because they are far away) – with the help of TensorFlow Object Detection, as here, but someday in the next life.

Now we point the cursor at the found monster and see if the highlighting has appeared using the cv2.matchTemplate method. It remains to press the LMB and the attack button.

The Click

With the search for the monster figured out, the bot can already find targets on the screen and point the mouse at them. To attack the target, you need to click the left mouse button and click “attack” (button “1” can attack the attack). Right-click to rotate the camera.

On the server where I tested the bot, I caused a click through AutoIt, but it somehow did not work.

As it turned out, games are protected from auto clickers in many ways:

  • Search for processes that emulate clicks
  • Record clicks and determine what color the object the bot is clicking on
  • Definition of patterns of clicks
  • Definition of the bot by the frequency of clicks

And some applications, like the client of this server, can determine the source of the click at the OS level. (it will be great if someone tells you exactly how).

Some frameworks which can click (including pyautogui, robot framework and something else) have been tried, but any of variants did not work. A thought slipped through the idea of building a device that would press a button (someone even did it). It seems that you need to click the most hardware. As a result, I started looking towards writing my driver.

On the Internet, a way to solve the problem was found: a USB device that can be programmed to give the desired signal – Digispark.

Bot in Python For Online Games 5

During my research for good language library:

  • As a result, was found a wonderful library at C.
  • There was for her and a wrapper in Python.

The library of python 3.6 didn’t work for me well – I was getting all the time the Access violation error. So I had to jump to python 2.7, everything worked like a charm.

Moving the cursor

The library can send any commands, including where to move the mouse. But it looks like the teleportation of the cursor. We need to make the cursor move smoothly so that we are not banned.

In essence, the task is reduced to moving the cursor from point A to point B using the AutoHotPy wrapper. Do you really have to remember math?

After a little reflection, he decided to google. It turned out that there is no need to invent anything – the problem is solved by the algorithm of Brenham, one of the oldest algorithms in computer graphics.

Logic of work

All the tools are there, the simplest thing left is to write a script.

  • If the monster is alive, we continue to attack.
  • If there is no goal, find a goal and start attacking.
  • If we could not find the target, we will turn a little.
  • If 5 times no one was able to find – go to the side and start again.

From more or less interesting I will describe how I received the health status of the victim. In general terms: we find from the pattern using OpenCV a control that shows the status of the health of the target, take a strip of one pixel high and count as a percentage, how many are red.

Code of method for obtaining the level of health of the victim

def get_targeted_hp(self):
        """
        return victim's hp
        or -1 if there is no target
        """

        hp_color = [214, 24, 65]
        target_widget_coordinates = {}
        filled_red_pixels = 1

        img = get_screen(
            self.window_info["x"],
            self.window_info["y"],
            self.window_info["x"] + self.window_info["width"],
            self.window_info["y"] + self.window_info["height"] - 190
        )

        img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

        template = cv2.imread('img/target_bar.png', 0)
        # w, h = template.shape[::-1]

        res = cv2.matchTemplate(img_gray, template, cv2.TM_CCOEFF_NORMED)
        threshold = 0.8
        loc = np.where(res &gt;= threshold)
        if count_nonzero(loc) == 2:
            for pt in zip(*loc[::-1]):
                target_widget_coordinates = {"x": pt[0], "y": pt[1]}
                # cv2.rectangle(img, pt, (pt[0] + w, pt[1] + h), (255, 255, 255), 2)

        if not target_widget_coordinates:
            return -1

        pil_image_hp = get_screen(
            self.window_info["x"] + target_widget_coordinates['x'] + 15,
            self.window_info["y"] + target_widget_coordinates['y'] + 31,
            self.window_info["x"] + target_widget_coordinates['x'] + 164,
            self.window_info["y"] + target_widget_coordinates['y'] + 62
        )

        pixels = pil_image_hp[0].tolist()
        for pixel in pixels:
            if pixel == hp_color:
                filled_red_pixels += 1

        percent = 100 * filled_red_pixels / 150
        return percent

Now the bot understands how much HP the victim has and whether it is still alive.

The basic logic is ready, here’s how it looks now in action:

Stop work

All work with the cursor and keyboard is done through the autohotpy object, which can be stopped at any time by pressing the ESC button.

The problem is that all the time the bot is busy executing the loop, responsible for the logic of the character’s actions and the event handlers of the object and autohotpy do not start listening to events until the loop ends. The work of the program cannot be stopped with the help of the mouse. The bot controls it and moves the cursor wherever it needs.

It does not suit us, so we had to divide the bot into 2 threads: listening to events and performing the logic of the character’s actions.

Create 2 threads

 # init bot stop event
        self.bot_thread_stop_event = threading.Event()

        # init threads
        self.auto_py_thread = threading.Thread(target=self.start_auto_py, args=(auto_py,))
        self.bot_thread = threading.Thread(target=self.start_bot, args=(auto_py, self.bot_thread_stop_event, character_class))

        # start threads
        self.auto_py_thread.start()
        self.bot_thread.start()

And now we hang the handler on ESC:

auto_py.registerExit(auto_py.ESC, self.stop_bot_event_handler)

And, when pressing ESC, set the event

self.bot_thread_stop_event.set()

And in the logic loop of the character check whether the event is set:

while not stop_event.is_set():

Now quietly stop the bot on the ESC button.

Conclusion

It would seem, why waste time on a product that does not bring any practical benefit?

In fact, a computer game in terms of computer vision is almost the same as the reality show on the camera, and there the possibilities for application are enormous. And. I hope it was interesting to you.

Repository reference

Привет друзья!

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

Роль Python в игровой индустрии

  • Поведение ИИ в SIMS записывается с использованием Python.
  • Редактор карт Civilization IV поддерживает программирование на Python.
  • Другой популярной игрой, поддерживающей Python, является Disney’s Toontown Online, многопользовательская ролевая онлайн-игра (MMORPG). Это помогает pandas3D для графики игры, и игра довольно легкая.
  • EVE Online — еще одна MMORPG, использующая Python без стека — вариант языка программирования Python для клиентского и серверного кода.
  • Battlefield Heroes использует Python для некоторых частей своей игровой логики, таких как подсчет очков с помощью открытого текста и игровых режимов.

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

Основные браузерные игры используют JavaScript и WebGL или Canvas2D для отображения графики в браузере. Невозможно создать всю браузерную игру с использованием Python, поскольку веб-браузеры используют HTML для отображения веб-страниц и их различных элементов. Таким образом, вы можете использовать Python для внутренней логики вашей игры и отображать ее как чистый HTML в браузерах.

Другой способ — использовать Python на серверной части для создания интерактивного Flash или JavaScript для запуска в браузере.

Популярные игровые движки, такие как Unity3D или UnrealEngine, позволяют экспортировать игровые проекты в совместимый код HTML5. Однако этот метод довольно сложен и требует много времени для реализации внутри игрового движка.

Лучше всего использовать любой собственный игровой движок HTML 5. Используя собственный игровой движок HTML5, вы можете создавать оптимизированный код JavaScript, который также повысит производительность ваших браузерных игр.

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

Технология разработки браузерных игр на Python

1 . Полноэкранный API — если вы хотите, чтобы ваша браузерная игра работала на весь экран.

2 . Gamepad API — для обеспечения поддержки геймпадов или других игровых контроллеров в вашей браузерной игре.

3 . Web Audio API — Игры без звука довольно скучны, и никто не хочет в них играть. Используйте API веб-аудио для управления воспроизведением и манипулированием аудио из кода JS. Вы можете добавлять крутые звуковые эффекты и применять их в режиме реального времени.

4 . HTML и CSS — самое важное для изучения, если вы хотите создать собственную браузерную игру. Используя HTML и CSS, вы можете спроектировать интерфейс своей браузерной игры. HTML-тег <canvas> обеспечивает простой способ создания 2D-графики.

5 . JavaScript — современный язык программирования для Интернета, который работает быстро. Используя его, вы можете применять логику Python из бэкэнда для управления игровым процессом.

6 . HTML-аудио — если вам требуется только базовое аудио для браузерной игры, вы можете просто применить HTML-элемент <audio>.

7 . WebGL — создавайте высокопроизводительную 3D- и 2D-графику с аппаратным ускорением из веб-контента.

8 . WebRTC — WebRTC или Web Real-Time Communications API поддерживает передачу видео- и аудиоданных, включая телеконференции между несколькими пользователями. Итак, если вы хотите создать многопользовательскую онлайн-игру и позволить игрокам общаться друг с другом, пока они заняты убийством зомби, то этот API для вас.

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

1о. Web Workers. Воспользуйтесь преимуществами современных многоядерных процессоров с помощью Web Workers. Создавайте фоновые игровые потоки, выполняющие собственный код JS, чтобы повысить производительность вашей онлайн-игры на основе Python.

11 . Pointer Lock API — получите точные координаты курсора вашего пользователя, чтобы точно измерить, что игроки делают в игре.

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

Библиотеки Python для разработки онлайн-игр

  1. Пигейм

PyGame — это простой и удобный набор модулей Python для разработки видеоигр. Он добавляет функциональность поверх библиотеки SDL. Таким образом, вы можете создавать первоклассные игры на Python.

На веб-сайтах PyGame создано более 660 игровых проектов, в том числе финалисты Indie Game Festival, Australian Game Festival и так далее.

Особенности :

  • Поддержка многоядерных процессоров
  • Использует оптимизированный код на C и ассемблере
  • Портативный
  • Отличный контроль над вашим основным циклом.
  • Модульный
  • Игры, разработанные с использованием Pygame — Magic Leap, Oculus Quest, Valve Index, Windows Mixed Reality и так далее.
  1. Киви

Kivy — это кроссплатформенная среда Python для разработки NUI, которую вы можете использовать для разработки увлекательных и захватывающих мобильных игр. Будь то простая игра-головоломка или аркадная игра, вы можете воплотить свою игровую идею в реальность с помощью Kivy.

Особенности:

  • Kivy поддерживает быстрое прототипирование приложений.
  • Напишите свой игровой код один раз и запустите его на нескольких платформах, включая Windows, Linux, OS X, iOS и Android.
  • Встроенная поддержка ввода данных пользователем с различных устройств, включая трекпад Mac OS X, WM_Pen, Linux Kernel HID и т. д.
  • Бесплатное использование для коммерческих проектов.
  • Графический движок использует OpenGL ES 2.
  • Игры, разработанные с использованием Kivy — Deflectouch, FishLife, memoryKivy, ArithmeBricks и т. д.

Мобильные игры, разработанные с использованием Kivy — Spikes Escape, Ants Must Die, CoinTex, Quadropoly и др.

  1. Пыганим

Pyganim, произносится как «свинья» и «анимация», представляет собой модуль Pygame для быстрого добавления анимации спрайтов и поддерживает как Python 2, так и Python 3.

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

  1. OcempGUI

OcempGUI — это набор инструментов на основе Python для создания простых графических интерфейсов. Он использует возможности библиотеки PyGame и позволяет разрабатывать широкий спектр игровых проектов.

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

Особенности:

  • Экономьте свое время, используя широкий спектр функций рисования.
  • Сосредоточьтесь на своей основной задаче, вместо того чтобы возиться с низкоуровневыми компонентами.
  • Создавайте простые игры Python с графическим интерфейсом.
  1. пыжи

pyjs — это платформа разработки многофункциональных интернет-приложений (RIA) для разработки как веб-игр, так и настольных игр. С помощью pyjs вы можете написать свою игру на JavaScript полностью на Python, поскольку он содержит компилятор Python-to-JavaScript.

Вот обзор того, как работает pyjs:pyjs

Особенности:

  • Запустите веб-игру Python на рабочем столе как отдельное приложение.
  • Разрабатывайте игры, используя Python вместо HTML и CSS.
  • Библиотека AJAX для решения проблем совместимости браузеров.
  • Виджеты пользовательского интерфейса и библиотека DOM — написаны на Python и переведены на JS для развертывания вашей мобильной игры/

Рассмотрим на примере

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

Сегодня мы увидим, как написать бота на Python, который использует библиотеку компьютерного зрения OpenCV, чтобы победить Don’t touch the red, бесконечного бегуна от Addicting Games.Addicting Games

Начальный экран

Правила игры

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

Есть одна важная особенность; если играть в аркадном режиме, то кнопки падают с нарастающей скоростью. Это усложняет игру для игрока-человека. Но для нашего бота это не проблема!

Сопоставление шаблонов OpenCV

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

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

Пример обнаружения монет в Mario. В коде приложение сопоставления шаблонов выглядит так

В коде приложение сопоставления шаблонов выглядит так

import cv2

template = cv2.imread (‘template.png’)

target = cv2.imread (‘target.png’)

result = cv2.matchTemplate (target, template, cv2.TM_CCOEFF_NORMED)

_, max_val, _, max_loc = cv2.minMaxLoc (result)

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

Два типа используемых шаблонов

Создание снимков экрана и взаимодействие

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

Есть два пакета Python, которые помогают с вышеуказанными задачами: mssи pyautogui, мы используем их для получения скриншотов определенной части экрана и отправки кликов в окно браузера соответственно. Я также использую keyboard библиотеку, так как очень удобно установить «действие разрыва» на какую-либо клавишу в случае, когда вашей мышью управляет бот. Библиотеке keyboard (и, возможно, pyautogui) требуются sudoправа, поэтому запустите свой скрипт Python как исполняемый файл с правильным заголовком shebang.

Здесь я привожу фрагменты кода, как получать скриншоты и отправлять клики:

#!/hdd/anaconda2/envs/games_ai/bin/python

# ^ change above to your python path ^

import keyboard

import mss

import pyautogui

pyautogui.PAUSE = 0.0

print («Press ‘s’ to start»)

print («Press ‘q’ to quit»)

keyboard.wait (‘s’)

# setup mss and get the full size of your monitor

sct = mss.mss ()

mon = sct.monitors[0]

while True:

# decide on the part of the screen

roi = {

«left»: 0,

«top»: int (mon[«height»] * 0.2),

«width»: int (mon[«width»] / 2),

«height»: int (mon[«height»] * 0.23)

}

roi_crop = numpy.array (sct.grab (roi))[:,:,:3]

# do something with `roi_crop`

if keyboard.is_pressed (‘q’):

break

Алгоритм

Он превосходит предыдущую человеческую игровую оценку 170 с оценкой 445.Алгоритм

Бот в действии

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

Первая половина кода:

#!/hdd/anaconda2/envs/games_ai/bin/python

# if «Xlib.error.DisplayConnectionError» use «xhost +» on linux

import shutil

import os

import keyboard

import mss

import cv2

import numpy

from time import time, sleep

import pyautogui

from random import randint

import math

pyautogui.PAUSE = 0.0

print («Press ‘s’ to start»)

print («Press ‘q’ to quit»)

keyboard.wait (‘s’)

try:

shutil.rmtree («./screenshots»)

except FileNotFoundError:

pass

os.mkdir («./screenshots»)

# setup mss and get the full size of your monitor

sct = mss.mss ()

mon = sct.monitors[0]

frame_id = 0

# decide where is the region of interest

for idx in range (3,0, -1):

roi = {

«left»: 0,

«top»: int (mon[«height»] * (idx * 0.2)),

«width»: int (mon[«width»] / 2),

«height»: int (mon[«height»] * 0.23)

}

green_button = cv2.imread (‘green_button.png’)

offset_x = int (green_button.shape[0] / 2)

offset_y = int (green_button.shape[1] / 2)

roi_crop = numpy.array (sct.grab (roi))[:,:,:3]

result = cv2.matchTemplate (roi_crop, green_button, cv2.TM_CCOEFF_NORMED)

_, max_val, _, max_loc = cv2.minMaxLoc (result)

print (max_val, max_loc)

button_center = (max_loc[0] + offset_y, max_loc[1] + offset_x)

roi_crop = cv2.circle (roi_crop.astype (float), button_center, 20, (255, 0, 0), 2)

cv2.imwrite (f»./screenshots/{frame_id:03}.jpg», roi_crop)

abs_x_roi = roi[«left»] + button_center[0]

abs_y_roi = roi[«top»] + button_center[1]

pyautogui.click (x=abs_x_roi, y=abs_y_roi)

frame_id += 1

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

Вторая половина:

second_roi = {

«left»: 0,

«top»: int (mon[«height»] * 0.18),

«width»: int (mon[«width»] / 2),

«height»: int (mon[«height»] * 0.06)

}

btn = cv2.imread (‘center.png’)

offset_y = int (btn.shape[0])

offset_x = int (btn.shape[1] / 2)

thresh = 0.9

frame_list = []

btn_cnt = 1

while True:

frame_id += 1

second_roi_crop = numpy.array (sct.grab (second_roi))[:,:,:3]

result = cv2.matchTemplate (second_roi_crop, btn, cv2.TM_CCOEFF_NORMED)

_, max_val, _, max_loc = cv2.minMaxLoc (result)

# define the speed of the screen

speed = math.floor (math.log (frame_id)**2.5)

print (frame_id, max_val, max_loc, speed)

frame_list.append (max_loc[0])

if max_val > thresh:

button_center = (max_loc[0] + offset_x, max_loc[1] + offset_y)

second_roi_crop = cv2.circle (second_roi_crop.astype (float), button_center, 20, (255, 0, 0), 2)

cv2.imwrite (f»./screenshots/{frame_id:03}.jpg», second_roi_crop)

abs_x_sec = second_roi[«left»] + button_center[0]

abs_y_sec = second_roi[«top»] + button_center[1] + speed

pyautogui.click (x=abs_x_sec, y=abs_y_sec)

btn_cnt += 1

if keyboard.is_pressed (‘q’):

break

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

Чтобы не быть голословным, вот скриншот таблицы лидеров. В этой конкретной игре очки на всех уровнях сложности идут в таблицу лидеров, поэтому вам не нужно играть в «Сложно». «Легкий» уровень просто отличный (кстати, когда вы достигаете 100 нажатой кнопки, вы больше не можете сказать, что это легко)/

Таблица лидеров

Таблица лидеров

Код проекта доступен на Github https://github.com/zetyquickly/addicting-games-ai. Было бы здорово создать обширную библиотеку взломанных игр и хранить там все эти алгоритмы. Итак, вам предлагается создавать пулл-реквесты!

Итоги

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

Python может проделать большую работу, чтобы повысить свою игровую производительность по сравнению с C# и C++, которые являются гораздо лучшими альтернативами для разработки ресурсоемких игр. Тем не менее, Python все еще не отстает.

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

Привет! Меня зовут Сержик Сергеев. Мне 35 лет. Живу в г. Санкт-Петербург. Главная цель моего игрового блога Igamer.biz – охватить полное информационное пространство популярных сетевых игр.

Программирование, Python, Разработка игр, Из песочницы


Рекомендация: подборка платных и бесплатных курсов Unity — https://katalog-kursov.ru/

Предисловие

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

Когда-то в школьные годы был увлечен одной из популярных MMORPG — Lineage 2. В игре можно объединяться в кланы, группы, заводить друзей и сражаться с соперниками, но в общем игра наполнена однообразными действиями: выполнением квестов и фармом (сбор ресурсов, получение опыта).

В итоге решил, что бот должен решать одну задачу: фарм. Для управления будут использоваться эмулированные клики мыши и нажатия клавиш клавиатуры, а для ориентирования в пространстве — компьютерное зрение, язык программирования — Python.

Вообще, создание бота для L2 дело не новое и их готовых есть довольно много. Делятся они на 2 основные группы: те, которые внедряются в работу клиента и кликеры.

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

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

Итак, к делу.

Работа с окном

Тут все просто. Будем работать со скриншотами из окна с игрой.
Для этого определим координаты окна. С окном работаем с помощью модуля win32gui. Нужное окно определим по заголовку — “Lineage 2”.

Код методов получения положения окна

def get_window_info():
    # set window info
    window_info = {}
    win32gui.EnumWindows(set_window_coordinates, window_info)
    return window_info

# EnumWindows handler
# sets L2 window coordinates
def set_window_coordinates(hwnd, window_info):
    if win32gui.IsWindowVisible(hwnd):
        if WINDOW_SUBSTRING in win32gui.GetWindowText(hwnd):
            rect = win32gui.GetWindowRect(hwnd)
            x = rect[0]
            y = rect[1]
            w = rect[2] - x
            h = rect[3] - y
            window_info['x'] = x
            window_info['y'] = y
            window_info['width'] = w
            window_info['height'] = h
            window_info['name'] = win32gui.GetWindowText(hwnd)
            win32gui.SetForegroundWindow(hwnd)

Получаем картинку нужного окна с помощью ImageGrab:

def get_screen(x1, y1, x2, y2):
    box = (x1 + 8, y1 + 30, x2 - 8, y2)
    screen = ImageGrab.grab(box)
    img = array(screen.getdata(), dtype=uint8).reshape((screen.size[1], screen.size[0], 3))
    return img

Теперь будем работать с содержимым.

Поиск монстра

Самое интересное. Те реализации, которые я находил, мне не подошли. Например, в одном из популярных и даже платном это сделано через игровой макрос. И “игрок” должен для каждого типа монстра прописывать в макросе типа “/target Monster Name Bla Bla”.

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

Вот исходная картинка, с который будем работать:

Закрасим черным своё имя, чтобы не мешало и переведем картинку в ч/б. Исходная картинка в RGB — каждый пиксель это массив из трёх значений от 0 до 255, когда ч/б — это одно значение. Так мы значительно уменьшим объем данных:

img[210:230, 350:440] = (0, 0, 0)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

Найдем все объекты белого цвета (это белый текст с названиями монстров)

ret, threshold1 = cv2.threshold(gray, 252, 255, cv2.THRESH_BINARY)

Морфологические преобразования:

  1. Фильтровать будем по прямоугольнику размером 50×5. Такой прямоугольник подошел лучше всех.
  2. Убираем шум внутри прямоугольников с текстом (по сути закрашиваем всё между букв белым)
  3. Еще раз убираем шум, размывая и растягивая с применением фильтра

kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (50, 5))
closed = cv2.morphologyEx(threshold1, cv2.MORPH_CLOSE, kernel)
closed = cv2.erode(closed, kernel, iterations=1)
closed = cv2.dilate(closed, kernel, iterations=1)

Находим середины получившихся пятен

(_, centers, hierarchy) = cv2.findContours(closed, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

Работает, но можно сделать прикольнее (например, для монстров, имена которых не видны, т.к. находятся далеко) — с помощью TensorFlow Object Detection, как тут, но когда-нибудь в следующей жизни.

Теперь наводим курсор на найденного монстра и смотрим, появилась ли подсветка с помощью метода cv2.matchTemplate. Осталось нажать ЛКМ и кнопку атаки.

Клик

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

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

Как оказалось, игры защищаются от автокликеров разными способами:

  • поиск процессов, которые эмулируют клики
  • запись кликов и определение, какого цвета объект, на который кликает бот
  • определение паттернов кликов
  • определение бота по периодичности кликов

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

Были перепробованы некоторые фреймворки, которые могут кликать (в т.ч. pyautogui, robot framework и что-то еще), но ни один из вариантов не сработал. Проскользнула мысль соорудить устройство, которое будет нажимать кнопку (кто-то даже так делал). Похоже, что нужен клик максимально хардварный. В итоге стал смотреть в сторону написания своего драйвера.

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

Ждать несколько недель с Алиэкспресса не хочется, поэтому поиски продолжились.

В итоге была найдена замечательная библиотека на C
Нашлась для неё и обёртка на Python

Библиотека у меня не завелась на питоне 3.6 — вываливалась ошибка Access violation что-то там. Поэтому пришлось соскочить на питон 2.7, там всё заработало like a charm.

Движение курсора

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

По сути задача сводится к тому, чтобы перемещать курсор из точки A в точку B с помощью обертки AutoHotPy. Неужели придется вспоминать математику?

Немного поразмыслив, всё-таки решил погуглить. Оказалось, что ничего придумывать не надо — задачу решает алгоритм Брезенхэма, один из старейших алгоритмов в компьютерной графике:

Прямо с Википедии можно взять и реализацию

Логика работы

Все инструменты есть, осталось самое простое — написать сценарий.

  1. Если монстр жив, продолжаем атаковать
  2. Если нет цели, найти цель и начать атаковать
  3. Если не удалось найти цель, немного повернемся
  4. Если 5 раз никого не удалось найти — идём в сторону и начинаем заново

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

Код метода получения уровня здоровья жертвы

def get_targeted_hp(self):
        """
        return victim's hp
        or -1 if there is no target
        """

        hp_color = [214, 24, 65]
        target_widget_coordinates = {}
        filled_red_pixels = 1

        img = get_screen(
            self.window_info["x"],
            self.window_info["y"],
            self.window_info["x"] + self.window_info["width"],
            self.window_info["y"] + self.window_info["height"] - 190
        )

        img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

        template = cv2.imread('img/target_bar.png', 0)
        # w, h = template.shape[::-1]

        res = cv2.matchTemplate(img_gray, template, cv2.TM_CCOEFF_NORMED)
        threshold = 0.8
        loc = np.where(res >= threshold)
        if count_nonzero(loc) == 2:
            for pt in zip(*loc[::-1]):
                target_widget_coordinates = {"x": pt[0], "y": pt[1]}
                # cv2.rectangle(img, pt, (pt[0] + w, pt[1] + h), (255, 255, 255), 2)

        if not target_widget_coordinates:
            return -1

        pil_image_hp = get_screen(
            self.window_info["x"] + target_widget_coordinates['x'] + 15,
            self.window_info["y"] + target_widget_coordinates['y'] + 31,
            self.window_info["x"] + target_widget_coordinates['x'] + 164,
            self.window_info["y"] + target_widget_coordinates['y'] + 62
        )

        pixels = pil_image_hp[0].tolist()
        for pixel in pixels:
            if pixel == hp_color:
                filled_red_pixels += 1

        percent = 100 * filled_red_pixels / 150
        return percent

Теперь бот понимает, сколько HP у жертвы и жива ли она еще.

Основная логика готова, вот как теперь он выглядит в действии:
Для занятых я ускорил на 1.30

Остановка работы

Вся работа с курсором и клавиатурой ведется через объект autohotpy, работу которого в любой момент можно остановить нажатием кнопки ESC.

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

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

Создадим 2 потока

        # init bot stop event
        self.bot_thread_stop_event = threading.Event()

        # init threads
        self.auto_py_thread = threading.Thread(target=self.start_auto_py, args=(auto_py,))
        self.bot_thread = threading.Thread(target=self.start_bot, args=(auto_py, self.bot_thread_stop_event, character_class))

        # start threads
        self.auto_py_thread.start()
        self.bot_thread.start()

и теперь вешаем обработчик на ESC:

auto_py.registerExit(auto_py.ESC, self.stop_bot_event_handler)

при нажатии ESC устанавливаем событие

self.bot_thread_stop_event.set()

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

while not stop_event.is_set():

Теперь спокойно останавливаем бота по кнопке ESC.

Заключение

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

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

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

P.S. Ссылка на репозиторий

Понравилась статья? Поделить с друзьями:
  • Как написать игривое сообщение мужчине
  • Как написать игрек на клавиатуре
  • Как написать игорю прокопенко письмо
  • Как написать игорю крутому письмо
  • Как написать ивент