hrxdev
Polski

#1 Deterministyczny rdzeń, pilnowany przez kompilator

· Nick

Na razie nie ma na co patrzeć, więc ten pierwszy wpis jest o części, której nikt nie zobaczy: rdzeniu symulacji. Gra ma obsługiwać nawet 1000 więźniów i pracowników przy 20 tickach na sekundę i potrafić wyjaśnić, dlaczego ktokolwiek cokolwiek zrobił. To działa tylko wtedy, gdy te same dane wejściowe zawsze dają ten sam świat. Postanowiłem od pierwszego dnia uczynić to właściwością, którą sprawdza maszyna.

Na jakim etapie jesteśmy

Jestem w pierwszym kamieniu milowym, szkielecie rdzenia. Symulacja to zwykła biblioteka .NET bez kodu Godota, a klient w Godot 4 otwiera na razie pustą scenę 3D. Nie ma modelu świata, kafelków, postaci ani zapisów. Budżet 1000 postaci to cel; w takiej skali niczego jeszcze nie mierzyłem.

Tick

Czas w rdzeniu to całkowita liczba ticków, 20 na sekundę. Nie ma czasu zmiennoprzecinkowego. Tick przechodzi trzy fazy: zastosowanie komend z zewnątrz, uruchomienie systemów w kolejności zapisanej w jednym miejscu, a potem zatwierdzenie odroczonych zmian i zwiększenie licznika. Jeśli z ticka wydostanie się błąd, świat zostaje oznaczony jako uszkodzony i zatrzymany. Dziś lista systemów jest pusta; pętla istnieje po to, żeby pierwszy system miał gdzie trafić.

Komendy i dziennik

Świat zmienia się wyłącznie przez komendy. Komenda to mały rekord ze stałym identyfikatorem typu i ręcznie napisanym kodowaniem binarnym. Dla każdej z nich świat koduje ją, opatruje numerem ticka i numerem kolejnym, waliduje (tylko odczyt), zapisuje w dzienniku i dopiero wtedy stosuje. Odrzucone komendy też trafiają do dziennika, co powinno pomagać przy zgłoszeniach błędów. Ziarno plus dziennik mają dokładnie odtwarzać przebieg. Format pliku powtórki nie jest jeszcze napisany.

Liczby losowe

Generatory liczb losowych napisałem sam (SplitMix64 i xoshiro256**) i zakazałem w rdzeniu standardowego Random. Świat ma cztery nazwane strumienie (komendy, potrzeby, SI, zdarzenia), wyprowadzone z ziarna i przechowywane w stanie świata, więc są zapisywane i wchodzą do skrótu. Do pracy, w której kolejność nie może mieć znaczenia, służy bezstanowy generator kluczowany domeną, encją i tickiem. Wyniki dla ziarna 42 są przypięte, bo to w praktyce format powtórek: zmień generator, a wszystkie stare powtórki przestaną działać.

Skrót świata

Mały haszer wrażliwy na kolejność składa ziarno, tick, strumienie losowe i skrót dziennika komend w 64 bity. Dwa przebiegi, które powinny być identyczne, można porównać jedną liczbą. Na razie obejmuje on tylko te elementy, bo nic więcej w świecie jeszcze nie ma. Następny krok to porównywanie skrótów co N ticków podczas przebiegu.

Niedeterminizm jako błąd builda

Nie chciałem polegać na tym, że będę pamiętał zasady. Rdzeń jest budowany z analizatorami zakazanych API: czas zegarowy, liczby losowe bez ziarna, losowe GUID-y, losowane skróty ciągów, wątki i pomocniki równoległe, SIMD oraz API tekstowe zależne od kultury – wszystko to psuje build. Dalsze kontrole sprawdzają, że nie ma zmiennego stanu statycznego, i skanują skompilowany kod pod kątem wszystkiego, co się prześlizgnęło. Mój komputer ma rosyjską lokalizację z przecinkiem dziesiętnym, co jest dobrym powodem, by zakazać API zależnych od kultury.

Potem poszedłem dalej i zakazałem w rdzeniu standardowych kolekcji haszujących (Dictionary, HashSet i im podobnych), ponieważ kolejność ich iteracji jest klasycznym źródłem rozjazdów. Rdzeń dostaje tablice, listy i dwie własne małe kolekcje z udokumentowaną kolejnością. To nie jest szczelne: API, które zwraca kolekcję haszującą przez interfejs generyczny, nadal może się prześlizgnąć, a część ochrony działa tylko w testach.

Jak to jest zbudowane

Narzędzia wokół kodu też są zautomatyzowane, a te same skrypty działają w Windows i macOS. Ogólne podejście opisano na stronie Jak to powstaje.

Co dalej

W tym kamieniu milowym zostały jeszcze: model świata z piętrami, pliki zapisu i powtórek, test determinizmu oraz bezgłowy runner scenariuszy z punktem odniesienia benchmarku. Potem przyjdzie widok budowania w Godot. Zobacz plan rozwoju.

← Wszystkie wpisy devlogu