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:
@@ -6,7 +6,7 @@ const E = preload("res://world/owniverse/math/exact.gd")
|
|||||||
const SETTING_SECTION = "Settings"
|
const SETTING_SECTION = "Settings"
|
||||||
const SAVE_PATH = "user://settings.cfg"
|
const SAVE_PATH = "user://settings.cfg"
|
||||||
|
|
||||||
const FORMAT_VERSION : int = 5
|
const FORMAT_VERSION : int = 6
|
||||||
const SimulationTime = preload("res://world/owniverse/simulation_time.gd")
|
const SimulationTime = preload("res://world/owniverse/simulation_time.gd")
|
||||||
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
||||||
const STATE_VERSION : String = SimulationTime.STATE_VERSION
|
const STATE_VERSION : String = SimulationTime.STATE_VERSION
|
||||||
@@ -348,11 +348,19 @@ static func _validate_payload(data: Variant, speed_limit: int, stages: Array, en
|
|||||||
if ((not (saved.ethics is Dictionary)) or (not (saved.civilization_homes is Dictionary))):
|
if ((not (saved.ethics is Dictionary)) or (not (saved.civilization_homes is Dictionary))):
|
||||||
return "invalid civilization state"
|
return "invalid civilization state"
|
||||||
for id in saved.ethics:
|
for id in saved.ethics:
|
||||||
if (not E.contains(stages, id) or (not _number_map(saved.ethics[id], true))):
|
if (not E.contains(stages, id) or (not (saved.ethics[id] is Dictionary))):
|
||||||
return E.add("invalid state.ethics.", str(id))
|
return E.add("invalid state.ethics.", str(id))
|
||||||
|
if E.ne(saved.ethics[id].keys().size(), 6):
|
||||||
|
return E.add("invalid ethic profile size for ", str(id))
|
||||||
|
var share_sum: float = 0.0
|
||||||
for ethic in saved.ethics[id]:
|
for ethic in saved.ethics[id]:
|
||||||
if not E.contains(["m", "l", "w", "d", "c", "b"], ethic):
|
var share: Variant = saved.ethics[id][ethic]
|
||||||
|
var share_value: float = 0.0 + share if share is int or share is float else -1.0
|
||||||
|
if not E.contains(["m", "l", "w", "d", "c", "b"], ethic) or not is_finite(share_value) or share_value < 0.0 or share_value > 1.0:
|
||||||
return E.add("invalid ethic ", str(ethic))
|
return E.add("invalid ethic ", str(ethic))
|
||||||
|
share_sum += share_value
|
||||||
|
if absf(share_sum - 1.0) > 0.000001:
|
||||||
|
return E.add("invalid ethic profile total for ", str(id))
|
||||||
for id in saved.civilization_homes:
|
for id in saved.civilization_homes:
|
||||||
if (not _number_map(saved.civilization_homes[id], true)):
|
if (not _number_map(saved.civilization_homes[id], true)):
|
||||||
return "invalid civilization homes"
|
return "invalid civilization homes"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[runnable_presets]
|
[runnable_presets]
|
||||||
|
|
||||||
"Windows Desktop"="Windows Desktop"
|
"Windows Desktop"="Windows Desktop"
|
||||||
|
Android="Android"
|
||||||
|
|
||||||
[preset.0]
|
[preset.0]
|
||||||
|
|
||||||
|
|||||||
@@ -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 --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
|
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.state.batches[id] = [{"amount": count, "start_year": time, "end_year": time}]
|
||||||
world.evolution.initialize_life_batches()
|
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:
|
func invariants(world) -> void:
|
||||||
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
||||||
for id in world.state.ethics:
|
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():
|
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")
|
var share: float = 0.0 + n
|
||||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Civilization state validates")
|
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:
|
func run() -> void:
|
||||||
var global: Node = root.get_node("Global")
|
var global: Node = root.get_node("Global")
|
||||||
|
|||||||
@@ -48,18 +48,23 @@ func run() -> void:
|
|||||||
world.evolution.initialize_life_batches()
|
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.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.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["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.eq(n, 2)), "Bound civilization ethics count physical population")
|
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)
|
var before: Dictionary = world.state.duplicate(true)
|
||||||
world.evolution.initialize_life_batches()
|
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.state.ethics["C0.2"] = {"m": 8, "l": 0, "w": 0, "d": 0, "c": 0, "b": 0}
|
||||||
world.evolution.initialize_life_batches()
|
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.state.amount["C0.2"] = 3
|
||||||
world.evolution.initialize_life_batches()
|
world.evolution.initialize_life_batches()
|
||||||
check(E.eq(world.state.ethics["C0.2"].m, 3), "Losses reduce ethics without inventing population")
|
check(E.eq(world.state.ethics["C0.2"].m, 1.0), "Losses retain normalized ethical composition")
|
||||||
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.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()
|
var restored = fixture()
|
||||||
restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
|
restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
|
||||||
restored.evolution.initialize_life_batches()
|
restored.evolution.initialize_life_batches()
|
||||||
@@ -117,7 +122,7 @@ func run() -> void:
|
|||||||
world.evolution.initialize_life_batches()
|
world.evolution.initialize_life_batches()
|
||||||
check(E.eq(world.state.amount["C4.2"], 2), "SMBH capacity uses stellar mass equivalent")
|
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(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)
|
dispose(world)
|
||||||
|
|
||||||
world = fixture()
|
world = fixture()
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ func run():
|
|||||||
var before: Dictionary = world.state.duplicate(true)
|
var before: Dictionary = world.state.duplicate(true)
|
||||||
world.evolution.evolute_to_datetime(10000)
|
world.evolution.evolute_to_datetime(10000)
|
||||||
check(E.eq(world.state, before), "Repeating completed clock does not repeat transitions")
|
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)
|
check(validation.is_empty(), "Coarse state validates: " + validation)
|
||||||
world.evolution.evolute_to_datetime(20000)
|
world.evolution.evolute_to_datetime(20000)
|
||||||
if id in ["C0.0", "C0.1"]:
|
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")
|
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))
|
print("100 changing-composition cycles: %d ms" % (Time.get_ticks_msec() - started))
|
||||||
var saves = root.get_node("SaveManager")
|
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()
|
for entity in world.catalog.entities.values(): entity.free()
|
||||||
world.free()
|
world.free()
|
||||||
print("Exact math: %d checks, %d failures" % [checks, failures])
|
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")
|
check(E.gt(world.state.batches.HP.size(), 0), "Survivors get next attempt")
|
||||||
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
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(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()
|
var restored = fixture()
|
||||||
restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
|
restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
|
||||||
world.evolution.evolve_batches(world.state, 11000000)
|
world.evolution.evolve_batches(world.state, 11000000)
|
||||||
@@ -120,7 +120,7 @@ func run() -> void:
|
|||||||
world.evolution.evolve_batches(world.state, 2050)
|
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.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(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)
|
dispose(world)
|
||||||
print(("Life: %d checks, %d failures" % [checks, failures]))
|
print(("Life: %d checks, %d failures" % [checks, failures]))
|
||||||
quit((0 if E.eq(failures, 0) else 1))
|
quit((0 if E.eq(failures, 0) else 1))
|
||||||
|
|||||||
@@ -78,30 +78,41 @@ func run():
|
|||||||
changed = Civ.coefficients(world, local_rules, id, modifiers)
|
changed = Civ.coefficients(world, local_rules, id, modifiers)
|
||||||
check(E.eq(changed.m.pro, E.divide(E.of("0.375"), E.maximum(1, E.add(E.of("0.375"), d)))), "Coefficient cache follows rule edits")
|
check(E.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": {}}
|
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})
|
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})
|
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})
|
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)
|
Civ.worship(world, target, rules)
|
||||||
target.ethics[id].m = E.add(target.ethics[id].m, 5)
|
target.ethics[id].m = 0.5
|
||||||
var expected: Variant = 0
|
target.ethics[id].l = 0.5
|
||||||
|
var expected: float = 0.0
|
||||||
for stage in rules:
|
for stage in rules:
|
||||||
if rules[stage].worships:
|
if rules[stage].worships:
|
||||||
for ethic in target.ethics[stage]:
|
for ethic in target.ethics[stage]:
|
||||||
expected = E.add(expected, E.multiply(target.ethics[stage][ethic], E.of(rules[stage].ethics[ethic].worship)))
|
var n: float = (0.0 + target.ethics[stage][ethic]) * E.approx(E.of(rules[stage].ethics[ethic].worship))
|
||||||
check(E.eq(Civ.worship(world, target, rules), expected), "Worship cache detects changed ethics")
|
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)
|
var worship_rules: Dictionary = rules.duplicate(true)
|
||||||
for stage in worship_rules:
|
for stage in worship_rules:
|
||||||
if worship_rules[stage].worships:
|
if worship_rules[stage].worships:
|
||||||
worship_rules[stage].ethics.m.worship = "0.375"
|
worship_rules[stage].ethics.m.worship = "0.375"
|
||||||
expected = 0
|
expected = 0.0
|
||||||
for stage in worship_rules:
|
for stage in worship_rules:
|
||||||
if worship_rules[stage].worships:
|
if worship_rules[stage].worships:
|
||||||
for ethic in target.ethics[stage]:
|
for ethic in target.ethics[stage]:
|
||||||
expected = E.add(expected, E.multiply(target.ethics[stage][ethic], E.of(worship_rules[stage].ethics[ethic].worship)))
|
var n: float = (0.0 + target.ethics[stage][ethic]) * E.approx(E.of(worship_rules[stage].ethics[ethic].worship))
|
||||||
check(E.eq(Civ.worship(world, target, worship_rules), expected), "Worship cache detects changed rules")
|
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.ledger = load("res://tests/fixtures/audit_rounding_ledger.gd").new(world)
|
||||||
world.speed = speed
|
world.speed = speed
|
||||||
for i in 3: world.evolute(1.0 if speed == 1 else 0.1)
|
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.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")
|
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")
|
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 = fixture()
|
||||||
restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
|
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"))
|
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")
|
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))
|
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(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)
|
dispose(world)
|
||||||
|
|
||||||
world = fixture()
|
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.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(((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((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)
|
dispose(world)
|
||||||
|
|
||||||
world = fixture()
|
world = fixture()
|
||||||
|
|||||||
+2
-2
@@ -51,10 +51,10 @@ func run() -> void:
|
|||||||
for invalid in [0, E.neg(1), NAN, INF, 1e308]:
|
for invalid in [0, E.neg(1), NAN, INF, 1e308]:
|
||||||
world.evolute(invalid)
|
world.evolute(invalid)
|
||||||
check(E.eq(world.state, before), "Invalid/overflow interval rejected")
|
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)
|
var invalid_save: Dictionary = world.state.duplicate(true)
|
||||||
invalid_save.time.year = E.of("0.25")
|
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)
|
dispose(world)
|
||||||
|
|
||||||
for fps in [30, 60, 144]:
|
for fps in [30, 60, 144]:
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ func run() -> void:
|
|||||||
|
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.state.amount["C0.2"] = 1000000
|
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 baseline: Dictionary = world.state.duplicate(true)
|
||||||
var advanced: Dictionary = baseline.duplicate(true)
|
var advanced: Dictionary = baseline.duplicate(true)
|
||||||
advanced.world_parameters.Agg = 19
|
advanced.world_parameters.Agg = 19
|
||||||
@@ -108,7 +108,9 @@ func run() -> void:
|
|||||||
# Trends use ethical proportions and stage weights, not population magnitude.
|
# Trends use ethical proportions and stage weights, not population magnitude.
|
||||||
world = fixture()
|
world = fixture()
|
||||||
var rules: Dictionary = {"C2.0": {"home": "DwStar"}, "C2.1": {"home": "DwStar"}}
|
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 source: Dictionary = world.state.duplicate(true)
|
||||||
var single: Dictionary = source.duplicate(true)
|
var single: Dictionary = source.duplicate(true)
|
||||||
var split: 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")
|
check(E.near(single.trends.Integration, E.of("0.018")), "Trend integrates elapsed years and doubled stage weights")
|
||||||
for key in Parameters.TRENDS:
|
for key in Parameters.TRENDS:
|
||||||
check(E.near(single.trends[key], split.trends[key]), E.add("Trend is independent of frame partition: ", key))
|
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)
|
var larger: Dictionary = world.state.duplicate(true)
|
||||||
|
larger.amount["C2.1"] = 1000000
|
||||||
|
larger.binds["RD"]["C2.0"] = 1000000
|
||||||
Parameters.advance(source, larger, rules, 1000000)
|
Parameters.advance(source, larger, rules, 1000000)
|
||||||
check(E.near(larger.trends.Integration, single.trends.Integration), "Population proportions determine trend influence")
|
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")
|
world.state.trends.Integration = E.of("0.99")
|
||||||
Parameters.advance(source, world.state, rules, 100000000)
|
Parameters.advance(source, world.state, rules, 100000000)
|
||||||
check(E.eq(world.state.civilization_stats["trend:Integration"], 2), "Long steps process every crossed threshold")
|
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"))
|
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.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")
|
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")
|
var manager: Node = root.get_node("SaveManager")
|
||||||
check(E.eq(manager.validate_data(payload), ""), "New parameters and trends pass save validation")
|
check(E.eq(manager.validate_data(payload), ""), "New parameters and trends pass save validation")
|
||||||
var old: Dictionary = payload.duplicate(true)
|
var old: Dictionary = payload.duplicate(true)
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ static func drift(target: Dictionary, key: String, pop: Dictionary, rows: Dictio
|
|||||||
weights.append(E.maximum(0, E.subtract(E.add(common, E.multiply(contributions[ETHICS.find(ethic)], 951 if colony else 195)), E.multiply(contributions[ETHICS.find(OPPOSITE[ethic])], 5 if colony else 1))))
|
weights.append(E.maximum(0, E.subtract(E.add(common, E.multiply(contributions[ETHICS.find(ethic)], 951 if colony else 195)), E.multiply(contributions[ETHICS.find(OPPOSITE[ethic])], 5 if colony else 1))))
|
||||||
if E.le(QuantityMath.sum_array(weights), 0): return pop.duplicate()
|
if E.le(QuantityMath.sum_array(weights), 0): return pop.duplicate()
|
||||||
if not target.has("civilization_remainders"): target.civilization_remainders = {}
|
if not target.has("civilization_remainders"): target.civilization_remainders = {}
|
||||||
return QuantityMath.split_arrays(target.civilization_remainders, key, population, ETHICS, weights, 0)
|
return QuantityMath.split_arrays(target.civilization_remainders, key, population, ETHICS, weights, 0, true)
|
||||||
|
|
||||||
static func diplomacy_rates(rows: Dictionary) -> Array:
|
static func diplomacy_rates(rows: Dictionary) -> Array:
|
||||||
var signature: String = ""
|
var signature: String = ""
|
||||||
@@ -188,18 +188,20 @@ static func consume_untriggered(world, target: Dictionary, plan: Dictionary, pop
|
|||||||
# Keeping the summation order preserves float fractional rounding exactly.
|
# Keeping the summation order preserves float fractional rounding exactly.
|
||||||
static func worship(world, target: Dictionary, rules: Dictionary) -> Variant:
|
static func worship(world, target: Dictionary, rules: Dictionary) -> Variant:
|
||||||
var result: Variant = 0
|
var result: Variant = 0
|
||||||
|
var physical: Dictionary = world.ledger.physical_amounts(target)
|
||||||
for id in rules:
|
for id in rules:
|
||||||
if not rules[id].worships: continue
|
if not rules[id].worships: continue
|
||||||
var pop: Dictionary = target.ethics[id]
|
var pop: Dictionary = State.normalize_profile(target.ethics.get(id, {}))
|
||||||
var coefficients: Dictionary = {}
|
var coefficients: Dictionary = {}
|
||||||
for ethic in pop: coefficients[ethic] = rules[id].ethics[ethic].worship
|
for ethic in pop: coefficients[ethic] = rules[id].ethics[ethic].worship
|
||||||
var cached: Dictionary = world.ledger.worship_products.get(id, {})
|
var cached: Dictionary = world.ledger.worship_products.get(id, {})
|
||||||
if cached.is_empty() or not E.eq(cached.population, pop) or not E.eq(cached.coefficients, coefficients):
|
var count: Variant = physical.get(id, 0)
|
||||||
var products: Dictionary = {}
|
if cached.is_empty() or not E.eq(cached.population, pop) or not E.eq(cached.coefficients, coefficients) or not E.eq(cached.count, count):
|
||||||
for ethic in pop: products[ethic] = E.multiply(pop[ethic], E.of(coefficients[ethic]))
|
var worship_rate: float = 0.0
|
||||||
cached = {"population": pop.duplicate(), "coefficients": coefficients, "products": products}
|
for ethic in pop: worship_rate += (0.0 + pop[ethic]) * E.approx(E.of(coefficients[ethic]))
|
||||||
|
cached = {"population": pop.duplicate(), "coefficients": coefficients, "count": count, "product": E.multiply(count, worship_rate)}
|
||||||
world.ledger.worship_products[id] = cached
|
world.ledger.worship_products[id] = cached
|
||||||
for ethic in pop: result = E.add(result, cached.products[ethic])
|
result = E.add(result, cached.product)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
static func coefficients(world, rules: Dictionary, id: String, modifiers: Dictionary) -> Dictionary:
|
static func coefficients(world, rules: Dictionary, id: String, modifiers: Dictionary) -> Dictionary:
|
||||||
@@ -236,8 +238,13 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: Varian
|
|||||||
for id in ids:
|
for id in ids:
|
||||||
reconcile_ids.append(id)
|
reconcile_ids.append(id)
|
||||||
if rules[id].fights and not reconcile_ids.has(rules[id].previous): reconcile_ids.append(rules[id].previous)
|
if rules[id].fights and not reconcile_ids.has(rules[id].previous): reconcile_ids.append(rules[id].previous)
|
||||||
|
if budget_timer != null: budget_timer.call("set_phase", "civilization_reconcile")
|
||||||
State.reconcile(world, target, rules, physical, reconcile_ids)
|
State.reconcile(world, target, rules, physical, reconcile_ids)
|
||||||
var remaining: Dictionary = target.ethics.duplicate()
|
if physical.is_empty(): physical = world.ledger.physical_amounts(target)
|
||||||
|
var remaining: Dictionary = {}
|
||||||
|
for id in reconcile_ids:
|
||||||
|
if rules.has(id):
|
||||||
|
remaining[id] = State.materialize_profile(target, id, target.ethics.get(id, {}), physical.get(id, 0))
|
||||||
var copied: Dictionary = {}
|
var copied: Dictionary = {}
|
||||||
var plans: Dictionary = {}
|
var plans: Dictionary = {}
|
||||||
var colonies: Array = []
|
var colonies: Array = []
|
||||||
@@ -247,6 +254,7 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: Varian
|
|||||||
if (not target.has("civilization_stats")): target.civilization_stats = {}
|
if (not target.has("civilization_stats")): target.civilization_stats = {}
|
||||||
ids.sort()
|
ids.sort()
|
||||||
for id in ids:
|
for id in ids:
|
||||||
|
if budget_timer != null: budget_timer.call("set_phase", "civilization_stage:" + id)
|
||||||
if budget_timer != null and not await budget_timer.pause(): return {}
|
if budget_timer != null and not await budget_timer.pause(): return {}
|
||||||
if ((not rules.has(id)) or expired[id].is_empty()): continue
|
if ((not rules.has(id)) or expired[id].is_empty()): continue
|
||||||
var plan: Dictionary = new_plan(id, year)
|
var plan: Dictionary = new_plan(id, year)
|
||||||
@@ -265,6 +273,8 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: Varian
|
|||||||
var settlers: Dictionary = {}
|
var settlers: Dictionary = {}
|
||||||
var stage_rates: Dictionary = coefficients(world, rules, id, modifiers)
|
var stage_rates: Dictionary = coefficients(world, rules, id, modifiers)
|
||||||
for ethic in triggered:
|
for ethic in triggered:
|
||||||
|
if budget_timer != null: budget_timer.call("set_phase", "civilization_ethic:" + id)
|
||||||
|
if budget_timer != null and not await budget_timer.pause(): return {}
|
||||||
var probability: Dictionary = stage_rates[ethic]
|
var probability: Dictionary = stage_rates[ethic]
|
||||||
var prefix: String = id + ":" + ethic
|
var prefix: String = id + ":" + ethic
|
||||||
var pro: Variant = rate(target, prefix + ":pro", triggered[ethic], probability.pro)
|
var pro: Variant = rate(target, prefix + ":pro", triggered[ethic], probability.pro)
|
||||||
@@ -277,11 +287,15 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: Varian
|
|||||||
triggered[ethic] = E.subtract(triggered[ethic], E.add(bounded.pro, bounded.deg))
|
triggered[ethic] = E.subtract(triggered[ethic], E.add(bounded.pro, bounded.deg))
|
||||||
var profile_transitions: int = world.profile_start()
|
var profile_transitions: int = world.profile_start()
|
||||||
add(triggered, progress(world, target, rules, budget, plan, successes))
|
add(triggered, progress(world, target, rules, budget, plan, successes))
|
||||||
|
if budget_timer != null and not await budget_timer.pause(): return {}
|
||||||
fall(target, plan, rules, failures)
|
fall(target, plan, rules, failures)
|
||||||
world.profile_end("civilization_transitions", profile_transitions)
|
world.profile_end("civilization_transitions", profile_transitions)
|
||||||
if rules[id].fights:
|
if rules[id].fights:
|
||||||
var profile_wars: int = world.profile_start()
|
var profile_wars: int = world.profile_start()
|
||||||
for ethic in warriors: warriors[ethic] = E.minimum(warriors[ethic], triggered[ethic])
|
for ethic in warriors:
|
||||||
|
if budget_timer != null: budget_timer.call("set_phase", "civilization_war:" + id)
|
||||||
|
if budget_timer != null and not await budget_timer.pause(): return {}
|
||||||
|
warriors[ethic] = E.minimum(warriors[ethic], triggered[ethic])
|
||||||
var warriors_total: Variant = total(warriors)
|
var warriors_total: Variant = total(warriors)
|
||||||
var triggered_total: Variant = total(triggered)
|
var triggered_total: Variant = total(triggered)
|
||||||
var remaining_total: Variant = total(remaining[id])
|
var remaining_total: Variant = total(remaining[id])
|
||||||
@@ -289,30 +303,36 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: Varian
|
|||||||
warriors = QuantityMath.allocate_whole_with_total(warriors, wars, warriors_total)
|
warriors = QuantityMath.allocate_whole_with_total(warriors, wars, warriors_total)
|
||||||
for ethic in warriors: triggered[ethic] = E.subtract(triggered[ethic], warriors[ethic])
|
for ethic in warriors: triggered[ethic] = E.subtract(triggered[ethic], warriors[ethic])
|
||||||
triggered_total = E.subtract(triggered_total, wars)
|
triggered_total = E.subtract(triggered_total, wars)
|
||||||
var outcomes: Dictionary = split(target, E.add(id, ":wars"), wars, {"deg": 1, "pro25": 2, "pro50": 3, "pro75": 3})
|
# Integrate the four equally weighted battle outcomes directly.
|
||||||
var win_expected: Variant = E.add(E.add(E.multiply(outcomes.get("pro25", 0), E.of("0.25")), E.multiply(outcomes.get("pro50", 0), E.of("0.5"))), E.multiply(outcomes.get("pro75", 0), E.of("0.75")))
|
# Their expected coefficients are wins=17/36, deaths=1/9,
|
||||||
var loss_expected: Variant = E.add(E.add(E.add(E.multiply(outcomes.get("deg", 0), 2), outcomes.get("pro25", 0)), E.multiply(outcomes.get("pro50", 0), E.of("1.5"))), E.multiply(outcomes.get("pro75", 0), 2))
|
# total losses=29/18, and collateral=40/3 per battle. Rate
|
||||||
var wins_requested: Variant = rate(target, E.add(id, ":wins"), win_expected, 1)
|
# remainders preserve fractional results between generations.
|
||||||
|
var wins_requested: Variant = rate(target, E.add(id, ":wins"), wars, E.of("17/36"))
|
||||||
var winners: Dictionary = take(world, warriors, wins_requested, wars)
|
var winners: Dictionary = take(world, warriors, wins_requested, wars)
|
||||||
var winner_total: Variant = E.floor_value(E.minimum(E.maximum(0, wins_requested), wars))
|
var winner_total: Variant = E.floor_value(E.minimum(E.maximum(0, wins_requested), wars))
|
||||||
warriors_total = E.subtract(wars, winner_total)
|
warriors_total = E.subtract(wars, winner_total)
|
||||||
var losses_requested: Variant = rate(target, E.add(id, ":attackers_lost"), E.divide(outcomes.get("deg", 0), order), 1)
|
var deaths_expected: Variant = E.divide(wars, 9)
|
||||||
|
var losses_requested: Variant = rate(target, E.add(id, ":attackers_lost"), E.divide(deaths_expected, order), 1)
|
||||||
var attackers_lost: Dictionary = take(world, warriors, losses_requested, warriors_total)
|
var attackers_lost: Dictionary = take(world, warriors, losses_requested, warriors_total)
|
||||||
var attackers_lost_total: Variant = E.floor_value(E.minimum(E.maximum(0, losses_requested), warriors_total))
|
var attackers_lost_total: Variant = E.floor_value(E.minimum(E.maximum(0, losses_requested), warriors_total))
|
||||||
warriors_total = E.subtract(warriors_total, attackers_lost_total)
|
warriors_total = E.subtract(warriors_total, attackers_lost_total)
|
||||||
add(triggered, progress(world, target, rules, budget, plan, winners, winner_total))
|
add(triggered, progress(world, target, rules, budget, plan, winners, winner_total))
|
||||||
triggered_total = total(triggered)
|
triggered_total = total(triggered)
|
||||||
|
if budget_timer != null and not await budget_timer.pause(): return {}
|
||||||
fall(target, plan, rules, attackers_lost)
|
fall(target, plan, rules, attackers_lost)
|
||||||
remaining_total = total(remaining[id])
|
remaining_total = total(remaining[id])
|
||||||
var losses: Variant = E.minimum(rate(target, E.add(id, ":loss"), E.divide(loss_expected, order), 1), E.add(triggered_total, remaining_total))
|
var losses_expected: Variant = E.multiply(wars, E.of("29/18"))
|
||||||
|
var losses: Variant = E.minimum(rate(target, E.add(id, ":loss"), E.divide(losses_expected, order), 1), E.add(triggered_total, remaining_total))
|
||||||
var sides: Dictionary = QuantityMath.allocate_whole_with_total({"due": triggered_total, "later": remaining_total}, losses, E.add(triggered_total, remaining_total))
|
var sides: Dictionary = QuantityMath.allocate_whole_with_total({"due": triggered_total, "later": remaining_total}, losses, E.add(triggered_total, remaining_total))
|
||||||
var victims: Dictionary = take(world, triggered, sides.due, triggered_total)
|
var victims: Dictionary = take(world, triggered, sides.due, triggered_total)
|
||||||
triggered_total = E.subtract(triggered_total, sides.due)
|
triggered_total = E.subtract(triggered_total, sides.due)
|
||||||
add(victims, consume_untriggered(world, target, plan, remaining[id], sides.later, year, remaining_total))
|
add(victims, consume_untriggered(world, target, plan, remaining[id], sides.later, year, remaining_total))
|
||||||
|
if budget_timer != null and not await budget_timer.pause(): return {}
|
||||||
fall(target, plan, rules, victims)
|
fall(target, plan, rules, victims)
|
||||||
add(triggered, warriors)
|
add(triggered, warriors)
|
||||||
triggered_total = E.add(triggered_total, warriors_total)
|
triggered_total = E.add(triggered_total, warriors_total)
|
||||||
var damage: Variant = rate(target, E.add(id, ":collateral"), E.divide((E.add(E.multiply(outcomes.get("pro50", 0), 10), E.multiply(outcomes.get("pro75", 0), 30))), order), 1)
|
var damage_expected: Variant = E.multiply(wars, E.of("40/3"))
|
||||||
|
var damage: Variant = rate(target, E.add(id, ":collateral"), E.divide(damage_expected, order), 1)
|
||||||
collateral[rules[id].previous] = E.add(collateral.get(rules[id].previous, 0), damage)
|
collateral[rules[id].previous] = E.add(collateral.get(rules[id].previous, 0), damage)
|
||||||
target.civilization_stats.wars = E.add(target.civilization_stats.get("wars", 0), wars)
|
target.civilization_stats.wars = E.add(target.civilization_stats.get("wars", 0), wars)
|
||||||
world.profile_end("wars", profile_wars)
|
world.profile_end("wars", profile_wars)
|
||||||
@@ -333,15 +353,19 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: Varian
|
|||||||
var sides: Dictionary = QuantityMath.allocate_whole_with_total({"due": retry_total, "later": remaining_total}, amount, E.add(retry_total, remaining_total))
|
var sides: Dictionary = QuantityMath.allocate_whole_with_total({"due": retry_total, "later": remaining_total}, amount, E.add(retry_total, remaining_total))
|
||||||
var victims: Dictionary = take(world, retry, sides.due, retry_total)
|
var victims: Dictionary = take(world, retry, sides.due, retry_total)
|
||||||
add(victims, consume_untriggered(world, target, plan, remaining[id], sides.later, year, remaining_total))
|
add(victims, consume_untriggered(world, target, plan, remaining[id], sides.later, year, remaining_total))
|
||||||
|
if budget_timer != null and not await budget_timer.pause(): return {}
|
||||||
fall(target, plan, rules, victims)
|
fall(target, plan, rules, victims)
|
||||||
target.civilization_stats.collateral = E.add(target.civilization_stats.get("collateral", 0), total(victims))
|
target.civilization_stats.collateral = E.add(target.civilization_stats.get("collateral", 0), total(victims))
|
||||||
var incoming: Dictionary = {}
|
var incoming: Dictionary = {}
|
||||||
var planetary: Dictionary = {}
|
var planetary: Dictionary = {}
|
||||||
for id in plans:
|
for id in plans:
|
||||||
|
if budget_timer != null: budget_timer.call("set_phase", "civilization_products:" + id)
|
||||||
if budget_timer != null and not await budget_timer.pause(): return {}
|
if budget_timer != null and not await budget_timer.pause(): return {}
|
||||||
var plan: Dictionary = plans[id]
|
var plan: Dictionary = plans[id]
|
||||||
var products: Dictionary = {}
|
var products: Dictionary = {}
|
||||||
for destination in plan.outputs:
|
for destination in plan.outputs:
|
||||||
|
if budget_timer != null: budget_timer.call("set_phase", "civilization_drift:" + id)
|
||||||
|
if budget_timer != null and not await budget_timer.pause(): return {}
|
||||||
var pop: Dictionary = plan.outputs[destination]
|
var pop: Dictionary = plan.outputs[destination]
|
||||||
var count: Variant = total(pop)
|
var count: Variant = total(pop)
|
||||||
if rules.has(destination):
|
if rules.has(destination):
|
||||||
@@ -356,7 +380,7 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: Varian
|
|||||||
planetary[destination] = E.add(planetary.get(destination, 0), count)
|
planetary[destination] = E.add(planetary.get(destination, 0), count)
|
||||||
expired[id] = [{"amount": plan.amount, "from": plan.from, "to": E.maximum(plan.from, plan.to), "products": products}]
|
expired[id] = [{"amount": plan.amount, "from": plan.from, "to": E.maximum(plan.from, plan.to), "products": products}]
|
||||||
world.profile_end("civilization_prepare", profile_started)
|
world.profile_end("civilization_prepare", profile_started)
|
||||||
return {"active": true, "remaining": remaining, "incoming": incoming, "planetary": planetary, "colonies": colonies}
|
return {"active": true, "remaining": remaining, "incoming": incoming, "planetary": planetary, "colonies": colonies, "base_physical": physical.duplicate()}
|
||||||
|
|
||||||
static func finish(world, target: Dictionary, transaction: Dictionary, year: Variant, rules: Dictionary, budget_timer: RefCounted = null) -> void:
|
static func finish(world, target: Dictionary, transaction: Dictionary, year: Variant, rules: Dictionary, budget_timer: RefCounted = null) -> void:
|
||||||
var profile_started: int = world.profile_start()
|
var profile_started: int = world.profile_start()
|
||||||
@@ -366,10 +390,15 @@ static func finish(world, target: Dictionary, transaction: Dictionary, year: Var
|
|||||||
target.civilization_stats.worship = worship(world, target, rules)
|
target.civilization_stats.worship = worship(world, target, rules)
|
||||||
world.profile_end("civilization_finish", profile_started)
|
world.profile_end("civilization_finish", profile_started)
|
||||||
return
|
return
|
||||||
target.ethics = transaction.remaining
|
var remaining: Dictionary = transaction.remaining
|
||||||
for id in transaction.incoming:
|
var incoming: Dictionary = transaction.incoming
|
||||||
if (not target.ethics.has(id)): target.ethics[id] = {}
|
for id in remaining:
|
||||||
add(target.ethics[id], transaction.incoming[id])
|
var counts: Dictionary = remaining[id].duplicate()
|
||||||
|
add(counts, incoming.get(id, {}))
|
||||||
|
target.ethics[id] = State.profile_from_counts(counts)
|
||||||
|
for id in incoming:
|
||||||
|
if not remaining.has(id):
|
||||||
|
target.ethics[id] = State.merge_profile(target.ethics.get(id, {}), transaction.base_physical.get(id, 0), incoming[id])
|
||||||
# Space civilizations that fall back to planetary stages need a stellar host.
|
# Space civilizations that fall back to planetary stages need a stellar host.
|
||||||
var physical: Dictionary = world.ledger.physical_amounts(target)
|
var physical: Dictionary = world.ledger.physical_amounts(target)
|
||||||
var hosts: Dictionary = {}
|
var hosts: Dictionary = {}
|
||||||
@@ -399,6 +428,16 @@ static func colonize(world, target: Dictionary, colonies: Array, year: Variant,
|
|||||||
var requests: Dictionary = {}
|
var requests: Dictionary = {}
|
||||||
for i in colonies.size(): requests[str(i)] = total(colonies[i].population)
|
for i in colonies.size(): requests[str(i)] = total(colonies[i].population)
|
||||||
var granted: Dictionary = QuantityMath.allocate_whole(requests, total(stock))
|
var granted: Dictionary = QuantityMath.allocate_whole(requests, total(stock))
|
||||||
|
var additions: Dictionary = {}
|
||||||
|
var credit_events: Array = []
|
||||||
|
var batch_additions: Dictionary = {}
|
||||||
|
var debits: Dictionary = {}
|
||||||
|
var slots_by_resource: Dictionary = {}
|
||||||
|
for id in stock:
|
||||||
|
var slots: Dictionary = {"": target.amount.get(id, 0)}
|
||||||
|
for owner in target.binds: slots[owner] = target.binds[owner].get(id, 0)
|
||||||
|
slots_by_resource[id] = slots
|
||||||
|
debits[id] = {}
|
||||||
for i in colonies.size():
|
for i in colonies.size():
|
||||||
if budget_timer != null and not await budget_timer.pause(): return
|
if budget_timer != null and not await budget_timer.pause(): return
|
||||||
var colony: Dictionary = colonies[i]
|
var colony: Dictionary = colonies[i]
|
||||||
@@ -409,21 +448,40 @@ static func colonize(world, target: Dictionary, colonies: Array, year: Variant,
|
|||||||
for id in targets:
|
for id in targets:
|
||||||
var amount: Variant = targets[id]
|
var amount: Variant = targets[id]
|
||||||
if E.le(amount, 0): continue
|
if E.le(amount, 0): continue
|
||||||
var slots: Dictionary = {"": target.amount.get(id, 0)}
|
var slots: Dictionary = slots_by_resource[id]
|
||||||
for owner in target.binds: slots[owner] = target.binds[owner].get(id, 0)
|
|
||||||
var taken: Dictionary = QuantityMath.allocate_whole(slots, amount)
|
var taken: Dictionary = QuantityMath.allocate_whole(slots, amount)
|
||||||
world.batches.scale_batches(target, id, E.subtract(1, E.divide(amount, stock[id])))
|
|
||||||
stock[id] = E.subtract(stock[id], amount)
|
stock[id] = E.subtract(stock[id], amount)
|
||||||
for owner in taken:
|
for owner in taken:
|
||||||
if E.le(taken[owner], 0): continue
|
if E.le(taken[owner], 0): continue
|
||||||
if E.eq(owner, ""): target.amount[id] = E.subtract(target.amount[id], taken[owner])
|
slots[owner] = E.subtract(slots.get(owner, 0), taken[owner])
|
||||||
else: target.binds[owner][id] = E.subtract(target.binds[owner][id], taken[owner])
|
debits[id][owner] = E.add(debits[id].get(owner, 0), taken[owner])
|
||||||
var created: Variant = world.ledger.credit(target, colony.stage, taken[owner])
|
credit_events.append({"stage": colony.stage, "owner": owner, "amount": taken[owner]})
|
||||||
if E.ne(owner, ""):
|
batch_additions[colony.stage] = E.add(batch_additions.get(colony.stage, 0), taken[owner])
|
||||||
target.amount[colony.stage] = E.subtract(target.amount[colony.stage], created)
|
if not additions.has(colony.stage): additions[colony.stage] = {}
|
||||||
target.binds[owner][colony.stage] = E.add(target.binds[owner].get(colony.stage, 0), created)
|
add(additions[colony.stage], population)
|
||||||
world.batches.append_batch(target, colony.stage, amount, year, year)
|
|
||||||
if (not target.ethics.has(colony.stage)): target.ethics[colony.stage] = {}
|
|
||||||
add(target.ethics[colony.stage], population)
|
|
||||||
target.civilization_stats.colonies = E.add(target.civilization_stats.get("colonies", 0), count)
|
target.civilization_stats.colonies = E.add(target.civilization_stats.get("colonies", 0), count)
|
||||||
|
# Commit each resource and owner once after all colonies have consumed their
|
||||||
|
# sequentially allocated shares. This keeps allocation order exact while
|
||||||
|
# avoiding repeated batch scaling and ledger mutations in the inner loop.
|
||||||
|
for id in debits:
|
||||||
|
var removed: Variant = E.subtract(physical.get(id, 0), stock[id])
|
||||||
|
if E.gt(physical.get(id, 0), 0):
|
||||||
|
world.batches.scale_batches(target, id, E.subtract(1, E.divide(removed, physical[id])))
|
||||||
|
for owner in debits[id]:
|
||||||
|
if E.eq(owner, ""):
|
||||||
|
target.amount[id] = E.subtract(target.amount.get(id, 0), debits[id][owner])
|
||||||
|
elif target.binds.has(owner):
|
||||||
|
target.binds[owner][id] = E.subtract(target.binds[owner].get(id, 0), debits[id][owner])
|
||||||
|
for event: Dictionary in credit_events:
|
||||||
|
var stage: String = event.stage
|
||||||
|
var owner: String = event.owner
|
||||||
|
var created: Variant = world.ledger.credit(target, stage, event.amount)
|
||||||
|
if E.ne(owner, ""):
|
||||||
|
target.amount[stage] = E.subtract(target.amount[stage], created)
|
||||||
|
if not target.binds.has(owner): target.binds[owner] = {}
|
||||||
|
target.binds[owner][stage] = E.add(target.binds[owner].get(stage, 0), created)
|
||||||
|
for stage in batch_additions:
|
||||||
|
world.batches.append_batch(target, stage, batch_additions[stage], year, year)
|
||||||
|
for id in additions:
|
||||||
|
target.ethics[id] = State.merge_profile(target.ethics.get(id, {}), physical.get(id, 0), additions[id])
|
||||||
world.profile_end("colonization", profile_colonies)
|
world.profile_end("colonization", profile_colonies)
|
||||||
|
|||||||
@@ -5,11 +5,68 @@ const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
|
|||||||
|
|
||||||
const ETHICS = ["m", "l", "w", "d", "c", "b"]
|
const ETHICS = ["m", "l", "w", "d", "c", "b"]
|
||||||
|
|
||||||
|
static func empty_profile() -> Dictionary:
|
||||||
|
var result: Dictionary = {}
|
||||||
|
for ethic in ETHICS: result[ethic] = 1.0 / ETHICS.size()
|
||||||
|
return result
|
||||||
|
|
||||||
|
static func normalize_profile(profile: Dictionary) -> Dictionary:
|
||||||
|
var result: Dictionary = {}
|
||||||
|
var total: float = 0.0
|
||||||
|
for ethic in ETHICS:
|
||||||
|
var value: float = maxf(0.0, E.approx(profile.get(ethic, 0.0)))
|
||||||
|
result[ethic] = value
|
||||||
|
total += value
|
||||||
|
if not is_finite(total) or total <= 0.0: return empty_profile()
|
||||||
|
if profile.size() == ETHICS.size() and absf(total - 1.0) <= 0.000000000001:
|
||||||
|
return result
|
||||||
|
var assigned: float = 0.0
|
||||||
|
for i in ETHICS.size() - 1:
|
||||||
|
var ethic: String = ETHICS[i]
|
||||||
|
result[ethic] = (0.0 + result[ethic]) / total
|
||||||
|
assigned += result[ethic]
|
||||||
|
result[ETHICS.back()] = maxf(0.0, 1.0 - assigned)
|
||||||
|
return result
|
||||||
|
|
||||||
|
static func profile_from_counts(population: Dictionary) -> Dictionary:
|
||||||
|
var total: Variant = QuantityMath.sum(population)
|
||||||
|
if E.le(total, 0): return empty_profile()
|
||||||
|
var profile: Dictionary = {}
|
||||||
|
for ethic in ETHICS:
|
||||||
|
profile[ethic] = E.approx(E.divide(population.get(ethic, 0), total))
|
||||||
|
return normalize_profile(profile)
|
||||||
|
|
||||||
|
static func materialize_profile(target: Dictionary, id: String, profile: Dictionary, count: Variant) -> Dictionary:
|
||||||
|
var normalized: Dictionary = normalize_profile(profile)
|
||||||
|
if not target.has("civilization_remainders"): target.civilization_remainders = {}
|
||||||
|
var shares: Array = []
|
||||||
|
for ethic in ETHICS: shares.append(normalized[ethic])
|
||||||
|
var result: Dictionary = QuantityMath.split_profile(target.civilization_remainders, "profile:" + id, count, ETHICS, shares)
|
||||||
|
for ethic in ETHICS:
|
||||||
|
if not result.has(ethic): result[ethic] = 0
|
||||||
|
return result
|
||||||
|
|
||||||
|
static func merge_profile(base: Dictionary, base_count: Variant, incoming: Dictionary) -> Dictionary:
|
||||||
|
var incoming_count: Variant = QuantityMath.sum(incoming)
|
||||||
|
return merge_profiles(base, base_count, profile_from_counts(incoming), incoming_count)
|
||||||
|
|
||||||
|
static func merge_profiles(base: Dictionary, base_count: Variant, incoming_profile: Dictionary, incoming_count: Variant) -> Dictionary:
|
||||||
|
var total: Variant = E.add(base_count, incoming_count)
|
||||||
|
if E.le(total, 0): return empty_profile()
|
||||||
|
var base_ratio: float = E.approx(E.divide(base_count, total)) if E.gt(base_count, 0) else 0.0
|
||||||
|
var incoming_ratio: float = E.approx(E.divide(incoming_count, total)) if E.gt(incoming_count, 0) else 0.0
|
||||||
|
var normalized_base: Dictionary = normalize_profile(base)
|
||||||
|
var result: Dictionary = {}
|
||||||
|
for ethic in ETHICS:
|
||||||
|
var incoming_share: float = E.approx(incoming_profile.get(ethic, 0.0)) if E.gt(incoming_count, 0) else 0.0
|
||||||
|
result[ethic] = (0.0 + normalized_base[ethic]) * base_ratio + incoming_share * incoming_ratio
|
||||||
|
return normalize_profile(result)
|
||||||
|
|
||||||
static func is_space_civilization(id: String, rules: Dictionary) -> bool:
|
static func is_space_civilization(id: String, rules: Dictionary) -> bool:
|
||||||
return (rules.has(id) and E.ne(str(rules[id].home), ""))
|
return (rules.has(id) and E.ne(str(rules[id].home), ""))
|
||||||
|
|
||||||
# Preserve any existing ethics. New populations start equally distributed;
|
# Keep one six-value float profile per stage. Absolute population remains in
|
||||||
# reductions (including blasts) are proportional and never leave negative counts.
|
# the exact physical object ledger and is materialized only when a transition needs it.
|
||||||
static func reconcile(world, target: Dictionary, rules: Dictionary, physical: Dictionary = {}, only_ids: Array = []) -> void:
|
static func reconcile(world, target: Dictionary, rules: Dictionary, physical: Dictionary = {}, only_ids: Array = []) -> void:
|
||||||
if (not target.has("ethics")):
|
if (not target.has("ethics")):
|
||||||
target.ethics = {}
|
target.ethics = {}
|
||||||
@@ -19,22 +76,10 @@ static func reconcile(world, target: Dictionary, rules: Dictionary, physical: Di
|
|||||||
for id in ids:
|
for id in ids:
|
||||||
if not rules.has(id): continue
|
if not rules.has(id): continue
|
||||||
var count: Variant = physical.get(id, 0)
|
var count: Variant = physical.get(id, 0)
|
||||||
var current: Dictionary = target.ethics.get(id, {})
|
var current: Dictionary = normalize_profile(target.ethics.get(id, {}))
|
||||||
var cached: Dictionary = world.ledger.reconciled_ethics.get(id, {})
|
var cached: Dictionary = world.ledger.reconciled_ethics.get(id, {})
|
||||||
if not cached.is_empty() and E.eq(cached.count, count) and E.eq(cached.population, current):
|
if not cached.is_empty() and E.eq(cached.count, count) and E.eq(cached.population, current):
|
||||||
continue
|
continue
|
||||||
var total: Variant = 0
|
|
||||||
for ethic in ETHICS:
|
|
||||||
current[ethic] = E.maximum(0, current.get(ethic, 0))
|
|
||||||
total = E.add(total, current[ethic])
|
|
||||||
if E.gt(total, count):
|
|
||||||
current = QuantityMath.allocate_whole(current, count)
|
|
||||||
elif E.lt(total, count):
|
|
||||||
var missing: Variant = E.subtract(count, total)
|
|
||||||
var base: Variant = E.floor_value(E.divide(missing, ETHICS.size()))
|
|
||||||
var rest: int = E.integer(E.subtract(missing, E.multiply(base, ETHICS.size())))
|
|
||||||
for i in ETHICS.size():
|
|
||||||
current[ETHICS[i]] = E.add(current[ETHICS[i]], E.add(base, ((1 if E.lt(i, rest) else 0))))
|
|
||||||
target.ethics[id] = current
|
target.ethics[id] = current
|
||||||
world.ledger.reconciled_ethics[id] = {"count": count, "population": current.duplicate()}
|
world.ledger.reconciled_ethics[id] = {"count": count, "population": current.duplicate()}
|
||||||
|
|
||||||
@@ -115,6 +160,7 @@ static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year:
|
|||||||
detach_space_civilizations(target, rules)
|
detach_space_civilizations(target, rules)
|
||||||
var physical: Dictionary = world.ledger.physical_amounts(target)
|
var physical: Dictionary = world.ledger.physical_amounts(target)
|
||||||
reconcile(world, target, rules, physical)
|
reconcile(world, target, rules, physical)
|
||||||
|
var ethical_totals: Dictionary = physical.duplicate()
|
||||||
var budget: Dictionary = stellar_budget(world, target, rules, physical)
|
var budget: Dictionary = stellar_budget(world, target, rules, physical)
|
||||||
var remaining: Dictionary = budget.capacity.duplicate()
|
var remaining: Dictionary = budget.capacity.duplicate()
|
||||||
var remaining_homes: Dictionary = budget.habitats.capacity.duplicate()
|
var remaining_homes: Dictionary = budget.habitats.capacity.duplicate()
|
||||||
@@ -141,12 +187,13 @@ static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year:
|
|||||||
target.amount[id] = supported
|
target.amount[id] = supported
|
||||||
world.ledger.credit(target, previous, excess)
|
world.ledger.credit(target, previous, excess)
|
||||||
world.batches.append_batch(target, previous, excess, year, year)
|
world.batches.append_batch(target, previous, excess, year, year)
|
||||||
var removed: Dictionary = QuantityMath.allocate_whole(target.ethics[id], excess)
|
var previous_count: Variant = ethical_totals.get(previous, physical.get(previous, target.amount.get(previous, 0)))
|
||||||
if (not target.ethics.has(previous)):
|
var previous_profile: Dictionary = target.ethics.get(previous, empty_profile())
|
||||||
target.ethics[previous] = {}
|
var source_profile: Dictionary = target.ethics.get(id, empty_profile())
|
||||||
for ethic in ETHICS:
|
target.ethics[previous] = merge_profiles(previous_profile, previous_count, source_profile, excess)
|
||||||
target.ethics[id][ethic] = E.subtract(target.ethics[id][ethic], removed[ethic])
|
ethical_totals[id] = supported
|
||||||
target.ethics[previous][ethic] = E.add(target.ethics[previous].get(ethic, 0), removed[ethic])
|
ethical_totals[previous] = E.add(previous_count, excess)
|
||||||
|
# Capacity losses are proportional, so the surviving source keeps its profile.
|
||||||
if E.eq(previous, "C1.9"):
|
if E.eq(previous, "C1.9"):
|
||||||
var stars: Dictionary = {}
|
var stars: Dictionary = {}
|
||||||
var current_physical: Dictionary = world.ledger.physical_amounts(target)
|
var current_physical: Dictionary = world.ledger.physical_amounts(target)
|
||||||
|
|||||||
@@ -30,10 +30,12 @@ func evolute_to_datetime(datetime: Variant, clock: Dictionary = {}, budget: RefC
|
|||||||
# Fractional frame updates cannot change capacity or populations.
|
# Fractional frame updates cannot change capacity or populations.
|
||||||
var processed_year: Variant = SimulationTime.processed_year(datetime)
|
var processed_year: Variant = SimulationTime.processed_year(datetime)
|
||||||
if E.gt(processed_year, world.state.time.year):
|
if E.gt(processed_year, world.state.time.year):
|
||||||
|
if budget != null: budget.call("set_phase", "initialize_life_batches")
|
||||||
var profile_initialize: int = world.profile_start()
|
var profile_initialize: int = world.profile_start()
|
||||||
initialize_life_batches()
|
initialize_life_batches()
|
||||||
world.profile_end("initialize", profile_initialize)
|
world.profile_end("initialize", profile_initialize)
|
||||||
var previous: Dictionary = world.state
|
var previous: Dictionary = world.state
|
||||||
|
if budget != null: budget.call("set_phase", "deep_copy_and_coarsen")
|
||||||
var next: Dictionary = previous.duplicate_deep()
|
var next: Dictionary = previous.duplicate_deep()
|
||||||
next.version = SimulationTime.STATE_VERSION
|
next.version = SimulationTime.STATE_VERSION
|
||||||
next.speed = clampi(E.integer(previous.speed), 1, Global.SPEED_LIMIT)
|
next.speed = clampi(E.integer(previous.speed), 1, Global.SPEED_LIMIT)
|
||||||
@@ -87,6 +89,7 @@ func evolve_batch_generation(target: Dictionary, to_year: Variant, completed: Di
|
|||||||
# Apply direct completions at the original next-generation boundary, so
|
# Apply direct completions at the original next-generation boundary, so
|
||||||
# binding transfers, the blast environment and capacity checks keep order.
|
# binding transfers, the blast environment and capacity checks keep order.
|
||||||
if budget != null and not await budget.pause(): return false
|
if budget != null and not await budget.pause(): return false
|
||||||
|
if budget != null: budget.call("set_phase", "generation_setup_and_withdrawal")
|
||||||
var completed_now: Dictionary = completed.duplicate()
|
var completed_now: Dictionary = completed.duplicate()
|
||||||
completed.clear()
|
completed.clear()
|
||||||
if context.is_empty():
|
if context.is_empty():
|
||||||
@@ -132,7 +135,9 @@ func evolve_batch_generation(target: Dictionary, to_year: Variant, completed: Di
|
|||||||
var fractions: Dictionary = {}
|
var fractions: Dictionary = {}
|
||||||
var outputs: Dictionary = {}
|
var outputs: Dictionary = {}
|
||||||
var current_quantities: Dictionary = quantities
|
var current_quantities: Dictionary = quantities
|
||||||
|
if budget != null: budget.call("set_phase", "civilization_stellar_budget")
|
||||||
var star_budget: Dictionary = CivilizationState.stellar_budget(world, target, Global.CivilizationRules, current_quantities)
|
var star_budget: Dictionary = CivilizationState.stellar_budget(world, target, Global.CivilizationRules, current_quantities)
|
||||||
|
if budget != null: budget.call("set_phase", "civilization_prepare")
|
||||||
var civilization_transaction: Dictionary = await CivilizationEvolution.prepare(world, target, expired, to_year, star_budget, Global.CivilizationRules, current_quantities, budget, expired_totals)
|
var civilization_transaction: Dictionary = await CivilizationEvolution.prepare(world, target, expired, to_year, star_budget, Global.CivilizationRules, current_quantities, budget, expired_totals)
|
||||||
if budget != null and budget.cancelled: return false
|
if budget != null and budget.cancelled: return false
|
||||||
var profile_products: int = world.profile_start()
|
var profile_products: int = world.profile_start()
|
||||||
@@ -166,6 +171,7 @@ func evolve_batch_generation(target: Dictionary, to_year: Variant, completed: Di
|
|||||||
affected[id] = true
|
affected[id] = true
|
||||||
for held in target.binds.get(id, {}): affected[held] = true
|
for held in target.binds.get(id, {}): affected[held] = true
|
||||||
for successor in outputs[id]: affected[successor] = true
|
for successor in outputs[id]: affected[successor] = true
|
||||||
|
if budget != null: budget.call("set_phase", "transfer_evolution")
|
||||||
var incoming: Dictionary = world.ledger.transfer_evolution(target, fractions, outputs)
|
var incoming: Dictionary = world.ledger.transfer_evolution(target, fractions, outputs)
|
||||||
|
|
||||||
var new_owners: Dictionary = {}
|
var new_owners: Dictionary = {}
|
||||||
@@ -177,6 +183,7 @@ func evolve_batch_generation(target: Dictionary, to_year: Variant, completed: Di
|
|||||||
# blast retain the original snapshot before crediting newborn populations.
|
# blast retain the original snapshot before crediting newborn populations.
|
||||||
var environment: Dictionary = world.environment.stellar_environment(target) if E.gt(blast_power, 0) else {}
|
var environment: Dictionary = world.environment.stellar_environment(target) if E.gt(blast_power, 0) else {}
|
||||||
var resolution: Variant = world.batches.temporal_resolution(target)
|
var resolution: Variant = world.batches.temporal_resolution(target)
|
||||||
|
if budget != null: budget.call("set_phase", "transfer_and_newborns")
|
||||||
for id in expired:
|
for id in expired:
|
||||||
for part : Dictionary in expired[id]:
|
for part : Dictionary in expired[id]:
|
||||||
for successor in part.products:
|
for successor in part.products:
|
||||||
@@ -214,6 +221,7 @@ func evolve_batch_generation(target: Dictionary, to_year: Variant, completed: Di
|
|||||||
world.profile_end("newborn_batches", profile_newborn)
|
world.profile_end("newborn_batches", profile_newborn)
|
||||||
new_owners[successor] = E.add(new_owners.get(successor, 0), amount)
|
new_owners[successor] = E.add(new_owners.get(successor, 0), amount)
|
||||||
world.ledger.accept_transfers(target, incoming)
|
world.ledger.accept_transfers(target, incoming)
|
||||||
|
if budget != null: budget.call("set_phase", "finish_and_capacity")
|
||||||
world.ledger.bind_new_owners(target, new_owners)
|
world.ledger.bind_new_owners(target, new_owners)
|
||||||
for owner in new_owners:
|
for owner in new_owners:
|
||||||
for resource in world.catalog.entities[owner].bind:
|
for resource in world.catalog.entities[owner].bind:
|
||||||
|
|||||||
@@ -5,18 +5,32 @@ var slice_usec: int = 40000
|
|||||||
var deadline: int
|
var deadline: int
|
||||||
var cancelled: bool = false
|
var cancelled: bool = false
|
||||||
var waiting_usec: int = 0
|
var waiting_usec: int = 0
|
||||||
|
var checkpoint_usec: int = 0
|
||||||
|
var max_slice_usec: int = 0
|
||||||
|
var max_slice_phase: String = "startup"
|
||||||
|
var phase: String = "startup"
|
||||||
|
|
||||||
func _init(scene_tree: SceneTree, milliseconds: int = 40):
|
func _init(scene_tree: SceneTree, milliseconds: int = 40):
|
||||||
tree = scene_tree
|
tree = scene_tree
|
||||||
slice_usec = milliseconds * 1000
|
slice_usec = milliseconds * 1000
|
||||||
deadline = Time.get_ticks_usec() + slice_usec
|
deadline = Time.get_ticks_usec() + slice_usec
|
||||||
|
checkpoint_usec = deadline - slice_usec
|
||||||
|
|
||||||
|
func set_phase(value: String) -> void:
|
||||||
|
phase = value
|
||||||
|
|
||||||
func pause() -> bool:
|
func pause() -> bool:
|
||||||
var keep_alive: RefCounted = self
|
var keep_alive: RefCounted = self
|
||||||
if cancelled: return false
|
if cancelled: return false
|
||||||
|
var now: int = Time.get_ticks_usec()
|
||||||
|
var work_usec: int = now - checkpoint_usec
|
||||||
|
if work_usec > max_slice_usec:
|
||||||
|
max_slice_usec = work_usec
|
||||||
|
max_slice_phase = phase
|
||||||
if Time.get_ticks_usec() >= deadline:
|
if Time.get_ticks_usec() >= deadline:
|
||||||
var waiting_from: int = Time.get_ticks_usec()
|
var waiting_from: int = Time.get_ticks_usec()
|
||||||
await tree.process_frame
|
await tree.process_frame
|
||||||
keep_alive.waiting_usec += Time.get_ticks_usec() - waiting_from
|
keep_alive.waiting_usec += Time.get_ticks_usec() - waiting_from
|
||||||
deadline = Time.get_ticks_usec() + slice_usec
|
deadline = Time.get_ticks_usec() + slice_usec
|
||||||
|
checkpoint_usec = Time.get_ticks_usec()
|
||||||
return not keep_alive.cancelled
|
return not keep_alive.cancelled
|
||||||
|
|||||||
@@ -123,11 +123,69 @@ static func sum(pop: Dictionary) -> Variant:
|
|||||||
|
|
||||||
static var _split_keys: Dictionary = {}
|
static var _split_keys: Dictionary = {}
|
||||||
|
|
||||||
|
## Split a whole population using normalized float shares. Quantizing the six
|
||||||
|
## shares to 15 decimal places matches their stored float precision, while the
|
||||||
|
## integer denominator avoids a second expensive exact normalization pass.
|
||||||
|
static func split_profile(remainders: Dictionary, key: String, count: Variant, ids: Array, shares: Array) -> Dictionary:
|
||||||
|
var result: Dictionary = {}
|
||||||
|
if E.le(count, 0): return result
|
||||||
|
assert(E.eq(count, E.floor_value(count)) and ids.size() == shares.size() and not ids.is_empty())
|
||||||
|
const SCALE: int = 1000000000000000
|
||||||
|
var weights: Array = []
|
||||||
|
var assigned_weight: int = 0
|
||||||
|
for i in ids.size() - 1:
|
||||||
|
var weight: int = clampi(roundi(clampf(float(shares[i]), 0.0, 1.0) * SCALE), 0, SCALE)
|
||||||
|
weights.append(weight)
|
||||||
|
assigned_weight += weight
|
||||||
|
var last_weight: int = SCALE - assigned_weight
|
||||||
|
if last_weight < 0:
|
||||||
|
var excess: int = -last_weight
|
||||||
|
for i in range(weights.size() - 1, -1, -1):
|
||||||
|
var reduction: int = mini(excess, weights[i])
|
||||||
|
weights[i] -= reduction
|
||||||
|
excess -= reduction
|
||||||
|
if excess == 0: break
|
||||||
|
last_weight = 0
|
||||||
|
weights.append(last_weight)
|
||||||
|
var cached: Dictionary = _split_keys.get(key, {})
|
||||||
|
if cached.is_empty() or cached.ids != ids:
|
||||||
|
var keys: Array = []
|
||||||
|
for id in ids: keys.append(key + ":" + str(id))
|
||||||
|
if _split_keys.size() >= 512: _split_keys.clear()
|
||||||
|
cached = {"ids": ids.duplicate(), "keys": keys}
|
||||||
|
_split_keys[key] = cached
|
||||||
|
var expected: Array = E.mul_div_many(count, weights, SCALE)
|
||||||
|
var rests: Array = []
|
||||||
|
var order: Array = []
|
||||||
|
var total: Variant = 0
|
||||||
|
for i in ids.size():
|
||||||
|
expected[i] = E.add(expected[i], remainders.get(cached.keys[i], 0))
|
||||||
|
result[ids[i]] = E.floor_value(E.maximum(0, expected[i]))
|
||||||
|
total = E.add(total, result[ids[i]])
|
||||||
|
rests.append(E.subtract(expected[i], 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]))
|
||||||
|
if E.lt(total, count):
|
||||||
|
for i in order:
|
||||||
|
if E.ge(total, count): break
|
||||||
|
result[ids[i]] = E.add(result[ids[i]], 1)
|
||||||
|
total = E.add(total, 1)
|
||||||
|
elif E.gt(total, count):
|
||||||
|
order.reverse()
|
||||||
|
for i in order:
|
||||||
|
if E.le(total, count): break
|
||||||
|
if E.le(result[ids[i]], 0): continue
|
||||||
|
result[ids[i]] = E.subtract(result[ids[i]], 1)
|
||||||
|
total = E.subtract(total, 1)
|
||||||
|
for i in ids.size():
|
||||||
|
remainders[cached.keys[i]] = E.clamp_value(E.subtract(expected[i], result[ids[i]]), E.neg(1), 1)
|
||||||
|
return result
|
||||||
|
|
||||||
static func split(remainders: Dictionary, key: String, count: Variant, weights: Dictionary) -> Dictionary:
|
static func split(remainders: Dictionary, key: String, count: Variant, weights: Dictionary) -> Dictionary:
|
||||||
if E.le(count, 0): return {}
|
if E.le(count, 0): return {}
|
||||||
return split_arrays(remainders, key, count, weights.keys(), weights.values())
|
return split_arrays(remainders, key, count, weights.keys(), weights.values())
|
||||||
|
|
||||||
static func split_arrays(remainders: Dictionary, key: String, count: Variant, ids: Array, weights: Array, population_factor: int = 0) -> Dictionary:
|
static func split_arrays(remainders: Dictionary, key: String, count: Variant, ids: Array, weights: Array, population_factor: int = 0, skip_conserved_identity: bool = false) -> Dictionary:
|
||||||
var result: Dictionary = {}
|
var result: Dictionary = {}
|
||||||
if E.le(count, 0): return result
|
if E.le(count, 0): return result
|
||||||
var sum_weights: Variant = 0
|
var sum_weights: Variant = 0
|
||||||
@@ -177,7 +235,7 @@ static func split_arrays(remainders: Dictionary, key: String, count: Variant, id
|
|||||||
# Fractional identity ratios still round-trip through parts/make in the
|
# Fractional identity ratios still round-trip through parts/make in the
|
||||||
# original algorithm. Retain them to preserve ties and saved errors exactly.
|
# original algorithm. Retain them to preserve ties and saved errors exactly.
|
||||||
var normalized: Array = positives
|
var normalized: Array = positives
|
||||||
if E.gt(positive_sum, 0) and (E.ne(positive_sum, count) or not whole_quotas):
|
if E.gt(positive_sum, 0) and (E.ne(positive_sum, count) or (not whole_quotas and not (skip_conserved_identity and E.eq(positive_sum, count)))):
|
||||||
normalized = E.mul_div_many(count, positives, positive_sum)
|
normalized = E.mul_div_many(count, positives, positive_sum)
|
||||||
var order: Array = []
|
var order: Array = []
|
||||||
var rests: Array = []
|
var rests: Array = []
|
||||||
|
|||||||
@@ -62,16 +62,26 @@ static func advance(previous: Dictionary, target: Dictionary, rules: Dictionary,
|
|||||||
for id in rules:
|
for id in rules:
|
||||||
if E.ne(str(rules[id].home), ""): stages.append(id)
|
if E.ne(str(rules[id].home), ""): stages.append(id)
|
||||||
stages.sort()
|
stages.sort()
|
||||||
|
# Profiles remain in the save after a stage becomes empty. Only living
|
||||||
|
# populations (including populations bound to a host object) contribute.
|
||||||
|
var populated: Dictionary = {}
|
||||||
|
var amounts: Dictionary = previous.get("amount", {})
|
||||||
|
for id in stages:
|
||||||
|
if E.gt(amounts.get(id, 0), 0): populated[id] = true
|
||||||
|
for owner in previous.get("binds", {}):
|
||||||
|
for id in previous.binds[owner]:
|
||||||
|
if rules.has(id) and E.ne(str(rules[id].home), "") and E.gt(previous.binds[owner][id], 0):
|
||||||
|
populated[id] = true
|
||||||
var weight: Variant = 1
|
var weight: Variant = 1
|
||||||
for id in stages:
|
for id in stages:
|
||||||
|
if not populated.has(id):
|
||||||
|
weight = E.multiply(weight, 2)
|
||||||
|
continue
|
||||||
var population: Dictionary = previous.get("ethics", {}).get(id, {})
|
var population: Dictionary = previous.get("ethics", {}).get(id, {})
|
||||||
var count: Variant = 0
|
for ethic in population:
|
||||||
for n in population.values(): count = E.add(count, n)
|
var proportion: float = maxf(0.0, float(population[ethic]))
|
||||||
if E.gt(count, 0):
|
for i in TRENDS.size():
|
||||||
for ethic in population:
|
rates[i] = E.add(rates[i], E.multiply(E.multiply(proportion, WEIGHTS[ethic][i]), weight))
|
||||||
var proportion: Variant = E.divide(population[ethic], count)
|
|
||||||
for i in TRENDS.size():
|
|
||||||
rates[i] = E.add(rates[i], E.multiply(E.multiply(proportion, WEIGHTS[ethic][i]), weight))
|
|
||||||
weight = E.multiply(weight, 2)
|
weight = E.multiply(weight, 2)
|
||||||
for i in TRENDS.size():
|
for i in TRENDS.size():
|
||||||
var name: String = TRENDS[i]
|
var name: String = TRENDS[i]
|
||||||
|
|||||||
Reference in New Issue
Block a user