Technologia · Projektowanie gier

Bot gra w to
czterysta razy,
żebyś ty nie musiał.

Zielony zestaw testów dowodzi, że twoje reguły są spójne. Nie mówi nic o tym, czy grę da się wygrać, czy stanie w miejscu przegrywa i czy którakolwiek z twoich decyzji ma znaczenie. To wymaga innego rodzaju testu — i niewygodnego pytania za każdym razem, gdy taki test oblewa.

Marcin Firmuga·2026-10-05·10 min czytania·Technologia

To wychodzi z symulacji zarządzania z rdzeniem działającym bez sceny, kampanią na cztery do pięciu lat i 1624 testami automatycznymi. Piętnaście z nich robi coś, czego pozostałe 1609 nie potrafi: gra w tę grę.

Lukę, którą zamykają, przeoczyłem. Miałem komplet zielonych testów, podczas gdy gra była po cichu niegrywalna, i nic w tych testach nie było błędne. Każda reguła robiła to, co deklarowała. Reguły razem nie składały się na grę.

Czego to wymaga od twojego projektu. Rdzenia symulacji, który uruchomi się bez sceny, renderera i pętli klatek — czegoś, co da się krokować w zwykłym teście C#. Jeśli logika twojej gry działa wyłącznie w MonoBehaviour.Update, ten artykuł jest najpierw argumentem za jej wydzieleniem, a dopiero potem za testowaniem.

Dlaczego test jednostkowy nie widzi balansu

Test jednostkowy pyta, czy reguła zachowuje się poprawnie. Balans nie jest właściwością żadnej reguły. Jest właściwością wszystkich naraz, działających przez cztery lata, czyli mieszka w miejscu, w które żaden test jednostkowy nie zagląda.

Komentarz na górze mojego pliku z testami grywalności to najkrótsza wersja tego problemu, jaką udało mi się napisać:

/// Czy w tę grę da się grać. /// /// Każdy inny test pyta, czy reguła zachowuje się poprawnie. Te pytają, czy reguły razem /// tworzą grę, którą ktoś może wygrać, i czy stanie w miejscu przegrywa. Symulacja może /// być idealnie spójna i zupełnie niegrywalna, a jedynym sposobem, żeby się o tym /// dowiedzieć, jest w nią zagrać.

Więc ten plik w nią gra. Nie próbkowane przybliżenie rozgrywki, nie model statystyczny rozgrywki — prawdziwa kampania, wewnątrz testu, jeden symulowany dzień po drugim, ze skryptowanym graczem podejmującym decyzje, które podejmowałby człowiek.

Bot jest podłogą, nie sufitem

Pierwszy odruch to zrobić bota dobrym. Ten odruch jest błędny i marnuje całe ćwiczenie, bo bot, który gra dobrze, mówi ci wyłącznie, że ekspert umie wygrać. Nikt o to nie pytał.

Mój jest celowo przeciętny i tak jest napisane:

/// Celowo zwyczajna strategia. Bez patrzenia w przód, bez wywiadu, bez sprytnego wyczucia /// czasu. Istnieje po to, żeby wyznaczyć podłogę: cokolwiek zrobi rozsądny człowiek, ma to /// pobić, a to nie ma zbankrutować.

Cała jego pętla decyzyjna to dziesięć prostych kroków w stałej kolejności, powtarzanych każdego symulowanego dnia. Żadnego przeszukiwania, żadnej funkcji oceny, nic, czego gracz grający pierwszy raz nie zrobiłby, czytając ekran przed sobą:

private void Decide() { BalanceTheCluster(); ShipAnythingFinished(); SizeTheFleet(); RaiseWhenOffered(); PushTheTree(); AdoptBetterArchitectures(); BuyDataWhenAffordable(); KeepOptimising(); StartARunWhenIdle(); StaffTheDesk(); }

Ta zwyczajność jest tym, co nadaje asercji znaczenie. Gdy ten gracz bankrutuje, zły jest balans, a nie gracz, i nie ma o czym dyskutować w kwestii tego, czy istniała lepsza strategia. Test podłogi czyta się wtedy dokładnie tak, jak brzmi:

[Test] public void AnOrdinaryCompetentPlayerSurvivesFourYears() { var simulation = NewGame(); var operatorBot = new ScriptedOperator(simulation); operatorBot.Run(1460); Assert.That(simulation.State.IsBankrupt, Is.False, $"Went under on {simulation.State.Date} with {operatorBot.ModelsShipped} model(s) shipped."); Assert.That(operatorBot.ModelsShipped, Is.GreaterThanOrEqualTo(3), "Four years should fit several model generations."); Assert.That(simulation.State.BestCapability, Is.GreaterThan(30.0), $"Capability stalled at {simulation.State.BestCapability:0.0}."); }

Sprawdzaj przedział, a nie liczbę

Tu jest błąd, którego szukałem najdłużej, i ten, który naprawiłbym pierwszy w cudzym projekcie: test balansu z samą dolną granicą gnije po cichu.

Każda łatka czyni grę odrobinę hojniejszą. Nowy węzeł badań, przeliczona cena, poprawka błędu, która przy okazji usunęła koszt. Każda zmiana z osobna jest do obrony, test podłogi przechodzi coraz wygodniej, nic nigdy nie zapala się na czerwono. Kończysz z grą, której nikt nie umie przegrać, i z zestawem testów, który jest z tego dumny.

Lekarstwem jest zapisanie również sufitu. Komentarz przy moim opisuje tryb awarii w obie strony i to jest fragment wart skopiowania, nawet jeśli nie weźmiesz z tego artykułu nic innego:

/// Przedział trudności. Zwyczajny gracz ma skończyć cztery lata w wyścigu, a nie przed /// nim: wciąż za czołówką, wciąż daleko od zdominowania rynku, wciąż wypłacalny. Jeśli ten /// test zacznie przechodzić trywialnie, gra zmiękła; jeśli zacznie oblewać od dołu, gra /// zrobiła się niesprawiedliwa.

W praktyce to cztery górne granice stojące obok dolnych:

Assert.That(simulation.State.IsBankrupt, Is.False, context); Assert.That(gap, Is.GreaterThan(-8.0), $"An ordinary player should not be comfortably ahead of the whole field. {context}"); Assert.That(report.MarketShare, Is.LessThan(0.75), $"An ordinary player should not own the market. {context}"); Assert.That(simulation.State.UnlockedResearch.Count, Is.LessThan(ResearchTree.All.Count), $"Four years must not be enough to finish the technology tree. {context}"); Assert.That(simulation.State.HasResearch(ResearchNodeId.ArtificialSuperintelligence), Is.False, $"The end game must stay out of reach in the first four years. {context}");

Czytaj asercje jak decyzje projektowe, bo nimi są

„Cztery lata nie mogą wystarczyć na skończenie drzewa technologii” nie jest stwierdzeniem technicznym. To decyzja o tym, czym jest gra, zapisana w miejscu, w którym będzie egzekwowana — a to więcej, niż osiąga większość dokumentów projektowych.

To okazuje się cichą korzyścią z całego podejścia. Testy balansu stały się jedynym miejscem, gdzie moje intencje co do trudności są zapisane w formie, która potrafi się odszczeknąć.

Przypadek kontrolny: dowód, że granie ma znaczenie

Każdy dotychczasowy test może przejść w grze, w której twoje decyzje są dekoracją. Gra, która gra się sama, też jest wypłacalna, też coś wydaje, też jest zielona. Żeby to wyłapać, potrzebujesz eksperymentu, którego uczą na każdej lekcji przyrody i którego prawie żaden zestaw testów nie zawiera: kontroli.

Dwie kampanie, to samo ziarno. W jednej bot gra porządnie. W drugiej ktoś wydaje jedną rzecz i przez cztery lata nie robi zupełnie nic:

[Test] public void ShippingOnceAndDoingNothingElseIsPunished() { // Przypadek kontrolny. Ten sam start, to samo ziarno, zero utrzymania po pierwszym modelu. var passive = NewGame(); passive.SetRentedAccelerators(500); passive.TryStartTraining(new ModelBlueprint("Only model", ArchitectureId.DenseTransformer, 20, 400, DatasetSource.WebCrawl), out _); passive.Advance(40); passive.TryReleaseModel(0, 1.0, out _); passive.Advance(1400); var active = NewGame(); new ScriptedOperator(active).Run(1440); Assert.That(active.State.BestCapability, Is.GreaterThan(passive.State.BestCapability + 10.0), "Playing well has to beat playing once by a wide margin."); Assert.That(active.State.LifetimeRevenueUsd, Is.GreaterThan(passive.State.LifetimeRevenueUsd)); }

To + 10.0 waży więcej niż sam kierunek. „Lepiej niż nic nie robić” to poprzeczka, którą przechodzi gra idle. Duży zapas to twierdzenie, że uwaga jest nagradzana, i to jest różnica między symulacją a zabawką.

Ten sam kształt się uogólnia, a gdy raz go zobaczysz, napiszesz cztery kolejne. Czy zbieranie kapitału faktycznie ma znaczenie? Porównaj przebieg z finansowaniem i bez. Czy architektura odblokowana w trzecim roku jest naprawdę lepsza od tej, z której powstała? Uruchom obie i sprawdź, że nowa wygrywa. Mam jeden nazwany AHouseFamilyBeatsTheDenseBaselineItWasBuiltFrom, czyli zdanie o projektowaniu przebrane za test.

Pytanie, które zadaje każdy oblany test balansu

Teraz część niewygodna i powód, dla którego chciałem napisać ten artykuł, a nie jego uładzoną wersję.

Gdy test balansu zapala się na czerwono, masz dwa ruchy. Możesz zmienić grę albo zrobić bota mądrzejszym. Oba dają zielony zestaw i oba wyglądają w diffie mniej więcej tak samo, a uczciwy jest tylko jeden — z tym że nie zawsze ten sam, i to czyni rzecz naprawdę trudną, a nie tylko kuszącą.

Trafiłem na to, gdy strona usługowa mojej symulacji zaczęła się przewracać. Naprawą było pozwolenie botowi ruszyć suwak, który dotąd ignorował. Oto, co napisałem obok, bo nie ufałem sobie, że zapamiętam, dlaczego było to dozwolone:

/// **To jest zmiana operatora, a nie zmiękczenie asercji, i rozróżnienie ma znaczenie, /// bo ten sam ruch może być jednym albo drugim.** Obsługa brała całą flotę zawsze, gdy nie /// trwał trening, a badania i tak brały swoją część, więc klaster wykonywał sto siedemdziesiąt /// procent własnej pracy. Sprawienie, żeby połowy sumowały się do jedności, jest naprawą; /// oznacza też, że firma może teraz naprawdę zagłodzić własnych klientów badaniami, co ten /// operator robił przez pięć lat, nie zauważając. /// /// Gracz, który widzi czerwony wskaźnik usługi, daje klientom więcej floty. To jeden suwak /// na ekranie COMPUTE i jest dla niego dokładnie tak dostępny, jak tutaj. Modeluj gracza, /// który widzi kontrolkę, a nie takiego, który jej nie widzi.

To ostatnie zdanie jest regułą, którą teraz stosuję, i da się ją odpalić na sobie: czy gracz patrzący pierwszy raz na ten ekran zrobiłby to? Jeśli tak, douczanie bota jest modelowaniem rzeczywistości i test robi się uczciwszy, a nie mniej uczciwy. Jeśli kontrolka istnieje tylko w teście, jeśli bot dostaje informacje niedostępne graczowi albo jeśli liczby są podkręcane, aż przejdzie — już nie testujesz gry, tylko z nią negocjujesz.

Dwa szczegóły z tego komentarza warte zabrania

„Klaster wykonywał sto siedemdziesiąt procent własnej pracy.” To jest prawdziwy błąd i żaden test jednostkowy nie mógł go zobaczyć: każdy przydział z osobna był poprawny, po prostu połowy nie musiały się sumować. Do wyjścia na wierzch potrzeba było bota grającego przez pięć lat.

„Co ten operator robił przez pięć lat, nie zauważając.” Po naprawieniu błędu stała się osiągalna nowa, uprawniona awaria: firma może teraz zagłodzić własnych klientów. Naprawa błędu w symulacji nie zmniejsza powierzchni do testowania. Zwykle ją poszerza.

Oblany test balansu ma opisywać grę, a nie liczbę

Oblany test jednostkowy mówi ci, że funkcja jest zła, i idziesz przeczytać funkcję. Oblany test balansu mówi, że firma zbankrutowała w trzecim roku, a to samo w sobie jest prawie bezużyteczne, bo przyczyna leży gdzieś w 1095 symulowanych dniach.

Dlatego każda asercja w tych testach niesie ze sobą migawkę świata:

var context = $"cap {simulation.State.BestCapability:F1}, frontier {report.FrontierCapability:F1}, " + $"share {report.MarketShare:P1}, cash {simulation.State.CashUsd:N0}, " + $"models {bot.ModelsShipped}, rounds {bot.RoundsRaised}, " + $"research {simulation.State.UnlockedResearch.Count}/{ResearchTree.All.Count}";

Różnica jest taka jak między półgodziną bisekcji a przeczytaniem jednej linijki. Awaria, która mówi share 94%, research 31/31, gotówka ogromna, to gra, która zrobiła się za łatwa. Awaria, która mówi models 1, gotówka ujemna, rounds 0, to gracz, który nigdy nie wystartował. Ten sam czerwony test, odwrotne problemy, a napis mówi ci który, zanim cokolwiek otworzysz.

Najtańsza poprawka w całym tym artykule

Jeśli masz już testy balansu i chcesz jednej poprawy dziś wieczorem, to jest właśnie ta. Wstaw pięć czy sześć liczb opisujących stan gry do każdego komunikatu asercji w tym pliku. Kosztuje cztery linijki i zmienia to, co znaczy czerwony build.

Bot musi być bezstanowy, a to nie jest oczywiste

Jedno ograniczenie łapie każdego raz, mnie też, i pojawia się dopiero, gdy połączysz testy balansu z testami zapisu — a powinieneś, bo zapis, który po cichu zmienia kampanię, jest błędem balansu, który podróżuje.

Test brzmi: zagraj cztery lata, zapisz, wczytaj, zagraj piąty rok i sprawdź, czy wczytany przebieg zgadza się z tym, który nigdy nie zapisywał. W chwili, gdy to napiszesz, bot dostaje regułę, której wcześniej nie miał. Cokolwiek bot pamięta we własnym polu, jest stanem, którego nie ma w pliku zapisu, więc wczytany przebieg się rozjedzie — a test obwini symulację za różnicę, którą stworzył sam plik testowy.

// **Czytaj z biurka, nigdy z ostatniego raportu.** `AYearFourSaveRunsIdentically` // odbudowuje operatora po wczytaniu, więc cokolwiek ta decyzja pamięta między dniami, jest // różnicą między przebiegiem, który zapisał, a tym, który wczytał, i strażnik czyta to jako // rozjeżdżanie się symulacji. Kolejka jest zapisywana; pole na operatorze nie.

Reguła brzmi więc: każda decyzja bota ma wynikać ze stanu symulacji, nigdy z czegoś, co bot nosi ze sobą. To ta sama dyscyplina, która w ogóle czyni kampanię powtarzalną, czyli temat siostrzanego artykułu o deterministycznym losowaniu — i warto zauważyć, że to ograniczenie przychodzi ze strony, z której nikt nie ostrzega.

Ile to kosztuje, uczciwie

To nie są tanie testy i udawanie czegoś innego byłoby dokładnie tym rodzajem porady, przed którym ta strona ma bronić.

KosztJak to wygląda naprawdę
Czas wykonania Każdy z nich rozgrywa od 1460 do 1826 symulowanych dni, kilka z nich dwukrotnie dla porównania. To zdecydowanie najwolniejsze testy w całym zestawie.
Utrzymanie Realne. Według mojego własnego licznika w kodzie ten plik musiał być uczony korzystania z kontrolki, którą wyhodowała gra, trzy osobne razy — sufit parametrów, linia produktowa i biurko obsługi. Każdy nowy system widoczny dla gracza to możliwe nowe zachowanie bota.
Osąd Ten się nie zmniejsza. Każdy czerwony test pyta, czy naprawiasz grę, czy douczasz bota, i żadne narzędzie nie odpowie za ciebie.

Po drugiej stronie: piętnaście testów pokrywa pytanie „czy to nadal jest gra”, chodzą przy każdej zmianie i znalazły klaster wykonujący sto siedemdziesiąt procent własnej pracy. Zapłaciłbym za ten czas wykonania drugi raz.

Od czego zacząć, jeśli nie masz nic z tego

W kolejności, w której napisałbym je ponownie:

Dwa pierwsze to jeden wieczór i w nich siedzi prawie cała wartość. Reszta to doszlifowanie.

Pytania, które ludzie zadają

Czy da się testować balans gry testami jednostkowymi?

Zwykłymi nie. Test jednostkowy pyta, czy reguła zachowuje się poprawnie, a balans nie jest właściwością żadnej pojedynczej reguły — jest właściwością wszystkich naraz, działających przez długi czas. Działa skryptowany gracz z celowo przeciętną strategią, który rozgrywa pełną kampanię wewnątrz testu, po czym sprawdzasz stan, do którego doszedł. Symulacja może być idealnie spójna na poziomie każdej reguły i zupełnie niegrywalna jako gra.

Jak dobry powinien być bot?

Celowo przeciętny. Istnieje po to, żeby wyznaczyć podłogę, a nie sufit: bez patrzenia w przód, bez sprytnego wyczucia czasu, bez informacji, których nie miałby gracz grający pierwszy raz. Wtedy asercja czyta się jako jeśli ten gracz nie przeżyje, to zły jest balans, a nie gracz. Bot nastrojony na grę optymalną mówi tylko, że ekspert umie wygrać, a o to nikt nie pytał.

Co powinien sprawdzać test balansu?

Przedział, obustronny. Jedna strona mówi, że grę da się przeżyć; druga, że nie jest trywialna — gracz nie powinien być wygodnie przed stawką, nie powinien mieć trzech czwartych rynku, nie powinien skończyć drzewa technologii. Test z samą dolną granicą po cichu zielenieje, gdy gra dryfuje w stronę łatwiejszej, i to najczęstszy sposób, w jaki balans gnije niezauważony.

Gdy test balansu oblewa, naprawiasz grę czy bota?

To prawdziwe pytanie, a diff wygląda tak samo w obu przypadkach. Douczenie bota jest uprawnione, gdy gra wyhodowała kontrolkę, której człowiek oczywiście by użył: modelujesz gracza, który widzi suwak. Jest nieuczciwe, gdy kontrolka istnieje tylko w teście, gdy bot dostaje informacje niedostępne graczowi albo gdy stroisz go, aż liczba przejdzie. Użyteczny sprawdzian: czy gracz patrzący pierwszy raz na ten ekran zrobiłby to samo?

Jak udowodnić, że dobra gra ma znaczenie?

Przypadkiem kontrolnym, którego większość projektów nie ma. Dwie kampanie z tego samego ziarna: jedna rozegrana porządnie, druga wydaje jedną rzecz i przez cztery lata nie robi nic. Sprawdź, że aktywna wygrywa z dużym zapasem. Bez tego wszystkie pozostałe asercje mogą przechodzić w grze, w której decyzje są dekoracją.

Czy to zastępuje playtesty z ludźmi?

Nie i nie ma takiego zamiaru. Pokrywa dokładnie jedno pytanie — czy liczby składają się na grę, którą da się wygrać i da się przegrać — i pokrywa je przy każdej zmianie, czego nie zrobi żaden człowiek. Wszystko o tym, czy gra jest ciekawa, czy ekrany mają sens i czy decyzja czuje się jak decyzja, wymaga ludzi. Kupujesz to, że ludzie przestają zużywać swoją sesję na odkrywanie, że trzeci rok jest niemożliwy.

Wszystko da się przeczytać. Skryptowany operator, przedział trudności, przypadek kontrolny i porównanie po zapisie są w publicznym repozytorium Scaling Laws, w pliku PlayabilityTests.cs, podlinkowanym w źródłach tego artykułu. Cytowane tu komentarze są prawdziwe, nie wygładzone na potrzeby tekstu.
Dalej w tej serii: 1541 testów, które nie uruchamiają gry · ekonomia tycoona jako prawa · dlaczego solowe projekty umierają na tych samych czterech funkcjach.
MF

Marcin Firmuga

Solo developer · HCK_Labs · buduję publicznie

Piszę o tym, co naprawdę wypuściłem, z prawdziwymi liczbami i prawdziwym kodem, łącznie z tym, co nie zadziałało. Więcej: moja historia.