[Из песочницы] Python, под пиратским флагом

image Йо-хо-хо, хабровчане!

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

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


Сразу стоит сделать некоторые ремарки:

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


Соль задачи


Есть игра GuildWars2, в ней запущен рождественский эвент (мини игра), которая очень похожа на guitar hero только с колокольчиком.

image

Смысл игры заключается в своевременном нажатии кнопок-нот 1–2–3–4 и 6–7–8–9 в зависимости от того, какой кружок по какой дорожке приедет к центру, где стоит персонаж. Если очень интересно, то можно посмотреть видео на ютубе, набрав guild wars 2 choir bell.

За полное прохождение, пусть и с погрешностями, дают максимальное количество ценных подарочков, которые можно продать на рынок по цене примерно 5.5 серебра за штуку. Я подсчитал, что за сутки неприрывного прохождения этого эвента, можно делать ~3300 подарочков, а это больше 180 чистого золота, цена которому 2 рубля за ед. на черном рынке. Копейки в абсолютных величинах, но очень неплохо в сравнении с тем же криптомайнингом, а? Особенно если учесть, что для этого нам не требуется дорогостоящая видеокарта или платный аккаунт.

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

Шаг 0. Анализируем


Для автоматизации нам нужно всего 2 вещи: распознавать пиксели и нажимать кнопки.
Тут требуется сделать небольшое отступление и сказать, что я совсем не будучи программистом, пробовал быдлокодить на с++, с#, PHP, delphi и даже ассемблере в среде masm32. Выбор питона на этой стадии был почти случайным. Я просто подумал «А почему бы не попробовать до кучи на питоне? Вдруг будет удобнее?». Это не был какой то осознанный выбор, я тогда еще и не предполагал, насколько питон классный.

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

Код пипетки спрятал под спойлер
from graphics import *# Здесь и далее импортируемый модуль графических примитивов(скачивается отдельно)
import pyautogui #Здесь и далее импортируемый модуль автоматизации ( скачивается отдельно )
import time # время же. (Стандартный модуль)

def main():# определяем мэйн функцию
    win = GraphWin("pipetka", 200, 200, autoflush=True)#создаем графическую форму размером 200х200 и элементы на ней
    x, y = pyautogui.position()#получаем в x, y координаты мыши
    r, g, b = pyautogui.pixel(x, y)# получаем в r, g, b цвет

    ColorDot = Circle(Point(100, 100), 25)# создаем точку, отображающую цвет
    ColorDot.setFill(color_rgb(r, g, b))# устанавливает ей заливку из ранее полученных цветов
    ColorDot.draw(win)# рисуем на форме win

    RGBtext = Entry(Point(win.getWidth()/2, 25), 10)# создаем RGB вывод
    RGBtext.draw(win)# рисуем на форме win

    RGBstring = Entry(Point(win.getWidth()/2, 45), 10)#создаем вывод цвета в web стиле
    RGBstring.draw(win)# рисуем на форме win

    Coordstring = Entry(Point(win.getWidth() / 2, 185), 10)# создаем отображение координат
    Coordstring.draw(win)# рисуем на форме win


    while True: # цикл перереисовки формы
        time.sleep(0.1)# задержка в 0.1 с, чтобы питон не сходил с ума
        x, y = pyautogui.position()#получаем в x, y координаты мыши
        r, g, b = pyautogui.pixel(x, y)# получаем в r, g, b цвет
        ColorDot.setFill(color_rgb(r, g, b))#Обновляем цвет
        RGBtext.setText(pyautogui.pixel(x, y))#Обновляем RGB
        RGBstring.setText(color_rgb(r, g, b))#Обновляем web цвет
        Coordstring.setText(str(x)+" "+ str(y) )#Обновляем координаты
        win.flush()# Даем команду на перерисовку формы
#основной код начинается ниже.

main()#вызываем нашу функцию.


image

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

1) мало времени на определение цвета и посыл нажатия

2) цвета кружков очень неоднородные

3) незначительные отклонения в позиционировании камеры сильно мешают

Однако поковырявшись еще чуть чуть, мне в голову пришла идея захватывать не пиксели нужного цвета, а изменения яркости в нужных местах т.к. эксперементы с пипеткой показали, что кружки гораздо выше по шкале R, G или B (в зависимости от цвета) нежели фон игрового поля. В итоге я выбрал 8 точек где кружки проходят в наибольшем размере.

image

Шаг 1. Кодим распознавание кружка


Код анализатора
import time# модуль времени ( стандартный )
import pyautogui# автоматизатор ( скачивается отдельно )
import winsound#модуль для сигнала, чтобы знать, что программа запустилась ( стандартный )
import keyboard#модуль для работы с клавиатурой ( скачивается отдельно )



def analyzer():

    etalon = [pyautogui.pixel(355, 288), pyautogui.pixel(460, 200), pyautogui.pixel(600, 130),\
        pyautogui.pixel(735, 112), pyautogui.pixel(875, 109), pyautogui.pixel(1000, 145), \
         pyautogui.pixel(1139, 203), pyautogui.pixel(1260, 290) ]#массив эталонных цветов

    trigger = [0,0,0,0,0,0,0,0]#массив триггеров, инициализируем нулями



    while True:

        change = [pyautogui.pixel(355, 288), pyautogui.pixel(460, 200), pyautogui.pixel(600, 130),\
        pyautogui.pixel(735, 112), pyautogui.pixel(875, 109), pyautogui.pixel(1000, 145), \
         pyautogui.pixel(1139, 203), pyautogui.pixel(1260, 290) ] #Забираем в цикле change массив текущего состояния точек

        for nomer in range(0,8):
            if change[nomer][0] > etalon[nomer][0]+50 or change[nomer][1] > etalon[nomer][1]+50 or change[nomer][2] > etalon[nomer][2]+50:

                if trigger[nomer] == 0: #если точка изменила цвет по R,G или B больше чем на +50 и триггер прохождения выключен
                    trigger[nomer] = 1#включаем триггер прохождения
            else:
                if trigger[nomer] == 1: #проверяем не включен ли триггер прохождения
                    trigger[nomer] = 0 #обнуляем триггер прохождения
                    print("push " +str(nomer) + time.strftime(' %X'))

#основной код начинается ниже.

keyboard.wait(combination="home")#после старта ждем нажатия клавиши "Home"
winsound.Beep(1000, 100) #сигналим что программа стартанула
analyzer()# запускаем анализ


Разбор алгоритма:

  1. Кладем в массив etalon цвета фона игрового поля, с которым впоследствии будет происходить сравнение цветов массива change Строчка
      
    keyboard.wait(combination="home")

    нужна как раз, чтобы забрать эталонные цвета в момент, когда игра уже развернута и камера отцентрирована
  2. Инициализируем массив триггеров trigger. Он нам нужен, поскольку питон отрабатывает много раз, пока кругляш бежит к центру. Кругляш большой, а точка в которой мы снимаем показания маленькая. То есть, для того, чтобы один и тот же кругляш мы не зарегистрировали дважды и более.
  3. Запускаем бесконечный цикл в котором мы постоянно забираем значение цветов в массив change и сравниваем цвета каждой из забранных точек с эталонными цветами
    for nomer in range(0,8):
                if change[nomer][0] > etalon[nomer][0]+50 or change[nomer][1] > etalon[nomer][1]+50 or change[nomer][2] > etalon[nomer][2]+50:
  4. Если в предыдущем IF’e мы выяснили что цвет точки с номером nomer ярче хотя бы на 50 единиц по любому из значений RGB, то проверяем зажжен ли триггер для этой точки и если нет — зажигаем
        if trigger[nomer] == 0: 
                        trigger[nomer] = 1
  5. Если же цвет в постоянно обновляемом change соответствует эталонному цвету, то тут два варианта. Либо все тихо и кружка еще нет, либо кружок только что съехал дальше с проверяемой точки, что и является сигналом для нажатия кнопки. Определяемся мы в итоге по состоянию триггера. Если триггер был зажжен — значит кругляш уехал. Обнуляем триггер и пока условно нажимаем кнопку.
                    if trigger[nomer] == 1: 
                        trigger[nomer] = 0 
                        print("push " +str(nomer) + time.strftime(' %X'))


Шаг 2. Запускаем и тестим


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

image

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

Шаг 3. Кодим нажималку кнопок через потоки


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

Добавляем в код

from threading import Timer


и сам обработчик отложенного нажатия

def delaypress(keynum):
    if keynum < 4:
        keynum +=1
    else:
        keynum +=2
    t = Timer(2, keyboard.send, args=[str(keynum)])
    t.start()


Если на вход поступает номер точки 0–1–2–3 нажимаем через 2 секунды номер точки + 1, если же поступает 4–5–6–7, то нажимаем через 2 секунды номер + 2 (т.к. кнопка 5 не задействована в мини игре)

Конечный вид кода:
import time# модуль времени ( стандартный )
import pyautogui# автоматизатор ( скачивается отдельно )
import winsound#модуль для сигнала, чтобы значть, что программа запустилась ( стандартный )
import keyboard#модуль для работы с клавиатурой ( скачивается отдельно )
from threading import Timer#импортим таймер из модуля потоков ( стандартный )

def delaypress(keynum):
    if keynum < 4:
        keynum +=1
    else:
        keynum +=2
    t = Timer(2, keyboard.send, args=[str(keynum)])
    t.start()

def analyzer():

    etalon = [pyautogui.pixel(355, 288), pyautogui.pixel(460, 200), pyautogui.pixel(600, 130),\
        pyautogui.pixel(735, 112), pyautogui.pixel(875, 109), pyautogui.pixel(1000, 145), \
         pyautogui.pixel(1139, 203), pyautogui.pixel(1260, 290) ]#массив эталонных цветов

    trigger = [0,0,0,0,0,0,0,0]#массив триггеров, инициализируем нулями



    while True:

        change = [pyautogui.pixel(355, 288), pyautogui.pixel(460, 200), pyautogui.pixel(600, 130),\
        pyautogui.pixel(735, 112), pyautogui.pixel(875, 109), pyautogui.pixel(1000, 145), \
         pyautogui.pixel(1139, 203), pyautogui.pixel(1260, 290) ] #Забираем в цикле change массив текущего состояния точек

        for nomer in range(0,8):
            if change[nomer][0] > etalon[nomer][0]+50 or change[nomer][1] > etalon[nomer][1]+50 or change[nomer][2] > etalon[nomer][2]+50:

                if trigger[nomer] == 0: #если точка изменила цвет по R,G или B больше чем на +50 и триггер прохождения выключен
                    trigger[nomer] = 1#включаем триггер прохождения
            else:
                if trigger[nomer] == 1: #проверяем не включен ли триггер прохождения
                    trigger[nomer] = 0 #обнуляем триггер прохождения
                    #print("push " +str(nomer) + time.strftime(' %X'))#заменяем печать на вызов нажималки
                    delaypress(nomer)


#основной код начинается ниже.

keyboard.wait(combination="home")#после старта ждем нажатия клавиши "Home"
winsound.Beep(1000, 100) #сигналим что программа стартанула
analyzer()# запускаем анализ


Шаг 4. Гребем профит


image


Заключение и ссылки


А теперь, как и обещано, расхваливаю питон (надеюсь достаточно обоснованно).

  • Питон отлично подходит людям с базовыми знаниями в области программирования и гуглинга для решения интересных задач.
  • Реально юзер френдли язык. Лично по моим ощущениям синтаксиса меньше раза в три и нет идиотского количества ошибок, как в других языках, даже при написании небольших программ.
  • Отличная экосистема. Если вы знаете, как сформулировать вопрос гуглу, вы с большой долей вероятности найдете или готовое решение или инструмент, позволяющей сделать, то, что вы хотите.
  • Нет танцев с бесконечными перекомпиляциями. На современных ПК небольшие программки конечно компилятся быстро, но все равно это раздражает если вы учитесь и приходиться пересобирать проект десятки раз.
  • Документация. По простоте, понятности и лаконичности ближайший аналог видел только у PHP.
  • В питоне прослеживаются черты линукс философии. Даже установка новых модулей легка и приятна и не вызовет у вас трудностей если вы освоили какой нибудь apt или yum
  • Питон удобен для создания всяких хаков и автоматизаций в играх, особенно тех, где требуется частая модификация. Если статья хорошо зайдет — обязательно расскажу как просто на питоне писать в память чужого процесса, делать zoom/speed хак и управлять всем этим безобразием с клавиатуры.


Pyautogui документация
Graphics документация
Документация по модулю keyboard

Если есть интересные идеи для хаков на питоне — пишите в личку.

© Habrahabr.ru