Vianoce v ITnetwork sú tu! Dobí si teraz kredity a získaj až 80 % extra kreditov na e-learningové kurzy ZADARMO. Zisti viac.
Hľadáme nové posily do ITnetwork tímu. Pozri sa na voľné pozície a pridaj sa k najagilnejšej firme na trhu - Viac informácií.

Ako som urobil herného topánka, čo zarábal 15 USD za deň

Úvod do témy

Aby som vás uviedol do kontextu, tak musím najskôr spomenúť aké druhy topánok vlastne sú. Najefektívnejšie sú boti, ktorí sa napoja priamo na funkcie hry napríklad analýzou volaní funkcií programu alebo sieťovou komunikáciou.

Avšak vývoj týchto topánok trvá dlho a množstvo reverse engineeringu, čo je nutná je vyčerpávajúce. Na druhú stranu zisk je oveľa väčší, pretože sa škáluje oveľa lepšie.

Ďalší druh topánka je vizuálna a teda ten čo simuluje hráča. Ten zaberie na vývoj podstatne kratšiu dobu, a nemusíte sa toľko obávať o anti-cheat systém. V mojom prípade vývoj trval dva víkendy.

Zdrojové kódy a článok má čisto vzdelávací účel, pokiaľ informácie použijete v nelegálnom spôsobe neberiem za to žiadnu zodpovednosť.

O čo v hre Dual Universe ide?

Do nedávneho updatu išlo len o ťaženie ložísk a následne o predaj naťaženej rudy. Ťažili sme teda pomocou nástroja "Mining tool" hrudy skryté pod zemou v podobe rozdielnej textúry. Túto rudu sme potom odviezli a predali za hernú menu podľa trhovej hodnoty. Táto herná mena sa dá ďalej predať za peniaze cez portály, kde ponúkneme za koľko predáme.

Príklad rudy v hre: Zdrojákoviště Python - Objektovo orientované programovanie

FAQ chvíľka

Teraz si preberieme najčastejšie otázky.
Prečo s tým prichádzam teraz?
Samozrejme by bola odo mňa hlúposť zverejňovať zdrojové kódy a informácie, keď topánok zarábal. Aktuálne sa stalo ťažšie predávať herné suroviny za peniaze a tak topánok nie je ďalej profitabilný.
Aký bol profit a ako sa zvládol zaplatiť?
Topánok zarábal okolo 15 USD hrubého zisku za deň, ale musíte odpočítať cenu virtuálneho servera, ktorý musel bežať, aby bol topánok v prevádzke. To urobilo zhruba 4,68 USD/deň. Finálnu čiastku nechcem oznamovať, ale pre zvedavých prezradím, že topánok bežal zhruba 4 mesiace.
Sú k dispozícii zdrojové kódy ?
Áno, zdrojové kódy budú k dispozícii na mojom Githube na konci článku. Poprosil by som, ale o trochu rešpekte a pokiaľ to plánujete použiť, využite fork Githubu alebo spomeňte zdroj. Ak sa vám to páčilo, bol by som veľmi vďačný za hviezdičku na repozitári.

Počiatočná myšlienka

Určite ste už premýšľali, ako taká topánka urobiť, myšlienka vám prešla nad identifikáciou farieb a ovládaním klávesnice. Z jedného pohľadu ste bližšie ako sa zdá, ale z druhého je tento pohľad dobrý iba pre extrémne jednoduché veci. Tento nápad na computer vision totiž ľudia mali dávno predtým, už keď automatizovali továrne postupne sa ale dostali k tomu, že pri akejkoľvek zmene nasvietenia to bohužiaľ už nefungovalo. Avšak v hre Metin2 bol tento jednoduchý spôsob veľmi úspešný a to s chytaním rýb.

Vývoj Topánka

Teraz k samotnému vytváraniu topánka.

Plánovanie vývoja

Na začiatok bola úvaha jasná, je potrebné klasifikovať, čo sa okolo topánka deje a zanášať to do jeho riadiacej jednotky. Jedno riešenie bolo urobiť si vlastný klasifikátor / neurónovú sieť, , Čo bude klasifikovať materiály a druhé riešenie bolo použiť open source knižnicu ako Tensorflow a netráviť čas na vlastnom klasifikátore. Tensorflow som vtedy ešte nepoužil a chcel som vytvoriť niečo, čomu rozumiem do podrobnosti.

Je ľahké si povedať, je potrebné vedieť, čo sa deje okolo topánka. Ale čo všetko vlastne potrebujeme vedieť z obrazovky a čo všetko môžeme dopočítať?

Musíme vedieť:

  • Kde je ruda
  • Sme príliš ďaleko pre ťažbu?

Rudu lokalizujeme podľa jej unikátnej textúry v hre, problém ale je obmedzenie diaľky ťažby. Našťastie pre nás v tomto prípade zobrazí hra v dolnej časti varovania

Veci ktoré sa dajú odvodiť:

  • Veľkosť ťažiaceho kruhu
  • Pozícia hráča
  • Náradie v ruke

Veľkosť ťažiaceho kruhu je medzi 0 - 1 (0-100%) nedá sa prečítať spoľahlivo ľahko, pretože je znázornené kruhom na obrazovke. Nepotrebujeme, ale vedieť hodnotu medzi 0 a 1, a tak zvýšime alebo znížime vždy na maximum. V hre existuje súradnicový systém, ktorý je možné prečítať. Náradie v hre ako je "Mining Tool" nepotrebujeme rozlišovať, stačí použiť nástroj a vieme, že ho postava bude držať.

Vývoj Klasifikátora

Naivný prístup k problému bol rozpoznávať pixely, ktoré sa vyskytujú cez textúru najčastejšie. Táto technika spočívala v "navzorkovaní" tisícok obrázkov textúr rudy. Toto prehnať matematickými funkciami pre zjednodušenie ako Gaussovo rozostrenie. Vstupný obrázok bol prevedený do matice, a následne prelínaný s výskytom pixelov každou vzorkou. Prelínanie bolo klasifikované True / False na základe medze, čo bola určená napríklad 0.7 by znamenalo, že z 70 % musí obrázok obsahovať rovnaké pixely ako vzorka. Ak toto bolo splnené bolo zvýšené skóre. Výsledkom bolo skóre vstupného obrázku, ktoré sa porovnalo s finálnym číslom, ktoré bolo vypočítané ako sum_samples - sum_samples * corruption_constant, kde sum_samples bol celkový počet vzoriek pre rudu a corruption_constant bolo percento chybovosti.

Aby sa nevyhadzovali štvorce, ktoré nevyhovujú, ale len pixely, hodnotil algoritmus po pixeloch, a výstupom nebolo len skóre, ale aj binárne matice.

Ospravedlňujem sa za nepeknosť, ale bola to pracovná verzia a nakoniec sa prešlo k inému prístupu. Zdrojový kód vyzeral 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: číslo indexu farieb 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: číslo indexu farieb 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)

Príklad

Vstup:
Zdrojákoviště Python - Objektovo orientované programovanie

Výstup:

Zdrojákoviště Python - Objektovo orientované programovanie

Filter na dilatáciu bol použitý na zjednotenie necelých plôch pixelov označených ako True.

Možno budete prekvapení, ale tento prístup bol rýchlejší ako Tensorflow, ale požadoval manuálnu zmenu typu rudy. Processing celej obrazovky trval zhruba 0.54s, malá časť obrazovky o veľkosti 500x500 bola spracovaná za 0.02s (CPU).

Dúfam, že vás článok zaujal.


 

Stiahnuť

Stiahnutím nasledujúceho súboru súhlasíš s licenčnými podmienkami

Stiahnuté 34x (5.63 kB)
Aplikácia je vrátane zdrojových kódov v jazyku Python

 

Všetky články v sekcii
Zdrojákoviště Python - Objektovo orientované programovanie
Článok pre vás napísal Jiri Otoupal
Avatar
Užívateľské hodnotenie:
Ešte nikto nehodnotil, buď prvý!
Autor se věnuje Zabezpečení Softwaru, Inovaci v sítích , Správa Serverů,Malware,Exploiting, Penetration Testing
Aktivity