Статьи

Как создать бота Python, который может играть в веб-игры

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


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


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

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

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

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

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

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

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

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

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

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

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

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

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

Веселиться!


В новой папке щелкните правой кнопкой мыши и выберите « 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

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

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

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

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

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

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

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

Python_Snapshot_of_sushi_game_full_screen

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


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

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

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


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

Итак, мы собираемся создать две новые переменные: 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)

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


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

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

В качестве примера, у меня обычно есть следующие комментарии в верхней части моего кода 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-координату.


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

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

В вашей папке щелкните правой кнопкой мыши 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 соответствует своему названию. Дважды щелкните файл, и он тихо выполнит свой код в фоновом режиме и сохранит снимок в рабочий каталог.

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


Работа с 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 правильных аргументов в нужное место. Давайте теперь перейдем к более полезным функциям


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

Откройте 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’

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

Добавьте следующие функции в 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() .


В этом и следующих нескольких шагах мы попытаемся собрать как можно больше координат события, используя наш 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 секунды. Совсем неплохо!


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

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_ ‘ позволяет нам знать, что мы ссылаемся на расположение продуктов, а не, скажем, на местоположение в телефоне меню.

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


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

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
«»»

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


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

У этого есть намного больше, чтобы отследить, поэтому вы можете сделать это вручную, вызывая 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)

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


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

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


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

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() . Мат катается довольно долго, и во время анимации нельзя щелкать продукты, поэтому вам просто нужно подождать.


На этом шаге мы установим все 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, когда мы запускаем первый уровень, бот выкладывает достаточно точные данные о том, что происходит на экране, и все, что нам нужно сделать с этого момента, это взять эти данные, которые он «видит» и просто скажи, как на это реагировать.

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

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

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


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

1
2
import ImageOps
from numpy import *

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

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

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


Первый метод, который мы рассмотрим, — это проверка определенного значения 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()
 
   ##im.save(os.getcwd() + '\\Snap__' + str(int(time.time())) +'.png', 'PNG')
   return im

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

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

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

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

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


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

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

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

play_buttonpng

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

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

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


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

Давайте начнем с расширения 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)

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


Теперь, когда у нас есть наши 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()тип ингредиента в качестве параметра.

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

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


Чтобы продолжить работу с нашим ботом, нам нужно собрать информацию о том, какой тип суши находится в пузыре клиента. Делать это с помощью этого 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)
 
    ##im.save(os.getcwd() + '\\Snap__' + str(int(time.time())) + '.png', 'PNG')
    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: мы суммируем все значения массива и выводим их на экран. Эти числа мы будем использовать при сравнении двух изображений.


Начните новую игру и подождите, пока все клиенты заполнятся. Дважды щелкните, 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 и выводим сумму на экран.

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


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

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

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


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

Запустите новую игру и быстро бегите, 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

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


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

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

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

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() называется каждые два места.

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


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

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

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

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

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


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

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

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