Saved searches
Use saved searches to filter your results more quickly
Cancel Create saved search
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session. You switched accounts on another tab or window. Reload to refresh your session.
Игровой бот на основе Python и PostgreSQL: https://t.me/LotEasyBot
License
qouglou/LotEasyBotAio
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Switch branches/tags
Branches Tags
Could not load branches
Nothing to show
Could not load tags
Nothing to show
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Cancel Create
- Local
- Codespaces
HTTPS GitHub CLI
Use Git or checkout with SVN using the web URL.
Work fast with our official CLI. Learn more about the CLI.
Sign In Required
Please sign in to use Codespaces.
Launching GitHub Desktop
If nothing happens, download GitHub Desktop and try again.
Launching GitHub Desktop
If nothing happens, download GitHub Desktop and try again.
Launching Xcode
If nothing happens, download Xcode and try again.
Launching Visual Studio Code
Your codespace will open once ready.
There was a problem preparing your codespace, please try again.
Latest commit
5ee5947 Mar 21, 2023
Git stats
Files
Failed to load latest commit information.
Latest commit message
Commit time
March 20, 2023 21:43
March 20, 2023 21:45
March 20, 2023 18:26
March 21, 2023 10:35
README.md
Игровой Telegram бот на Python, позволяющий играть как в быстрые (с помощью Telegram Dice), так и в онлайн игры с другими пользователями.
Технологии и инструменты
Pydantic — передача значений токена бота и настроек подключения к БД.
Termius — SSH клиент для управления хостом.
Навигация по файлам
- __main__ — запуск проекта, настройки диспетчера, подключение роутеров, внешних миддлварей, некоторых шаблонных и кастомных фильтров.
- callback_factory — фабрика Telegram коллбеков.
- checkers — ручные и авточекеры, проверяющие возможность вывода, ставки, а также проверка уведомлений пользователей о результатах операций и игр.
- db — инициализация базы данных, а также запросы к ней.
- db_connect_create — создание коннектора БД для последующего использования во всех обращениях к ней.
- fsm — установка состояний машины состояний (конечного автомата).
- games — обработчик онлайн и оффлайн игр.
- handlers — определение хэндлеров.
- filters — определение кастомных фильтров.
- middlewares — определение внешних и внутренних миддлварей.
- templates — определение шаблонных сообщений, клавиатур, кнопок и текстов.
- configs — настройки бота, логирования, а также токена бота и данных подключения к БД.
- difs — текстовые файлы с логами, номерами реквизитов пополнения.
Функции и возможности бота
Следующие смайлы — , используемые в боте, с помощью Telegram превращаются в настоящий генератор (псевдо)случайных чисел, что позволяет использовать эту механику для игр прямо в мессенджере. Выбрав тип игры и ставку, бот отправляет соответствующее Dice-сообщение, считывавает выпавшее значение, и приводит результат.
Пользователи бота также могут посоревноваться с другими пользователями, выбрав категорию Онлайн игр. В разных играх участвтует разное количество игроков (по умолчанию в Дуэли — 2, в Русской рулетке — 6, в Королевской битве — 10). В зависимости от игры и ставки пользователя закидывает в одну из соответствующих комнат.
Выбор псевдослучайного числа происходит при помощи Python модуля random. В процессе игры производится анимация загрузки игры, а также выбора числа при помощи стикера. Ниже предоставлен пример игры в Дуэль с разных аккаунтов.
Пополнения и выводы
Данный радел несет исключительно информативный формат и представляет собой метод возможной реализации ввода и вывода условных единиц. Бот не производит никких финансовых операций!
Для пополнения внутриигрового баланса и вывода условных единиц с него, реализованы соответствующие функции пополнения и вывода. Если пользователь хочет пополнить или вывести сумму, отличающуюся от стандартных (по умолчанию 50, 100, 500, 1000 у.е.), реализована функция ввода другой суммы, которая подвергается различным проверкам (корректность числа, величина суммы, при выводе наличие введенной суммы у пользователя на балансе).
В случае, если пользователь совершает пополнение, это отображается в базе данных, и при нажатии на кнопку проверки пополнения, происходит зачисление средств и завершение операции с последующим уведомлением пользователя. Также в боте реализованы авточекеры, которые раз в заданное время (по умолчанию 30 секунд) проверяют завершенность операций. Если перевод совершен, но средства не зачислены на баланс и операция не завершена, авточекер автоматически завершает операцию и уведомляет пользователя об этом. Процесс пополнения и работы авточекера показаны ниже.
Вывод средств организован так же, только с дополнительным вводом пользователем реквизитов для вывода.
История операций и игр
Для того, чтобы пользователь мог ознакомиться со всеми операциями ввода/вывода, которые он совершил, а также сввоей историей игр, реализованы соответсвующие разделы.
Разделы имеют пагинацию, а также с помощью эмоджи визуально информативно отображают завершенность и результат. Приложенное ниже GIF-изображение показывает данные разделы.
Для управления операциями, пользователями и администраторами разработана админ панель BPManage. Доступ к админ панели имеют только занесенные в базу данных администраторы.
С помощью BPManage можно просматривать, подтверждать и изменять операции, управлять пользователями (блокировать, изменять баланс, просматривать их историю операций и игр), а также управлять администраторами, выдавая и забирая полномочия, повышая или понижая их звание.
Существует 4 уровня администраторов. С каждым новым уровнем к предыдущим возможностям добавляются новые.
- 0 (JUNIOR) — может только просматривать информацию об операциях и пользователях.
- 1 (MIDDLE) — добавляется возможность подтверждать операции, блокировать пользователей.
- 2 (MASTER) — добавляется возможность изменять способы и реквизиты операций вывода.
- 3 (SUPERUSER) — добавляется возможность изменять баланс пользователей, а также полный доступ к панели управления администраторами.
Приведенный ниже короткий пример отображает возможности управления админ панелью BPManage от лица админа 3 уровня. (GIF плохого качества из-за ограничений в 10 МБ GitHub)
Мелочь, но приятно
В боте реализованы некоторые мелочи, которые порой даже удобны в использовании.
- При пересылке любого сообщения в бот выводится сообщение об ID пользователя и его имени (если у пользователя выставлены разрешающие настройки профиля). Если пересылает администратор, а сообщение от пользователя, который уже использовал бот, то к информации добавляется кнопка перехода в админ панель управления пользователем (картинка ниже).
- При попытке сделать ставку, превышающую баланс пользователя, выведется сообщение с вариантом пополнить баланс. Если пользователь решит пополнить, выберет способ и/или сумму пополнения, а потом решит вернуться назад, переход будет совершен к выбору ставок в игре, из которой он нажал кнопку пополнения.
- В боте ведутся логи некоторых событий.
- В боте настроены некоторые шаблонные и кастомные фильтры (например работа только в приватных ЛС чатах).
- С помощью эмоджи визуально представлены некоторые разделы бота и статус всевозможных действий, что позволяет гораздо удобнее, интуитивнее и быстрее воспринимать информацию.
- Созданы разделы справки, где есть некоторые ответы на возможные вопросы пользователей, а также кнопка обратной связи с администрацией.
- Настроены авточекеры пополнения и уведомления о результатах игры (пример авточекера пополнения указан выше).
- Если отправить боту стикер, он выведет ID стикера, эмодзи, прикрепленное к стикеру, в также статус анимации.
- Если отправить боту сообщение типа Dice (анимированный стикер с миниигрой), бот отреагирует, предложив сыграть все таки в его игры.
Puthon Бот в фоновом режиме.
Вы используете устаревший браузер. Этот и другие сайты могут отображаться в нём некорректно.
Вам необходимо обновить браузер или попробовать использовать другой.
GasikPasik
Участник
Автор темы
Можно ли сделать бота на питоне, чтобы он работал полностью в свертнутом режиме? А юзер даже этого не замечал и не мешало ему делать что-то другое?
В том же ГТА са или ГТА 5. Привожу их как пример, т.к. на этом форуме это самые популярные игры.
Допустим есть бот на ГТА са, он зажимает кнопку бега, в то время как человек смотрит видео на Ютубе во весь экран?
Хорошо, я предлагаю примерно как можно сделать. Но что насчёт нажатия клавиш мыши?
Знаю, что на с++ есть способ вывести за пределы монитора окно игры или программе, но вроде бы на новой Винде не сработает.
Так вот:
Как эмулировать нажатия клавиш клавиатуры и мыши(желательно по кординатам) в свертнутом окне игры?
Надеюсь на помощь, спасибо.
Пишем бота-кликера на Python для Lineage 2
Как можно развлечься в новогодние праздники? Поиграть в компьютерные игры? Нет! Лучше написать бота, который это будет делать за тебя, а самому пойти лепить снеговика и пить глинтвейн.
Когда-то в школьные годы был увлечен одной из популярных 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)

- Фильтровать будем по прямоугольнику размером 50×5. Такой прямоугольник подошел лучше всех.
- Убираем шум внутри прямоугольников с текстом (по сути закрашиваем всё между букв белым)
- Еще раз убираем шум, размывая и растягивая с применением фильтра
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.
Ждать несколько недель с Алиэкспресса не хочется, поэтому поиски продолжились.
Библиотека у меня не завелась на питоне 3.6 — вываливалась ошибка Access violation что-то там. Поэтому пришлось соскочить на питон 2.7, там всё заработало like a charm.
Движение курсора
Библиотека может посылать любые команды, в том числе, куда переместить мышь. Но выглядит это как телепортация курсора. Нужно сделать движение курсора плавным, чтобы нас не забанили.
По сути задача сводится к тому, чтобы перемещать курсор из точки A в точку B с помощью обертки AutoHotPy. Неужели придется вспоминать математику?
Немного поразмыслив, всё-таки решил погуглить. Оказалось, что ничего придумывать не надо — задачу решает алгоритм Брезенхэма, один из старейших алгоритмов в компьютерной графике:
Прямо с Википедии можно взять и реализацию
Логика работы
Все инструменты есть, осталось самое простое — написать сценарий.
- Если монстр жив, продолжаем атаковать
- Если нет цели, найти цель и начать атаковать
- Если не удалось найти цель, немного повернемся
- Если 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 = # 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, прикоснулся к компьютерному зрению, написал свой первый слабоумный искусственный интеллект и получил массу удовольствия.
Надеюсь, было интересно и вам.
Как написать игрового бота на Python для Web
Этот туториал, и код в нем, требует установки нескольких дополнительных библиотек для Python. Они обеспечивают обертку Python’а в кусок низкоуровневого C-кода, который значительно упрощает создание и скорость исполнения.
Некоторые библиотеки существуют только под Windows. У них могут быть эквиваленты под Mac или linux, но мы не будем их рассматривать.
Вам нужно скачать и установить следующие библиотеки:
Все представленные библиотеки комплектуются установщиками. Запуск их автоматически установит модуль в директорию \lib\site-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()
Запустив этот код, вы получите скриншот экрана:

Данный код забирает всю ширину и высоту области экрана и сохраняет в 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), где
- Первая пара значение (x,y. определяет левый верхний угол рамки;
- Вторая пара . x,y) определяет правый нижний.
Это дает нам возможность скопировать только часть экрана, которая нам нужна.
Рассмотрим это на практике.
Для примера рассмотрим игру Sushi Go Round (Довольно увлекательная. Я Вас предупредил). Откройте игру в новой вкладке и сделайте скриншот использую существующий код screenGrab():

Шаг 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( 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 начальных меню.
- Кнопка «Play»

- Кнопка «continue»

- Пропустить обучение «skip»

- Кнопка «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() , вместо цикла. Мы собираемся прокликать всё меню телефона, чтобы получить координаты каждого пункта.
Здесь есть сложности, так как нам нужно иметь достаточно денег, чтобы купить что-то. Поэтому нам нужно сделать несколько порций суши, прежде чем вбивать координаты телефонных сделок. Нам придется сделать два суши рола , чтобы купить немного риса.
Есть шесть меню, через которые нам надо пройти.
- Телефон

- Начальное меню

- Начинки

- Рис

- Доставка

Нам нужно получить координаты всех, кроме Саке (можете её тоже добавить, если хотите. На мой взгляд бот отлично работает и без нее).
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 =
Ключи массива содержат имена ингредиентов, по ним мы получаем доступ к значениям.
Шаг 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
Мы будем циклом обходить пары ключ:значение в массиве с запасами ингредиентов. Если нори, риса или икры останется меньше 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 =
Здесь значение стоит на месте ключа потому, что по нему будет осуществляться поиск.
Шаг 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()
Вот и все! Обновите страницу, дождитесь загрузки игры и запустите бота!
Бот немного неуклюжий и нуждается в доработках. Но это отличный скелет для того чтобы продолжить экспериментировать!
