Files
owniverse/tests
Kirill 3fcc7211ce 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.
2026-10-07 22:51:55 +03:00
..
2026-10-03 11:06:52 +03:00

Проверки поэтапного переноса из Owniverse.U

test_simulation_optimizations.gd сравнивает прямые формулы дрейфа с прежней матрицей, включая целые количества больше E27 и накопленные остатки. Тестовый ledger сверяет выборочное округление с полным на каждом поколении нагруженной симуляции. test_ui_updates.gd проверяет обновление по публикации, выбору объекта и изменению параметров мира, а также пропуск повторных расчётов для неизменившегося состояния.

Динамический размер партий проверяется в test_batch_boundaries.gd: скорости 1–6, сохранение больших целых количеств и дробных остатков, повторное укрупнение и снижение скорости. Высокие скорости допускают приближение времени переходов; сравнение со старым состоянием целиком не является проверкой эквивалентности такого режима.

test_coarse_generations.gd проверяет отсутствие повторного перехода C0.0, C0.1 и C0.2 внутри одного интервала, старое смещение сетки партий, продолжение эволюции в следующем интервале, большие целые количества и дробные остатки. test_frame_evolution.gd сравнивает целый и разбитый по кадрам расчёт, включая возобновление сохранённого незавершённого шага.

Производительность на сохранении

godot --headless --path Owniverse.GI --script res://tests/benchmark_saved_state.gd -- /tmp/save.adventure.json

Читает диагностический JSON формата 4 в отдельные экземпляры мира и замеряет шесть последовательных шагов на скоростях 1, 4, 6. Запись сохранений не вызывается. Последним аргументом можно задать длительность шага в реальных секундах (по умолчанию 1/60). Это время расчёта симуляции, без отрисовки интерфейса.

На диагностической копии с 120 партиями прежняя рациональная арифметика после оптимизации пустых окон давала 90–99 мс на шаг 1/60 секунды на скорости 6. После перехода на целые блоки и устранения повторного построения физических количеств повторный замер дал 53–58 мс. При шаге 0,1 секунды: 65–67 мс вместо 117–138 мс. Это замеры расчёта на данной копии, а не обещание FPS. Для сравнения старый JSON был преобразован только во временный тестовый файл; в загрузчик игры миграция не добавлена. Разреженные сценарии сравниваются с полным расчётом в test_short_lived_events.gd.

Целые блоки и дробные остатки

godot --headless --path Owniverse.GI --script res://tests/test_exact_math.gd

Независимый эталон Python Fraction: 160 пар с произведением внутри E27; целые проверяются точно, дроби — с допуском 10^-12. Также границы int64/E27, сериализация, рецепты с дробными стоимостями, сохранение сумм, время и 100 циклов с меняющимися долями. Все остальные тесты используют числовое сравнение и сериализацию через E, а не сравнение ссылок на большие числа. Границы точности и новый формат сохранения описаны в документации арифметики.

Модули симуляции

godot --headless --path Owniverse.GI --script res://tests/test_module_lifecycle.gd

Проверяются отдельные экземпляры модулей и состояния для каждой вселенной, слабая обратная ссылка, освобождение модулей вместе с миром и защита от некорректного delta. Остальные тесты используют прямые API batches, ledger, production, automatic, stellar, life, environment и evolution; временные методы-переадресации Owniverse удалены. Описание границ модулей и порядка фаз: ../world/owniverse/README.md.

Математика распределений и каталог

godot --headless --path Owniverse.GI --script res://tests/test_quantity_catalog.gd

Проверяются целочисленность и границы распределения, сохранение суммы, разрешение равных остатков, накопление дробей и долгов округления, нормализация весов, размер производственной партии, загрузка каталога и его индексы. Архитектура описана в ../world/owniverse/README.md. Сохранение игрока не используется.

Иконки стадий жизни

Тест также проверяет модуль item_presentation.gd: отсутствие изменений входного состояния и списка закрытых иконок, решения по разблокировке, отложенную и принудительную публикацию количеств.

godot --headless --path Owniverse.GI --script res://tests/test_life_icons.gd

Проверяются одновременная разблокировка подробных и общих иконок EON/LP и C0.0/C0, количества свободных и связанных планет без двойного учёта, переключение подпанелей и очистка счётчика при исчезновении стадии. Сохранение игрока не используется.

Параметры мира и индикаторы

godot --headless --path Owniverse.GI --script res://tests/test_world_indicators.gd

Проверяются сроки жизни существующих и новых звёзд при смене горючести, вероятности и мощность взрывов, влияние агрессии/порядка/просвещения на жизнь, накопление тенденций по времени и этикам, несколько событий за шаг, продолжение после сериализации, валидация сохранения, границы шкал и описания на двух языках. Сцена использует фикстуру без доступа к сохранению игрока.

Прямой расчёт завершившихся сверхновых

godot --headless --path Owniverse.GI --script res://tests/test_short_lived_events.gd

Сравниваются ускоренный путь и прежний путь с созданием всех партий, отключаемый только в тестовой фикстуре. Проверяются LRG, HRG, RSG и RHG на границах коротких и длинных интервалов, существующие SN, нехватка энергии, ресурсы, бласты, привязки, остатки округления и живые партии. Дополнительно проверяется, что длинный интервал создаёт не более двух живых окон и одну запись завершений, сохраняя общее количество. Сохранение игрока этот тест не использует.

test_stellar_rules.gd также проверяет автоматическое создание HBD: долю 0,1% нового автоматического водорода, появление по одному со случайным округлением, сохранение оплаченного остатка топлива, отсутствие повторного начисления на старый запас, продолжение случайной последовательности после сериализации и переход HBD → CBD с сохранением счётчика BD. Формулы описаны в ../simulation_rules.md.

Фоновое сохранение и частота обновления интерфейса

godot --headless --path Owniverse.GI --script res://tests/test_async_save.gd
godot --headless --path Owniverse.GI --script res://tests/test_ui_updates.gd

Сохранение: основной поток создаёт глубокий снимок состояния и параметров. Рабочий поток проверяет данные, записывает зашифрованный временный файл, проверяет чтением и заменяет основное сохранение; диагностический JSON создаётся из того же снимка. Одновременно работает один писатель. Для каждого имени файла хранится только последний ожидающий запрос. request_save() подтверждает постановку в очередь; результат записи сообщает сигнал save_completed. При закрытии игра ждёт saves_drained, при приостановке приложения и уничтожении менеджера очередь завершается через flush_saves(). Копирование снимка остаётся в основном потоке.

Тест сохранений пишет только в уникальную временную папку /tmp, затем удаляет собственные файлы. Проверяются независимость снимка, работа кадров при записи, объединение запросов, диагностическая копия, сохранность предыдущего файла при ошибках и завершение очереди при уничтожении менеджера. Сообщения Invalid save, Save blocked и Cannot write в этом тесте ожидаемы.

Тест интерфейса проверяет передачу количеств из presentation в контролы, очистку очереди после публикации, отложенное и принудительное обновление, производство по нажатию и формат времени. Stage в текущей реализации применяет очередь счётчиков и время каждый кадр; интервал 0,1 секунды используется для индикаторов. Рефакторинг этот порядок не меняет. Тест использует игровую сцену с отключёнными загрузкой, сохранением и автоматической симуляцией, не обращаясь к сохранению игрока.

Время: шаг 0,1 игрового года

godot --headless --path Owniverse.GI --script res://tests/test_time.gd

Команда выполняется из общей папки проектов. Тесты используют искусственные состояния; сохранение игрока не читается и не перезаписывается.

Проверяются границы десятых долей года, производство ресурсов на этих границах, накопление дробного остатка, скорости 1–10 000 лет/сек, продолжение после загрузки и недопустимые интервалы.

Схема состояния 0.3.0 хранит:

  • time.datetime — накопленный возраст, включая необработанную дробную часть;
  • time.year — последний рассчитанный возраст с точностью до десятой доли года.

Сохранения используют формат 4 и актуальную структуру состояния. Проверяются целостность данных, фоновая запись и повторная загрузка; поддержка и миграция прежних форматов отсутствуют.

Файл с несовместимым номером формата при загрузке переносится в *.adventure.incompatible.bak (при совпадении имени добавляется числовой суффикс). Игра начинает новую сессию и может сохранять её в формате 4. Прогресс несовместимого файла не переносится, исходные байты сохраняются в архиве. Ошибка архивирования, расшифровки или повреждение данных текущего формата по-прежнему блокируют перезапись.

Согласованные правила эволюции

Сохраняются постепенные интервальные окна GI: срок жизни делится на 40 возрастных групп; начало преобразования — через lifespan, длительность окна — lifespan / 40 при lifespan > 10, иначе lifespan.

На скоростях 4–6 узкие звёздные партии могут объединяться по границе полного цикла смерти шириной 200/2000/20000 игровых лет. Прежнее окно шире цикла сохраняется: долгоживущие звёзды не дробятся. Из существующих партий сливаются только две или более одного типа, чей оставшийся переход полностью попадает в общий цикл; широкие и пересекающие границу партии сохраняются. test_batch_boundaries проверяет объединение, точные большие количества и остатки, однократное изъятие, сериализацию, сохранение широких окон, распределённые рождения и интеграл производства.

benchmark_large_star_cohorts.gd создаёт отдельные большие диагностические сохранения со старой и новой звёздной сеткой при одинаковых количествах и входных интервалах рождения. Переключатель batches.cycle_stars используется только для сравнительного замера, в игре он включён. Парные прогоны меняют порядок моделей и сбрасывают исходное состояние перед каждым запуском:

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 синтетическими звёздными партиями, попадающими группами в общие циклы. Это отдельный стресс-сценарий большого числа партий; старый и новый расчёты используют одинаковый исходный состав.

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%). Парсер планет сохраняет дроби и поддерживает один элемент без &.

Уязвимость к бластам читается из blastability таблицы GI: HP, EON и цивилизации до C1.2 включительно; C1.3 и выше неуязвимы. Это ограничение относится к бластам уровня 1; локальное уничтожение планет уровня 2 в Unity охватывает все планеты.

godot --headless --path Owniverse.GI --script res://tests/test_stellar_rules.gd

Проверяются парсер планет, граница уязвимости, накопление редких исходов, сериализация остатков и постепенное преобразование реальной партии RD в TWD.

Создание планет подключено к ручному и автоматическому производству звёзд. Бласты обрабатываются как события, а не ресурс. Выжившие планеты переходят преемнику звезды пропорционально числу реально полученных преемников; при отсутствии преемника освобождаются. Локальное разрушение выполняется перед передачей.

Дополнительно проверяются частичная передача, альтернативные преемники, уровни разрушения 1/2, накопление слабых бластов, сохранения, ручное/автоматическое создание планет и несколько поколений за длинный шаг. Правила и формулы описаны в ../simulation_rules.md.

Описания выбранных объектов

godot --headless --path Owniverse.GI --script res://tests/test_descriptions.gd

Источник текста — Global.Templates[Global.Locale]. Заголовок берётся из Name, основной текст — из Description. Поддерживаются ключи с классом (RD.Entity.Description) и старые ключи без класса (RD.Description).

%valueN% заменяется значением переменной, имя которой записано в поле valueN этой строки. Поддерживаются amount, colored costs, lifespan, voids, he / h, metallicity, показатели энергии, плотности и скорости времени. amount1–amount4 сил пока отображаются как —; неизвестные непустые обозначения тоже дают —, пустые поля — пустую строку. [p] превращается в разделение абзацев, цветовые BBCode-теги сохраняются.

Доступные требования зелёные, недоступные красные. Для объектов со связанными экземплярами выводится доступное количество и серым общее; для планет — общее количество. Скорость изменения вычисляется по реальному времени и обновляется раз в 0,25 секунды. Доли He/H вычисляются из текущих свободных запасов; при пустой смеси показывается —. Металличность и плотность используют расчёт среды GI. Счётчики исходной материи и этики Unity в текущей GI отсутствуют и здесь не добавляются.

Первичная жизнь

godot --headless --path Owniverse.GI --script res://tests/test_life.gd

Проверяются абиогенез, деградация, повторные попытки без создания дополнительных планет, появление C0.0 на прежней звезде, остатки вероятностей, сохранение/продолжение, однократная инициализация старых планет и отсутствие воскрешения после бластов.

Проверка загрузки этических правил, распределений и отделения космических цивилизаций:

godot --headless --path Owniverse.GI --script res://tests/test_civilization_state.gd

Тест цивилизаций дополнительно проверяет общий звёздный бюджет, повторное использование резерва при переходе, звёзды в структурах, массовый эквивалент SMBH, блокировку неподдержанного прогресса, каскад деградации и повторное применение ограничений без потерь.

Полная эволюция цивилизаций

godot --headless --path Owniverse.GI --script res://tests/test_civilization_evolution.gd

Проверяются реальные коэффициенты C0.2, войны и побочный ущерб C2.0, замена планет колониями с сохранением хозяина, дипломатическое влияние, бонус периода от структур и деградация при их отсутствии. Ускоренная модель проходит все стадии C0.2 → C4.4 → Ct. Проверки контролируют сохранение населения, целочисленность этик, валидацию сохранения и детерминированное продолжение после сериализации.

Профиль цивилизаций на шестой скорости

godot --headless --path Owniverse.GI --script res://tests/benchmark_civilizations.gd -- frames=20 profile
godot --path Owniverse.GI --script res://tests/benchmark_civilizations.gd -- ui scenario=mixed frames=10 profile

Сценарии mixed, large, fragmented заполняют все стадии и этики, синхронизируя переходы; large использует по 6e20 цивилизаций, fragmented — десять партий. Это искусственные стрессовые сценарии. scenario=stable использует штатные сроки стадий. Параметр snapshot=/tmp/civil сохраняет состояние каждого сценария для сравнения до и после изменения. UI запускает настоящую сцену со страницей жизни и реальным delta, без загрузки и записи сохранения игрока.

profile выводит суммарные микросекунды и число вызовов по этапам. Вложенные этапы входят во время родительских; складывать их нельзя. В игре сбор выключен. Кэши физических количеств принадлежат отдельному миру и проверяют изменения количеств, вложенных привязок и индексов каталога. Кэш структуры правил не входит в сохранение: при изменении Claim/бонусов нужно перестроить индексы каталога, при изменении home в существующей таблице — invalidate_civilization_layout().

После оптимизации на M4 Pro, по 20 шагов 1/60 секунды, среднее время: mixed 295 → 235 мс, large 666 → 539 мс, fragmented 359 → 282 мс. Полные состояния всех трёх сценариев после 20 шагов совпали до и после изменения. Дополнительно тест распределений сравнивает новый путь с прежним на 300 случайных наборах, включая 1e40 и накопленные долги округления.

Расчёт между кадрами

godot --headless --path Owniverse.GI --script res://tests/test_frame_evolution.gd

Stage вызывает evolute_frame: транзакция получает бюджет 40 мс и уступает следующему кадру внутри создания партий и обработки стадий. Расчёт остаётся на главном потоке, состояние публикуется только после полного завершения. Одна транзакция обрабатывает 0,2 реальной секунды на любой скорости. Запрошенное время хранится отдельно, поэтому отставание не превращается в один всё более тяжёлый шаг. При предельной нагрузке время симуляции может отставать от запрошенного; это не ограничение FPS интерфейса и не удаление событий.

Производство во время транзакции выполняется после её завершения. Запрошенное время, активный интервал, скорость и очередь производства входят в сохранение; при загрузке незавершённый интервал пересчитывается с опубликованного состояния. Сброс мира отменяет отложенный расчёт. Тест проверяет точное совпадение непрерывного и распределённого расчёта, очереди, переключение скорости, восстановление и отмену.

На M4 Pro, 180 кадров реального UI на шестой скорости: смешанный сценарий 15.9 FPS (максимальный кадр 98 мс), большие количества 17.5 FPS (106 мс), много партий 16.3 FPS (97 мс). Это FPS отрисовки, а не число завершённых транзакций симуляции в секунду.

Индекс партий переиспользуется внутри одной транзакции, отдельно для каждого массива партий. Замена массива при изъятии или переход к другому состоянию сбрасывает индекс. Сортировка выполняется при нарушении порядка новых окон, а не после каждого добавления в уже существующее окно. test_batch_boundaries проверяет объединение, раннюю вставку, замену массива и изоляцию состояний. На копии текущего сохранения с 785 партиями шесть шагов на скорости 6: среднее время 167 → 145 мс; полное состояние после шести шагов совпало точно.

Следующий этап исключает арифметическую работу при прибавлении нуля, временные знаменатели при умножении целых блоков и нулевые вклады этик. На той же копии сохранения среднее время шага скорости 6 снизилось с 145 до 138 мс; полный результат после шести шагов совпал с исходным состоянием. Тест появления разума задаёт свой progress явно, поскольку проверяет сохранение хозяина планеты, а не актуальное значение коэффициента баланса.

Следующие шесть оптимизаций: однократное изъятие умирающих партий; поверхностные снимки неизменяемых таблиц связей; пропуск планирования цивилизаций без переходов; кэш нормализованных этик и произведений поклонения; пропуск укрупнения для нуля/одной партии; кэш вероятностей переходов. Кэши принадлежат миру и не входят в сохранение. Изменение количества, этик, коэффициентов правил или модификаторов обнаруживается по значениям. Суммирование поклонения сохраняет прежний порядок дробных операций. test_simulation_optimizations проверяет также обновление этих кэшей.

Полное состояние совпало с реализацией до этих шести изменений после каждого из восьми шагов на скоростях 1, 4 и 6, отдельно на большом фрагментированном сценарии и на диагностической копии сохранения (48 сравнений). На M4 Pro шесть шагов по 0.1 секунды скорости 6: 103.3 мс первый, 70.9–76.0 мс последующие; пропускная способность без UI 1.278. Замер 600 кадров с реальным UI: 12.24 FPS, p95 97.16 мс, максимум 104.86 мс, 520 завершённых транзакций, отношение обработанного времени к поступившему 1.057. Замер включает сокращение накопленного отставания сохранения; это результат конкретного сценария на Mac, а не гарантия для Android.

Полный шаг фиксирован: 0,2 реальной секунды на всех скоростях, пять циклов на секунду запрошенного времени. Неполный интервал накапливается. При отставании шаг сохраняется, долг не отбрасывается. Возобновляемая транзакция сохраняет границу из checkpoint. Бюджет кадра остаётся 40 мс. test_frame_evolution проверяет расписание всех шести скоростей, сохранение долга и пять циклов на одну секунду.

Дипломатический drift использует шесть взвешенных вкладов вместо матрицы пар этик: вес = сумма вкладов + 195 × свой вклад − противоположный вклад. Для колониальной матрицы коэффициенты 9, 951 и 5 соответственно. Целая численность и механизм сохранения ошибок распределения не меняются, но порядок операций над дробными весами отличается от матричного варианта. Тест сравнивает с матрицей на точно представимых коэффициентах и проверяет целочисленное сохранение популяций до 1e40 на десятичных коэффициентах. Пустые популяции пропускают drift; численность выходов суммируется один раз. На профильной копии при шаге 0.5 число вызовов снизилось 1038 → 849 за шесть шагов, но время drift осталось около 303 мс суммарно; заметного ускорения полного расчёта в этом сценарии не зафиксировано.

test_split_optimizations.gd сравнивает распределение и все сохранённые остатки с прежним scalar split в последовательных вызовах: пустые ветви, единственная ветвь, равные веса, десятичные веса, положительные/отрицательные остатки, количества до 1e80. Также проверяет grouped mul_div против scalar. Split переиспользует разложение количества и знаменателя между ветвями, кэширует ключи остатков (не более 512 наборов) и получает веса дрифта массивом. Вторая нормализация пропускается только при целых долях с нужной суммой: при дробях даже единичное отношение может изменить округление, поэтому прежний путь сохраняется. Единственная положительная ветвь обрабатывается напрямую только при целом количестве, неотрицательных весах и нулевых остатках. На профильной копии время drift за шесть шагов 0.5 снизилось примерно 303 → 287 мс, но общее время осталось около 1.31 секунды.

Обычный drift сокращает целое количество из отношения countweight/(200count), колониальный — из отношения с 1000. Целая часть делится на малый знаменатель; для дроби используются исходные масштабированные числитель и знаменатель, чтобы сохранить float округление. test_split_optimizations проверяет совпадение со scalar mul_div до 1e80. Производство новорождённых сохраняет только created_from → amount вместо глубокой копии партий. Повторное мгновенное рождение с теми же сроками, длительностью и сеткой сразу добавляется в найденную партию. Индекс ближайшей границы позволяет повторным проходам пропускать будущие партии; замена массива, добавление и масштабирование сбрасывают индекс. Тест границ проверяет объединение и сброс обоих индексов. Адаптивный шаг дополнительно учитывает сглаженное отношение реальной длительности транзакции к обработанному времени. Если отношение выше 0.85 и долг больше одного шага, ступень увеличивается раньше порога двух шагов. Долг и граница сохраняются как прежде; измерения являются временным кэшем.