AI přepracovala 300 tisíc řádků Street Fighteru III. Hlídal ji hash každého snímku

AI agenti za tři týdny přepracovali dekompilovaný kód Street Fighteru III: 3rd Strike a podle metriky CodeScene dosáhli nejvyššího možného skóre kvality. Shoda s původní verzí se ověřovala přehráváním herních záznamů a porovnáním hashe herního stavu po každém snímku. Taková kontrola je přísnější než běžné testy, zachytí ale jen to, co do hashe vstupuje.
Street Fighter III: 3rd Strike vyšel v herních automatech v roce 1999 a fanoušci bojových her ho hrají dodnes. Komunita nadšenců mezitím dekompilovala jeho verzi pro PlayStation 2 do zdrojového kódu v jazyce C. Švédská firma CodeScene na něm předvedla, co dnes zvládnou AI agenti. Ti za tři týdny vytvořili v dekompilovaném kódu 2 903 commitů, změnili 252 055 řádků v 726 souborech a průměrné skóre kvality Code Health zvýšili z 5,6 na 10. Případovou studii zveřejnil 24. září zakladatel firmy Adam Tornhill.
Objem změn ale není to nejzajímavější. Tolik úprav nelze projít řádek po řádku, a kontrolu proto převzala sama hra: po každé změně se přehrály předem nahrané záznamy a hash herního stavu musel v každém snímku souhlasit s verzí před změnou. Na tom, co taková kontrola zachytí a co jí unikne, závisí, zda lze podobný refaktoring zopakovat i u softwaru, který není hrou.
Dekompilovaný kód se známkou 5,6
Pokus vycházel z otevřeného kódu dvou navazujících projektů. Základ položil 3s-decomp, takzvaná matching dekompilace verze pro PlayStation 2. Její autoři psali kód v jazyce C tak, aby po kompilaci vznikl binární soubor shodný s originálem. Kód proto věrně zachovává strukturu hry z přelomu tisíciletí, včetně opakovaných smyček a téměř shodných větví, na které později narazili agenti. Navazující projekt 3SX z něj vytváří nativní port pro Windows, macOS a Linux pod licencí AGPL-3.0 a přidává hru po síti. Herní data si hráč musí dodat z vlastní kopie hry pro PlayStation 2.
Celý postup navrhli a provedli Daniel Webb, technický ředitel firmy NeoSee, a Markus Borg z CodeScene. Nejdřív zvažovali systém pro námořní licence z britského vládního portálu Gov.UK, na kterém Webb dříve pracoval. Kód tohoto systému však byl pro potřeby navazujícího výzkumu příliš kvalitní. Street Fighter zvolili i proto, že ho sami hrají, a jsou tedy zároveň jeho uživateli.
Kvalitu měřila metrika Code Health, kterou CodeScene skládá z více než 25 faktorů od složitosti funkcí přes zanoření podmínek po duplicitu. Pro celý projekt počítá její průměr vážený počtem řádků. Agenti k ní měli přístup přes CodeHealth MCP Server, takže po každé úpravě získali deterministické skóre, podle kterého poznali, zda se kvalita kódu zlepšila.
Autoři se pokusu věnovali tři týdny, ale ne na plný úvazek. Tokeny stály asi 4 000 dolarů. CodeScene tuto částku přirovnává k polovině měsíčního platu vývojáře a odhaduje, že bez AI by na podobném projektu zkušení vývojáři strávili 12 až 18 měsíců. Změny se do hlavní větve jejich forku dostaly prostřednictvím 54 pull requestů.
Refaktoring vznikl jako příprava na studii s Lundskou univerzitou, pro kterou jsou potřeba dvě funkčně shodné verze hry, jedna se skóre 5,6 a druhá s 10. Proto musel přesně zachovat chování hry. Studenti budou pomocí současných modelů přidávat do obou verzí nové funkce a porovnají náklady a kvalitu práce. CodeScene si od kvalitnějšího kódu slibuje zhruba o 70 % méně chyb způsobených AI a asi o 45 % menší plýtvání tokeny. Obě čísla ale odvozuje ze svých starších výzkumů a teprve studie ukáže, zda platí i v tomto případě.
Postupy, které si agenti sepsali sami
Agenti nedostali hotový seznam úprav, které by jen mechanicky použili. Sbírku osvědčených postupů si sestavovali sami během práce a na konci v ní bylo 22 postupů a 82 doplňujících poznámek. Najdou se v ní běžné refaktoringy z katalogu Martina Fowlera, například vyčlenění části kódu do samostatné funkce (Extract Function), nahrazení vnořených podmínek časným návratem z funkce (Guard Clauses) nebo sloučení několika parametrů do jednoho objektu (Parameter Object). Vedle nich ale i postupy, které agenti objevili přímo v tomto kódu, sami pojmenovali a doplnili o podmínky použití. Shared Index Range slučuje opakované smyčky, které se liší jen počáteční a koncovou hodnotou. Action Parameter řeší duplicitní řídicí struktury, které se liší hlavně volanou funkcí. Uniform Step Table převádí řadu různorodých volání na průchod jednotnou tabulkou. Agenti do ní zapisovali i neúspěšné pokusy, protože mnoho úprav skóre nezměnilo a některé ho dokonce zhoršily.
Při sestavování sbírky se projevil i rozdíl mezi modely. Většinu práce nakonec dostal Opus v Claude Code, protože objevené vzory zachycoval a dokumentoval výrazně lépe než Codex s modelem Sol. S menšími modely Sonnet a Terra se zlepšování jednotlivých souborů často zastavilo na lokálním optimu a zbývající problémy se odstranit nepodařilo.
Agent, který dostane metriku a dostatek tokenů, ji ochotně optimalizuje, ať měří cokoli. Code Health přitom hodnotí strukturu kódu a jeho chování nesleduje, takže skóre lze zlepšit i úpravou, která nepozorovaně změní, co program dělá. Jako protiváha proto sloužil druhý signál, který o kvalitě kódu nic neříká, ale pozná, zda se hra začala chovat jinak: hash herního stavu po každém snímku.
Náhoda je tabulka o 64 položkách
Ověřování pomocí přehrávání záznamů předpokládá, že se hra při stejných vstupech chová pokaždé stejně. U bojových her tuto vlastnost vyžaduje hraní po síti. Takzvaný rollback netcode nečeká na vstupy soupeře. Hra je odhadne a pokračuje, a pokud skutečný vstup přijde jiný, načte uložený stav z několika snímků zpět a aktuální stav přepočítá. Hra proto musí umět kdykoli uložit všechno, co popisuje aktuální situaci v zápase, a ze stejné situace a stejných stisků tlačítek pokaždé dojít ke stejnému výsledku.
3SX k tomu používá knihovnu GekkoNet a herní stav uchovává v jediné struktuře State. Patří do ní pozice a zdraví obou postav, ukazatele energie pro nejsilnější speciální útoky, časovač kola, skóre, pozadí, aktivní efekty i indexy generátorů náhodných čísel. Náhoda totiž ve hře znamená jen posun indexu v pevné tabulce:
s32 random_16() {
Random_ix16++;
Random_ix16 &= 0x3F;
return random_tbl_16[Random_ix16];
}Code language: JavaScript (javascript)
Funkce ze souboru pls02.c vrací jednu z 64 předem daných hodnot a mezi voláními se mění jen index. Ten je součástí uloženého stavu, takže stejný zápas přehraný podruhé dostane stejná „náhodná“ čísla ve stejném pořadí. Port navíc z tohoto stavu v každém snímku počítá kontrolní součet, podle kterého při hře po síti pozná, že se simulace u obou hráčů rozešly.
Hash po každém snímku
Kontrola použitá při pokusu pracuje se stejným uloženým stavem. Hra dostane nahrané vstupy, po každém snímku se spočítá hash rollback stavu a výsledná řada hodnot musí souhlasit s řadou, kterou pro stejné vstupy spočítala verze před změnou. Kontrola běžela jako pre-commit hook, takže se do historie dostaly jen změny, které jí prošly. Pokud se řady rozejdou, číslo prvního odlišného snímku ukáže, ve kterém okamžiku záznamu se chování změnilo.

Oproti běžným testům má takové porovnání dvě výhody. Hlídá všechno, co struktura stavu obsahuje, tedy i hodnoty, které by autor testů nejspíš vůbec nekontroloval. A hlídá je v každém snímku záznamu. Tvrzení o úspěšném refaktoringu se obvykle opírají jen o to, že testy po změně dál procházejí, což dokazuje pouze to, že změnu přežily testy. Přehrávání záznamů z bojové hry nastavuje laťku výrazně výš.
Co hash nevidí
Kontrola vidí jen to, co se v záznamech skutečně odehrálo. Pokud v nich nikdo nehrál za určitou postavu nebo nepoužil určitý útok, příslušný kód se při přehrávání vůbec nespustí a agent ho mohl změnit, aniž by si toho kontrola všimla. Hash navíc počítá jen s údaji, které hra ukládá kvůli hraní po síti. Zachycují, co se v zápase děje, ale ne to, jak to vypadá a zní, takže chyba ve vykreslování nebo ve zvuku by porovnáním prošla.
Další slabinou je čas. Hash zachycuje, co hra v daném snímku spočítala, ale ne to, jak dlouho výpočet trval. U bojové hry přitom na rychlosti výpočtu závisí plynulost snímků i odezva ovládání. Jak refaktoring ovlivnil rychlost hry, spotřebu paměti a odezvu ovládání, chce tým teprve nechat změřit odborníkem na optimalizaci.
Chybí také přehled toho, co kontrola během práce zachytila. Běžela před každým commitem a část selhání byla opravena, aniž by je někdo zaznamenal. Historie repozitáře proto obsahuje jen změny, které kontrolou prošly, a nelze z ní zjistit, kolikrát kontrola agenta zastavila a co přitom zachytila. Není tak ani jisté, zda odhalila jemné regrese v časování snímků.
Porovnání hashů nakonec ověřuje shodu s předchozí verzí, ne správnost. Refaktoring proto zachoval i odchylky od automatové verze, které v kódu v té době zůstávaly, například změny ve vyvážení zavedené ve verzi pro PlayStation 2, které projekt 3SX postupně opravuje. K jejich hledání používá projekt jiný nástroj založený na stejném principu. Statcheck přehrává skutečné zápasy z platformy Fightcade, kde se hraje emulovaná automatová verze, a snímek po snímku porovnává zdraví, pozice, omráčení, energii pro speciální útoky nebo časovač s výpisy paměti z emulátoru. Jedna kontrola tak říká „chová se stejně jako minule“, druhá „chová se jako originál“, a refaktoring potřeboval jen tu první.
Hash také nic neříká o tom, zda má nová podoba kódu smysl. Postup, který sloučí dvě podobně vypadající smyčky do jedné, chování hry nezmění, i když každá z nich mohla sloužit jinému účelu. Problém se projeví až ve chvíli, kdy bude potřeba upravit jen jednu z nich. Neodhalí ho ani Code Health, protože tato metrika neměří architekturu, a kód tak může vypadat kvalitně, i když se zásadní problémy projeví až později.
Kde vzít vlastní hash
U Street Fighteru byl test ekvivalence k dispozici téměř bez práce navíc. Původní autoři napsali hru deterministicky a podpora hry po síti přiměla autory portu soustředit herní stav do jedné struktury. Porovnávání hashů tak jen využilo mechanismus, který už existoval. Většina starších aplikací nic podobného nemá, a právě proto je jejich refaktoring riskantní i bez agentů. CodeScene označuje automatické testy a kontroly ekvivalence za nezbytnou pojistku. Právě ty ale v kódu nízké kvality obvykle chybí nejvíc.
Nejbližší obdobou v běžné aplikaci jsou charakterizační testy. Zaznamenají, jak se systém při reálných vstupech chová dnes, bez ohledu na to, zda správně, a po změně porovnají výstupy. Podobně lze přehrát zaznamenaný provoz proti staré i nové verzi. Spolehlivé porovnání ale vyžaduje nejdřív dostat pod kontrolu všechno, co se mezi dvěma běhy liší samo od sebe, typicky aktuální čas, náhodně generované identifikátory nebo pořadí práce paralelních vláken. U 3rd Strike tuto práci udělali původní autoři hry a později autoři portu kvůli hře po síti. U ostatních aplikací na ni musí dojít na začátku projektu, ještě než agent dostane první úkol.