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:
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:Výstup:
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