NOVINKA - Online rekvalifikační kurz Python programátor. Oblíbená a studenty ověřená rekvalifikace - nyní i online.
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

Jak jsem udělal herního bota, co vydělával 15 USD za den

Úvod do tématu

Abych vás uvedl do kontextu, tak musím nejdříve zmínit jaké druhy botů vlastně jsou. Nejefektivnější jsou boti, kteří se napojí přímo na funkce hry například analýzou volání funkcí programu nebo síťovou komunikací.

Ovšem vývoj těchto botů trvá dlouho a množství reverse engineeringu, co je nutná je vyčerpávající. Na druhou stranu zisk je daleko větší, protože se škáluje daleko lépe.

Další druh bota je vizuální a tedy ten co simuluje hráče. Ten zabere na vývoj podstatně kratší dobu, a nemusíte se tolik obávat o anti-cheat systém. V mém případě vývoj trval dva víkendy.

Zdrojové kódy a článek má čistě vzdělávací účel, pokud informace použijete v nelegálním způsobu neberu za to žádnou zodpovědnost.

O co ve hře Dual Universe jde?

Do nedávného updatu šlo jen o těžení ložisek a následně o prodej natěžené rudy. Těžily jsme tedy pomocí nástroje "Mining tool" hroudy skryté pod zemí v podobě rozdílné textury. Tuhle rudu jsme poté odvezli a prodali za herní měnu podle tržní hodnoty. Tato herní měna se dá dále prodat za peníze přes portály, kde nabídneme za kolik prodáme.

Příklad rudy ve hře: Zdrojákoviště Python - Objektově orientované programování

FAQ chvilka

Nyní si probereme nejčastější dotazy.

Proč s tím přicházím teď ?

Samozřejmě by byla ode mě hloupost zveřejňovat zdrojové kódy a informace, když bot vydělával. Aktuálně se stalo těžší prodávat herní suroviny za peníze a tak bot není dále profitabilní.

Jaký byl profit a jak se zvládl zaplatit ?

Bot vydělával kolem 15 USD hrubého zisku za den, ale musíte odečíst cenu virtuálního serveru, který musel běžet, aby byl bot v provozu. To udělalo zhruba 4,68 USD/den. Finální částku nechci sdělovat, ale pro zvídavé prozradím, že bot běžel zhruba 4 měsíce.

Jsou k dispozici zdrojové kódy ?

Ano, zdrojové kódy budou k dispozici na mém Githubu na konci článku. Poprosil bych, ale o trochu respektu a pokud to plánujete použít, využijte fork Githubu nebo zmiňte zdroj. Pokud se vám to líbilo, byl bych velmi vděčný za hvězdičku na repozitáři.

Počáteční myšlenka

Určitě jste už přemýšlel, jak takového bota udělat, myšlenka vám prošla nad identifikací barev a ovládáním klávesnice. Z jednoho pohledu jste blíž než se zdá, ale z druhého je tento pohled dobrý jen pro extrémně jednoduché věci. Tento nápad ke computer vision totiž lidé měli dávno předtím, už když automatizovali továrny postupně se ale dostali k tomu, že při jakékoli změně nasvícení to bohužel už nefungovalo. Avšak ve hře Metin2 byl tento jednoduchý způsob velmi úspěšný a to s chytáním ryb.

Vývoj Bota

Nyní k samotnému vytváření bota.

Plánování vývoje

Na začátek byla úvaha jasná, je potřeba klasifikovat, co se kolem bota děje a zanášet to do jeho řídící jednotky. Jedno řešení bylo udělat si vlastní klasifikátor / neuronovou síť, , co bude klasifikovat materiály a druhé řešení bylo použít open source knihovnu jako Tensorflow a netrávit čas na vlastním klasifikátoru. Tensorflow jsem tehdy ještě nepoužil a chtěl jsem vytvořit něco, čemu rozumím do podrobnosti.

Je snadné si říct, je potřeba vědět, co se děje kolem bota. Ale co vše vlastně potřebujeme vědět z obrazovky a co vše můžeme dopočítat ?

Musíme vědět:

  • Kde je ruda
  • Jsme příliš daleko pro těžbu ?

Rudu lokalizujeme podle její unikátní textury ve hře, problém ale je omezení dálky těžby. Naštěstí pro nás v tomto případě zobrazí hra v dolní části varování: Zdrojákoviště Python - Objektově orientované programování

Věci které se dají odvodit:

  • Velikost těžícího kruhu
  • Pozice hráče
  • Nářadí v ruce

Velikost těžícího kruhu je mezi 0 - 1 (0-100%) nedá se přečíst spolehlivě lehce jelikož je znázorněno kruhem na obrazovce. Nepotřebujeme, ale vědět hodnotu mezi 0 a 1, a tak zvýšíme nebo snížíme vždy na maximum. Ve hře existuje souřadnicový systém, který lze přečíst. Nářadí ve hře jako je "Mining Tool" nepotřebujeme rozlišovat, stačí použít nástroj a víme, že ho postava bude držet.

Vývoj Klasifikátoru

Naivní přístup k problému byl rozpoznávat pixely, které se vyskytují skrz texturu nejčastěji. Tato technika spočívala v "navzorkování" tisíců obrázků textur rudy. Toto prohnat matematickými funkcemi pro zjednodušení jako Gaussovo rozostření. Vstupní obrázek byl převeden do matice, a následně prolnut s výskytem pixelů každým vzorkem. Prolnutí bylo klasifikováno True/False na základě meze, co byla určena například 0.7 by znamenalo, že z 70 % musí obrázek obsahovat stejné pixely jako vzorek. Pokud toto bylo splněno bylo zvýšeno skóre. Výsledkem bylo skóre vstupního obrázku, které se porovnalo s finálním číslem, které bylo vypočítáno jako sum_samples - sum_samples * corruption_constant, kde sum_samples byl celkový počet vzorků pro rudu a corruption_constant bylo procento chybovosti.

Aby se nevyhazovali čtverce, které nevyhovují, ale jen pixely, hodnotil algoritmus po pixelech, a výstupem nebylo jen skóre, ale i binární matice.

Omlouvám se za nehezkost, ale byla to pracovní verze a nakonec se přešlo k jinému přístupu. Zdrojový kód vypadal takto:

import os

from scipy.ndimage.morphology import grey_dilation, generate_binary_structure, iterate_structure, binary_dilation
import numpy as np
from PIL import Image

dir_scanned = "ore"

def image_to_matrix(path: str):
    img = Image.open(path)
    data = np.array(img)
    return data


def intersect_colors(red, green, blue):
    if not (red.shape == green.shape == blue.shape):
        return False

    mat_intersect = np.where((red == green), red, 0)
    mat_intersect = np.where((blue == mat_intersect), blue, 0)
    return mat_intersect


def image_recognize(screen_data: np.array, sample_data: np.array, cropped=True):
    ir = lambda rgb: image_recognize_by_color(screen_data, sample_data, rgb, cropped)
    return intersect_colors(ir(0), ir(1), ir(2))


def image_crop_center(img, width, height):
    y, x = img.shape
    startx = x // 2 - width // 2
    starty = y // 2 - height // 2
    return img[starty:starty + height, startx:startx + width]


def image_recognize_by_color(screen_data: np.array, sample_data: np.array, rgb: int, small_area=True):
    """

    :param small_area: cropped at center of screen
    :param screen_data: all data in screenshot
    :param sample_data: sample that we are comparing to
    :param rgb: number of index of color r=0 g=1 b=2
    :return:
    """
    sample = np.unique(sample_data[:, :, rgb]).flatten()
    if small_area:
        cropped_screen = image_crop_center(screen_data[:, :, rgb], 500, 500)
    else:
        cropped_screen = screen_data[:, :, rgb]
    chunk_weights = np.isin(cropped_screen, sample, assume_unique=True)
    return chunk_weights


def blur(img, radius):
    orig = np.zeros_like(img, dtype='int32')
    np.copyto(orig, img, casting='unsafe')

    d = 2 * radius - 1
    avg = np.zeros_like(orig)
    for i in range(d):
        for j in range(d):
            avg += np.pad(orig[i: _omit_zero(i - d + 1), j: _omit_zero(j - d + 1)],
                          _get_pad_tuple(len(img.shape), d, i, j), 'edge')
    avg = avg // (d ** 2)

    avg = avg.clip(0, 255)
    res = np.empty_like(img)

    np.copyto(res, avg, casting='unsafe')
    return res


def unsharp_mask(img, amount, radius, threshold):
    orig = np.zeros_like(img, dtype='int32')
    np.copyto(orig, img, casting='unsafe')

    d = 2 * radius - 1

    lowpass = np.zeros_like(orig)
    for i in range(d):
        for j in range(d):
            lowpass += np.pad(orig[i: _omit_zero(i - d + 1), j: _omit_zero(j - d + 1)],
                              _get_pad_tuple(len(img.shape), d, i, j), 'edge')
    lowpass = lowpass // (d ** 2)

    highpass = orig - lowpass

    tmp = orig + (amount / 100.) * highpass

    tmp = tmp.clip(0, 255)
    res = np.zeros_like(img)
    np.copyto(res, tmp, casting='unsafe')

    res = np.where(np.abs(img^res) < threshold, img, res)
    return res


def _omit_zero(x):
    if x == 0:
        return None
    return x


def _get_pad_tuple(dim, d, i, j):
    if dim == 2:  # greyscale
        return (d - i - 1, i), (d - j - 1, j)
    else:   # color (and alpha) channels
        return (d - i - 1, i), (d - j - 1, j), (0, 0)


def image_weight(screen_data: np.array, sample_data: np.array):
    chunk_weights = image_recognize(screen_data, sample_data)
    num_of_true = np.sum(chunk_weights)
    comp_size = np.size(chunk_weights)
    return num_of_true / comp_size


def image_weight_by_color(screen_data: np.array, sample_data: np.array, rgb: int):
    """

    :param screen_data: all data in screenshot
    :param sample_data: sample that we are comparing to
    :param rgb: number of index of color r=0 g=1 b=2
    :return:
    """
    chunk_weights = image_recognize_by_color(screen_data, sample_data, rgb)
    num_of_true = np.sum(chunk_weights)
    comp_size = np.size(chunk_weights)
    return num_of_true / comp_size


def compare_to_all_samples(screen_data: np.array, cropped=True):
    samples = os.listdir("../images/"+dir_scanned+"/")
    recognized = None
    for sample in samples:
        sample_mat = image_to_matrix("../images/"+dir_scanned+"/" + sample)
        if recognized is None:
            recognized = image_recognize(screen_data, sample_mat, cropped)
        else:
            recognized += image_recognize(screen_data, sample_mat, cropped)
    return recognized


def img_frombytes(data):
    """
    For debuging only graymap
    :param data:
    :return:
    """
    size = data.shape[::-1]
    databytes = np.packbits(data, axis=1)
    return Image.frombytes(mode='1', size=size, data=databytes)


def apply_filter(mat, confidence: int):
    """

    :param confidence: count of sample that matched ( put there number higher than 2 )
    :return: Filtered matrix
    """
    return np.greater(mat, confidence)


def apply_filter_dilation(mat, iterations):
    return binary_dilation(mat, iterations=iterations)

Příklad

Vstup:

Zdrojákoviště Python - Objektově orientované programování

Výstup:

Zdrojákoviště Python - Objektově orientované programování

Filtr na dilataci byl použit k sjednocení necelých ploch pixelů označených jako True.

Možná budete překvapeni, ale tento přístup byl rychlejší než Tensorflow, avšak požadoval manuální změnu typu rudy. Processing celé obrazovky trval zhruba 0.54s, malá část obrazovky o velikosti 500x500 byla zpracována za 0.02s (CPU).

Doufám, že vás článek zaujal.


 

Stáhnout

Stažením následujícího souboru souhlasíš s licenčními podmínkami

Staženo 37x (5.63 kB)
Aplikace je včetně zdrojových kódů v jazyce Python

 

Všechny články v sekci
Zdrojákoviště Python - Objektově orientované programování
Článek pro vás napsal Jiri Otoupal
Avatar
Uživatelské hodnocení:
Ještě nikdo nehodnotil, buď první!
Autor se věnuje Zabezpečení Softwaru, Inovaci v sítích , Správa Serverů,Malware,Exploiting, Penetration Testing
Aktivity