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:
+42
-17
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://d0plagt7blmcf
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]))
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user