hrxdev
Українська

№1 Детерміноване ядро, яке охороняє компілятор

· Nick

Дивитися поки ні на що, тому перший запис присвячено частині, якої ніхто не побачить: ядру симуляції. Гра має тягнути до 1000 в’язнів і працівників при 20 тіках на секунду й пояснювати, чому будь-хто щось зробив. Це можливо лише тоді, коли однакові вхідні дані завжди дають той самий світ. Я вирішив із першого дня зробити це властивістю, яку перевіряє машина.

Де ми зараз

Я на першому етапі, каркасі ядра. Симуляція – звичайна бібліотека .NET без коду Godot, а клієнт на Godot 4 поки що відкриває порожню 3D-сцену. Моделі світу, клітинок, персонажів і збережень немає. Бюджет у 1000 персонажів – це мета; у такому масштабі я поки нічого не вимірював.

Тік

Час у ядрі – ціле число тіків, 20 на секунду. Часу з рухомою комою немає. Тік проходить три фази: застосувати зовнішні команди, запустити системи в порядку, записаному в одному місці, потім зафіксувати відкладені зміни та збільшити лічильник. Якщо з тіка вислизає помилка, світ позначається як несправний і зупиняється. Зараз список систем порожній; цикл існує для того, щоб у першої системи було місце.

Команди та журнал

Світ змінюється лише командами. Команда – невеликий запис зі стабільним ідентифікатором типу та вручну написаним бінарним кодуванням. Для кожної світ кодує її, ставить позначку з номером тіка й порядковим номером, перевіряє (лише читання), записує в журнал і лише потім застосовує. Відхилені команди теж потрапляють у журнал, що має допомогти з баг-репортами. Сид плюс журнал мають точно відтворювати прогін. Формат файлу повтору поки не написано.

Випадкові числа

Генератори випадкових чисел я написав сам (SplitMix64 і xoshiro256**) та заборонив у ядрі стандартний Random. У світу чотири іменовані потоки (команди, потреби, ШІ, події), вони виводяться із сида й зберігаються у стані світу, тому зберігаються та входять у хеш. Для роботи, де порядок не повинен впливати на результат, є генератор без стану з ключем із домену, сутності та тіка. Вихідні значення для сида 42 закріплено, бо по суті це і є формат повторів: змінюєш генератор – ламаються всі старі повтори.

Хеш світу

Невеликий хешер, чутливий до порядку, згортає сид, тік, випадкові потоки та дайджест журналу команд у 64 біти. Два прогони, що мають збігатися, можна порівняти одним числом. Поки що хеш покриває лише ці частини, бо в світі більше нічого немає. Наступний крок – порівнювати хеші кожні N тіків під час прогону.

Недетермінізм як помилка збірки

Я не хотів покладатися на те, що пам’ятатиму правила. Ядро збирається з аналізаторами заборонених API: системний час, випадкові числа без сида, випадкові GUID, рандомізовані хеші рядків, потоки та паралельні помічники, SIMD і рядкові API, залежні від культури, – усе це ламає збірку. Додаткові перевірки стежать, щоб не було змінюваного статичного стану, і переглядають скомпільований код на предмет усього, що проскочило. У мене на машині російська локаль із десятковою комою, а це гарний привід заборонити API, залежні від культури.

Потім я пішов далі й заборонив у ядрі стандартні хеш-колекції (Dictionary, HashSet та їм подібні), оскільки порядок їх обходу – класичне джерело розбіжностей. Ядру дісталися масиви, списки та дві невеликі власні колекції з задокументованим порядком. Захист не герметичний: API, що повертає хеш-колекцію через узагальнений інтерфейс, усе ще може проскочити, а частина захисту працює лише в тестах.

Як це побудовано

Інструменти навколо коду теж автоматизовано, і ті самі скрипти працюють у Windows і macOS. Загальний підхід описано на сторінці Як це зроблено.

Що далі

На цьому етапі лишаються: модель світу з поверхами, файли збережень і повторів, тест детермінізму та безголовий запуск сценаріїв із базовою лінією бенчмарка. Після цього – режим будівництва на Godot. Див. дорожню карту.

← Усі записи девлогу