Refactor civilization evolution and state management
- Enhanced the `civilization_evolution.gd` script to improve population handling and resource allocation during civilization transitions. - Introduced new functions in `civilization_state.gd` for profile normalization and merging, ensuring consistent population representation across stages. - Updated `evolution_coordinator.gd` to incorporate budget phases for better tracking of simulation performance. - Improved `frame_budget.gd` to monitor maximum slice durations and phases during processing. - Added a new benchmarking tool in `android_benchmark.gd` to measure performance metrics during simulation runs, including frame processing times and throughput. - Created associated scene and resource files for the benchmarking tool to facilitate testing and performance evaluation.
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
extends Control
|
||||
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
const RESULT_PATH := "user://android_benchmark_result.txt"
|
||||
const FRAME_COUNT := 120
|
||||
|
||||
var status_label: Label
|
||||
|
||||
func _ready() -> void:
|
||||
status_label = Label.new()
|
||||
status_label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
status_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
status_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
status_label.add_theme_font_size_override("font_size", 32)
|
||||
add_child(status_label)
|
||||
call_deferred("_run")
|
||||
|
||||
func _set_status(message: String) -> void:
|
||||
status_label.text = message
|
||||
var file := FileAccess.open(RESULT_PATH, FileAccess.WRITE)
|
||||
if file != null:
|
||||
file.store_line(message)
|
||||
file.flush()
|
||||
|
||||
func _restore(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(value[key])
|
||||
elif value is Array:
|
||||
for index in value.size():
|
||||
value[index] = _restore(value[index])
|
||||
return value
|
||||
|
||||
func _load_payload_threaded() -> Dictionary:
|
||||
var file := FileAccess.open("res://tests/artifacts/all_objects_speed6.adventure.json", FileAccess.READ)
|
||||
if file == null:
|
||||
return {"error": "Cannot open benchmark save: %s" % FileAccess.get_open_error()}
|
||||
var parser := JSON.new()
|
||||
var error := parser.parse(file.get_as_text())
|
||||
if error != OK:
|
||||
return {"error": "JSON parse error at line %d: %s" % [parser.get_error_line(), parser.get_error_message()]}
|
||||
var data = E.decode(_restore(parser.data))
|
||||
if not data is Dictionary or not data.has("state"):
|
||||
return {"error": "Benchmark save has no state"}
|
||||
return {"payload": data}
|
||||
|
||||
func _run() -> void:
|
||||
_set_status("Reading and decoding benchmark save on a worker thread…")
|
||||
var worker := Thread.new()
|
||||
var start_error := worker.start(Callable(self, "_load_payload_threaded"))
|
||||
if start_error != OK:
|
||||
_set_status("ERROR: could not start loader thread (%s)" % start_error)
|
||||
return
|
||||
while worker.is_alive():
|
||||
await get_tree().process_frame
|
||||
var loaded: Dictionary = worker.wait_to_finish()
|
||||
if loaded.has("error"):
|
||||
_set_status("ERROR: " + str(loaded.error))
|
||||
return
|
||||
_set_status("Save decoded. Initializing UI scene…")
|
||||
var payload: Dictionary = loaded.payload
|
||||
var scene: Node = 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"))
|
||||
get_tree().root.add_child(scene)
|
||||
await get_tree().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 = stage.owniverse.frame_clock.get("datetime", stage.owniverse.state.time.datetime)
|
||||
var initial_published = stage.owniverse.state.time.datetime
|
||||
stage.owniverse.profile_enabled = true
|
||||
stage.measuring = true
|
||||
_set_status("Running speed 6: 0/%d frames" % FRAME_COUNT)
|
||||
for frame in FRAME_COUNT:
|
||||
await get_tree().process_frame
|
||||
if frame % 5 == 4:
|
||||
_set_status("Running speed 6: %d/%d frames; commits=%d" % [frame + 1, FRAME_COUNT, stage.owniverse.frame_commits])
|
||||
stage.measuring = false
|
||||
var samples: Array = stage.frame_samples
|
||||
if samples.is_empty():
|
||||
_set_status("ERROR: no rendered frame samples; frames=%d" % stage.frame_samples.size())
|
||||
return
|
||||
var total: float = 0.0
|
||||
for sample in samples:
|
||||
total += sample
|
||||
samples.sort()
|
||||
var requested_advance = E.subtract(stage.owniverse.frame_clock.datetime, initial_requested)
|
||||
var published_advance = E.subtract(stage.owniverse.state.time.datetime, initial_published)
|
||||
var process_samples: Array = stage.process_samples
|
||||
process_samples.sort()
|
||||
var process_total: float = 0.0
|
||||
for sample in process_samples:
|
||||
process_total += sample
|
||||
var process_summary := "no _process samples"
|
||||
if not process_samples.is_empty():
|
||||
process_summary = "mean=%.2fms p95=%.2fms max=%.2fms samples=%d" % [process_total / process_samples.size(), process_samples[int(ceil(process_samples.size() * 0.95)) - 1], process_samples.back(), process_samples.size()]
|
||||
var budget: Variant = stage.owniverse.frame_budget
|
||||
var max_slice_summary: String = "none"
|
||||
if budget != null:
|
||||
max_slice_summary = "%.2fms (%s)" % [budget.max_slice_usec / 1000.0, budget.max_slice_phase]
|
||||
var result := "DONE\nFPS=%.2f mean=%.2fms p95=%.2fms max=%.2fms frames=%d commits=%d\nStage _process CPU: %s\nMax uninterrupted simulation slice: %s\nRequested=%s published=%s throughput=%s\nFinal adaptive step=%s\nProfile=%s" % [1000.0 / (total / samples.size()), total / samples.size(), samples[int(ceil(samples.size() * 0.95)) - 1], samples.back(), samples.size(), stage.owniverse.frame_commits, process_summary, max_slice_summary, str(requested_advance), str(published_advance), str(E.divide(published_advance, requested_advance)), str(stage.owniverse.frame_step_seconds), str(stage.owniverse.profile)]
|
||||
_set_status(result)
|
||||
get_tree().quit()
|
||||
@@ -0,0 +1 @@
|
||||
uid://dkbyhj8ib0eyf
|
||||
@@ -0,0 +1,12 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://tests/android_benchmark.gd" id="1"]
|
||||
|
||||
[node name="AndroidBenchmark" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1")
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -61,3 +61,34 @@ godot --headless --path Owniverse.GI --script res://tests/create_full_benchmark_
|
||||
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
|
||||
```
|
||||
|
||||
## Проверка на Android Note8 (2026-10-06)
|
||||
|
||||
Сборка benchmark установлена отдельным пакетом `com.KIDGameProduction.Owniverse.AndroidBench`, сохранение основной игры не использовалось. Подготовка и декодирование сохранения выполнялись в фоновом потоке; ход и результат записывались в файл приложения. Устройство: Samsung SM-N950F, Android 9. Сценарий: все объекты и цивилизации, скорость 6, 120 кадров (119 интервалов).
|
||||
|
||||
| Метрика | Результат |
|
||||
|---|---:|
|
||||
| FPS UI | 1,76 |
|
||||
| Средний / p95 / максимальный кадр | 567,28 / 817,54 / 1278,33 мс |
|
||||
| Завершенные транзакции за прогон | 1 |
|
||||
| Запрошено / опубликовано игрового времени | 1599938,57 / 10000 лет |
|
||||
| Доля опубликованного от запрошенного | 0,00625 |
|
||||
| Итоговый динамический шаг | 0,2 с |
|
||||
|
||||
Самый дорогой участок профиля — `generations`: 5,350 с суммарно за единственную завершенную транзакцию; внутри него `civilization_prepare` занял 3,186 с, `wars` — 2,217 с, `civilization_transitions` — 1,034 с. Интервалы вложены, поэтому их нельзя складывать с `generations`. Этот экстремальный сценарий на Note8 не достигает целевых 5 FPS и сильно отстает по игровому времени. Это замер именно полного стресс-сохранения, а не обычного игрового сохранения.
|
||||
|
||||
### Покадровая уступка в расчётах цивилизаций
|
||||
|
||||
Добавлены контрольные точки внутри обработки этик, проигравших в войне, падения популяции и выходов между стадиями. Математика и порядок переходов не меняются. На том же Note8 повторный прогон дал 1,87 FPS против 1,76 FPS до изменения (прирост около 6%). Средний кадр сократился с 567 до 535 мс, но p95 остался 630 мс, максимум — 1281 мс. Это слишком малое улучшение для цели 5 FPS.
|
||||
|
||||
В повторном профиле `generations` заняли 5,166 с, `civilization_prepare` — 3,040 с, войны — 0,985 с, переходы между стадиями — 0,640 с. Профиль не показывает явного трёхкратного ускорения: контрольные точки лучше дробят отдельные подциклы, но главный синхронный участок остается слишком дорогим. Диагностическая сборка для измерения максимального непрерывного участка не завершила прогон на телефоне; при следующем проходе сначала нужно локализовать этот участок, а затем оптимизировать его отдельно.
|
||||
|
||||
Контрольный запуск UI-бенчмарка на Apple M4 Pro после изменения: 21,51 FPS, средний / p95 / максимальный кадр — 46,49 / 89,62 / 107,19 мс. Этот результат подтверждает отсутствие регрессии на компьютере, но не заменяет Android-замер.
|
||||
|
||||
### После перехода этик на доли (2026-10-06)
|
||||
|
||||
Этическое состояние теперь хранит шесть долей `float`, а точное целое население берётся из реестра объектов. Исходы одной стадии сначала собираются по назначениям; доли состояния обновляются после агрегации потоков. Формат сохранения повышен до версии 6; миграции прежних сохранений нет.
|
||||
|
||||
Финальная 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 за прогон не найдено.
|
||||
|
||||
@@ -42,18 +42,17 @@ func seed_population(world, id: String, count: Variant, time: Variant = 1.0) ->
|
||||
world.state.batches[id] = [{"amount": count, "start_year": time, "end_year": time}]
|
||||
world.evolution.initialize_life_batches()
|
||||
|
||||
func sum_ethics(world, id: String) -> Variant:
|
||||
var result: Variant = 0
|
||||
for n in world.state.ethics.get(id, {}).values(): result = E.add(result, n)
|
||||
return result
|
||||
|
||||
func invariants(world) -> void:
|
||||
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
||||
for id in world.state.ethics:
|
||||
check(E.eq(sum_ethics(world, id), physical.get(id, 0)), E.add("Ethics equal physical population for ", id))
|
||||
var total: float = 0.0
|
||||
for n in world.state.ethics[id].values():
|
||||
if (E.lt(n, 0) or E.ne(n, E.floor_value(n))): check(false, "Invalid ethic amount")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Civilization state validates")
|
||||
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")
|
||||
|
||||
func run() -> void:
|
||||
var global: Node = root.get_node("Global")
|
||||
|
||||
@@ -48,18 +48,23 @@ func run() -> void:
|
||||
world.evolution.initialize_life_batches()
|
||||
check((E.eq(world.state.amount["C2.0"], 12) and E.eq(world.state.binds.RD["C2.0"], 0)), "Old space civilizations detach without loss")
|
||||
check(E.eq(world.state.binds.RD["C1.9"], 3), "Planetary civilizations retain their host")
|
||||
check(E.eq(world.state.ethics["C0.2"], {"m": 2, "l": 2, "w": 1, "d": 1, "c": 1, "b": 1}), "Migration uses equal ethics and deterministic remainder")
|
||||
check(world.state.ethics["C2.0"].values().all(func ( n ) : return E.eq(n, 2)), "Bound civilization ethics count physical population")
|
||||
check(world.state.ethics["C0.2"].values().all(func ( n ) : return E.near(n, 1.0 / 6.0)), "Migration initializes equal ethic shares")
|
||||
check(world.state.ethics["C2.0"].values().all(func ( n ) : return E.near(n, 1.0 / 6.0)), "Bound civilization ethics are population-independent shares")
|
||||
var before: Dictionary = world.state.duplicate(true)
|
||||
world.evolution.initialize_life_batches()
|
||||
check(E.eq(world.state, before), "Repeated initialization preserves ethics")
|
||||
var profiles_stable: bool = true
|
||||
for ethic_id in before.ethics:
|
||||
for ethic in before.ethics[ethic_id]:
|
||||
profiles_stable = profiles_stable and E.near(world.state.ethics[ethic_id][ethic], before.ethics[ethic_id][ethic])
|
||||
check(profiles_stable, "Repeated initialization preserves ethics")
|
||||
world.state.ethics["C0.2"] = {"m": 8, "l": 0, "w": 0, "d": 0, "c": 0, "b": 0}
|
||||
world.evolution.initialize_life_batches()
|
||||
check(E.eq(world.state.ethics["C0.2"].m, 8), "Existing unequal distribution is retained")
|
||||
check(E.eq(world.state.ethics["C0.2"].m, 1.0), "Existing unequal distribution is retained")
|
||||
world.state.amount["C0.2"] = 3
|
||||
world.evolution.initialize_life_batches()
|
||||
check(E.eq(world.state.ethics["C0.2"].m, 3), "Losses reduce ethics without inventing population")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Ethics state validates")
|
||||
check(E.eq(world.state.ethics["C0.2"].m, 1.0), "Losses retain normalized ethical composition")
|
||||
check(E.eq(world.ledger.physical_amounts(world.state)["C0.2"], 3), "Exact population remains in the object ledger")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 6, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Ethics state validates")
|
||||
var restored = fixture()
|
||||
restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
|
||||
restored.evolution.initialize_life_batches()
|
||||
@@ -117,7 +122,7 @@ func run() -> void:
|
||||
world.evolution.initialize_life_batches()
|
||||
check(E.eq(world.state.amount["C4.2"], 2), "SMBH capacity uses stellar mass equivalent")
|
||||
check(E.eq(world.ledger.physical_amounts(world.state)["C1.9"], 1), "Unsupported civilization degrades through missing habitat families")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Capacity-limited state validates")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 6, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Capacity-limited state validates")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
|
||||
@@ -57,7 +57,7 @@ func run():
|
||||
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})
|
||||
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(), "Coarse state validates: " + validation)
|
||||
world.evolution.evolute_to_datetime(20000)
|
||||
if id in ["C0.0", "C0.1"]:
|
||||
|
||||
@@ -94,7 +94,7 @@ func run():
|
||||
check(str(world.state.auto_star_deficits).length() < 500, "true", "fraction storage stays bounded")
|
||||
print("100 changing-composition cycles: %d ms" % (Time.get_ticks_msec() - started))
|
||||
var saves = root.get_node("SaveManager")
|
||||
check(saves.validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), "", "long-running state saves")
|
||||
check(saves.validate_data({"format_version": 6, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), "", "long-running state saves")
|
||||
for entity in world.catalog.entities.values(): entity.free()
|
||||
world.free()
|
||||
print("Exact math: %d checks, %d failures" % [checks, failures])
|
||||
|
||||
+2
-2
@@ -54,7 +54,7 @@ func run() -> void:
|
||||
check(E.gt(world.state.batches.HP.size(), 0), "Survivors get next attempt")
|
||||
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
||||
check(E.eq(E.add(E.add(physical.HP, physical.EON1), physical.RP), 40000), "Life conserves planets")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Life save validates")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 6, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Life save validates")
|
||||
var restored = fixture()
|
||||
restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
|
||||
world.evolution.evolve_batches(world.state, 11000000)
|
||||
@@ -120,7 +120,7 @@ func run() -> void:
|
||||
world.evolution.evolve_batches(world.state, 2050)
|
||||
check(E.eq(world.state.binds.RD["C0.2"], 700), "C0.1 reaches ethical civilization without losing host")
|
||||
check(E.eq(world.state.binds.RD["C0.0"], 1000), "C0.1 degradation follows Unity")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Extended life save validates")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 6, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Extended life save validates")
|
||||
dispose(world)
|
||||
print(("Life: %d checks, %d failures" % [checks, failures]))
|
||||
quit((0 if E.eq(failures, 0) else 1))
|
||||
|
||||
@@ -78,30 +78,41 @@ func run():
|
||||
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": {}}
|
||||
target.amount = {}
|
||||
target.binds = {}
|
||||
target.stats = {"total_amount": {}}
|
||||
target.amount[id] = 60
|
||||
target.stats.total_amount[id] = 60
|
||||
Civ.State.reconcile(world, target, rules, {id: 60})
|
||||
target.ethics[id].m = 100
|
||||
target.ethics[id] = {"m": 1.0, "l": 0.0, "w": 0.0, "d": 0.0, "c": 0.0, "b": 0.0}
|
||||
Civ.State.reconcile(world, target, rules, {id: 60})
|
||||
check(E.eq(Civ.total(target.ethics[id]), 60), "Reconcile cache detects changed ethics")
|
||||
check(E.eq(target.ethics[id].m, 1.0), "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")
|
||||
check(E.eq(target.ethics[id].m, 1.0), "Reconcile cache detects changed population")
|
||||
target.amount[id] = 120
|
||||
Civ.worship(world, target, rules)
|
||||
target.ethics[id].m = E.add(target.ethics[id].m, 5)
|
||||
var expected: Variant = 0
|
||||
target.ethics[id].m = 0.5
|
||||
target.ethics[id].l = 0.5
|
||||
var expected: float = 0.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 n: float = (0.0 + target.ethics[stage][ethic]) * E.approx(E.of(rules[stage].ethics[ethic].worship))
|
||||
n *= E.approx(target.amount.get(stage, 0))
|
||||
expected += n
|
||||
check(E.near(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
|
||||
expected = 0.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")
|
||||
var n: float = (0.0 + target.ethics[stage][ethic]) * E.approx(E.of(worship_rules[stage].ethics[ethic].worship))
|
||||
n *= E.approx(target.amount.get(stage, 0))
|
||||
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)
|
||||
world.speed = speed
|
||||
for i in 3: world.evolute(1.0 if speed == 1 else 0.1)
|
||||
|
||||
@@ -96,7 +96,7 @@ func run() -> void:
|
||||
check((E.eq(world.state.binds.RD.GG, 16) and E.eq(world.state.binds.RD.AB, 120)), "Planet multiplicity applied")
|
||||
check((E.eq(world.state.amount.HP, 0) and E.eq(world.state.stats.total_amount.HP, 1)), "Created planets are bound, counted once")
|
||||
var saves: Node = root.get_node("SaveManager")
|
||||
check(E.eq(saves.validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Planet carry save validates")
|
||||
check(E.eq(saves.validate_data({"format_version": 6, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Planet carry save validates")
|
||||
restored = fixture()
|
||||
restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
|
||||
world.stellar.create_planets(world.state, "RD", 37, 0, 0, E.of("0.7"))
|
||||
@@ -118,7 +118,7 @@ func run() -> void:
|
||||
check((E.eq(world.state.amount.RP, 0) and E.eq(world.state.amount["C1.3"], 0)), "Inherited planets never become free")
|
||||
world.evolution.evolve_batches(world.state, E.add(lifespan, width))
|
||||
check((E.eq(world.state.binds.RD["C1.3"], 0) and E.eq(world.state.binds.TWD["C1.3"], 4)), "No planets left at extinct parent type")
|
||||
check(E.eq(saves.validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Inherited state validates")
|
||||
check(E.eq(saves.validate_data({"format_version": 6, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Inherited state validates")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
@@ -158,7 +158,7 @@ func run() -> void:
|
||||
check((E.eq(world.state.amount.SN, 0) and E.eq(world.state.amount.LNS, 1)), "Supernova produces a remnant")
|
||||
check(((E.eq(world.state.amount.H, 8) and E.eq(world.state.amount.He, 50)) and E.eq(world.state.amount.Met, 11)), "Supernova returns table resources")
|
||||
check((not world.state.amount.has("blast")), "Supernova dispatches blast output")
|
||||
check(E.eq(saves.validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Blast state validates")
|
||||
check(E.eq(saves.validate_data({"format_version": 6, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Blast state validates")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
|
||||
+2
-2
@@ -51,10 +51,10 @@ func run() -> void:
|
||||
for invalid in [0, E.neg(1), NAN, INF, 1e308]:
|
||||
world.evolute(invalid)
|
||||
check(E.eq(world.state, before), "Invalid/overflow interval rejected")
|
||||
check(E.eq(saves.validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Fractional save is valid")
|
||||
check(E.eq(saves.validate_data({"format_version": 6, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Fractional save is valid")
|
||||
var invalid_save: Dictionary = world.state.duplicate(true)
|
||||
invalid_save.time.year = E.of("0.25")
|
||||
check(E.ne(saves.validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": invalid_save}), ""), "Reject inconsistent fractional clock")
|
||||
check(E.ne(saves.validate_data({"format_version": 6, "app_version": "", "ui": {"current_page": 0}, "state": invalid_save}), ""), "Reject inconsistent fractional clock")
|
||||
dispose(world)
|
||||
|
||||
for fps in [30, 60, 144]:
|
||||
|
||||
@@ -91,7 +91,7 @@ func run() -> void:
|
||||
|
||||
world = fixture()
|
||||
world.state.amount["C0.2"] = 1000000
|
||||
world.state.ethics["C0.2"] = {"m": 1000000}
|
||||
world.state.ethics["C0.2"] = {"m": 1.0, "l": 0.0, "w": 0.0, "d": 0.0, "c": 0.0, "b": 0.0}
|
||||
var baseline: Dictionary = world.state.duplicate(true)
|
||||
var advanced: Dictionary = baseline.duplicate(true)
|
||||
advanced.world_parameters.Agg = 19
|
||||
@@ -108,7 +108,9 @@ func run() -> void:
|
||||
# Trends use ethical proportions and stage weights, not population magnitude.
|
||||
world = fixture()
|
||||
var rules: Dictionary = {"C2.0": {"home": "DwStar"}, "C2.1": {"home": "DwStar"}}
|
||||
world.state.ethics = {"C2.0": {"d": 100}, "C2.1": {"d": 100}}
|
||||
world.state.ethics = {"C2.0": {"m": 0.0, "l": 0.0, "w": 0.0, "d": 1.0, "c": 0.0, "b": 0.0}, "C2.1": {"m": 0.0, "l": 0.0, "w": 0.0, "d": 1.0, "c": 0.0, "b": 0.0}}
|
||||
world.state.amount["C2.1"] = 10
|
||||
world.state.binds["RD"] = {"C2.0": 10}
|
||||
var source: Dictionary = world.state.duplicate(true)
|
||||
var single: Dictionary = source.duplicate(true)
|
||||
var split: Dictionary = source.duplicate(true)
|
||||
@@ -117,10 +119,17 @@ func run() -> void:
|
||||
check(E.near(single.trends.Integration, E.of("0.018")), "Trend integrates elapsed years and doubled stage weights")
|
||||
for key in Parameters.TRENDS:
|
||||
check(E.near(single.trends[key], split.trends[key]), E.add("Trend is independent of frame partition: ", key))
|
||||
source.ethics["C2.0"].d = 1000000
|
||||
source.ethics["C2.0"].d = 1.0
|
||||
var larger: Dictionary = world.state.duplicate(true)
|
||||
larger.amount["C2.1"] = 1000000
|
||||
larger.binds["RD"]["C2.0"] = 1000000
|
||||
Parameters.advance(source, larger, rules, 1000000)
|
||||
check(E.near(larger.trends.Integration, single.trends.Integration), "Population proportions determine trend influence")
|
||||
var vacant: Dictionary = world.state.duplicate(true)
|
||||
vacant.amount["C2.1"] = 0
|
||||
vacant.binds["RD"]["C2.0"] = 0
|
||||
Parameters.advance(vacant, vacant, rules, 1000000)
|
||||
check(vacant.trends.values().all(func (value): return E.eq(value, 0)), "Empty saved ethic profiles do not generate trends")
|
||||
world.state.trends.Integration = E.of("0.99")
|
||||
Parameters.advance(source, world.state, rules, 100000000)
|
||||
check(E.eq(world.state.civilization_stats["trend:Integration"], 2), "Long steps process every crossed threshold")
|
||||
@@ -133,7 +142,7 @@ func run() -> void:
|
||||
Parameters.advance(source, world.state, rules, E.of("1.0e12"))
|
||||
check((E.ge(world.state.trends.Integration, 0) and E.lt(world.state.trends.Integration, 1)), "Very large intervals finish with a valid trend fraction")
|
||||
check(E.ge(world.state.world_parameters.Agg, 1), "Repeated integration events cannot reduce aggression below the scale")
|
||||
var payload: Dictionary = {"format_version": 5, "app_version": "test", "ui": {"current_page": 1}, "state": world.state}
|
||||
var payload: Dictionary = {"format_version": 6, "app_version": "test", "ui": {"current_page": 1}, "state": world.state}
|
||||
var manager: Node = root.get_node("SaveManager")
|
||||
check(E.eq(manager.validate_data(payload), ""), "New parameters and trends pass save validation")
|
||||
var old: Dictionary = payload.duplicate(true)
|
||||
|
||||
Reference in New Issue
Block a user