Add comprehensive tests and optimizations for simulation and UI performance

- Introduced a new benchmark save script to create a stress test scenario with extreme values for simulation performance.
- Added a full UI benchmark script to measure frame rates and performance metrics during UI interactions.
- Implemented tests for coarse generations to ensure population transitions are handled correctly.
- Created tests for extra parameters to validate the loading and application of configuration settings.
- Developed simulation optimization tests to verify the efficiency of population drift and rounding mechanisms.
- Added split optimization tests to ensure accurate allocation and rounding behavior in population management.
- Included startup parameter tests to confirm the correct initialization of global parameters before scene readiness.
- Enhanced the audit rounding ledger to compare selective and full rounding results for consistency.
This commit is contained in:
Kirill
2026-10-05 22:16:59 +03:00
parent 6f6a1be3bd
commit e48d4bbe88
57 changed files with 37279 additions and 784 deletions
+109
View File
@@ -1,5 +1,24 @@
# Проверки поэтапного переноса из 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` сравнивает целый и разбитый по
кадрам расчёт, включая возобновление сохранённого незавершённого шага.
## Производительность на сохранении
```sh
@@ -205,3 +224,93 @@ Stage вызывает `evolute_frame`: транзакция получает б
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.
Адаптивный шаг на скоростях 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.
Дипломатический 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 сокращает целое количество из отношения
count*weight/(200*count), колониальный — из отношения с 1000.
Целая часть делится на малый знаменатель; для дроби используются исходные
масштабированные числитель и знаменатель, чтобы сохранить float округление.
`test_split_optimizations` проверяет совпадение со scalar mul_div до 1e80.
Производство новорождённых сохраняет только created_from → amount вместо
глубокой копии партий. Повторное мгновенное рождение с теми же сроками,
длительностью и сеткой сразу добавляется в найденную партию.
Индекс ближайшей границы позволяет повторным проходам пропускать будущие
партии; замена массива, добавление и масштабирование сбрасывают индекс.
Тест границ проверяет объединение и сброс обоих индексов.
Адаптивный шаг дополнительно учитывает сглаженное отношение реальной
длительности транзакции к обработанному времени. Если отношение выше 0.85
и долг больше одного шага, ступень увеличивается раньше порога двух шагов.
Долг и граница сохраняются как прежде; измерения являются временным кэшем.
Binary file not shown.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,63 @@
# Все объекты, скорость 6
Создано скриптом `tests/create_full_benchmark_save.gd`. Пароль нативного сохранения: `qwerty`.
Генератор не читает и не перезаписывает сохранение игрока.
Начальное состояние: 151 запись каталога (включая группы), все 44 стадии правил цивилизаций, все шесть этик на каждой стадии, 1000 когорт (по десять на сущность с временными партиями). У большинства обычных объектов количество равно 6e60, у стадий цивилизаций — 6e20. Пространство и водород имеют количество 1e90, гелий и металлы — 1e80, энергия — 1e100. Возраст — 1e15 лет. Запасов достаточно для требований продвинутых стадий звезд и структур. Оставшиеся сроки жизни когорт покрывают весь срок жизни сущностей, поэтому переходы происходят во время замера.
Это искусственный экстремальный сценарий, а не прогноз для обычного сохранения.
Машина: Apple M4 Pro, Godot 4.7.2, рендерер Metal Mobile.
| Замер | Результат |
|---|---:|
| Headless, скорость 6, шесть шагов по 0,5 секунды | 943,936–1221,992 мс на шаг |
| Среднее время headless-шага | 1122,287 мс |
| Пропускная способность headless | 0,446 секунды симуляции на секунду CPU |
| Настоящий UI, страница цивилизаций, 600 кадров | 17,38 FPS |
| Средний / p95 / максимальный кадр UI | 57,55 / 99,78 / 116,94 мс |
| Завершенные транзакции | 23 |
| Запрошенный прирост игрового времени | 3464662,267 лет |
| Опубликованный прирост игрового времени | 1080000 лет |
| Пропускная способность с UI | 0,3117 |
| Накопленный долг за замер | 2384662,267 игровых лет (23,85 реальных секунд на скорости 6) |
| Записей каталога осталось в конце | 151 |
Динамический шаг достигает предела в 0,5 секунды. Рендеринг идет быстрее 5 FPS, но симуляция не успевает за заданным темпом в этом сценарии. На обработку поколений приходится примерно 94% времени CPU. Подготовка цивилизаций занимает в среднем 634 мс на шаг, в том числе дрифт этик — 218 мс, войны — 124 мс; колонизация — 121 мс на шаг. Интервалы профилировщика вложенные и частично перекрываются.
## Сравнение оптимизации дрифта (2026-10-05)
Замеры сделаны на одном сгенерированном сохранении и одной машине до и после прямого распределения потоков для обычного и колониального дрифта, а также кеширования ставок дипломатии в фиксированной точке. Каждый headless-запуск выполняет шесть последовательных шагов по 0,5 секунды на каждой скорости.
| Замер | До | После |
|---|---:|---:|
| Средний headless-шаг на скорости 6 | 1122,287 мс | 1018,559 мс |
| Пропускная способность headless на скорости 6 | 0,446 | 0,491 |
| Дрифт этик, сумма интервалов за шесть шагов | 1,338 с | 0,643 с |
| UI, FPS за 600 кадров | 17,38 | 17,04 |
| Средний / p95 / максимальный кадр UI | 57,55 / 99,78 / 116,94 мс | 58,70 / 102,67 / 114,30 мс |
| Завершенные UI-транзакции | 23 | 25 |
| Запрошено / опубликовано игровых лет в UI | 3464662 / 1080000 | 3533627 / 1180000 |
| Пропускная способность симуляции с UI | 0,3117 | 0,3339 |
Средний headless-шаг ускорился примерно на 9,2%, а интервал профиля дрифта этик сократился примерно на 52%. Частота кадров UI практически не изменилась (в этом запуске снизилась примерно на 2%), при этом завершилось больше транзакций и выросла доля опубликованного игрового времени. На скорости 6 стресс-сценарий все еще отстает от реального времени: пропускная способность — около 0,49 секунды симуляции на секунду CPU в headless и 0,33 с UI. Самыми затратными остаются планирование поколений и войны.
Прямые пути сохраняют целое население, но разносят малые целочисленные потоки по нескольким последовательным переходам с помощью остатков для каждого источника. Поэтому распределение в отдельном переходе отличается от независимого округленного распределения по матрице 6×6. Ставки дипломатии округляются до сетки фиксированной точки с шагом 1e-9. Целевая регрессия: 769 проверок эволюции цивилизаций, оптимизаций симуляции, границ партий, укрупненных поколений и эволюции кадров; все пройдены.
## Последующая оптимизация подготовки поколений и кешей (2026-10-05)
На той же нагрузке среднее время headless-шага на скорости 6 снизилось с 1018,559 до 1005,138 мс (примерно на 1,3%). Пропускная способность headless изменилась с 0,491 до 0,497. Суммарные интервалы `generations` близки: 5,703 с и 5,629 с за шесть шагов. Подготовка цивилизаций остается самой затратной измеренной подфазой — 3,130 с. UI также почти не изменился: 17,04 → 17,09 FPS, пропускная способность симуляции — 0,3339 → 0,3351. Улучшение небольшое: арифметика населения в поколениях по-прежнему дорога при сотнях активных когорт и множестве правил цивилизаций.
Профиль теперь отдельно показывает `generation_withdrawal` и `generation_products`. Первый интервал занимает 55,5 мс, второй — 120,2 мс за шесть headless-шагов на скорости 6. Эти значения нельзя складывать с общим временем поколений, поскольку интервалы профилировщика вложенные.
## Инкрементальная обработка партий новорожденных (2026-10-05)
Удалены снимки когорт до и после рождения и последующий полный обход когорт. `append_batch` теперь сразу накапливает поправку на возраст новорожденных при добавлении частей когорт. Финальный замер после повторного использования сумм завершившихся партий в пределах поколения дал 1028,991 мс на headless-шаг скорости 6 и пропускную способность 0,486. В повторных прогонах этой же версии получилось 1005–1011 мс, поэтому при выборке всего в шесть шагов разброс велик, и устойчивое ускорение утверждать нельзя. В UI получилось 17,05 FPS, среднее время кадра 58,66 мс и пропускная способность 0,3342. Новый профиль показывает 319 мс на обработку партий новорожденных за шесть шагов. Это изменение пока не демонстрирует снижения общего времени на этом сохранении; его потенциальная польза относится прежде всего к сценариям с гораздо большим числом когорт на сущность, где прежние обходы росли пропорционально числу когорт.
Повторить замеры из родительской папки проекта:
```sh
godot --headless --path Owniverse.GI --script res://tests/create_full_benchmark_save.gd
godot --headless --path Owniverse.GI --script res://tests/benchmark_saved_state.gd -- Owniverse.GI/tests/artifacts/all_objects_speed6.adventure.json 0.5 profile
godot --path Owniverse.GI --script res://tests/benchmark_full_ui.gd
```
+48
View File
@@ -0,0 +1,48 @@
extends SceneTree
const E = preload("res://world/owniverse/math/exact.gd")
func _initialize(): call_deferred("run")
func restore(value):
if value is float and value == floorf(value) and absf(value) <= 9007199254740991: return int(value)
if value is Dictionary:
for k in value: value[k] = restore(value[k])
elif value is Array:
for i in value.size(): value[i] = restore(value[i])
return value
func run():
var payload = E.decode(restore(JSON.parse_string(FileAccess.get_file_as_string("res://tests/artifacts/all_objects_speed6.adventure.json"))))
var scene = load("res://world/world.tscn").instantiate()
var stage = scene.get_node("Stage")
stage.owniverse.free()
stage.set_script(load("res://tests/fixtures/benchmark_ui_stage.gd"))
root.add_child(scene)
await process_frame
stage.owniverse.state = payload.state.duplicate(true)
stage.owniverse.restore_frame_checkpoint(payload.get("simulation", {}))
stage.owniverse.presentation.refresh(stage.owniverse.state, stage.owniverse.catalog)
stage.set_owniverse_item_value()
stage.stage_page_scroller.scroll_horizontal = 4 * stage.PAGE_WIDTH
var initial_requested: Variant = stage.owniverse.frame_clock.get("datetime", stage.owniverse.state.time.datetime)
var initial_published: Variant = stage.owniverse.state.time.datetime
stage.owniverse.profile_enabled = true
stage.measuring = true
for i in 600: await process_frame
stage.measuring = false
var samples = stage.frame_samples
var total: float = 0
for sample in samples: total += sample
samples.sort()
print("Saved UI: FPS=%.2f mean=%.2fms p95=%.2fms max=%.2fms commits=%d" % [1000.0/(total/samples.size()),total/samples.size(),samples[int(ceil(samples.size()*0.95))-1],samples.back(),stage.owniverse.frame_commits])
var requested_advance: Variant = E.subtract(stage.owniverse.frame_clock.datetime, initial_requested)
var published_advance: Variant = E.subtract(stage.owniverse.state.time.datetime, initial_published)
print("Requested years advanced=", requested_advance, " published=", published_advance, " throughput=", E.divide(published_advance, requested_advance))
print("UI CPU profile: ", stage.owniverse.profile)
var remaining_objects: int = 0
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)
for entity in stage.owniverse.catalog.entities.values(): entity.free()
stage.owniverse.free()
scene.free()
for i in 3: await process_frame
quit()
+1
View File
@@ -0,0 +1 @@
uid://3xjh23kg5vrn
+8
View File
@@ -27,6 +27,8 @@ func run():
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)
@@ -36,12 +38,18 @@ func run():
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 = []
for cycle in 6:
var start: int = Time.get_ticks_usec()
world.evolute(delta)
elapsed.append((Time.get_ticks_usec() - start) / 1000.0)
print("Speed %d, six consecutive %s-second steps, ms: %s" % [speed, delta, elapsed])
var total_ms: float = 0.0
for sample in elapsed: total_ms += sample
var throughput: float = 6.0 * delta / (total_ms / 1000.0)
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()
+51
View File
@@ -0,0 +1,51 @@
extends SceneTree
## Reproducible stress save; never writes to the player's user:// directory.
const E = preload("res://world/owniverse/math/exact.gd")
const State = preload("res://world/owniverse/civilization_state.gd")
func _initialize(): call_deferred("run")
func run():
var global: Node = root.get_node("Global")
var world = load("res://world/owniverse/owniverse.gd").new()
world.catalog.load_definitions(global.OwniverseEntities)
world.speed = 6
world.state.time = {"datetime": 1000000000000000, "year": 1000000000000000, "tick_remainder": 0}
world.state.energy = E.of("1e100")
for id in world.catalog.entities:
world.state.amount[id] = E.of("6e60")
world.state.stats.total_amount[id] = world.state.amount[id]
if global.CivilizationRules.has(id): world.state.amount[id] = E.of("6e20")
world.state.amount.S = E.of("1e90")
world.state.amount.H = E.of("1e90")
world.state.amount.He = E.of("1e80")
world.state.amount.Met = E.of("1e80")
world.ledger.refresh_groups(world.state)
State.enforce_capacity(world, world.state, global.CivilizationRules, world.state.time.year)
State.reconcile(world, world.state, global.CivilizationRules)
var cohorts: int = 0
for id in world.catalog.entities:
if world.catalog.is_group(id): continue
var lifespan: Variant = State.effective_span(world, world.state, id, global.CivilizationRules)
if E.le(lifespan, 0): continue
world.state.batches[id] = []
var width: Variant = E.divide(lifespan, 10)
for i in 10:
var start: Variant = E.add(world.state.time.year, E.multiply(width, i))
var end: Variant = E.add(start, width)
var birth: Variant = E.subtract(start, lifespan)
world.state.batches[id].append({"amount": E.divide(world.state.amount[id], 10), "created_from": birth, "created_to": E.add(birth, width), "start_year": start, "end_year": end})
cohorts += 1
world.state.batch_origins[id] = world.state.batches[id][0].created_from
world.state.stats.total_amount = world.state.amount.duplicate(true)
var manager: Node = root.get_node("SaveManager")
var folder: String = ProjectSettings.globalize_path("res://tests/artifacts")
DirAccess.make_dir_recursive_absolute(folder)
manager.save_directory = folder
global.save_plaintext_copy = true
var success: bool = manager.save_data("all_objects_speed6", "qwerty", {"state": world.state, "simulation": world.frame_checkpoint(), "ui": {"current_page": 4}})
var empty: Array = []
for id in world.catalog.entities:
if E.le(world.state.amount.get(id, 0), 0): empty.append(id)
print("Full save: objects=%d civilization stages=%d cohorts=%d empty=%s success=%s" % [world.catalog.entities.size(), global.CivilizationRules.size(), cohorts, empty, success])
for entity in world.catalog.entities.values(): entity.free()
world.free()
quit(0 if success and empty.is_empty() else 1)
+1
View File
@@ -0,0 +1 @@
uid://1icskkk5nld8
+12
View File
@@ -0,0 +1,12 @@
extends "res://world/owniverse/object_ledger.gd"
var checks: int = 0
var failures: int = 0
func round_object_allocations(target: Dictionary, affected: Variant = null) -> void:
var reference: Dictionary = target.duplicate(true)
super.round_object_allocations(reference)
super.round_object_allocations(target, affected)
checks += 1
if not E.eq(target, reference):
failures += 1
push_error("Selective rounding differs from full rounding")
+1
View File
@@ -0,0 +1 @@
uid://csew6jyuiwxh0
+8
View File
@@ -1,4 +1,12 @@
extends "res://stage/stage.gd"
var actions_refreshes: int = 0
func tick_ui(delta: float) -> void:
super._process(delta)
func _set_active_page_actions() -> void:
actions_refreshes += 1
super._set_active_page_actions()
# Keep scene wiring and display logic, without accessing the player's save.
func load_data() -> bool:
+55
View File
@@ -38,6 +38,43 @@ func dispose(world) -> void:
func run() -> void:
var world = fixture()
world.catalog.entities.N.lifespan = 100
world.batches.append_batch(world.state, "N", 2, 10, 10)
world.batches.append_batch(world.state, "N", 3, 10, 10)
check(world.state.batches.N.size() == 1 and E.eq(world.state.batches.N[0].amount, 5), "Repeated insertion merges using the cohort index")
world.batches.append_batch(world.state, "N", 1, 0, 0)
check(E.eq(world.state.batches.N[0].created_from, 0), "Earlier insertion preserves chronological order")
var replaced: Array = world.state.batches.N.duplicate(true)
world.state.batches.N = replaced
world.batches.append_batch(world.state, "N", 7, 10, 10)
check(E.eq(replaced[1].amount, 12), "Replacing cohort array invalidates cached references")
var separate: Dictionary = world.state.duplicate(true)
world.batches.append_batch(separate, "N", 9, 10, 10)
check(E.eq(separate.batches.N[1].amount, 21) and E.eq(world.state.batches.N[1].amount, 12), "Index is isolated between transaction states")
dispose(world)
world = fixture()
for speed in range(1, 7):
world.state.speed = speed
check(E.eq(world.batches.window_duration(world.state, 100), 2.5 if speed < 4 else pow(10, speed - 2)), "Windows follow selected speed")
check(E.eq(world.batches.window_duration(world.state, 5), 5), "Short explosions retain their timing")
var huge: Variant = E.of("10000000000000000000000000000000000000000")
world.catalog.entities.N.lifespan = 100
world.state.batches.N = [
{"amount": huge, "start_year": 100, "end_year": 110, "created_from": 0, "created_to": 10, "withdraw_remainder": 0.25},
{"amount": 7, "start_year": 120, "end_year": 130, "created_from": 20, "created_to": 30, "withdraw_remainder": 0.5}
]
world.batches.coarsen(world.state)
check(world.state.batches.N.size() == 1, "High speed combines nearby cohorts")
check(E.eq(world.state.batches.N[0].amount, E.add(huge, 7)), "Coarsening preserves exact large whole counts")
check(E.eq(world.state.batches.N[0].withdraw_remainder, 0.75), "Coarsening preserves withdrawal carry")
var combined: Dictionary = world.state.batches.N[0].duplicate(true)
world.batches.coarsen(world.state)
check(world.state.batches.N[0] == combined, "Coarsening is idempotent")
world.state.speed = 1
world.batches.coarsen(world.state)
check(world.state.batches.N[0] == combined, "Slowing down does not invent finer history")
dispose(world)
world = fixture()
world.state.batch_origins.LGRB = E.of("10357034.80000000074505806")
var boundary: Variant = E.of("41215289.79999999701976776")
for interval in [[E.subtract(boundary, E.of("2.5")), E.add(boundary, E.of("2.25"))], [boundary, E.add(boundary, 2)]]:
@@ -54,5 +91,23 @@ func run() -> void:
check(E.eq(total, 100), "Boundary correction preserves all objects")
check(E.le(world.state.batches.LGRB.size(), 7), "Finite number of windows")
dispose(world)
world = fixture()
world.batches.append_batch(world.state, "N", 100, 100, 100)
var first: Dictionary = world.state.batches.N[0].duplicate(true)
world.batches.append_batch(world.state, "N", 7, 100, 100)
check(world.state.batches.N.size() == 1 and E.eq(world.state.batches.N[0].amount, 107), "Repeated births merge directly into one window")
check(E.eq(world.state.batches.N[0].start_year, first.start_year), "Direct merge preserves transition time")
check(world.batches.withdraw_batches(world.state, "N", 0).is_empty(), "Future cohorts are not withdrawn")
var untouched: Array = world.state.batches.N
check(world.batches.withdraw_batches(world.state, "N", 1).is_empty() and is_same(untouched, world.state.batches.N), "Readiness index skips future cohorts")
world.state.batches.N = [{"amount": 7, "start_year": 0, "end_year": 0, "created_from": 0, "created_to": 0}]
check(world.batches.withdraw_batches(world.state, "N", 1).size() == 1, "Replacing cohorts invalidates readiness index")
world.batches.append_batch(world.state, "N", 3, 100, 100)
check(E.eq(world.state.batches.N[0].amount, 3), "Replacing cohorts invalidates instant merge cache")
world.batches.withdraw_batches(world.state, "N", 0)
world.batches.scale_batches(world.state, "N", 0)
world.batches.withdraw_batches(world.state, "N", 1)
check(world.state.batches.N.is_empty(), "Scaling invalidates readiness and removes zero cohorts")
dispose(world)
print(("Batch boundaries: %d checks, %d failures" % [checks, failures]))
quit((0 if E.eq(failures, 0) else 1))
+83
View File
@@ -0,0 +1,83 @@
extends SceneTree
const E = preload("res://world/owniverse/math/exact.gd")
var checks: int = 0
var failures: int = 0
func _initialize(): call_deferred("run")
func check(condition: bool, message: String):
checks += 1
if not condition:
failures += 1
push_error(message)
func fixture(id: String, speed: int):
var world = load("res://world/owniverse/owniverse.gd").new()
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
for entity in world.catalog.entities.values():
world.state.amount[entity.id] = 0
world.state.stats.total_amount[entity.id] = 0
if entity.group_as != "":
world.state.amount[entity.group_as] = 0
world.state.stats.total_amount[entity.group_as] = 0
world.state.amount.RD = 1
world.state.binds.RD = {id: 40000}
world.state.batches[id] = [{"amount": 40000, "start_year": 1, "end_year": 1}]
# A saved origin off the new grid must not backdate rounded successors.
world.state.batch_origins[id] = 500
world.speed = speed
world.profile_enabled = true
return world
func dispose(world):
for entity in world.catalog.entities.values(): entity.free()
world.free()
func run():
for id in ["C0.0", "C0.1", "C0.2"]:
var world = fixture(id, 6)
world.evolution.evolute_to_datetime(10000)
var physical: Dictionary = world.ledger.physical_amounts(world.state)
var population: Variant = 0
for stage in physical:
if world.catalog.entities[stage].group_as == "Planet":
population = E.add(population, physical[stage])
check(E.eq(population, 40000), "Coarse transitions preserve all whole populations: " + id)
for batches: Array in world.state.batches.values():
for batch: Dictionary in batches:
check(E.gt(batch.start_year, 10000), "Successors cannot expire inside their birth bucket: " + id)
if id == "C0.0":
check(E.eq(physical.get("C0.0", 0), 38300), "C0.0 attempts evolution once")
check(E.eq(physical.get("C0.1", 0), 700), "C0.0 progress rate is applied once")
check(E.eq(physical.get("EON5", 0), 1000), "C0.0 degradation rate is applied once")
elif id == "C0.1":
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")
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")
var validation: String = root.get_node("SaveManager").validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state})
check(validation.is_empty(), "Coarse state validates: " + validation)
world.evolution.evolute_to_datetime(20000)
if id in ["C0.0", "C0.1"]:
check(E.lt(world.ledger.physical_amounts(world.state)[id], 38300), "The next bucket still processes legitimate evolution: " + id)
dispose(world)
var fine = fixture("C0.0", 1)
fine.evolution.evolute_to_datetime(10000)
check(E.lt(fine.ledger.physical_amounts(fine.state)["C0.0"], 38300), "Low speeds retain multiple chronological generations")
dispose(fine)
var large = fixture("C0.0", 6)
var count: Variant = E.of("10000000000000000000000000000000000000001")
large.state.binds.RD["C0.0"] = count
large.state.batches["C0.0"][0].amount = count
var reference: Dictionary = large.state.duplicate(true)
var products: Dictionary = large.life.life_products(reference, large.catalog.entities["C0.0"], count)
large.evolution.evolute_to_datetime(10000)
var physical: Dictionary = large.ledger.physical_amounts(large.state)
for id in products:
check(E.eq(physical.get(id, 0), products[id]), "Large whole population undergoes one transition: " + id)
check(E.eq(large.state.life_remainders, reference.life_remainders), "Coarse timing preserves the single attempt's fractional carry")
dispose(large)
print("Coarse generations: %d checks, %d failures" % [checks, failures])
quit(1 if failures else 0)
+1
View File
@@ -0,0 +1 @@
uid://ggml5q061lwh
+4
View File
@@ -31,6 +31,10 @@ func run():
verify(not E.eq(E.of("1e1000"), INF), "huge number is not infinity")
verify(E.eq(E.floor_sum(huge, 2, 1, 2), E.divide(E.multiply(huge, E.subtract(huge, 1)), 2)), "floor sum supports huge window counts")
var rows = JSON.parse_string(FileAccess.get_file_as_string("res://tests/fixtures/big_integer_oracle.json"))
for row in rows.slice(0, 20):
var scaled = B.divmod(B.parse(row[0] + "000000000000000000"), B.parse(row[1] + "000000000000000000"))
verify(B.decimal(scaled[0]) == row[4], "cancel shared blocks preserves quotient")
verify(B.compare(scaled[1], B.multiply(B.parse(row[5]), B.parse("1000000000000000000"))) == 0, "cancel shared blocks restores original remainder")
for row in rows:
var a: Array = B.parse(row[0])
var b: Array = B.parse(row[1])
+54
View File
@@ -0,0 +1,54 @@
extends SceneTree
const E = preload("res://world/owniverse/math/exact.gd")
const Extra = preload("res://data/owniverse_extra_param_loader.gd")
const Parameters = preload("res://world/owniverse/world_parameters.gd")
var checks: int = 0
var failures: int = 0
func _initialize(): call_deferred("run")
func check(ok: bool, label: String):
checks += 1
if not ok:
failures += 1
push_error(label)
func run():
var global = root.get_node("Global")
check(E.eq(global.ExtraParams, Extra.get_data()), "Global table equals loader data")
check(E.eq(global.ExtraParams.E, E.of("1e27")), "Energy parsed as exact integer")
check(E.eq(global.ExtraParams.DA, 65536), "Export column is used, not formatted display column")
var saved: Dictionary = global.ExtraParams.duplicate()
global.ExtraParams.E = E.of("1e40")
var world = load("res://world/owniverse/owniverse.gd").new()
world.catalog.load_definitions(global.OwniverseEntities)
check(E.eq(world.state.energy, E.of("1e40")), "New world uses configured energy")
var initial: Dictionary = world.environment.stellar_environment(world.state)
global.ExtraParams.DA = E.multiply(saved.DA, 4)
check(E.near(world.environment.stellar_environment(world.state).density, E.multiply(initial.density, 2)), "Base mass changes density")
global.ExtraParams.DA = saved.DA
global.ExtraParams.NPC = E.multiply(saved.NPC, 2)
check(E.near(world.environment.stellar_environment(world.state).density, E.multiply(initial.density, 2)), "NPC changes density")
global.ExtraParams.NPC = saved.NPC
world.state.amount.S = E.of("1e24")
world.state.amount.LGF = 1
var old_density = world.environment.stellar_environment(world.state).density
global.ExtraParams.VoidSize = E.multiply(saved.VoidSize, 100)
check(E.lt(world.environment.stellar_environment(world.state).density, old_density), "Void volume uses configured size")
global.ExtraParams.bigcrunch1 = E.of("0.5")
global.ExtraParams.bigcrunch2 = E.of("0.6")
global.ExtraParams.bigcrunch3 = E.of("0.7")
check(Parameters.density_display(E.of("0.6")).mode == "DensityCrunch", "Configured crunch start")
check(E.near(Parameters.density_display(E.of("0.6")).position, E.of("0.5")), "Crunch indicator uses configured interval")
check(Parameters.density_warning_color(E.of("0.6")) == Color.html("20b0dfbf"), "Configured middle crunch warning")
global.ExtraParams.bigrip1 = E.of("0.01")
global.ExtraParams.bigrip2 = E.of("0.001")
global.ExtraParams.bigrip3 = E.of("0.0001")
check(Parameters.density_display(E.of("0.001")).mode == "DensityRip", "Configured rip start")
check(E.near(Parameters.density_display(E.of("0.001")).position, E.of("0.5")), "Rip indicator uses configured interval")
check(Parameters.density_warning_color(E.of("0.001")) == Color.html("df2020bf"), "Configured middle rip warning")
world.state.world_parameters.Bur = 19
global.ExtraParams.combustibility_bonus = 1
check(E.eq(Parameters.production(world.state, world.catalog.entities.RD), Parameters.effective(world.state, "Bur")), "Configured combustion exponent")
global.ExtraParams.merge(saved, true)
for entity in world.catalog.entities.values(): entity.free()
world.free()
print("Extra params: %d checks, %d failures" % [checks, failures])
quit(1 if failures else 0)
+1
View File
@@ -0,0 +1 @@
uid://doertyqlkrm26
+20
View File
@@ -96,5 +96,25 @@ 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)
print("Frame evolution: %d checks, %d failures" % [checks, failures])
quit(1 if failures else 0)
+2
View File
@@ -66,6 +66,8 @@ func run() -> void:
world = fixture()
world.state.amount.RD = 1
world.state.binds.RD = {"EON5": 40000}
# 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)))
check(E.eq(world.state.binds.RD["C0.0"], 10), "Intelligence emerges at the same host star")
+3
View File
@@ -30,6 +30,9 @@ func run() -> void:
for id in requests: requests[id] = E.multiply(requests[id], E.of("1e40"))
var limit: Variant = E.multiply(rng.randi_range(0, 600), E.of("1e40")) if sample % 3 == 0 else rng.randi_range(0, 600)
check(E.eq(Math.allocate_whole(requests, limit), Legacy.allocate_whole(requests, limit)), "Allocation matches legacy with limits, ties and big integers")
var whole_requests: Dictionary = {}
for id in requests: whole_requests[id] = E.floor_value(E.maximum(0, requests[id]))
check(E.eq(Math.allocate_whole(whole_requests, limit), Math.allocate_whole_with_total(whole_requests, limit, Math.sum(whole_requests))), "Known-total allocation matches ordinary allocation")
var count: Variant = limit
check(E.eq(Math.split(new_remainders, "population", count, weights), Legacy.split(old_remainders, "population", count, weights)), "Split matches legacy rounding")
check(E.eq(new_remainders, old_remainders), "Signed remainder debt matches legacy")
+112
View File
@@ -0,0 +1,112 @@
extends SceneTree
const E = preload("res://world/owniverse/math/exact.gd")
const Civ = preload("res://world/owniverse/civilization_evolution.gd")
const Fixture = preload("res://tests/fixtures/civilization_benchmark.gd")
var checks: int = 0
var failures: int = 0
func _initialize(): call_deferred("run")
func check(value: bool, message: String):
checks += 1
if not value:
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 speed in [1, 4, 6]:
var world = load("res://world/owniverse/owniverse.gd").new()
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
Fixture.seed(world, root.get_node("Global"), true, true)
var rules: Dictionary = root.get_node("Global").CivilizationRules
var id: String = rules.keys()[0]
var modifiers: Dictionary = {"progress": 1, "degradation": 1, "war": 1, "colonization": 1}
var original: Dictionary = Civ.coefficients(world, rules, id, modifiers).duplicate(true)
modifiers.progress = 2
var changed: Dictionary = Civ.coefficients(world, rules, id, modifiers)
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")
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")
var target: Dictionary = {"ethics": {}}
Civ.State.reconcile(world, target, rules, {id: 60})
target.ethics[id].m = 100
Civ.State.reconcile(world, target, rules, {id: 60})
check(E.eq(Civ.total(target.ethics[id]), 60), "Reconcile cache detects changed ethics")
Civ.State.reconcile(world, target, rules, {id: 120})
check(E.eq(Civ.total(target.ethics[id]), 120), "Reconcile cache detects changed population")
Civ.worship(world, target, rules)
target.ethics[id].m = E.add(target.ethics[id].m, 5)
var expected: Variant = 0
for stage in rules:
if rules[stage].worships:
for ethic in target.ethics[stage]:
expected = E.add(expected, E.multiply(target.ethics[stage][ethic], E.of(rules[stage].ethics[ethic].worship)))
check(E.eq(Civ.worship(world, target, rules), expected), "Worship cache detects changed ethics")
var worship_rules: Dictionary = rules.duplicate(true)
for stage in worship_rules:
if worship_rules[stage].worships:
worship_rules[stage].ethics.m.worship = "0.375"
expected = 0
for stage in worship_rules:
if worship_rules[stage].worships:
for ethic in target.ethics[stage]:
expected = E.add(expected, E.multiply(target.ethics[stage][ethic], E.of(worship_rules[stage].ethics[ethic].worship)))
check(E.eq(Civ.worship(world, target, worship_rules), expected), "Worship cache detects changed rules")
world.ledger = load("res://tests/fixtures/audit_rounding_ledger.gd").new(world)
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)
for entity in world.catalog.entities.values(): entity.free()
world.free()
print("Simulation optimizations: %d checks, %d failures" % [checks, failures])
quit(1 if failures else 0)
@@ -0,0 +1 @@
uid://bcvertbh5uxys
+66
View File
@@ -0,0 +1,66 @@
extends SceneTree
const E = preload("res://world/owniverse/math/exact.gd")
const Q = preload("res://world/owniverse/quantity_math.gd")
var checks: int = 0
var failures: int = 0
func _initialize(): call_deferred("run")
func check(value: bool, message: String):
checks += 1
if not value:
failures += 1
push_error(message)
static func reference_split(remainders: Dictionary, key: String, count: Variant, weights: Dictionary) -> Dictionary:
var result: Dictionary = {}
var sum_weights: Variant = Q.sum(weights)
if (E.le(count, 0) or E.le(sum_weights, 0)): return result
var ids: Array = weights.keys()
var expected: Array = []
var remainder_keys: Array = []
var positive_sum: Variant = 0
for id in ids:
var remainder_key: String = key + ":" + str(id)
remainder_keys.append(remainder_key)
var value: Variant = E.add(E.mul_div(count, weights[id], sum_weights), remainders.get(remainder_key, 0))
expected.append(value)
positive_sum = E.add(positive_sum, E.maximum(0, value))
var order: Array = []
var rests: Array = []
var assigned: Variant = 0
for i in ids.size():
var normalized: Variant = E.mul_div(E.maximum(0, expected[i]), count, positive_sum) if E.gt(positive_sum, 0) else 0
result[ids[i]] = E.floor_value(normalized)
assigned = E.add(assigned, result[ids[i]])
rests.append(E.subtract(normalized, result[ids[i]]))
order.append(i)
order.sort_custom(func(a: int, b: int) -> bool: return E.gt(rests[a], rests[b]) if E.ne(rests[a], rests[b]) else str(ids[a]) < str(ids[b]))
for i in order:
if E.ge(assigned, count): break
result[ids[i]] = E.add(result[ids[i]], 1)
assigned = E.add(assigned, 1)
for i in ids.size():
remainders[remainder_keys[i]] = E.clamp_value(E.subtract(expected[i], result[ids[i]]), E.neg(1), 1)
return result
func run():
for count in [0, 1, 7, E.of("1.375"), 1000000, E.of("1e40"), E.of("1e80")]:
for weights in [{}, {"a": 0, "b": 0}, {"a": 0, "b": 7}, {"b": 0, "a": 7}, {"a": 1, "b": 1, "c": 1}, {"a": E.of("0.017"), "b": E.of("0.333333333333"), "c": 0}, {"a": E.of("1e40"), "b": 17, "c": 1}]:
for carry in [0, E.of("0.25"), E.of("-0.75")]:
var actual: Dictionary = {"case:a": carry}
var expected: Dictionary = actual.duplicate(true)
for iteration in 5:
var reference: Dictionary = reference_split(expected, "case", count, weights)
var result: Dictionary = Q.split(actual, "case", count, weights)
check(E.eq(result, reference), "Split preserves allocation")
check(E.eq(actual, expected), "Split preserves rounding carries")
for a in [0, 7, E.of("1e40"), E.of("-1e40"), E.of("1.375")]:
var values: Array = [0, 1, -7, E.of("0.017"), E.of("1e30")]
for divisor in [1, 3, E.of("0.017"), E.of("1e40")]:
var results: Array = E.mul_div_many(a, values, divisor)
for i in values.size(): check(E.eq(results[i], E.mul_div(a, values[i], divisor)), "Grouped ratios preserve scalar results")
for count in [1, 7, 1000000000, E.of("1e40"), E.of("1e80")]:
for factor in [200, 1000]:
var weights: Array = [0, 17, E.of("1e40"), E.multiply(count, factor)]
var results: Array = E.population_quotas(count, weights, factor)
for i in weights.size(): check(E.eq(results[i], E.mul_div(count, weights[i], E.multiply(count, factor))), "Cancelled population preserves exact scalar quotas")
print("Split optimizations: %d checks, %d failures" % [checks, failures])
quit(1 if failures else 0)
+1
View File
@@ -0,0 +1 @@
uid://b4e1nd22q1705
+14
View File
@@ -0,0 +1,14 @@
extends SceneTree
const E = preload("res://world/owniverse/math/exact.gd")
func _initialize():
# SceneTree initialization runs before Global._ready, like main scene construction.
var global = root.get_node("Global")
assert(not global.is_node_ready(), "Test must cover pre-ready initialization")
assert(global.ExtraParams.has("E"), "Energy table exists before Global._ready")
var scene = load("res://world/world.tscn").instantiate()
var stage = scene.get_node("Stage")
assert(E.eq(stage.owniverse.state.energy, global.ExtraParams.E), "Stage starts with configured energy")
stage.owniverse.free()
scene.free()
print("Startup params: 3 checks, 0 failures")
quit()
+1
View File
@@ -0,0 +1 @@
uid://5cbgaqn5s1yw
+22
View File
@@ -65,6 +65,28 @@ func run() -> void:
check(stage.effects_player.stream == stage.effects_player._effects.window_open, "Opening a window plays its effect")
stage._on_topmenu_button_pressed("World")
check(stage.effects_player.stream == stage.effects_player._effects.window_close, "Closing a window plays its effect")
stage.indicator_elapsed = 1.0
stage.active_actions_elapsed = 1.0
stage.tick_ui(0.0)
var refreshes: int = stage.actions_refreshes
stage.date_time.text = "unchanged"
stage.active_actions_elapsed = 1.0
stage.tick_ui(0.0)
check(stage.actions_refreshes == refreshes, "Idle UI does not recalculate production limits")
check(stage.date_time.text == "unchanged", "Idle UI does not reformat the clock")
stage.owniverse.state.amount.H = 3456
stage.owniverse.presentation.refresh(stage.owniverse.state, stage.owniverse.catalog)
stage.tick_ui(0.0)
check(hydrogen.description.text == "3 456", "A manual publication updates idle UI")
check(stage.actions_refreshes == refreshes + 1, "A publication invalidates production limits")
stage.active_actions_dirty = true
stage.tick_ui(0.0)
check(stage.actions_refreshes == refreshes + 2, "Selection invalidation refreshes limits immediately")
var old_pointer: float = stage.world_indicators.pointers.Agg.position.x
stage.owniverse.environment.modify_world_parameter("Agg", 1)
stage.indicator_elapsed = 1.0
stage.tick_ui(0.0)
check(stage.world_indicators.pointers.Agg.position.x != old_pointer, "World parameter actions invalidate indicators")
for entity in stage.owniverse.catalog.entities.values(): entity.free()
stage.owniverse.free()
scene.free()