Не тапай хомяка! Как я автоматизировал игру Hamster Kombat

Комментов · 106 Просмотры

В этой статье я расскажу о том, как я изучал возможность автоматизации стремительно набирающей популярность игры Hamster Kombat

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

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

WARNING

Ре­дак­ция не рекомен­дует рас­смат­ривать Hamster Kombat как потен­циаль­ный источник заработ­ка: почему — есть под­робная статья на «Хаб­рахаб­ре». Мы будем под­разуме­вать, что име­ем дело с обыч­ной игрой, и исполь­зуем ее как популяр­ный при­мер, на котором мож­но про­демонс­три­ровать тех­ники авто­мати­зации.

ЗАДАЧА

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

В про­цес­се игры у меня быс­тро появи­лось желание авто­мати­зиро­вать две вещи: нажатие на кноп­ку и выбор опти­маль­ных кар­точек для покуп­ки. У каж­дой кар­точки две основные харак­терис­тики: раз­мер при­бав­ки к доходу и цена. Одни кар­точки дают неболь­шую при­бав­ку и дорого сто­ят, дру­гие, наобо­рот, очень выгод­ны. Для прос­тоты ана­лиза я ввел понятие удель­ного дохода, ну или его еще мож­но наз­вать «цена при­бав­ки». Допус­тим, если кар­точка сто­ит мил­лион монет и дает при­бав­ку к доходу 4000 монет, зна­чит, цена при­бав­ки будет 1 000 000 / 4000 = 250 монет. Чем мень­ше цена при­бав­ки, тем целесо­образнее покуп­ка монеты.

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

С ходу в голову при­шел оче­вид­ный спо­соб вза­имо­дей­ствия с игрой — ана­лиз изоб­ражения на экра­не и эму­ляция нажатий на экран с исполь­зовани­ем Android Debug Bridge (ADB). Конеч­но, в иде­але бы все сде­лать на зап­росах, вооб­ще без телефо­на, но для это­го надо ана­лизи­ровать тра­фик, а с уче­том того, что игра запус­кает­ся толь­ко на телефо­не, внут­ри Telegram, ана­лиз тра­фика мне показал­ся весь­ма тру­доем­кой задачей, поэто­му я решил начать с более оче­вид­ной реали­зации.

АВТОМАТИЗАЦИЯ ЧЕРЕЗ ADB

Тапаем на экран

На­деюсь, что уста­нав­ливать ADB и вза­имо­дей­ство­вать через него с телефо­ном все уме­ют, поэто­му не буду лить воду про это, перей­дем сра­зу к делу. Попыт­ки запус­тить игру в Windows закон­чились неуда­чей, пос­коль­ку игра каким‑то обра­зом опре­деля­ет, что запуще­на не на телефо­не, и выводит сооб­щение об этом. Что ж, телефон — зна­чит, телефон. Воору­жаем­ся телефо­ном с Android, рас­чехля­ем ADB. Воп­рос с тапань­ем по кноп­ке лег­ко реша­ется одной коман­дой:

e:\Android\sdk\platform-tools\adb.exe  shell input tap 540 1800

540 и 1800 — это коор­динаты точ­ки на экра­не, в которую мы хотим «ткнуть паль­цем». Что­бы не воз­вра­щать­ся к это­му вто­рой раз, сра­зу хочу ска­зать про так называ­емую мор­зянку. В игре каж­дый день есть воз­можность получить допол­нитель­ный мил­лион монет, вве­дя с помощью азбу­ки Мор­зе опре­делен­ный код. Точ­ка кодиру­ется корот­ким нажати­ем, тире — длин­ным. Делать корот­кое нажатие мы уже уме­ем, длин­ное же дела­ется вот такой коман­дой:

e:\Android\sdk\platform-tools\adb.exe  shell input swipe 500 1500 500 1500 555

Пер­вые четыре чис­ла — коор­динаты начала и кон­ца дви­жения, а 555 — вре­мя дви­жения. В нашем слу­чае начало и конец сов­пада­ют.

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

Фун­кция, которая эму­лиру­ет ввод точ­ки:

def tochka():    cmd = "adb.exe shell input tap 500 1500"    process = subprocess.Popen(cmd, stdout=subprocess.PIPE, creationflags=0x08000000)    process.wait()

Фун­кция, эму­лиру­ющая ввод тире:

def tire():    cmd = "adb.exe shell input swipe 500 1500 500 1500 500"    process = subprocess.Popen(cmd, stdout=subprocess.PIPE, creationflags=0x08000000)    process.wait()

Фун­кция, которая раз­бира­ет пос­ледова­тель­ность точек и тире и отправ­ляет ее в телефон (про­бел озна­чает паузу в три секун­ды, необ­ходимую для отде­ления букв друг от дру­га):

def mz2tap(morze):    for simvol in morze:        match simvol:            case ".":                tochka()            case "-":                tire()            case " ":                time.sleep(3)

Пе­рево­да тек­ста в азбу­ку Мор­зе касать­ся не буду, не ради это­го мы тут соб­рались.

При­мер исполь­зования:

mz2tap(".--")mz2tap(".")mz2tap("-...")mz2tap("...--")

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

Распознаём карточки

С кар­точка­ми приш­лось повозить­ся. К задаче я решил подой­ти в лоб — про­лис­тывать кар­точки и делать скрин­шоты, затем обре­зать лиш­нее и рас­позна­вать то, что оста­лось.

Для начала я наделал кучу скрин­шотов с помощью скрип­та на Python. Этот код в цик­ле дела­ет скрин­шот и ска­чива­ет его в каталог cards_dir на компь­юте­ре:

while True:    cmd = "adb.exe shell screencap -p /sdcard/screencap.png"    process = subprocess.Popen(cmd, stdout=subprocess.PIPE, creationflags=0x08000000)    process.wait()    cmd = f"adb.exe  pull /sdcard/screencap.png {os.path.join(cards_dir, datetime.datetime.now().strftime("%Y-%m-%d_%H_%M_%S_%f"))}.png"    process = subprocess.Popen(cmd, stdout=subprocess.PIPE, creationflags=0x08000000)    process.wait()    print(cmd)

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

hash_list = []list_of_files = os.walk(cards_dir)dubl_count = 0for root, folders, files in list_of_files:    for file in files:        file_path = os.path.join(root, file)        Hash_file = hashlib.md5(open(file_path, 'rb').read()).hexdigest()        if Hash_file not in hash_list:            hash_list.append(Hash_file)        else:            dubl_count += 1            os.remove(file_path)            print(file_path)if dubl_count > 0:    print(f"Удалено {dubl_count} дубликатов")else:    print("Нет дубликатов")

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

string = pytesseract.image_to_string(image, lang='eng')

Од­нако проб­ный запуск показал, что в рас­познан­ный текст попада­ет мно­го мусора. Явно нуж­но обре­зать кар­тинку так, что­бы на ней был толь­ко текст. При­водить весь скрипт для обрезки не буду, что­бы не заг­ромож­дать статью рутиной, ска­жу лишь, что исполь­зовал модуль PIL, из которо­го мне в основном понадо­бились фун­кции Image.crop и Image.getpixel.

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

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

АВТОМАТИЗАЦИЯ ЧЕРЕЗ ЗАПРОСЫ

Анализируем трафик

Де­вать­ся некуда, поп­робу­ем поз­накомить­ся с игрой более деталь­но. Пер­вым делом необ­ходимо понять, что меша­ет хомяку запус­кать­ся на компь­юте­ре. Давай откро­ем веб‑вер­сию телег­рама в бра­узе­ре, зай­дем в бота и поп­робу­ем сно­ва запус­тить игру. Хомяк про­сит­ся к нам на телефон.

Стартовое сообщение

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

Консоль разработчика

Взгля­нем под­робнее на параметр src у тега iframe и уви­дим там длин­нющую стро­ку парамет­ров. Если прис­мотреть­ся, то мож­но заметить параметр tgWebAppPlatform, содер­жащий зна­чение web. Поп­робу­ем заменить его android и открыть получен­ный URL в новом окне.

Получилось!

Ура, игра нор­маль­но запус­кает­ся в бра­узе­ре. Теперь нич­то не меша­ет поэк­спе­римен­тировать с зап­росами. Прин­цип тут такой: откры­ваем вклад­ку Network, очи­щаем ее от ста­рых зап­росов, выпол­няем какое‑то дей­ствие в игре и смот­рим, какие зап­росы при этом отправ­ляют­ся на сер­вер. Нап­ример, поп­робу­ем купить какую‑нибудь кар­точку.

Запрос на покупку

Нес­ложно заметить, что при этом отправ­ляет­ся POST-зап­рос на сле­дующий адрес:

https://api.hamsterkombatgame.io/clicker/buy-upgrade

Заг­лянув на вклад­ку Payload, мож­но уви­деть, что в зап­росе переда­ются два парамет­ра:

{upgradeId: "hamster_youtube_gold_button", timestamp: 1722086163455}

Па­раметр upgradeId очень напоми­нает наз­вание кар­точки, которую мы купили, ну а параметр timestamp, судя по его наз­ванию и содер­жимому, очень похож на вре­мя в юник­совом фор­мате. Любопытс­тва ради заг­лянем в ответ на зап­рос (вклад­ка Response), а там прос­то счастье — куча раз­ных парамет­ров игры, сре­ди которых есть пол­ные харак­терис­тики всех кар­точек:

{    "clickerUser": {        "id": "ХХХХХХХХХХХХ",        "totalCoins": 5389463.6559000015,        "balanceCoins": 4603871.6559000015,        "level": 6,        "availableTaps": 3500,        "lastSyncUpdate": 1722086163,        "exchangeId": "mexc",    ...    "upgradesForBuy": [        {            "id": "ceo",            "name": "CEO",            "price": 725363,            "profitPerHour": 2789,            "condition": null,            "section": "PR&Team",            "level": 16,            "currentProfitPerHour": 2513,            "profitPerHourDelta": 276,            "isAvailable": true,            "isExpired": false        },        {            "id": "marketing",            "name": "Marketing",            "price": 8557,            "profitPerHour": 838,            "condition": null,            "section": "PR&Team",            "level": 9,            "currentProfitPerHour": 718,            "profitPerHourDelta": 120,            "isAvailable": true,            "isExpired": false        },        ...    }    ...}

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

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

https://api.hamsterkombatgame.io/clicker/tap

И переда­ет эти парамет­ры:

    data = {        "count": 500, # Число нажатий        "availableTaps": 0,        "timestamp": timestamp()    }

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

Пишем скрипт

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

import requestsfrom time import timedef tap():    url = "https://api.hamsterkombatgame.io/clicker/tap"    data = {        "count": 500,        "availableTaps": 0,        "timestamp": int(time())    }    response = requests.post(url, json=data)    if response.status_code == 200:        try:            result = response.json()            return result        except ValueError as e:            print(f"{e}")            return None    print(f"Error: {response.status_code, response.text}")    return Noneprint(tap())

Пос­ле запус­ка получа­ем в ответ ошиб­ку 422. При­чин воз­никно­вения такой ошиб­ки может быть весь­ма мно­го. Сер­вер может ее вер­нуть, если в целом зап­рос вер­ный, но ему что‑то не нра­вит­ся. Вни­матель­но смот­рим на скрипт и понима­ем: в нем ниг­де не ска­зано, кто мы. Мы ука­зали, что мы тап­нули 500 раз, но как игра пой­мет, какой поль­зователь это сде­лал?

Ес­ли пов­ниматель­нее изу­чить ори­гиналь­ный зап­рос на tap через DevTools бра­узе­ра, то мож­но заметить, что в заголов­ках зап­роса есть поле Authorization, содер­жащее вот такое зна­чение:

Bearer 32432432432432432446t...cfC1y523432432432419

Поп­робу­ем изме­нить скрипт, вмес­то стро­ки с зап­росом response requests.post(...) напишем сле­дующий код:

    key = '32432432432432432446t...cfC1y523432432432419'    headers: dict = {        'User-Agent': 'Mozilla/5.0 (Linux; Android 14; Nokia 3310; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/78.0.3904.108 Mobile Safari/537.36',        'Content-Type': 'application/json',        "Authorization": f"Bearer {key}",    }    response = requests.post(url, json=data, headers=headers)

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

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

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

https://api.hamsterkombatgame.io/clicker/upgrades-for-buy

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

def get_cards():    key = '32432432432432432446t...cfC1y523432432432419'    headers: dict = {        'User-Agent': 'Mozilla/5.0 (Linux; Android 14; Nokia 3310; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/78.0.3904.108 Mobile Safari/537.36',        'Content-Type': 'application/json',        "Authorization": f"Bearer {key}",    }    url = "https://api.hamsterkombatgame.io/clicker/upgrades-for-buy"    response = requests.post(url, headers=headers)    if response.status_code == 200:        try:            data = response.json()            cards = []            for task in data["upgradesForBuy"]:                if task["isAvailable"] == True and task["isExpired"] == False and task["profitPerHourDelta"] > 0:                    section = task["section"]                    name = task["name"]                    price = task["price"]                    profitPerHour = task["profitPerHourDelta"]                    udel_cena = price / profitPerHour                    cards.append(                        [section, name, price, int(udel_cena)])            sorted_cards = sorted(cards, key=lambda x: x[3])            for card in sorted_cards:                print(f"{card[3]}:\t{card[2]}:\t{card[1]}:\t{card[0]}")        except ValueError as e:            print(f"{e}")

В резуль­тате получа­ем спи­сок кар­точек, отсорти­рован­ный по умень­шению «выгод­ности»:

7:  551:    HamsterBook:    PR&Team7:  606:    X:  PR&Team7:  2000:   Risk management team:   PR&Team7:  7757:   Special Hamster Conference: Specials7:  2327:   Hamster YouTube Channel:    Specials7:  11000:  Web3 academy launch:    Specials8:  2205:   IT team:    PR&Team...200:    1000000:    TON + Hamster Kombat = Success: Specials1000:   5000000:    Call for BTC to rise:   Specials1000:   1000000:    HamsterWatch for soulmate:  Specials2628:   725363: CEO:    PR&Team4000:   12000000:   Business jet:   Specials

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

ВЫВОДЫ

Итак, мы научи­лись:

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

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

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

Что каса­ется опти­маль­ных стра­тегий в Hamster Kombat, мож­но подумать, что выгод­нее: покупать любые дос­тупные кар­точки, лишь бы уве­личи­вать доход­ность, или подож­дать какое‑то вре­мя и под­копить на наибо­лее выгод­ные кар­точки, или пла­ниро­вать покуп­ку нес­коль­ких кар­точек в опре­делен­ной пос­ледова­тель­нос­ти в зависи­мос­ти от их харак­терис­тик.

В общем, теперь вы знаете как работает у нас раздел Free накрутка

Комментов