Implement civilization flow mechanics and profiling for resource allocation

- Added `civilization_flow.gd` to manage continuous expected flows and resource allocation within civilizations.
- Introduced `allocate_profiled` and `whole_quotas` functions in `quantity_math.gd` for precise stock allocation based on float shares.
- Enhanced `credit` logic in `object_ledger.gd` to handle production remainders more effectively.
- Updated `append_batch` in `object_batches.gd` to prevent batch creation for invalid IDs.
- Introduced `ratio_approx` function in `exact.gd` for better handling of float profiles.
- Added a benchmark script `benchmark_civilization_flows.gd` to evaluate the performance of civilization flow mechanics.
- Refactored various functions to improve clarity and efficiency in handling resource flows and allocations.
This commit is contained in:
Kirill
2026-10-07 22:51:55 +03:00
parent 6ee66a627d
commit 3fcc7211ce
41 changed files with 247959 additions and 817 deletions
+42 -17
View File
@@ -122,7 +122,41 @@ godot --headless --path Owniverse.GI --script res://tests/test_time.gd
## Согласованные правила эволюции
Сохраняются постепенные интервальные окна GI: начало преобразования — через `lifespan`, длительность окна — `lifespan / 40` при `lifespan > 10`, иначе `lifespan`.
Сохраняются постепенные интервальные окна GI: срок жизни делится на 40 возрастных групп; начало преобразования — через `lifespan`, длительность окна — `lifespan / 40` при `lifespan > 10`, иначе `lifespan`.
На скоростях 4–6 узкие звёздные партии могут объединяться по границе полного
цикла смерти шириной 200/2000/20000 игровых лет. Прежнее окно шире цикла
сохраняется: долгоживущие звёзды не дробятся. Из существующих партий сливаются
только две или более одного типа, чей оставшийся переход полностью попадает
в общий цикл; широкие и пересекающие границу партии сохраняются.
`test_batch_boundaries` проверяет объединение, точные большие количества и
остатки, однократное изъятие, сериализацию, сохранение широких окон,
распределённые рождения и интеграл производства.
`benchmark_large_star_cohorts.gd` создаёт отдельные большие диагностические
сохранения со старой и новой звёздной сеткой при одинаковых количествах и
входных интервалах рождения. Переключатель `batches.cycle_stars` используется
только для сравнительного замера, в игре он включён. Парные прогоны меняют
порядок моделей и сбрасывают исходное состояние перед каждым запуском:
```sh
godot --headless --path Owniverse.GI --script res://tests/benchmark_large_star_cohorts.gd -- cycles=50 repeats=2
```
Сохранения и полные профили записываются в `tests/artifacts/large_star_cohorts_*`;
сохранение игрока не затрагивается. `generate_only` проверяет запись/чтение и
исходное равенство физических количеств без запуска замеров.
`benchmark_many_objects.gd` создаёт новую диагностическую вселенную со всеми
типами объектов и цивилизаций и 4800 синтетическими звёздными партиями,
попадающими группами в общие циклы. Это отдельный стресс-сценарий большого
числа партий; старый и новый расчёты используют одинаковый исходный состав.
```sh
godot --headless --path Owniverse.GI --script res://tests/benchmark_many_objects.gd -- cycles=40 repeats=2
```
Файлы сценария: `tests/artifacts/many_objects_fragmented_stars_*`.
Вероятностные исходы используют накопление дробного остатка GI, без случайного округления. Этот же подход выбран для будущего создания планет. Шансы планет записаны долями (`0.01` = 1%), а альтернативные исходы звёзд — процентами (`%0.1` = 0.1%). Парсер планет сохраняет дроби и поддерживает один элемент без `&`.
@@ -209,8 +243,7 @@ godot --path Owniverse.GI --script res://tests/benchmark_civilizations.gd -- ui
Stage вызывает `evolute_frame`: транзакция получает бюджет 40 мс и уступает
следующему кадру внутри создания партий и обработки стадий. Расчёт остаётся
на главном потоке, состояние публикуется только после полного завершения.
Длительность одной транзакции ограничена обычным шагом 1/60 секунды выбранной
скорости. Запрошенное время хранится отдельно, поэтому отставание не превращается
Одна транзакция обрабатывает 0,2 реальной секунды на любой скорости. Запрошенное время хранится отдельно, поэтому отставание не превращается
в один всё более тяжёлый шаг. При предельной нагрузке время симуляции может
отставать от запрошенного; это не ограничение FPS интерфейса и не удаление событий.
@@ -259,20 +292,12 @@ Stage вызывает `evolute_frame`: транзакция получает б
1.057. Замер включает сокращение накопленного отставания сохранения;
это результат конкретного сценария на Mac, а не гарантия для Android.
Адаптивный шаг на скоростях 4–6 выбирает 0.1, 0.2 или 0.5 реальной секунды.
Долг больше двух шагов повышает ступень, меньше половины шага понижает её.
Шаг ограничен доступным запрошенным временем. Скорости 1–3 сохраняют 1/60;
смена скорости сбрасывает ступень. Возобновляемая транзакция сохраняет точную
границу из checkpoint, адаптация применяется к следующей транзакции.
Бюджет кадра остаётся 40 мс; ширина временных партий не меняется.
`test_frame_evolution` проверяет ступени, гистерезис, сохранение долга,
переключение скорости, возобновление и совпадение с прямым расчётом того же шага.
На свежей диагностической копии сохранения с долгом 407.79 млн лет:
600 кадров реального UI на M4 Pro дали 18.05 FPS (p95 92.91 мс,
максимум 122.45 мс), поступило 3.335 млн лет, обработано 6.480 млн:
пропускная способность 1.943 вместо 0.787 при фиксированном шаге 0.1.
Разбиение времени влияет на дискретные производство и глобальные параметры;
адаптивные шаги не обещают совпадения результата с последовательностью 0.1.
Полный шаг фиксирован: 0,2 реальной секунды на всех скоростях, пять циклов
на секунду запрошенного времени. Неполный интервал накапливается. При отставании
шаг сохраняется, долг не отбрасывается. Возобновляемая транзакция сохраняет
границу из checkpoint. Бюджет кадра остаётся 40 мс. `test_frame_evolution`
проверяет расписание всех шести скоростей, сохранение долга и пять циклов
на одну секунду.
Дипломатический drift использует шесть взвешенных вкладов вместо матрицы
пар этик: вес = сумма вкладов + 195 × свой вклад − противоположный вклад.
@@ -92,3 +92,290 @@ godot --path Owniverse.GI --script res://tests/benchmark_full_ui.gd
Финальная APK повторно проверена на Samsung SM-N950F, Android 9, тем же полным сценарием и 120 кадрами. Два последовательных прогона дали **14,57 и 15,02 FPS**, средний кадр — **68,61 и 66,58 мс**, p95 — **113,03 и 123,03 мс**, по одной завершённой транзакции из 119 интервалов. Опубликовано 10 000 лет из 647 331 и 634 134 запрошенных; пропускная способность — 0,01545 и 0,01577. В последнем профиле `generations` заняли 5,120 с, `civilization_prepare` — 3,162 с, войны — 1,026 с, переходы — 0,728 с, дрейф этик — 0,474 с. Самый длинный непрерывный участок составил **458,14 мс** в `deep_copy_and_coarsen`, максимальный кадр — 976 мс. Предыдущий одиночный запуск старой benchmark-сборки показал 1,83 FPS, поэтому сравнивать его с этой парой напрямую нельзя; оба финальных прогона выполнены на одной APK и подтверждают результат около 15 FPS.
На компьютере прошли тесты состояния (70), эволюции (555), оптимизаций симуляции (364), индикаторов (63) и жизни (25). Полный набор всё ещё сообщает о двух отказах в `test_descriptions.gd` при обновлении выбранного описания и смене языка; оба относятся к UI-описаниям, а не к модели этик. В логах Android ошибок GDScript/Godot за прогон не найдено.
## Непрерывная модель цивилизаций — 2026-10-06
Сравнены исходная симуляция из коммита `6ee66a6` и новая модель потоков. Оба запуска используют один диагностический JSON, скорость 6, 100 последовательных полных циклов с запросом 0,1 секунды каждый и включённый CPU-профиль. Подготовка каталога и чтение JSON не включены в длительность цикла. Запуски выполнены последовательно, без параллельных тестов. Сохранение игрока не читалось и не изменялось.
| Полный цикл и пропускная способность | До | После |
|---|---:|---:|
| Среднее время полного цикла | 811,292 мс | 401,796 мс |
| Минимум / максимум | 488,914 / 963,141 мс | 323,427 / 435,607 мс |
| Завершённых полных циклов в секунду | 1,233 | 2,489 |
| Обработанных секунд симуляции на секунду CPU | 0,123 | 0,249 |
| Применено игровых лет за 100 циклов | 1 000 000 | 1 000 000 |
Полный цикл ускорился в **2,02 раза**, среднее время сократилось на **50,5%**. Цель **100 мс / 10 полных циклов в секунду не достигнута**: для неё требуется ещё примерно 4,02-кратное ускорение текущей версии. При непрерывном запросе времени на шестой скорости и шаге 0,1 секунды этот стресс-сценарий продолжит накапливать долг. Это измерение полных завершённых шагов симуляции, без показателя FPS.
Текущие внешние интервалы CPU-профиля, по убыванию среднего времени на цикл:
| Расчёт | Среднее за цикл |
|---|---:|
| Поколения звёзд и биологической жизни | 151,6 мс |
| Подготовка непрерывных потоков цивилизаций | 98,5 мс |
| Применение потоков к количествам и владельцам | 53,2 мс |
| Производство за период | 28,7 мс |
| Тренды | 18,1 мс |
| Дрейф, проверка вместимости и поклонение после потоков | 8,8 мс |
| Инициализация жизни | 6,1 мс |
| Ограничение потоков ресурсными бюджетами | 5,8 мс |
| Применение колонизации | 5,5 мс |
Внутри подготовки потоков разреженный оператор занимает 24,5 мс на цикл; этот вложенный интервал нельзя прибавлять к 98,5 мс. Внешние интервалы не покрывают всё время: остальная работа включает копирование транзакции, построение физических снимков и обновление представления.
Модель C0.2–C4.4 теперь использует точные целые количества и шесть долей float без возрастных партий. Оператор учитывает несколько последовательных переходов за интервал. Конечные количества и ресурсные потолки сохраняются точно; пропорции потоков и владельцев приближены float и квантованием долей до 15 десятичных знаков. Изменяется распределение событий во времени, поэтому тесты проверяют законы новой модели, сохранение количества, владение, ресурсные ограничения и воспроизводимое продолжение сохранения.
Регрессия: 27 из 28 наборов прошли. Новые переходы — 1895 проверок; арифметика распределения и кеши — 1889; кадровое выполнение — 24. В `test_descriptions.gd` сохраняются две ранее обнаруженные ошибки обновления выбранного описания и языка; этот UI-код не изменён в данном рефакторинге.
Повторение из папки `Owniverse.GI`:
```sh
godot --headless --path . --script res://tests/benchmark_civilization_flows.gd -- tests/artifacts/all_objects_speed6.adventure.json 0.1 profile cycles=100
```
## Фиксированные пять циклов в секунду — 2026-10-07
Текущая версия после отката агрегации и возврата 40 возрастных групп.
Планировщик использует шаг 0,2 реальной секунды на всех скоростях. Замер:
диагностическое сохранение со всеми объектами и цивилизациями, скорость 6,
100 полных синхронных циклов с CPU-профилем, без отрисовки интерфейса.
Проверка сохранения: OK. Исходное сохранение не изменялось.
| Метрика | Результат |
|---|---:|
| Среднее время полного цикла | 439,228 мс |
| Минимум / максимум | 340,370 / 483,152 мс |
| Завершённых полных циклов в секунду | 2,277 |
| Требуется циклов в секунду | 5 |
| Обработано игровых лет | 2 000 000 |
| Запрошенное время сценария | 20 с |
| Время расчёта 100 циклов | 43,923 с |
| Пропускная способность | 0,455 с симуляции / с расчёта |
Цель 200 мс на полный цикл не достигнута: по среднему времени требуется
ускорение примерно в 2,20 раза. Увеличение шага с 0,1 до 0,2 секунды улучшило
отношение обработанного времени к затратам, но при непрерывном поступлении
времени этот стресс-сценарий продолжит накапливать долг. Это измерение
пропускной способности симуляции, без оценки FPS. Один прогон не задаёт
гарантированную производительность для других сохранений или устройств.
Внешние интервалы CPU-профиля, по убыванию среднего времени на цикл:
| Расчёт | Среднее на цикл |
|---|---:|
| Поколения звёзд и биологической жизни | 168,294 мс |
| Подготовка непрерывных потоков цивилизаций | 111,835 мс |
| Применение потоков к количествам и владельцам | 59,516 мс |
| Производство за период | 29,248 мс |
| Тренды | 17,787 мс |
| Завершение цивилизационных потоков | 8,864 мс |
| Ограничение потоков вместимостью | 6,439 мс |
| Инициализация жизни | 6,172 мс |
| Колонизация | 6,111 мс |
Вложенные интервалы нельзя прибавлять к внешним: построение физических
снимков — 27,845 мс/цикл; изъятие партий — 42,015 мс/цикл; решение оператора
цивилизаций — 31,835 мс/цикл. Проверки кадрового планировщика:
35 проверок, 0 ошибок, включая все шесть скоростей, сохранение долга,
возобновление расчёта и пять завершённых циклов на одну секунду запроса.
```sh
godot --headless --path . --script res://tests/benchmark_civilization_flows.gd -- tests/artifacts/all_objects_speed6.adventure.json 0.2 profile cycles=100
```
## Новые звёздные партии с целым переходом за цикл — 2026-10-07
На скоростях 4–6 новые звёзды одного типа объединяются по общей границе
завершения жизни и переходят целиком. Временная сетка равна одному полному
циклу (200/2000/20000 игровых лет). Старые постепенные партии из сохранения
не преобразуются; SN/GRB сохраняют прежнюю обработку.
Повторён тот же замер: диагностическое сохранение, скорость 6, шаг 0,2 с,
100 полных циклов с CPU-профилем. Проверка сохранения OK.
| Метрика | До | После |
|---|---:|---:|
| Среднее время полного цикла | 439,228 мс | 445,690 мс |
| Минимум / максимум | 340,370 / 483,152 мс | 345,276 / 486,647 мс |
| Полных циклов в секунду | 2,277 | 2,244 |
| Пропускная способность | 0,455 | 0,449 |
| Обработано игровых лет | 2 000 000 | 2 000 000 |
| Поколения, среднее на цикл | 168,294 мс | 171,039 мс |
| Обработка партий новорождённых, среднее на цикл | 7,768 мс | 6,199 мс |
| Вызовы обработки партий новорождённых | 16 147 | 13 885 |
Общего ускорения на этой выборке нет: среднее выросло примерно на 1,5%.
Обработка новорождённых занимает на 20% меньше времени, но это небольшая
доля полного цикла. Замер включает существующие постепенные партии;
он не доказывает ускорения вселенной, целиком созданной по новой схеме.
Цель пяти полных циклов в секунду остаётся недостигнутой.
Проверки: границы партий — 109, звёздные правила — 333, короткие события —
841, укрупнённые поколения — 28, кадровое выполнение — 35; все без ошибок.
Тестируются точные большие количества, целый однократный переход,
распределённые рождения, сохранения и интеграл производства.
## Первоначальный замер больших сохранений до исправления сетки — 2026-10-07
Созданы `large_star_cohorts_legacy.adventure.json` и
`large_star_cohorts_cycle.adventure.json`: по 6×10^60 звёзд каждого из 15
типов (главная последовательность и красные гиганты), остальные объекты,
жизнь и цивилизации взяты из диагностического сценария. Возраст — 10^12 лет.
Для каждого типа заново сформирована одинаковая история рождения длиной
`min(эффективный срок жизни, 2 млн лет)` перед началом завершения жизни.
Исходные физические количества и этические профили совпадают строго.
Сохранения валидируются и проходят точное сравнение состояния после записи
и чтения JSON с полной точностью float. Сохранение игрока не изменялось.
Старая схема измеряется переключателем `batches.cycle_stars = false`, новая
включена по умолчанию. В обоих вариантах одинаковый остальной код симуляции,
скорость 6, шаг 0,2 с и CPU-профиль. Два прогона по 50 полных циклов на модель,
порядок запусков: старая → новая → новая → старая. Перед каждым прогоном
восстанавливается исходное состояние; каждый обрабатывает 1 млн игровых лет.
| Метрика | Старая схема | Новая схема |
|---|---:|---:|
| Среднее первого прогона | 417,265 мс | 423,419 мс |
| Среднее второго прогона | 431,081 мс | 427,011 мс |
| Среднее всех 100 циклов на модель | 424,173 мс | 425,215 мс |
| Полных циклов в секунду | 2,358 | 2,352 |
| Пропускная способность | 0,472 | 0,470 |
| Звёздных партий в исходном сохранении | 118 | 1 300 |
| Пиковое число звёздных партий | 132 | 1 300 |
| Поколения, среднее на цикл | 157,711 мс | 147,906 мс |
| Производство, среднее на цикл | 28,692 мс | 34,704 мс |
| Применение потоков цивилизаций, среднее на цикл | 58,642 мс | 62,160 мс |
| Изъятие партий, вложенное в поколения | 37,015 мс | 41,379 мс |
| Партии новорождённых, вложенное в поколения | 7,648 мс | 4,248 мс |
**Устойчивого выигрыша полного цикла не обнаружено.** Разница средних —
около +0,25% у новой схемы; в первом парном сравнении она медленнее, во втором
быстрее. Поколения занимают примерно на 6,2% меньше времени, но производство
стало дороже примерно на 21%. Пять полных циклов в секунду не достигнуты.
Причина роста числа партий: для долгоживущих звёзд сетка одного цикла
значительно мельче `lifespan / 40`. Например, у RD прежняя ширина — 1 млрд
лет, новая на скорости 6 — 20 тыс. лет. Поэтому изменение не всегда
укрупняет партии: на таком наборе исходных рождений их стало в 11 раз больше.
Итоговое временное распределение переходов у моделей различается; совпадение
их конечных состояний не является условием этого замера.
Для дальнейшего упрощения следует сохранять широкие группы долгоживущих
звёзд и применять целый переход только к партиям, уже укладывающимся в один
цикл. Это предложение в данном замере не реализовано.
```sh
godot --headless --path . --script res://tests/benchmark_large_star_cohorts.gd -- cycles=50 repeats=2
```
Полные результаты первоначального замера: `large_star_cohorts_before_dynamic_fix_results.json`.
Набор с 1300 партиями сохранён в `large_star_cohorts_before_dynamic_fix_cycle.adventure.json`.
Последующие файлы без этого суффикса относятся к исправленной динамической схеме.
## Исправленное динамическое укрупнение без дробления — 2026-10-07
Ошибка первоначальной реализации устранена: циклическая сетка больше не
заменяет широкие возрастные окна долгоживущих звёзд. При создании новая сетка
используется только если прежнее окно не шире цикла. Иначе используется
прежнее широкое окно. Из существующих партий объединяются две или более
одного типа, чей оставшийся интервал завершения целиком лежит в общем цикле.
Партии, пересекающие границу циклов или занимающие несколько циклов, не делятся.
Повторён парный замер на отдельных больших сохранениях, с теми же исходными
количествами и входными интервалами рождения: скорость 6, шаг 0,2 с,
два прогона по 50 циклов на модель в порядке старая → новая → новая → старая.
Каждый прогон обрабатывает 1 млн игровых лет; состояния валидируются до и
после, записанные файлы проходят точное сравнение после чтения.
| Метрика | Старая схема | Исправленная динамическая |
|---|---:|---:|
| Исходных звёздных партий | 118 | 113 |
| Пиковое число партий | 132 | 127 |
| Число партий после 50 циклов | 130 | 125 |
| Среднее первого прогона | 420,388 мс | 424,307 мс |
| Среднее второго прогона | 431,605 мс | 428,498 мс |
| Среднее всех 100 циклов на модель | 425,996 мс | 426,402 мс |
| Полных циклов в секунду | 2,347 | 2,345 |
| Поколения, среднее на цикл | 158,622 мс | 157,606 мс |
| Партии новорождённых, вложенный интервал | 7,679 мс | 6,693 мс |
На этом наборе партиям стало меньше примерно на 4,2%, вместо прежнего
ошибочного роста до 1300. Проверено, что долгая группа RD остаётся одной
широкой партией и её окно не меняется при добавлении новых звёзд.
Новый тест запрещает рост числа исходных партий в диагностическом сценарии.
Устойчивого ускорения полного цикла по этим прогонам нет: разница средних
около +0,10%; знак разницы в отдельных парных сравнениях меняется.
Обработка партий новорождённых занимает примерно на 13% меньше времени,
но её доля недостаточна для заметного выигрыша всего расчёта.
Цель пяти циклов в секунду по-прежнему не достигнута.
Проверки: границы партий — 117, звёздные правила — 333, короткие события —
841, кадровое выполнение — 35; все без ошибок. Дополнительные проверки
охватывают сохранение широких окон, объединение старых партий с точными
количествами и остатками, соседние циклы и повторное укрупнение.
Актуальные сохранения: `large_star_cohorts_legacy.adventure.json` и
`large_star_cohorts_cycle.adventure.json`; результаты: `large_star_cohorts_results.json`.
## Новая вселенная с множеством объектов и мелких звёздных партий — 2026-10-07
Создано отдельное состояние с нуля по текущим таблицам: 150 типов объектов,
все 44 стадии этических цивилизаций (включая Ct), 15 типов живых звёзд.
Запасы большинства объектов — 10^60 каждого типа; планет и цивилизаций —
10^28. Жизнь размещена у четырёх типов карликов, четверть звёзд каждого типа
связана со структурами. Этические профили различаются между стадиями.
Высокий запас пространства сохраняет жизнь при бластах. Интерфейс не отрисовывался.
Для проверки укрупнения заданы валидные синтетические исторические партии:
40 последовательных циклов завершения × 8 узких партий × 15 типов звёзд.
Все восемь партий типа целиком завершаются в общем цикле; это целевой
стресс-сценарий большого числа партий, а не утверждение, что обычное создание
с неизменным временем жизни обязательно порождает такую фрагментацию.
Исходный состав и интервалы одинаковы для обеих моделей. Долгоживущие звёзды
не дробились алгоритмом: ему переданы уже существующие узкие партии.
До укрупнения — 4800 звёздных партий. Старая схема сохраняет 4560,
исправленная динамическая — 600. Созданы два отдельных диагностических файла,
проверены запись/чтение без изменения состояния и валидация до и после каждого
прогона. Сохранение игрока не затрагивалось.
Два прогона по 40 полных циклов на модель; порядок старая → новая → новая →
старая. Скорость 6, фиксированный шаг 0,2 с, CPU-профиль включён.
Каждый прогон восстанавливает исходное состояние и обрабатывает 800 тыс. лет.
| Метрика | Старая схема | Динамическое укрупнение |
|---|---:|---:|
| Исходных звёздных партий после укрупнения | 4 560 | 600 |
| Пиковое число партий | 4 560 | 607 |
| Среднее первого прогона | 612,606 мс | 530,285 мс |
| Среднее второго прогона | 624,625 мс | 532,578 мс |
| Среднее всех 80 циклов на модель | 618,616 мс | 531,431 мс |
| Полных циклов в секунду | 1,617 | 1,882 |
| Обработанных секунд симуляции / секунду расчёта | 0,323 | 0,376 |
| Поколения, среднее на цикл | 211,916 мс | 138,339 мс |
| Производство, среднее на цикл | 51,201 мс | 38,038 мс |
| Подготовка потоков цивилизаций | 113,371 мс | 113,270 мс |
| Применение потоков цивилизаций | 114,343 мс | 113,820 мс |
| Колонизация | 55,284 мс | 55,291 мс |
| Обработка партий новорождённых, вложенная в поколения | 29,464 мс | 2,967 мс |
**Выигрыш полного цикла подтверждён обоими парными сравнениями:** время
сократилось на 14,1%, ускорение около 1,16 раза. Поколения занимают на 34,7%
меньше времени. Механизм даёт эффект при множестве подходящих для объединения
партий; в предыдущем сценарии с небольшим числом широких партий эффект был
незаметен. Изменение моментов переходов допускается моделью: конечные состояния
двух схем не обязаны совпадать, но обе полностью обработали запрошенный интервал.
Пять циклов в секунду не достигнуты: для данного сценария требуется ещё
примерно 2,66-кратное ускорение. Главные оставшиеся затраты — поколения и
подготовка/применение цивилизационных потоков; колонизация также заметна.
Сохранения: `many_objects_fragmented_stars_legacy.adventure.json` и
`many_objects_fragmented_stars_cycle.adventure.json`.
Полные результаты: `many_objects_fragmented_stars_results.json`.
```sh
godot --headless --path . --script res://tests/benchmark_many_objects.gd -- cycles=40 repeats=2
```
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,345 @@
{
"cycles_per_run": 50,
"delta_seconds": 0.2,
"profile_enabled": true,
"repeats": 2,
"runs": [
{
"cycles": 50,
"cycles_per_second": 2.39655612966922,
"final_stellar_cohorts": 130,
"game_years": "1000000",
"max_ms": 454.421,
"mean_ms": 417.26542,
"min_ms": 349.968,
"model": "legacy",
"p95_ms": 448.31,
"peak_stellar_cohorts": 132,
"profile": {
"assign_homes": [
58334,
200
],
"civilization_flow_capacity": [
308485,
50
],
"civilization_flow_colonies": [
272661,
50
],
"civilization_flow_finish": [
432935,
50
],
"civilization_flow_ledger": [
2884631,
50
],
"civilization_flow_plan": [
5425115,
50
],
"civilization_operator_build": [
151687,
50
],
"civilization_operator_solve": [
1609348,
2150
],
"enforce_capacity": [
871224,
200
],
"generation_products": [
542267,
100
],
"generation_withdrawal": [
1820057,
150
],
"generations": [
7751170,
50
],
"initialize": [
297786,
50
],
"newborn_batches": [
375307,
8322
],
"physical_amounts_build": [
1351719,
900
],
"production": [
1410303,
50
],
"trends": [
879262,
50
]
},
"repeat": 1,
"throughput": 0.479311225933843
},
{
"cycles": 50,
"cycles_per_second": 2.36172713483247,
"final_stellar_cohorts": 1300,
"game_years": "1000000",
"max_ms": 460.373,
"mean_ms": 423.41894,
"min_ms": 350.862,
"model": "cycle",
"p95_ms": 458.712,
"peak_stellar_cohorts": 1300,
"profile": {
"assign_homes": [
52297,
176
],
"civilization_flow_capacity": [
314752,
50
],
"civilization_flow_colonies": [
277416,
50
],
"civilization_flow_finish": [
442436,
50
],
"civilization_flow_ledger": [
3097867,
50
],
"civilization_flow_plan": [
5500302,
50
],
"civilization_operator_build": [
156156,
50
],
"civilization_operator_solve": [
1624350,
2150
],
"enforce_capacity": [
841074,
176
],
"generation_products": [
441260,
76
],
"generation_withdrawal": [
2058882,
126
],
"generations": [
7358962,
50
],
"initialize": [
304251,
50
],
"newborn_batches": [
211077,
5960
],
"physical_amounts_build": [
1293092,
826
],
"production": [
1725690,
50
],
"trends": [
899707,
50
]
},
"repeat": 1,
"throughput": 0.472345426966493
},
{
"cycles": 50,
"cycles_per_second": 2.34186004575995,
"final_stellar_cohorts": 1300,
"game_years": "1000000",
"max_ms": 465.408,
"mean_ms": 427.011,
"min_ms": 351.168,
"model": "cycle",
"p95_ms": 460.645,
"peak_stellar_cohorts": 1300,
"profile": {
"assign_homes": [
52872,
176
],
"civilization_flow_capacity": [
317075,
50
],
"civilization_flow_colonies": [
279484,
50
],
"civilization_flow_finish": [
446195,
50
],
"civilization_flow_ledger": [
3118109,
50
],
"civilization_flow_plan": [
5537625,
50
],
"civilization_operator_build": [
158082,
50
],
"civilization_operator_solve": [
1637405,
2150
],
"enforce_capacity": [
854481,
176
],
"generation_products": [
445328,
76
],
"generation_withdrawal": [
2078977,
126
],
"generations": [
7431634,
50
],
"initialize": [
308950,
50
],
"newborn_batches": [
213741,
5960
],
"physical_amounts_build": [
1308770,
826
],
"production": [
1744677,
50
],
"trends": [
903605,
50
]
},
"repeat": 2,
"throughput": 0.468372009151989
},
{
"cycles": 50,
"cycles_per_second": 2.31974771537327,
"final_stellar_cohorts": 130,
"game_years": "1000000",
"max_ms": 462.587,
"mean_ms": 431.08136,
"min_ms": 364.143,
"model": "legacy",
"p95_ms": 461.998,
"peak_stellar_cohorts": 132,
"profile": {
"assign_homes": [
60468,
200
],
"civilization_flow_capacity": [
318914,
50
],
"civilization_flow_colonies": [
281067,
50
],
"civilization_flow_finish": [
446947,
50
],
"civilization_flow_ledger": [
2979604,
50
],
"civilization_flow_plan": [
5570636,
50
],
"civilization_operator_build": [
158593,
50
],
"civilization_operator_solve": [
1643754,
2150
],
"enforce_capacity": [
904863,
200
],
"generation_products": [
564253,
100
],
"generation_withdrawal": [
1881469,
150
],
"generations": [
8019908,
50
],
"initialize": [
312899,
50
],
"newborn_batches": [
389499,
8322
],
"physical_amounts_build": [
1396769,
900
],
"production": [
1458932,
50
],
"trends": [
909402,
50
]
},
"repeat": 2,
"throughput": 0.463949543074653
}
],
"speed": 6
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,345 @@
{
"cycles_per_run": 50,
"delta_seconds": 0.2,
"profile_enabled": true,
"repeats": 2,
"runs": [
{
"cycles": 50,
"cycles_per_second": 2.3787548645537,
"final_stellar_cohorts": 130,
"game_years": "1000000",
"max_ms": 453.742,
"mean_ms": 420.388,
"min_ms": 354.113,
"model": "legacy",
"p95_ms": 452.604,
"peak_stellar_cohorts": 132,
"profile": {
"assign_homes": [
59021,
200
],
"civilization_flow_capacity": [
310616,
50
],
"civilization_flow_colonies": [
276485,
50
],
"civilization_flow_finish": [
436890,
50
],
"civilization_flow_ledger": [
2908982,
50
],
"civilization_flow_plan": [
5444724,
50
],
"civilization_operator_build": [
152980,
50
],
"civilization_operator_solve": [
1604917,
2150
],
"enforce_capacity": [
879602,
200
],
"generation_products": [
550010,
100
],
"generation_withdrawal": [
1834842,
150
],
"generations": [
7827667,
50
],
"initialize": [
300448,
50
],
"newborn_batches": [
379245,
8322
],
"physical_amounts_build": [
1360704,
900
],
"production": [
1417179,
50
],
"trends": [
884554,
50
]
},
"repeat": 1,
"throughput": 0.47575097291074
},
{
"cycles": 50,
"cycles_per_second": 2.3567861205283,
"final_stellar_cohorts": 125,
"game_years": "1000000",
"max_ms": 461.919,
"mean_ms": 424.30664,
"min_ms": 355.62,
"model": "cycle",
"p95_ms": 459.138,
"peak_stellar_cohorts": 127,
"profile": {
"assign_homes": [
59817,
200
],
"civilization_flow_capacity": [
314882,
50
],
"civilization_flow_colonies": [
279378,
50
],
"civilization_flow_finish": [
442443,
50
],
"civilization_flow_ledger": [
2950938,
50
],
"civilization_flow_plan": [
5516132,
50
],
"civilization_operator_build": [
156869,
50
],
"civilization_operator_solve": [
1618413,
2150
],
"enforce_capacity": [
892637,
200
],
"generation_products": [
529169,
100
],
"generation_withdrawal": [
1861386,
150
],
"generations": [
7833909,
50
],
"initialize": [
304443,
50
],
"newborn_batches": [
332349,
7522
],
"physical_amounts_build": [
1382887,
900
],
"production": [
1444907,
50
],
"trends": [
896615,
50
]
},
"repeat": 1,
"throughput": 0.471357224105661
},
{
"cycles": 50,
"cycles_per_second": 2.33373492243879,
"final_stellar_cohorts": 125,
"game_years": "1000000",
"max_ms": 471.891,
"mean_ms": 428.49768,
"min_ms": 363.562,
"model": "cycle",
"p95_ms": 458.377,
"peak_stellar_cohorts": 127,
"profile": {
"assign_homes": [
60419,
200
],
"civilization_flow_capacity": [
317546,
50
],
"civilization_flow_colonies": [
281187,
50
],
"civilization_flow_finish": [
447446,
50
],
"civilization_flow_ledger": [
2973320,
50
],
"civilization_flow_plan": [
5561963,
50
],
"civilization_operator_build": [
157327,
50
],
"civilization_operator_solve": [
1631869,
2150
],
"enforce_capacity": [
900410,
200
],
"generation_products": [
541714,
100
],
"generation_withdrawal": [
1875079,
150
],
"generations": [
7926735,
50
],
"initialize": [
307215,
50
],
"newborn_batches": [
336903,
7522
],
"physical_amounts_build": [
1400661,
900
],
"production": [
1457871,
50
],
"trends": [
908259,
50
]
},
"repeat": 2,
"throughput": 0.466746984487757
},
{
"cycles": 50,
"cycles_per_second": 2.31693373652923,
"final_stellar_cohorts": 130,
"game_years": "1000000",
"max_ms": 466.374,
"mean_ms": 431.60492,
"min_ms": 366.359,
"model": "legacy",
"p95_ms": 465.621,
"peak_stellar_cohorts": 132,
"profile": {
"assign_homes": [
60684,
200
],
"civilization_flow_capacity": [
318805,
50
],
"civilization_flow_colonies": [
282225,
50
],
"civilization_flow_finish": [
448335,
50
],
"civilization_flow_ledger": [
2980134,
50
],
"civilization_flow_plan": [
5579243,
50
],
"civilization_operator_build": [
157141,
50
],
"civilization_operator_solve": [
1638102,
2150
],
"enforce_capacity": [
904255,
200
],
"generation_products": [
565486,
100
],
"generation_withdrawal": [
1885470,
150
],
"generations": [
8034541,
50
],
"initialize": [
308996,
50
],
"newborn_batches": [
388637,
8322
],
"physical_amounts_build": [
1400258,
900
],
"production": [
1465783,
50
],
"trends": [
913581,
50
]
},
"repeat": 2,
"throughput": 0.463386747305846
}
],
"speed": 6
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,345 @@
{
"cycles_per_run": 40,
"delta_seconds": 0.2,
"profile_enabled": true,
"repeats": 2,
"runs": [
{
"cycles": 40,
"cycles_per_second": 1.6323701614814,
"final_stellar_cohorts": 105,
"game_years": "800000",
"max_ms": 628.888,
"mean_ms": 612.60615,
"min_ms": 564.108,
"model": "legacy",
"p95_ms": 624.987,
"peak_stellar_cohorts": 4560,
"profile": {
"assign_homes": [
48558,
160
],
"civilization_flow_capacity": [
263456,
40
],
"civilization_flow_colonies": [
2192836,
40
],
"civilization_flow_finish": [
401558,
40
],
"civilization_flow_ledger": [
4523753,
40
],
"civilization_flow_plan": [
4491368,
40
],
"civilization_operator_build": [
128185,
40
],
"civilization_operator_solve": [
1314272,
1720
],
"enforce_capacity": [
755412,
160
],
"generation_products": [
621550,
80
],
"generation_withdrawal": [
2524452,
120
],
"generations": [
8393123,
40
],
"initialize": [
293736,
40
],
"newborn_batches": [
1166060,
19424
],
"physical_amounts_build": [
1707597,
675
],
"production": [
2026863,
40
],
"trends": [
716666,
40
]
},
"repeat": 1,
"throughput": 0.32647403229628
},
{
"cycles": 40,
"cycles_per_second": 1.88577938012452,
"final_stellar_cohorts": 99,
"game_years": "800000",
"max_ms": 564.379,
"mean_ms": 530.284725,
"min_ms": 448.092,
"model": "cycle",
"p95_ms": 560.287,
"peak_stellar_cohorts": 607,
"profile": {
"assign_homes": [
39924,
132
],
"civilization_flow_capacity": [
265954,
40
],
"civilization_flow_colonies": [
2204837,
40
],
"civilization_flow_finish": [
404568,
40
],
"civilization_flow_ledger": [
4540902,
40
],
"civilization_flow_plan": [
4517593,
40
],
"civilization_operator_build": [
128322,
40
],
"civilization_operator_solve": [
1324328,
1720
],
"enforce_capacity": [
672871,
132
],
"generation_products": [
184761,
52
],
"generation_withdrawal": [
2150050,
92
],
"generations": [
5519639,
40
],
"initialize": [
295834,
40
],
"newborn_batches": [
118202,
3115
],
"physical_amounts_build": [
1594387,
612
],
"production": [
1526313,
40
],
"trends": [
723713,
40
]
},
"repeat": 1,
"throughput": 0.377155876024903
},
{
"cycles": 40,
"cycles_per_second": 1.87765994001252,
"final_stellar_cohorts": 99,
"game_years": "800000",
"max_ms": 566.428,
"mean_ms": 532.5778,
"min_ms": 456.191,
"model": "cycle",
"p95_ms": 565.345,
"peak_stellar_cohorts": 607,
"profile": {
"assign_homes": [
40238,
132
],
"civilization_flow_capacity": [
264957,
40
],
"civilization_flow_colonies": [
2218466,
40
],
"civilization_flow_finish": [
405624,
40
],
"civilization_flow_ledger": [
4564735,
40
],
"civilization_flow_plan": [
4543992,
40
],
"civilization_operator_build": [
127427,
40
],
"civilization_operator_solve": [
1338172,
1720
],
"enforce_capacity": [
672773,
132
],
"generation_products": [
185633,
52
],
"generation_withdrawal": [
2159985,
92
],
"generations": [
5547513,
40
],
"initialize": [
297516,
40
],
"newborn_batches": [
119187,
3115
],
"physical_amounts_build": [
1590461,
612
],
"production": [
1516716,
40
],
"trends": [
731042,
40
]
},
"repeat": 2,
"throughput": 0.375531988002504
},
{
"cycles": 40,
"cycles_per_second": 1.60095961519335,
"final_stellar_cohorts": 105,
"game_years": "800000",
"max_ms": 660.704,
"mean_ms": 624.625375,
"min_ms": 562.732,
"model": "legacy",
"p95_ms": 638.671,
"peak_stellar_cohorts": 4560,
"profile": {
"assign_homes": [
49183,
160
],
"civilization_flow_capacity": [
268901,
40
],
"civilization_flow_colonies": [
2229856,
40
],
"civilization_flow_finish": [
406135,
40
],
"civilization_flow_ledger": [
4623708,
40
],
"civilization_flow_plan": [
4578330,
40
],
"civilization_operator_build": [
130249,
40
],
"civilization_operator_solve": [
1340466,
1720
],
"enforce_capacity": [
765788,
160
],
"generation_products": [
635386,
80
],
"generation_withdrawal": [
2573324,
120
],
"generations": [
8560126,
40
],
"initialize": [
297396,
40
],
"newborn_batches": [
1191081,
19424
],
"physical_amounts_build": [
1738195,
675
],
"production": [
2069255,
40
],
"trends": [
735591,
40
]
},
"repeat": 2,
"throughput": 0.320191923038669
}
],
"speed": 6
}
+63
View File
@@ -0,0 +1,63 @@
extends SceneTree
## Full-cycle throughput benchmark; diagnostic input is never modified.
const E = preload("res://world/owniverse/math/exact.gd")
func _initialize():
call_deferred("run")
func restore_json_integers(value):
# JSON parses ordinary integer tokens as float. Large integers are $exact.
if value is float and value == floorf(value) and absf(value) <= 9007199254740991:
return int(value)
if value is Dictionary:
for key in value: value[key] = restore_json_integers(value[key])
elif value is Array:
for i in value.size(): value[i] = restore_json_integers(value[i])
return value
func run():
var args = OS.get_cmdline_user_args()
if args.is_empty():
print("Usage: --script tests/benchmark_civilization_flows.gd -- /path/to/save.adventure.json [delta_seconds] [profile] [cycles=100]")
quit(1)
return
var data = JSON.parse_string(FileAccess.get_file_as_string(args[0]))
if not data is Dictionary or data.get("format_version") != 6:
push_error("Expected diagnostic JSON format 6")
quit(1)
return
var initial: Dictionary = E.decode(restore_json_integers(data)).state
var validation: String = root.get_node("SaveManager").validate_data(E.decode(restore_json_integers(data)))
print("Save validation: ", "OK" if validation.is_empty() else validation)
var delta: float = args[1].to_float() if args.size() > 1 else 1.0 / 60.0
if not is_finite(delta) or delta <= 0:
quit(1)
return
for speed in [6]:
var world = load("res://world/owniverse/owniverse.gd").new()
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
world.state = initial.duplicate(true)
world.speed = speed
world.profile_enabled = args.has("profile")
var elapsed: Array = []
var cycles: int = 100
for arg in args:
if arg.begins_with("cycles="): cycles = maxi(1, arg.trim_prefix("cycles=").to_int())
var initial_clock: Variant = world.state.time.datetime
for cycle in cycles:
var start: int = Time.get_ticks_usec()
world.evolute(delta)
elapsed.append((Time.get_ticks_usec() - start) / 1000.0)
print("Speed %d, %d complete %.3f-second steps" % [speed, cycles, delta])
print("Min/max ms: ", elapsed.min(), "/", elapsed.max())
print("Game years applied: ", E.fixed(E.subtract(world.state.time.datetime, initial_clock)))
var total_ms: float = 0.0
for sample in elapsed: total_ms += sample
var throughput: float = float(cycles) * delta / (total_ms / 1000.0)
print("Mean complete cycle ms: ", total_ms / cycles)
print("Full cycles/s: ", float(cycles) * 1000.0 / total_ms)
print("Simulation throughput: %.3f real-time seconds processed per CPU second; needs >= 1.0 to keep up" % throughput)
if world.profile_enabled: print("CPU profile (usec, calls): ", world.profile)
for entity in world.catalog.entities.values(): entity.free()
world.free()
quit()
@@ -0,0 +1 @@
uid://biwpwcsboh0s7
+1 -1
View File
@@ -40,7 +40,7 @@ func run():
for id in stage.owniverse.catalog.entities:
if E.gt(stage.owniverse.ledger.physical_amount(stage.owniverse.state, id), 0): remaining_objects += 1
print("Remaining objects: ", remaining_objects)
print("Final adaptive step: ", stage.owniverse.frame_step_seconds)
print("Simulation step: ", stage.owniverse.frame_step_seconds)
for entity in stage.owniverse.catalog.entities.values(): entity.free()
stage.owniverse.free()
scene.free()
+130
View File
@@ -0,0 +1,130 @@
extends SceneTree
## Paired old/new cohort timing on separate generated large diagnostic saves.
const E = preload("res://world/owniverse/math/exact.gd")
var cycles: int = 50
var repeats: int = 2
var artifact_prefix: String = "large_star_cohorts"
func _initialize(): call_deferred("run")
func restore_json_integers(value):
if value is float and value == floorf(value) and absf(value) <= 9007199254740991: return int(value)
if value is Dictionary:
for key in value: value[key] = restore_json_integers(value[key])
elif value is Array:
for i in value.size(): value[i] = restore_json_integers(value[i])
return value
func dispose(world):
for entity in world.catalog.entities.values(): entity.free()
world.free()
func world_from(state: Dictionary, use_cycles: bool):
var world = load("res://world/owniverse/owniverse.gd").new()
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
world.state = state.duplicate(true)
world.speed = 6
world.batches.cycle_stars = use_cycles
return world
func cohort_count(world) -> int:
var total: int = 0
for id in world.state.batches:
if world.catalog.entities[id].group_as == "Star": total += world.state.batches[id].size()
return total
func make_state(initial: Dictionary, use_cycles: bool) -> Dictionary:
var world = world_from(initial, use_cycles)
var anchor: Variant = E.of("1e12")
var shift: Variant = E.subtract(anchor, world.state.time.datetime)
for id in world.state.batches:
for batch: Dictionary in world.state.batches[id]:
for field in ["created_from", "created_to", "start_year", "end_year", "processed_until"]:
if batch.has(field): batch[field] = E.add(batch[field], shift)
for id in world.state.batch_origins:
world.state.batch_origins[id] = E.add(world.state.batch_origins[id], shift)
world.state.time = {"datetime": anchor, "year": anchor}
var physical: Dictionary = world.ledger.physical_amounts(world.state)
var populations: Dictionary = {}
for id in world.catalog.entities:
var entity = world.catalog.entities[id]
if entity.group_as != "Star" or E.le(entity.lifespan, 0) or E.le(physical.get(id, 0), 0): continue
world.state.batches[id] = []
world.state.batch_origins.erase(id)
var lifespan: Variant = world.batches.effective_lifespan(world.state, id)
var birth_from: Variant = E.subtract(anchor, lifespan)
var duration: Variant = E.minimum(lifespan, 2000000)
world.batches.append_batch(world.state, id, physical[id], birth_from, E.add(birth_from, duration))
populations[id] = E.fixed(physical[id])
var payload: Dictionary = {"format_version": 6, "app_version": "0.7.0", "ui": {"current_page": 0}, "state": world.state,
"simulation": {"active_clock": {}, "requested_clock": {}, "productions": [], "speed": 6}}
var error: String = root.get_node("SaveManager").validate_data(payload)
assert(error.is_empty(), error)
var suffix: String = "cycle" if use_cycles else "legacy"
var path: String = "res://tests/artifacts/%s_%s.adventure.json" % [artifact_prefix, suffix]
var output = FileAccess.open(path, FileAccess.WRITE)
assert(output != null)
output.store_string(JSON.stringify(E.encode(payload), " ", true, true))
output.close()
print("Generated %s: star_types=%d stellar_cohorts=%d populations=%s" % [suffix, populations.size(), cohort_count(world), str(populations)])
var restored: Dictionary = E.decode(restore_json_integers(JSON.parse_string(FileAccess.get_file_as_string(path))))
assert(E.eq(restored.state, world.state), "Diagnostic file roundtrip changed initial state")
dispose(world)
return restored.state
func measure(initial: Dictionary, use_cycles: bool, run_index: int) -> Dictionary:
var world = world_from(initial, use_cycles)
world.profile_enabled = true
var samples: Array = []
var total_usec: int = 0
var initial_year: Variant = world.state.time.datetime
var peak_cohorts: int = cohort_count(world)
for cycle in cycles:
var started: int = Time.get_ticks_usec()
world.evolute(0.2)
var elapsed: int = Time.get_ticks_usec() - started
total_usec += elapsed
samples.append(elapsed / 1000.0)
peak_cohorts = maxi(peak_cohorts, cohort_count(world))
var applied: Variant = E.subtract(world.state.time.datetime, initial_year)
assert(E.eq(applied, cycles * 20000))
var error: String = root.get_node("SaveManager").validate_data({"format_version": 6, "app_version": "0.7.0", "ui": {"current_page": 0}, "state": world.state})
assert(error.is_empty(), error)
samples.sort()
var result: Dictionary = {"model": "cycle" if use_cycles else "legacy", "repeat": run_index, "cycles": cycles,
"mean_ms": total_usec / (1000.0 * cycles), "min_ms": samples.front(), "max_ms": samples.back(), "p95_ms": samples[int(ceil(samples.size() * 0.95)) - 1],
"cycles_per_second": cycles * 1000000.0 / total_usec, "throughput": cycles * 200000.0 / total_usec,
"game_years": E.fixed(applied), "peak_stellar_cohorts": peak_cohorts, "final_stellar_cohorts": cohort_count(world), "profile": world.profile.duplicate(true)}
print("PAIRED_RESULT ", JSON.stringify(result))
dispose(world)
return result
func run():
for arg in OS.get_cmdline_user_args():
if arg.begins_with("cycles="): cycles = maxi(1, arg.trim_prefix("cycles=").to_int())
if arg.begins_with("repeats="): repeats = maxi(1, arg.trim_prefix("repeats=").to_int())
var raw = JSON.parse_string(FileAccess.get_file_as_string("res://tests/artifacts/all_objects_speed6.adventure.json"))
var payload: Dictionary = E.decode(restore_json_integers(raw))
assert(root.get_node("SaveManager").validate_data(payload).is_empty())
var legacy: Dictionary = make_state(payload.state, false)
var current: Dictionary = make_state(payload.state, true)
var reference = world_from(legacy, false)
var candidate = world_from(current, true)
assert(E.eq(reference.ledger.physical_amounts(reference.state), candidate.ledger.physical_amounts(candidate.state)))
assert(E.eq(reference.state.ethics, candidate.state.ethics))
assert(cohort_count(candidate) <= cohort_count(reference), "Dynamic widening increased the fixture cohort count")
dispose(reference)
dispose(candidate)
if OS.get_cmdline_user_args().has("generate_only"):
print("Generated states validate and survive disk roundtrip; physical amounts and ethics match")
quit()
return
var results: Array = []
for repeat_index in repeats:
var order: Array = [false, true] if repeat_index % 2 == 0 else [true, false]
for use_cycles in order:
results.append(measure(current if use_cycles else legacy, use_cycles, repeat_index + 1))
var output = FileAccess.open("res://tests/artifacts/%s_results.json" % artifact_prefix, FileAccess.WRITE)
output.store_string(JSON.stringify({"delta_seconds": 0.2, "speed": 6, "cycles_per_run": cycles, "repeats": repeats, "profile_enabled": true, "runs": results}, " "))
output.close()
quit()
@@ -0,0 +1 @@
uid://hl20vbt440nb
+111
View File
@@ -0,0 +1,111 @@
extends "res://tests/benchmark_large_star_cohorts.gd"
## Fresh all-object stress world with many valid cohorts sharing death cycles.
const Fixture = preload("res://tests/fixtures/civilization_benchmark.gd")
var death_cycles: int = 40
var fragments_per_cycle: int = 8
func _initialize():
artifact_prefix = "many_objects_fragmented_stars"
cycles = 40
call_deferred("run")
func make_state(_initial: Dictionary, use_cycles: bool) -> Dictionary:
var world = load("res://world/owniverse/owniverse.gd").new()
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
var global: Node = root.get_node("Global")
Fixture.seed(world, global)
world.batches.cycle_stars = use_cycles
var anchor: Variant = E.of("1e12")
var stock: Variant = E.of("1e60")
var planets: Variant = E.of("1e28")
world.state.time = {"datetime": anchor, "year": anchor}
world.state.energy = E.of("1e100")
world.state.batches = {}
world.state.batch_origins = {}
world.state.binds = {}
world.state.ethics = {}
for id in world.catalog.entities:
world.state.amount[id] = 0 if world.catalog.is_group(id) else (planets if world.catalog.entities[id].group_as == "Planet" else stock)
world.state.stats.total_amount[id] = world.state.amount[id]
# Keep the synthetic universe sparse enough for blast damage to coexist
# with all life stages instead of sterilizing them on the first cycle.
world.state.amount.S = E.of("1e300")
world.state.amount.H = E.of("1e80")
world.state.amount.He = E.of("1e79")
world.state.amount.Met = E.of("1e79")
var dwarfs: Array = ["RD", "OD", "YD", "WYD"]
var stages: int = 0
for id in global.CivilizationRules:
world.state.amount[id] = planets
var weights: Array = [0.35, 0.2, 0.15, 0.12, 0.1, 0.08]
var profile: Dictionary = {}
var ethics: Array = ["m", "l", "w", "d", "c", "b"]
for e in 6: profile[ethics[e]] = weights[(e + stages) % 6]
world.state.ethics[id] = profile
stages += 1
# Planetary stages are hosted by all four kinds of dwarf stars.
for id in world.catalog.entities:
if world.catalog.entities[id].group_as != "Planet" or world.catalog.is_group(id): continue
if global.CivilizationRules.has(id) and str(global.CivilizationRules[id].home) != "": continue
var held: Variant = E.divide(world.state.amount[id], 4)
world.state.amount[id] = 0
for dwarf in dwarfs:
if not world.state.binds.has(dwarf): world.state.binds[dwarf] = {}
world.state.binds[dwarf][id] = held
# Bound stars remain physical and continue producing inside structures.
var structures: Array = ["SCD", "SCM", "SCG", "GEM", "GSD"]
var stellar_types: int = 0
for id in world.catalog.entities:
if world.catalog.entities[id].group_as != "Star" or E.le(world.catalog.entities[id].lifespan, 0): continue
var held: Variant = E.divide(stock, 4)
var owner: String = structures[stellar_types % structures.size()]
if not world.state.binds.has(owner): world.state.binds[owner] = {}
world.state.binds[owner][id] = held
world.state.amount[id] = E.subtract(stock, held)
stellar_types += 1
world.ledger.refresh_groups(world.state)
var physical: Dictionary = world.ledger.physical_amounts(world.state)
var active_types: int = 0
for id in world.catalog.entities:
if world.catalog.is_group(id) or E.le(physical.get(id, 0), 0): continue
active_types += 1
world.state.stats.total_amount[id] = physical[id]
var entity = world.catalog.entities[id]
if E.le(entity.lifespan, 0) or global.CivilizationRules.has(id): continue
if entity.group_as != "Star":
world.batches.append_batch(world.state, id, physical[id], anchor, anchor)
continue
# Valid but deliberately fragmented historical age cohorts. The test
# stresses merge eligibility, rather than claiming these are produced
# by the ordinary steady-state creation path with one fixed origin.
var span: Variant = world.batches.effective_lifespan(world.state, id)
var count: int = death_cycles * fragments_per_cycle
var assigned: Variant = 0
world.state.batches[id] = []
for n in count:
var cycle: int = n / fragments_per_cycle
var fragment: int = n % fragments_per_cycle
var start: Variant = E.add(anchor, cycle * 20000 + (fragment + 1) * 1800)
var end: Variant = E.add(start, 500)
var quota: Variant = E.floor_value(E.mul_div(physical[id], n + 1, count))
world.state.batches[id].append({"amount": E.subtract(quota, assigned), "created_from": E.subtract(start, span), "created_to": E.subtract(end, span), "start_year": start, "end_year": end})
assigned = quota
assert(E.eq(assigned, physical[id]))
world.state.batch_origins[id] = world.state.batches[id][0].created_from
var raw_cohorts: int = cohort_count(world)
world.batches.coarsen(world.state)
var payload: Dictionary = {"format_version": 6, "app_version": "0.7.0", "ui": {"current_page": 0}, "state": world.state,
"simulation": {"active_clock": {}, "requested_clock": {}, "productions": [], "speed": 6}}
var error: String = root.get_node("SaveManager").validate_data(payload)
assert(error.is_empty(), error)
var suffix: String = "cycle" if use_cycles else "legacy"
var path: String = "res://tests/artifacts/%s_%s.adventure.json" % [artifact_prefix, suffix]
var output = FileAccess.open(path, FileAccess.WRITE)
assert(output != null)
output.store_string(JSON.stringify(E.encode(payload), " ", true, true))
output.close()
var restored: Dictionary = E.decode(restore_json_integers(JSON.parse_string(FileAccess.get_file_as_string(path))))
assert(E.eq(restored.state, world.state), "Save roundtrip changed the stress state")
print("Generated %s: active_types=%d civilization_stages=%d stellar_types=%d raw_cohorts=%d stored_stellar_cohorts=%d" % [suffix, active_types, stages, stellar_types, raw_cohorts, cohort_count(world)])
dispose(world)
return restored.state
+1
View File
@@ -0,0 +1 @@
uid://d0plagt7blmcf
+98
View File
@@ -109,5 +109,103 @@ func run() -> void:
world.batches.withdraw_batches(world.state, "N", 1)
check(world.state.batches.N.is_empty(), "Scaling invalidates readiness and removes zero cohorts")
dispose(world)
for selected_speed in [4, 5, 6]:
world = fixture()
world.speed = selected_speed
world.state.time = {"datetime": 37, "year": 37}
var cycle_width: Variant = E.multiply(E.power(10, selected_speed - 1), E.of("0.2"))
world.catalog.entities.HG.lifespan = E.multiply(cycle_width, 3)
world.state.batches = {}
var life: Variant = world.batches.effective_lifespan(world.state, "HG")
var deadline: Variant = E.add(37, E.multiply(cycle_width, 4))
world.batches.append_batch(world.state, "HG", huge, E.add(37, 1), E.add(37, 1))
world.batches.append_batch(world.state, "HG", 7, E.add(37, 2), E.add(37, 2))
check(world.state.batches.HG.size() == 1 and E.eq(world.state.batches.HG[0].amount, E.add(huge, 7)), "Births in the same death cycle merge exactly")
var point: Dictionary = world.state.batches.HG[0]
check(E.eq(point.start_year, deadline) and E.eq(point.end_year, deadline), "High-speed stellar cohort transitions at one cycle boundary")
var born: Variant = E.add(37, 1)
var delay: Variant = E.subtract(deadline, E.add(born, life))
check(E.ge(delay, 0) and E.le(delay, cycle_width), "Transition is delayed by at most one cycle")
var encoded: Dictionary = E.decode(E.encode(world.state))
check(E.eq(encoded.batches, world.state.batches), "Cycle cohorts survive exact save roundtrip")
check(world.batches.withdraw_batches(world.state, "HG", E.subtract(deadline, 1)).is_empty(), "Cycle cohort is not withdrawn partially")
var expired: Array = world.batches.withdraw_batches(world.state, "HG", deadline)
check(expired.size() == 1 and E.eq(expired[0].amount, E.add(huge, 7)) and world.state.batches.HG.is_empty(), "Boundary withdraws the exact whole cohort once")
check(world.batches.withdraw_batches(world.state, "HG", deadline).is_empty(), "Repeated boundary cannot withdraw twice")
world.batches.append_batch(world.state, "HG", 3, E.add(37, 1), E.add(37, 1))
check(E.eq(world.state.batches.HG[0].amount, 3), "Withdrawal invalidates cycle insertion references")
world.state.batches.HG = []
world.batches.append_batch(world.state, "HG", 11, 37, E.add(37, E.multiply(cycle_width, 3)))
var total: Variant = 0
for batch: Dictionary in world.state.batches.HG:
total = E.add(total, batch.amount)
check(E.eq(batch.start_year, batch.end_year), "Distributed births create point cohorts")
check(E.eq(total, 11) and world.state.batches.HG.size() == 3, "Distributed births retain all integers across death cycles")
var before_coarsen: Dictionary = world.state.duplicate(true)
world.batches.coarsen(world.state)
check(E.eq(before_coarsen, world.state), "Coarsening cannot smear point stellar transitions")
world.state.batches.HG = []
var completed: Dictionary = {}
var cutoff: Variant = E.add(37, E.multiply(cycle_width, 10))
var integral: Variant = world.batches.append_batch(world.state, "HG", 11, 37, E.add(37, E.multiply(cycle_width, 3)), completed, cutoff)
var reference_integral: Variant = 0
total = 0
for part: Dictionary in completed.HG:
total = E.add(total, part.amount)
reference_integral = E.add(reference_integral, E.multiply(part.amount, E.subtract(cutoff, part.to)))
reference_integral = E.subtract(E.multiply(11, E.subtract(cutoff, E.add(37, E.multiply(cycle_width, E.of("1.5"))))), reference_integral)
check(E.eq(total, 11) and world.state.batches.HG.is_empty(), "Already completed stars retain counts without stored batches")
check(E.eq(integral, reference_integral), "Lifetime production agrees with point transitions")
world.speed = 1
world.state.batches.HG = []
world.batches.append_batch(world.state, "HG", 10, 100, 100)
check(E.gt(world.state.batches.HG[0].end_year, world.state.batches.HG[0].start_year), "Low speed retains gradual stellar transitions")
dispose(world)
world = fixture()
world.speed = 6
world.state.amount.RHG = 5
world.batches.append_batch(world.state, "RHG", 5, 0, 0)
world.ledger.refresh_groups(world.state)
var validation: String = root.get_node("SaveManager").validate_data({"format_version": 6, "app_version": "", "ui": {"current_page": 0}, "state": world.state})
check(validation.is_empty(), "Cycle-star state born at zero validates: " + validation)
var lifespan: Variant = world.batches.effective_lifespan(world.state, "RHG")
check(E.eq(world.state.batches.RHG[0].start_year, lifespan), "Birth exactly aligned with a death boundary adds no extra cycle")
dispose(world)
world = fixture()
world.speed = 6
world.state.batches.RD = []
world.batches.append_batch(world.state, "RD", huge, 0, 2000000)
check(world.state.batches.RD.size() == 1, "Long-lived stars are not subdivided into cycle buckets")
var long_cohort: Dictionary = world.state.batches.RD[0].duplicate(true)
world.batches.append_batch(world.state, "RD", 7, 10000, 20000)
check(world.state.batches.RD.size() == 1 and E.eq(world.state.batches.RD[0].amount, E.add(huge, 7)), "New births still merge into their original wide stellar group")
check(E.eq(world.state.batches.RD[0].start_year, long_cohort.start_year) and E.eq(world.state.batches.RD[0].end_year, long_cohort.end_year), "Long-lived transition interval is preserved")
world.state.batches.RD = [
{"amount": huge, "start_year": 1000, "end_year": 2000, "withdraw_remainder": 0.25},
{"amount": 7, "start_year": 3000, "end_year": 4000, "withdraw_remainder": 0.5},
{"amount": 9, "start_year": 19000, "end_year": 21000},
{"amount": 11, "start_year": 22000, "end_year": 1000000},
{"amount": 13, "start_year": 21000, "end_year": 22000},
]
var spanning: Dictionary = world.state.batches.RD[2].duplicate(true)
var wide: Dictionary = world.state.batches.RD[3].duplicate(true)
world.batches.coarsen(world.state)
check(world.state.batches.RD.size() == 4, "Only cohorts ending entirely within the same cycle are merged")
var merged: Dictionary = {}
var broad_kept: bool = false
var crossing_kept: bool = false
for batch: Dictionary in world.state.batches.RD:
if E.eq(batch.start_year, 20000) and E.eq(batch.end_year, 20000): merged = batch
if E.eq(batch, wide): broad_kept = true
if E.eq(batch, spanning): crossing_kept = true
check(not merged.is_empty() and E.eq(merged.amount, E.add(huge, 7)) and E.eq(merged.withdraw_remainder, 0.75), "Merged stellar cohort retains exact population and carries")
check(broad_kept and crossing_kept, "Wide and cycle-crossing cohorts remain unchanged")
var once: Dictionary = world.state.duplicate(true)
world.batches.coarsen(world.state)
check(E.eq(world.state, once), "Dynamic stellar merging is idempotent")
world.state.batches.HG = [{"amount": 3, "start_year": 20000, "end_year": 20000}, {"amount": 4, "start_year": 20001, "end_year": 21000}]
world.batches.coarsen(world.state)
check(world.state.batches.HG.size() == 2, "An exact boundary belongs to the preceding cycle")
dispose(world)
print(("Batch boundaries: %d checks, %d failures" % [checks, failures]))
quit((0 if E.eq(failures, 0) else 1))
+96 -124
View File
@@ -1,164 +1,136 @@
extends SceneTree
const E = preload("res://world/owniverse/math/exact.gd")
const Clock = preload("res://world/owniverse/simulation_time.gd")
const State = preload("res://world/owniverse/civilization_state.gd")
var checks: int = 0
var failures: int = 0
func _initialize() -> void:
call_deferred("run")
func check(condition: bool, message: String) -> void:
checks = E.add(checks, 1)
if (not condition):
failures = E.add(failures, 1)
func _initialize(): call_deferred("run")
func check(value: bool, message: String) -> void:
checks += 1
if not value:
failures += 1
push_error(message)
func fixture():
var world = load("res://world/owniverse/owniverse.gd").new()
var global: Node = root.get_node("Global")
for id in global.OwniverseEntities:
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
world.catalog.entities[id] = entity
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
for id in world.catalog.entities:
world.state.amount[id] = 0
world.state.stats.total_amount[id] = 0
if E.ne(entity.group_as, ""):
world.state.amount[entity.group_as] = 0
world.state.stats.total_amount[entity.group_as] = 0
world.catalog.entities["N"].life_product = {}
world.catalog.entities["N"].product_per_year_type = "H"
world.catalog.entities["N"].product_per_year_amount = 10
world.catalog.entities["N"].auto = 0
world.state.amount.N = 1
for id in ["RD", "OD", "YD", "WYD", "BG", "TWD", "SNS", "MGT", "BH25", "SMBH", "SCD", "SCS", "SCM", "SCL", "SCG", "GED", "GES", "GEM", "GEL", "GEG", "LGG", "LSC", "LGF", "LGW", "LUB"]:
if world.state.amount.has(id): world.state.amount[id] = E.of("1e60")
return world
func dispose(world) -> void:
for entity in world.catalog.entities.values():
entity.free()
func dispose(world):
for entity in world.catalog.entities.values(): entity.free()
world.free()
func seed_population(world, id: String, count: Variant, time: Variant = 1.0) -> void:
world.state.amount.N = 0
world.state.amount[id] = count
world.state.batches[id] = [{"amount": count, "start_year": time, "end_year": time}]
world.evolution.initialize_life_batches()
func population(world) -> Variant:
var result: Variant = 0
for id in world.ledger.physical_amounts(world.state):
if root.get_node("Global").CivilizationRules.has(id) or world.catalog.entities[id].group_as == "Planet":
result = E.add(result, world.ledger.physical_amount(world.state, id))
return result
func invariants(world) -> void:
var physical: Dictionary = world.ledger.physical_amounts(world.state)
for id in physical:
check(E.ge(physical[id], 0) and E.eq(physical[id], E.floor_value(physical[id])), "Physical counts remain nonnegative whole: " + id)
for id in world.state.ethics:
var total: float = 0.0
for n in world.state.ethics[id].values():
var share: float = 0.0 + n
if not is_finite(share) or share < 0.0 or share > 1.0: check(false, "Invalid ethic share")
total += share
check(absf(total - 1.0) < 0.000001, E.add("Ethic shares sum to one for ", id))
check(E.ge(physical.get(id, 0), 0), E.add("Exact physical population remains available for ", id))
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 6, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Civilization state validates")
for value in world.state.ethics[id].values():
check(is_finite(value) and value >= 0.0 and value <= 1.0, "Finite normalized ethic shares")
total += value
check(absf(total - 1.0) < 1e-12, "Ethic shares sum to one: " + id)
func run() -> void:
func configure(rules: Dictionary, progress: float, degradation: float, span: int = 100) -> void:
for id in rules:
if rules[id].ethics.is_empty(): continue
rules[id].span = span
rules[id].fights = false
rules[id].colonizes = false
rules[id].diplomacy = false
for row in rules[id].ethics.values():
row.progress = progress
row.degradation = degradation
row.war = 0
row.colonization = 0
row.fall = rules[id].previous + ":1"
func run():
var global: Node = root.get_node("Global")
var baseline: Dictionary = global.CivilizationRules.duplicate(true)
configure(global.CivilizationRules, 1.0, 0.0)
var world = fixture()
seed_population(world, "C0.2", 60000)
world.evolution.evolve_batches(world.state, 1)
check(E.gt(world.state.amount["C0.3"], 0), "C0.2 actually progresses")
check(E.gt(world.state.amount["C0.1"], 0), "C0.2 actually degrades")
check(E.eq(world.state.civilization_stats.get("wars", 0), 0), "Table disables early wars")
check(E.eq(E.add(E.add(world.state.amount["C0.2"], world.state.amount["C0.3"]), world.state.amount["C0.1"]), 60000), "Ordinary evolution conserves population")
check(E.eq(world.state.stats.total_amount["C0.2"], 0), "Survivors do not count as new civilizations")
check(E.gt(world.state.civilization_stats.worship, 0), "Worship derived from surviving ethics")
var count: Variant = E.of("10000000000000000000000000000000000000007")
world.state.amount["C0.2"] = count
world.state.ethics["C0.2"] = State.empty_profile()
world.civilization_flow.evolve(world.state, 0, 100)
check(E.eq(population(world), count), "Continuous chain conserves arbitrary-size whole population")
check(absf(E.approx(E.divide(world.state.amount["C0.2"], count)) - exp(-1.0)) < 1e-12, "Survival follows the exponential interval law")
check(E.gt(world.state.amount["C0.4"], 0), "One interval includes multiple successive stages")
check(not world.state.batches.has("C0.2") and not world.state.batches.has("C0.3"), "Civilizations do not create age batches")
invariants(world)
var restored = fixture()
restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
world.evolution.evolve_batches(world.state, 2100)
restored.evolution.evolve_batches(restored.state, 2100)
check(E.eq(world.state, restored.state), "Save resumes deterministic ethical evolution")
invariants(world)
dispose(restored)
var half = fixture()
half.state.amount["C0.2"] = count
half.state.ethics["C0.2"] = State.empty_profile()
half.civilization_flow.evolve(half.state, 0, 50)
half.civilization_flow.evolve(half.state, 50, 100)
for id in global.CivilizationRules:
check(E.approx(E.divide(E.absolute(E.subtract(world.state.amount.get(id, 0), half.state.amount.get(id, 0))), count)) < 1e-12, "Splitting the interval preserves continuous dynamics: " + id)
dispose(half)
dispose(world)
configure(global.CivilizationRules, 0.0, 0.0)
world = fixture()
world.state.amount.RD = 100000000
seed_population(world, "C2.0", 60000)
world.state.amount["C1.9"] = 10000
world.evolution.initialize_life_batches()
world.evolution.evolve_batches(world.state, 1)
check(E.gt(world.state.civilization_stats.wars, 0), "Wars execute when enabled")
check(E.gt(world.state.civilization_stats.collateral, 0), "Wars affect previous-stage populations")
check(E.gt(world.state.amount["C2.1"], 0), "Winners and peaceful progress reach next stage")
world.state.amount["C0.2"] = 10001
world.state.ethics["C0.2"] = {"m": 1.0, "l": 0.0, "w": 0.0, "d": 0.0, "c": 0.0, "b": 0.0}
world.civilization_flow.evolve(world.state, 0, 100)
check(E.eq(world.state.amount["C0.2"], 10001), "Disabled stage movement retains every individual")
check(world.state.ethics["C0.2"].m < 1.0 and world.state.ethics["C0.2"].m > 0.9, "Analytical drift changes shares without materializing populations")
invariants(world)
dispose(world)
configure(global.CivilizationRules, 1.0, 0.0)
world = fixture()
world.state.amount.RD = 1
world.state.binds.RD = {"HP": 5, "GG": 5, "AB": 5, "RP": 5}
seed_population(world, "C1.6", 60000)
for row : Dictionary in global.CivilizationRules["C1.6"].ethics.values(): row.degradation = "0"
world.evolution.evolve_batches(world.state, 1)
var after: Dictionary = world.ledger.physical_amounts(world.state)
check(E.eq(world.state.civilization_stats.colonies, 20), "Colonization limited to real targets")
check(E.eq(E.add(E.add(E.add(after.HP, after.GG), after.AB), after.RP), 0), "Colonized objects consumed once")
check(E.eq(world.state.binds.RD.get("C1.4", 0), 20), "Colonies retain target host and use two-stages-earlier level")
var totals: Variant = 0
for id in after:
if E.eq(world.catalog.entities[id].group_as, "Planet"): totals = E.add(totals, after[id])
check(E.eq(totals, 60020), "Colonization replaces planets, does not create extra worlds")
for id in ["RD", "OD", "YD", "WYD"]: world.state.amount[id] = 0
world.state.amount["C1.9"] = 101
world.civilization_flow.evolve(world.state, 0, 100)
check(E.eq(world.state.amount["C1.9"], 101), "Unfunded interstellar flows return to their source")
check(E.eq(population(world), 101), "Resource admission conserves exact population")
dispose(world)
configure(global.CivilizationRules, 0.0, 0.0, 1)
world = fixture()
world.state.amount["C1.6"] = 60000
world.state.binds.RD = {"HP": 5, "RP": 5, "GG": 5, "AB": 5}
global.CivilizationRules["C1.6"].colonizes = true
for row in global.CivilizationRules["C1.6"].ethics.values(): row.colonization = 1
world.civilization_flow.evolve(world.state, 0, 1)
check(E.eq(world.state.civilization_stats.colonies, 20), "Integrated colonization is limited by real planet stock")
check(E.eq(world.state.binds.RD.get("C1.4", 0), 20), "Colonies retain the target stellar owner")
check(E.eq(population(world), 60020), "Colonization creates precisely the consumed number of occupied worlds")
var physical: Dictionary = world.ledger.physical_amounts(world.state)
for resource in ["HP", "RP", "GG", "AB"]: check(E.eq(physical[resource], 0), "Planet stock consumed once: " + resource)
invariants(world)
dispose(world)
global.CivilizationRules = baseline.duplicate(true)
world = fixture()
world.state.amount.RD = E.of("1e25")
world.state.amount.BG = E.of("1e25")
world.state.amount.TWD = E.of("1e25")
world.state.amount.SNS = E.of("1e25")
world.state.amount.MGT = E.of("1e25")
world.state.amount.BH25 = E.of("1e25")
world.state.amount.SMBH = E.of("1e26")
for id in world.catalog.entities:
if E.ne(world.catalog.entities[id].claim, ""): world.state.amount[id] = E.of("1e12")
# Accelerated deterministic fixture traverses every enabled stage, including Ct.
for id in global.CivilizationRules:
if E.eq(id, "Ct"): continue
global.CivilizationRules[id].fights = false
global.CivilizationRules[id].colonizes = false
global.CivilizationRules[id].diplomacy = false
for row : Dictionary in global.CivilizationRules[id].ethics.values():
row.progress = "1"
row.degradation = "0"
world.catalog.entities[id].lifespan = 1
world.catalog.entities[id].lifespan_delta = 1
global.CivilizationRules[id].span = 1
seed_population(world, "C0.2", 6)
world.evolution.evolve_batches(world.state, 1000)
check(E.eq(world.state.amount.Ct, 6), "Complete C0.2 -> C4.4 -> Ct chain runs")
check((not world.state.batches.has("Ct")), "Transcendence is terminal")
world.state.amount["C2.0"] = 60000
world.state.amount["C1.9"] = 10000
world.civilization_flow.evolve(world.state, 0, 20000)
check(E.gt(world.state.civilization_stats.wars, 0), "War rewards integrate encounters during the interval")
check(E.gt(world.state.civilization_stats.collateral, 0), "Collateral damage reaches previous stages")
check(E.eq(population(world), 70000), "War outcomes and collateral conserve occupied-world counts")
invariants(world)
var restored = fixture()
restored.state = E.decode(E.encode(world.state))
world.civilization_flow.evolve(world.state, 20000, 30000)
restored.civilization_flow.evolve(restored.state, 20000, 30000)
check(E.eq(world.state, restored.state), "Save resume reproduces flow populations and carries")
dispose(restored)
dispose(world)
global.CivilizationRules = baseline
world = fixture()
world.state.amount.RD = 10000
world.state.amount.SCS = 1
world.state.amount["C2.4"] = 1
world.evolution.initialize_life_batches()
var span: Variant = world.state.batches["C2.4"][0].start_year
check(E.near(span, E.divide(20000, E.of("3.6"))), "Structure bonus shortens newly registered lifespan")
dispose(world)
var evolution = load("res://world/owniverse/civilization_evolution.gd")
var balanced: Dictionary = {"m": 10000, "l": 10000, "w": 10000, "d": 10000, "c": 10000, "b": 10000}
var neutral: Dictionary = evolution.drift({}, "test", balanced, global.CivilizationRules["C2.0"].ethics)
var diplomatic: Dictionary = evolution.drift({}, "test", balanced, global.CivilizationRules["C2.0"].ethics, true)
check(E.eq(neutral, balanced), "Ordinary drift preserves balanced ethics")
check(E.gt(diplomatic.d, diplomatic.w), "Diplomacy uses per-ethic influence")
check(E.eq(evolution.total(diplomatic), 60000), "Diplomacy conserves population")
world = fixture()
world.state.amount.RD = E.of("1e15")
world.state.amount.BG = E.of("1e15")
world.state.amount["C3.0"] = 6
world.evolution.initialize_life_batches()
check((E.eq(world.state.amount["C3.0"], 0) and E.eq(world.state.amount["C2.9"], 6)), "Missing mandatory structures degrade existing civilizations")
invariants(world)
dispose(world)
print(("Civilization evolution: %d checks, %d failures" % [checks, failures]))
quit((0 if E.eq(failures, 0) else 1))
print("Civilization flows: %d checks, %d failures" % [checks, failures])
quit(1 if failures else 0)
+3 -2
View File
@@ -79,7 +79,8 @@ func run() -> void:
row.colonization = "0"
world.state.binds.RD = {"C1.9": 10}
world.state.batches["C1.9"] = [{"amount": 10, "start_year": 1, "end_year": 1}]
world.evolution.evolve_batches(world.state, 1)
global.CivilizationRules["C1.9"].span = E.of("0.001")
world.civilization_flow.evolve(world.state, 0, 1)
check((E.eq(world.state.amount["C2.0"], 10) and E.eq(world.state.binds.RD.get("C2.0", 0), 0)), "Space successor does not inherit planetary binding")
dispose(world)
var capacity = load("res://world/owniverse/civilization_state.gd")
@@ -133,7 +134,7 @@ func run() -> void:
row.colonization = "0"
world.state.binds.RD = {"C1.9": 5}
world.state.batches["C1.9"] = [{"amount": 5, "start_year": 1, "end_year": 1}]
world.evolution.evolve_batches(world.state, 1)
world.civilization_flow.evolve(world.state, 0, 1)
check((E.eq(world.state.amount["C2.0"], 2) and E.eq(world.state.binds.RD["C1.9"], 3)), "Unfunded transition retains the original planetary civilization")
dispose(world)
print(("Civilization state: %d checks, %d failures" % [checks, failures]))
+1 -1
View File
@@ -53,7 +53,7 @@ func run():
check(E.eq(physical.get("C0.1", 0), 38300), "C0.1 attempts evolution once")
check(E.eq(physical.get("C0.2", 0), 700), "C0.1 successor does not immediately attempt again")
else:
check(world.profile.civilization_transitions[1] == 1, "Ethical civilization is processed only once per birth bucket")
check(world.profile.civilization_flow_plan[1] == 1, "Ethical civilization uses one continuous interval plan")
var before: Dictionary = world.state.duplicate(true)
world.evolution.evolute_to_datetime(10000)
check(E.eq(world.state, before), "Repeating completed clock does not repeat transitions")
+25 -24
View File
@@ -35,7 +35,7 @@ func run():
direct = fixture()
sliced = fixture()
var initial_h: Variant = sliced.state.amount.H
sliced.evolute_frame(1.0 / 60.0, self)
sliced.evolute_frame(0.2, self)
check(sliced.frame_busy, "Heavy transaction yields before publishing")
check(E.eq(sliced.state.time.year, 0), "Published clock stays at completed transaction")
sliced.request_production("H", 1, true)
@@ -45,7 +45,7 @@ func run():
sliced.speed = 3
check(sliced.speed == 3 and sliced.state.speed == 6, "Speed change is visible and cannot mutate an unfinished transaction")
while sliced.frame_busy: await process_frame
direct.evolute(1.0 / 60.0)
direct.evolute(0.2)
direct.production.produce_hs("H", 1)
direct.speed = 3
check(E.eq(direct.state, sliced.state), "Queued action and speed change survive publication")
@@ -60,7 +60,7 @@ func run():
dispose(direct)
dispose(sliced)
var original = fixture()
original.evolute_frame(1.0 / 60.0, self)
original.evolute_frame(0.2, self)
original.request_production("H", 123, true)
original.evolute_frame(0.01, self)
original.speed = 3
@@ -84,7 +84,7 @@ func run():
dispose(resumed)
dispose(expected)
var cancelled = fixture()
cancelled.evolute_frame(0.133333333333, self)
cancelled.evolute_frame(0.2, self)
check(cancelled.frame_busy, "Cancellation fixture has suspended work")
var budget_ref: RefCounted = cancelled.frame_budget
dispose(cancelled)
@@ -96,25 +96,26 @@ func run():
invalid.evolute(1e308)
check(not invalid.frame_busy and invalid.frame_clock.is_empty() and E.eq(before, invalid.state), "Overflowing elapsed float cannot enter either clock API")
dispose(invalid)
var adaptive = fixture()
adaptive.frame_clock = Clock.advance(adaptive.state.time, 10.0, 6)
var debt_clock: Dictionary = adaptive.frame_clock.duplicate(true)
check(adaptive.update_frame_step() == 0.2, "Growing debt increases step to 0.2 seconds")
check(adaptive.update_frame_step() == 0.5, "Large debt increases step to bounded 0.5 seconds")
check(adaptive.update_frame_step() == 0.5 and E.eq(adaptive.frame_clock, debt_clock), "Maximum step preserves requested time")
adaptive.frame_clock = Clock.advance(adaptive.state.time, 0.3, 6)
check(adaptive.update_frame_step() == 0.5, "Hysteresis keeps step between thresholds")
adaptive.frame_clock = Clock.advance(adaptive.state.time, 0.05, 6)
check(adaptive.update_frame_step() == 0.2, "Small debt reduces step")
check(adaptive.update_frame_step() == 0.1, "Caught-up simulation returns to base step")
adaptive.frame_clock = Clock.advance(adaptive.state.time, 0.15, 6)
adaptive._frame_cost_ratio = 1.1
check(adaptive.update_frame_step() == 0.2, "Measured slow transactions increase step before two-step debt")
adaptive.frame_clock = Clock.advance(adaptive.state.time, 0.05, 6)
adaptive.speed = 3
check(adaptive.update_frame_step() == 1.0 / 60.0, "Low speeds retain 60 Hz scheduling")
adaptive.speed = 6
check(adaptive.update_frame_step() == 0.1, "Speed switch resets adaptive step")
dispose(adaptive)
for selected_speed in range(1, 7):
var scheduled = fixture()
scheduled.speed = selected_speed
scheduled.evolute_frame(0.1, self)
check(not scheduled.frame_busy and scheduled.frame_commits == 0, "Wait for a full step at speed %d" % selected_speed)
scheduled.evolute_frame(0.1, self)
while scheduled.frame_busy: await process_frame
var cadence_clock: Dictionary = Clock.advance({"datetime": 0, "year": 0}, 0.2, selected_speed)
check(scheduled.frame_commits == 1 and E.eq(scheduled.state.time, cadence_clock), "One complete 0.2 second cycle at speed %d" % selected_speed)
scheduled.evolute_frame(0.0, self)
check(not scheduled.frame_busy and scheduled.frame_commits == 1, "No extra cycle without elapsed time")
dispose(scheduled)
var behind = fixture()
behind.evolute_frame(1.0, self)
while behind.frame_busy: await process_frame
check(E.eq(behind.state.time.datetime, 20000) and E.eq(behind.frame_clock.datetime, 100000), "Debt keeps the fixed step and preserves all requested time")
for cycle in 4:
behind.evolute_frame(0.0, self)
while behind.frame_busy: await process_frame
check(behind.frame_commits == 5 and E.eq(behind.state.time, behind.frame_clock), "One second is processed in exactly five full cycles")
dispose(behind)
print("Frame evolution: %d checks, %d failures" % [checks, failures])
quit(1 if failures else 0)
+2 -2
View File
@@ -46,7 +46,7 @@ func run() -> void:
var before: Dictionary = world.state.duplicate(true)
world.evolution.initialize_life_batches()
check(E.eq(world.state, before), "Initialization is idempotent")
world.evolution.evolve_batches(world.state, E.add(5000000, E.divide(5000000, 40)))
world.evolution.evolve_batches(world.state, E.add(5000000, E.divide(5000000, root.get_node("Global").batch_count)))
check(E.eq(world.state.amount.EON1, 10), "HP abiogenesis follows source probability")
check(E.eq(world.state.amount.RP, 100), "HP degradation produces RP")
check(E.eq(world.state.amount.HP, 39890), "Unchanged planets retry, not disappear")
@@ -69,7 +69,7 @@ func run() -> void:
# Test host preservation with a fixed rate, independent of game balance.
world.catalog.entities.EON5.life_rule.progress = "0.00025"
world.evolution.initialize_life_batches()
world.evolution.evolve_batches(world.state, E.add(500000, E.divide(500000, 40)))
world.evolution.evolve_batches(world.state, E.add(500000, E.divide(500000, root.get_node("Global").batch_count)))
check(E.eq(world.state.binds.RD["C0.0"], 10), "Intelligence emerges at the same host star")
check(E.eq(world.state.amount["C0.0"], 0), "Life transition keeps planet bound")
check(world.state.batches.has("C0.0"), "New C0.0 starts civilization evolution")
+1 -1
View File
@@ -17,7 +17,7 @@ func run() -> void:
var World = load("res://world/owniverse/owniverse.gd")
var first = World.new()
var second = World.new()
var names: Array = ["batches", "ledger", "production", "automatic", "stellar", "life", "environment", "evolution"]
var names: Array = ["batches", "ledger", "production", "automatic", "stellar", "life", "environment", "evolution", "civilization_flow"]
var references: Array = []
for name in names:
check(E.ne(first.get(name), second.get(name)), E.add("World has its own ", name))
+35 -47
View File
@@ -1,6 +1,7 @@
extends SceneTree
const E = preload("res://world/owniverse/math/exact.gd")
const Civ = preload("res://world/owniverse/civilization_evolution.gd")
const Q = preload("res://world/owniverse/quantity_math.gd")
const Fixture = preload("res://tests/fixtures/civilization_benchmark.gd")
var checks: int = 0
var failures: int = 0
@@ -11,52 +12,36 @@ func check(value: bool, message: String):
failures += 1
push_error(message)
func reference_drift(target: Dictionary, pop: Dictionary, rows: Dictionary, diplomacy: bool, colony: bool) -> Dictionary:
var weights: Dictionary = {}
for other in Civ.ETHICS: weights[other] = 0
for ethic in pop:
var contribution: Variant = E.multiply(pop[ethic], E.of(rows[ethic].diplomacy_rate) if diplomacy else 1)
if E.eq(contribution, 0): continue
for other in Civ.ETHICS:
var weight: int = 9 if colony else 1
if other == ethic: weight = 960 if colony else 196
elif other == Civ.OPPOSITE[ethic]: weight = 4 if colony else 0
weights[other] = E.add(weights[other], E.multiply(contribution, weight))
if E.le(Civ.total(weights), 0): return pop.duplicate()
return Civ.split(target, "test", Civ.total(pop), weights)
func run():
var rows: Dictionary = {}
for ethic in Civ.ETHICS: rows[ethic] = {"diplomacy_rate": "0.125" if ethic == "m" else "1.75"}
for count in [0, 1, 7, 999999999, E.of("1e40")]:
for diplomacy in [false, true]:
for colony in [false, true]:
var actual: Dictionary = {}
var reference: Dictionary = {}
for round_index in 5:
var pop: Dictionary = {"m": count, "l": round_index, "w": 9, "d": 0, "c": 137, "b": 2}
var result: Dictionary = Civ.drift(actual, "test", pop, rows, diplomacy, colony)
if diplomacy:
check(E.eq(result, reference_drift(reference, pop, rows, diplomacy, colony)), "Diplomatic drift preserves allocation")
var carries_valid: bool = true
for value in actual.get("civilization_remainders", {}).values():
carries_valid = carries_valid and E.ge(value, -1) and E.le(value, 1)
check(carries_valid, "Diplomatic drift carries stay bounded")
else:
check(E.eq(Civ.total(result), Civ.total(pop)), "Integer flow drift conserves population")
var valid_whole: bool = true
for ethic in Civ.ETHICS:
valid_whole = valid_whole and E.ge(result[ethic], 0) and E.eq(result[ethic], E.floor_value(result[ethic]))
check(valid_whole, "Integer flow drift has nonnegative whole counts")
var decimal_rows: Dictionary = {}
for ethic in Civ.ETHICS: decimal_rows[ethic] = {"diplomacy_rate": "0.333333333333" if ethic == "m" else "0.017"}
var decimal_target: Dictionary = {}
for round_index in 20:
var pop: Dictionary = {"m": E.of("1e40"), "l": round_index, "w": 17, "d": 4, "c": 91, "b": 2}
var result: Dictionary = Civ.drift(decimal_target, "decimal", pop, decimal_rows, true)
check(E.eq(Civ.total(result), Civ.total(pop)), "Decimal diplomatic drift preserves whole population")
for ethic in result:
check(E.ge(result[ethic], 0) and E.eq(result[ethic], E.floor_value(result[ethic])), "Decimal drift returns nonnegative whole counts")
for count in [0, 1, 7, 999999999, E.of("1e40"), E.of("1e400")]:
var carry: Dictionary = {}
for round_index in 20:
var weights: Dictionary = {"m": round_index + 1.0, "l": 0.0, "w": 9.0, "d": 4.0, "c": 137.0, "b": 2.0}
var profile: Dictionary = Civ.State.normalize_profile(weights)
var shares: Array = []
for ethic in Civ.ETHICS: shares.append(profile[ethic])
var result: Dictionary = Q.split_profile(carry, "profile", count, Civ.ETHICS, shares)
check(E.eq(Q.sum(result), count), "Profile quotas conserve arbitrary-size whole totals")
for value in result.values(): check(E.ge(value, 0) and E.eq(value, E.floor_value(value)), "Profile quotas stay nonnegative whole")
for value in carry.values(): check(is_finite(value) and value >= -1.0 and value <= 1.0, "Signed profile carries stay bounded")
check(absf(E.ratio_approx(E.of("1e400"), E.of("3e400")) - 1.0 / 3.0) < 1e-15, "Float profile ratio handles values beyond float range")
for count in [1, 101, E.of("1e40"), E.of("1e400")]:
var stocks: Dictionary = {"a": count, "b": E.add(count, 1), "c": 0}
var total: Variant = Q.sum(stocks)
for limit in [0, count, E.subtract(total, 1), total]:
var allocated: Dictionary = Q.allocate_profiled(stocks, limit)
check(E.eq(Q.sum(allocated), limit), "Profiled stock allocation retains the exact granted total")
for id in stocks: check(E.ge(allocated[id], 0) and E.le(allocated[id], stocks[id]), "Profiled stock allocation respects every owner ceiling")
for probability in [0.1, 1.0 / 3.0, 1.23456789]:
var carries: Dictionary = {}
var old_carry: Variant = 0
for iteration in 20:
var expected: Variant = E.add(E.multiply(count, probability), old_carry)
var whole: Variant = E.floor_value(expected)
old_carry = E.maximum(0, E.subtract(expected, whole))
var actual: Variant = Q.accumulate_rate(carries, "rate", count, probability)
check(E.eq(actual, whole), "Fast float carries preserve the whole result of rate accumulation")
check(E.near(carries.rate, old_carry), "Fast float carries preserve the fractional remainder")
for speed in [1, 4, 6]:
var world = load("res://world/owniverse/owniverse.gd").new()
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
@@ -70,13 +55,13 @@ func run():
var row: Dictionary = rules[id].ethics.m
var p: Variant = E.maximum(0, E.multiply(E.of(row.progress), 2))
var d: Variant = E.maximum(0, E.of(row.degradation))
check(E.eq(changed.m.pro, E.divide(p, E.maximum(1, E.add(p, d)))), "Coefficient cache follows modifiers")
check(E.near(changed.m.pro, E.divide(p, E.maximum(1, E.add(p, d)))), "Coefficient cache follows modifiers")
modifiers.progress = 1
check(E.eq(Civ.coefficients(world, rules, id, modifiers), original), "Coefficient cache restores original rates")
var local_rules: Dictionary = rules.duplicate(true)
local_rules[id].ethics.m.progress = "0.375"
changed = Civ.coefficients(world, local_rules, id, modifiers)
check(E.eq(changed.m.pro, E.divide(E.of("0.375"), E.maximum(1, E.add(E.of("0.375"), d)))), "Coefficient cache follows rule edits")
check(E.near(changed.m.pro, E.divide(E.of("0.375"), E.maximum(1, E.add(E.of("0.375"), d)))), "Coefficient cache follows rule edits")
var target: Dictionary = {"ethics": {}}
target.amount = {}
target.binds = {}
@@ -114,6 +99,9 @@ func run():
expected += n
check(E.near(Civ.worship(world, target, worship_rules), expected), "Worship cache detects changed rules")
world.ledger = load("res://tests/fixtures/audit_rounding_ledger.gd").new(world)
# Civilizations no longer trigger batch generations. Seed an actual
# stellar completion so the selective-rounding path is exercised.
world.state.batches.BG = [{"amount": world.state.amount.BG, "start_year": 1, "end_year": 1}]
world.speed = speed
for i in 3: world.evolute(1.0 if speed == 1 else 0.1)
check(world.ledger.checks > 0 and world.ledger.failures == 0, "Selective and full rounding agree at speed %d" % speed)
+1 -1
View File
@@ -70,7 +70,7 @@ func run() -> void:
check(E.eq(world.stellar.evolution_products(world.state, world.catalog.entities.HRG, 37), restored.stellar.evolution_products(restored.state, restored.catalog.entities.HRG, 37)), "Evolution carry survives serialization")
dispose(restored)
# Real RD lifespan, four stars spread over the existing lifespan/40 window.
# Real RD lifespan, four stars spread over one age-group window.
world.state.amount.N = 0
world.state.amount.RD = 4
world.batches.append_batch(world.state, "RD", 4, 0, 0)
+2 -5
View File
@@ -1,7 +1,6 @@
extends SceneTree
const E = preload("res://world/owniverse/math/exact.gd")
const CivilizationState = preload("res://world/owniverse/civilization_state.gd")
const CivilizationEvolution = preload("res://world/owniverse/civilization_evolution.gd")
const Parameters = preload("res://world/owniverse/world_parameters.gd")
const Descriptions = preload("res://world/owniverse/item_description.gd")
@@ -98,10 +97,8 @@ func run() -> void:
var next_stage: String = global.CivilizationRules["C0.2"].next
var outcomes: Array = []
for target in [baseline, advanced]:
var expired: Dictionary = {"C0.2": [{"amount": 1000000, "from": 0, "to": 1}]}
var budget: Dictionary = CivilizationState.stellar_budget(world, target, global.CivilizationRules)
CivilizationEvolution.prepare(world, target, expired, 1, budget, global.CivilizationRules)
outcomes.append(expired["C0.2"][0].products.get(next_stage, 0))
world.civilization_flow.evolve(target, 0, 2000)
outcomes.append(target.amount.get(next_stage, 0))
check(E.gt(outcomes[1], outcomes[0]), "Aggression changes actual ethical civilization transitions")
dispose(world)