16 Commits

Author SHA1 Message Date
Kirill a7c05e3796 Add report generation functionality to the UI
- Introduced a new script `report.gd` that extends `VBoxContainer` to manage the display of reports.
- Implemented dynamic localization support for report headers and body content based on the global locale.
- Created a `refresh` method to update the report content and header image when visibility changes or locale updates.
- Added a static `build` method to format and compile report data from the world state, including calculations for star totals and effects.
- Integrated presentation logic to render report templates based on the current state of the game world.
2026-10-18 21:48:15 +03:00
Kirill 2aced0c61a Add localization support for achievements: implement language-specific titles and update UI components 2026-10-18 20:32:20 +03:00
Kirill e1782c7b10 Implement achievement system: add achievement controller, UI components, and tests 2026-10-18 20:14:27 +03:00
Kirill e41d652631 Add achievement item UI and logic for displaying achievements 2026-10-18 20:03:27 +03:00
Kirill fe68e788b3 Add achievement loader script with unique identifier 2026-10-18 18:57:42 +03:00
Kirill 3047ef5a85 Implement apocalypse mechanics: add apocalypse queue, controller, and integrate into production logic 2026-10-18 00:05:53 +03:00
Kirill f53181b9ad Refactor production logic to incorporate helium share calculations and lifespan multipliers for stellar entities 2026-10-17 23:51:08 +03:00
Kirill 273093bd9e Разделить частоту обновления часов и расчёта симуляции
Счётчик времени UI обновляется до 10 Гц по запрошенному времени, а тяжёлые циклы по-прежнему запускаются с адаптивным интервалом и обрабатывают весь накопленный промежуток. Обновить тесты и описание модели.
2026-10-17 23:41:18 +03:00
Kirill 00b6192d19 Refactor production request handling: implement queuing mechanism and improve production application logic 2026-10-08 22:19:29 +03:00
Kirill 02c9843e1b Enhance civilization flow mechanics and adaptive quality control
- Implemented runtime-only adaptive quality controller to optimize simulation performance based on CPU time.
- Added adaptive quality thresholds and recovery cycles to dynamically adjust simulation fidelity.
- Introduced single-step solving method for civilization transitions to improve performance under high loads.
- Updated benchmark scripts to include speed parameter for testing various simulation speeds.
- Added tests for adaptive quality adjustments and their effects on civilization flow calculations.
- Documented results of optimization attempts and their impact on performance metrics.
2026-10-08 22:05:08 +03:00
Kirill 459fbb2a4d Refactor civilization flow reward calculations for improved accuracy and performance 2026-10-08 21:25:39 +03:00
Kirill c42940369a Refactor capacity enforcement and production logic
- Updated `enforce_capacity` function to accept an optional `physical` parameter, allowing for more flexible handling of physical amounts.
- Modified `initialize_life_batches` to return physical amounts after enforcing capacity, improving data flow.
- Enhanced `try_production` to utilize shared physical amounts, reducing redundant calculations.
- Adjusted `stellar_environment` to accept an optional `quantities` parameter, streamlining its usage.
- Added new test results for physical totals to validate performance improvements across various scenarios.
2026-10-08 21:10:36 +03:00
Kirill 9357f757a9 Refactor civilization flow mechanics: enhance commit and colonization logic, add tests for flow application and resource allocation, and optimize performance metrics. 2026-10-08 20:43:51 +03:00
Kirill 413f2be904 Refactor simulation timing and frame processing logic for improved accuracy and performance 2026-10-08 00:15:23 +03:00
Kirill 3fcc7211ce Implement civilization flow mechanics and profiling for resource allocation
- Added `civilization_flow.gd` to manage continuous expected flows and resource allocation within civilizations.
- Introduced `allocate_profiled` and `whole_quotas` functions in `quantity_math.gd` for precise stock allocation based on float shares.
- Enhanced `credit` logic in `object_ledger.gd` to handle production remainders more effectively.
- Updated `append_batch` in `object_batches.gd` to prevent batch creation for invalid IDs.
- Introduced `ratio_approx` function in `exact.gd` for better handling of float profiles.
- Added a benchmark script `benchmark_civilization_flows.gd` to evaluate the performance of civilization flow mechanics.
- Refactored various functions to improve clarity and efficiency in handling resource flows and allocations.
2026-10-07 22:51:55 +03:00
Kirill 6ee66a627d 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.
2026-10-06 23:02:42 +03:00
98 changed files with 254427 additions and 3812 deletions
+6 -1
View File
@@ -30,11 +30,14 @@ var Templates: Dictionary = {}
var LifeRules: Dictionary = {} var LifeRules: Dictionary = {}
var CivilizationRules: Dictionary = {} var CivilizationRules: Dictionary = {}
var ExtraParams: Dictionary = {} var ExtraParams: Dictionary = {}
var ApocalypseQueue: Dictionary = {}
var Achievements: Dictionary = {}
var Music: Dictionary = {} var Music: Dictionary = {}
var Effects: Dictionary = {} var Effects: Dictionary = {}
var batch_count: float = 40.0 # must be even not odd var batch_count: int = 40 # number of age groups per lifespan
func set_locale(locale: String) -> void: func set_locale(locale: String) -> void:
@@ -92,6 +95,8 @@ func _ready() -> void:
OwniverseItems = OwniverseItemLoader.get_data() OwniverseItems = OwniverseItemLoader.get_data()
Templates = OwniverseTemplateLoader.get_data() Templates = OwniverseTemplateLoader.get_data()
CivilizationRules = OwniverseCivilizationRuleLoader.get_data() CivilizationRules = OwniverseCivilizationRuleLoader.get_data()
ApocalypseQueue = OwniverseApocalypseQueueLoader.get_data()
Achievements = OwniverseAchievementLoader.get_data()
Music = OwniverseAudioLoader.get_music() Music = OwniverseAudioLoader.get_music()
Effects = OwniverseAudioLoader.get_effects() Effects = OwniverseAudioLoader.get_effects()
+20 -4
View File
@@ -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
@@ -250,8 +250,16 @@ static func _validate_payload(data: Variant, speed_limit: int, stages: Array, en
# Optional additions to the current format. Saves predating indicators keep # Optional additions to the current format. Saves predating indicators keep
# their quantities and receive neutral defaults from Owniverse on load. # their quantities and receive neutral defaults from Owniverse on load.
for field in saved: for field in saved:
if (not E.contains(required, field) and not E.contains(["world_parameters", "trends"], field)): if (not E.contains(required, field) and not E.contains(["world_parameters", "trends", "apocalypse"], field)):
return "unsupported state structure" return "unsupported state structure"
if saved.has("apocalypse"):
var apocalypse: Variant = saved.apocalypse
if not apocalypse is Dictionary: return "invalid state.apocalypse"
for key in ["phase", "stage", "tick", "end_ticks"]:
if not _number(apocalypse.get(key), true): return E.add("invalid state.apocalypse.", key)
for key in ["type", "warning"]:
if not _number(apocalypse.get(key), true, false): return E.add("invalid state.apocalypse.", key)
if E.lt(apocalypse.phase, 0) or E.gt(apocalypse.phase, 2): return "invalid apocalypse phase"
if saved.has("world_parameters"): if saved.has("world_parameters"):
if ((not _number_map(saved.world_parameters)) or E.ne(saved.world_parameters.size(), WorldParameters.defaults().size())): if ((not _number_map(saved.world_parameters)) or E.ne(saved.world_parameters.size(), WorldParameters.defaults().size())):
return "invalid state.world_parameters" return "invalid state.world_parameters"
@@ -348,11 +356,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"
+420
View File
@@ -0,0 +1,420 @@
class_name OwniverseAchievementLoader
static func get_data() -> Dictionary:
return {
'en': CSVLoader.load(EN),
'ru': CSVLoader.load(RU),
}
#region RU
const RU := """
id|Category|Points|progr: 60|Ignore|OpenCondition|AchieveCondition|Ru1|Ru2
a1|время|1|||age::1E9|age::1E9|Хорошее начало!|Вселенная существует миллиард лет.
a2|время|3|||age::1E10|age::1E10|Средний возраст.|Вселенная существует десять миллиардов лет.
a3|время|6|||age::1E11|age::1E11|Внушительный срок.|Вселенная существует сто миллиардов лет.
a4|время|10|||age::1E12|age::1E12|А не слишком ли это долго?|Вселенная существует триллион лет
a5|время|1||1||uptime|Вселенная под вашим управлением 13 минут подряд.|Неплохо для начала!
a6|время|3||1||uptime|Вселенная под вашим управлением 34 минуты подряд.|Без вас было бы хуже.
a7|время|6||1||uptime|Вселенная под вашим управлением 1 час 29 минут подряд.|Не привлекайте ненужного внимания.
a8|время|10||1||uptime|Вселенная под вашим управлением 3 часа 53 минуты подряд!|Можно и отдохнуть, это финальное достижение в данной линейке!
a9|время|1||1||played|Вы управляете вселенной 8 часов.|Её обитатели должны быть вам благодарны.
a10|время|3||1||played|Вы управляете вселенной 21 час.|Наверняка уже многое стало понятно.
a11|время|6||1||played|Вы управляете вселенной 2 суток и 7 часов.|Расскажите об игре друзьям!
a12|время|10||1||played|Вы управляете вселенной 6 суток.|Поделитесь опытом в социальных сетях!
a13|ресурсы|1||||total:H:1000|Получи всего 1 000 водорода|
a14|ресурсы|1|1||total:H:5E4|total:H:1E6|Получи всего 1 миллион водорода|
a15|ресурсы|1|1||total:H:5E7|total:H:1E9|Получи всего 1 миллиард водорода|
a16|ресурсы|3|1||total:H:5E10|total:H:1E12|Получи всего 1 триллион водорода|
a17|ресурсы|3|1||total:H:5E13|total:H:1E15|Получи всего 1 квадриллион водорода|
a18|ресурсы|3|1||total:H:5E16|total:H:1E18|Получи всего 1 квинтиллион водорода|
a19|ресурсы|6|1||total:H:5E19|total:H:1E21|Получи всего 1 секстиллион водорода|
a20|ресурсы|6|1||total:H:5E22|total:H:1E24|Получи всего 1 септиллион водорода|
a21|ресурсы|1||||total:S:1000|Создай 1 000 кубических парсек пространства|
a22|ресурсы|1|||total:S:5E4|total:S:1E6|Создай 1 миллион кубических парсек пространства|
a23|ресурсы|1|||total:S:5E7|total:S:1E9|Создай 1 миллиард кубических парсек пространства|
a24|ресурсы|3|||total:S:5E10|total:S:1E12|Создай 1 триллион кубических парсек пространства|
a25|ресурсы|3|||total:S:5E13|total:S:1E15|Создай 1 квадриллион кубических парсек пространства|
a26|ресурсы|3|||total:S:5E16|total:S:1E18|Создай 1 квинтиллион кубических парсек пространства|
a27|ресурсы|6|||total:S:5E19|total:S:1E21|Создай 1 секстиллион кубических парсек пространства|
a28|ресурсы|6|||total:S:5E22|total:S:1E24|Создай 1 септиллион кубических парсек пространства|
a29|ресурсы|1|||total:He:1E9|total:He:1E9|Получи 1 миллиард гелия|
a30|ресурсы|3|||total:He:1E15|total:He:1E15|Получи 1 квадриллион гелия|
a31|ресурсы|6|||total:He:1E21|total:He:1E21|Получи 1 секстиллион гелия|
a32|ресурсы|1||1|||Израсходуй 1 миллион гелия|
a33|ресурсы|3||1|||Израсходуй 1 триллион гелия|
a34|ресурсы|6||1|||Израсходуй 1 квинтиллион гелия|
a35|ресурсы|1||1|total:Voids:5|total:Voids:25|Сформируй 25 войдов|
a36|ресурсы|1||1|total:Voids:25|total:Voids:125|Сформируй 125 войдов|
a37|ресурсы|1||1|total:Voids:125|total:Voids:625|Сформируй 625 войдов|
a38|ресурсы|3||1|total:Voids:625|total:Voids:3125|Сформируй 3125 войдов|
a39|ресурсы|3||1|total:Voids:3125|total:Voids:15625|Сформируй 15625 войдов|
a40|ресурсы|6||1|total:Voids:15625|total:Voids:78125|Сформируй 78125 войдов|Продолжай в том же духе, без войдов вселенная обречена!
a41|ресурсы|10||1|energy::1E27|energy::1E27|На складе пусто!|Потрачен весь запас энергии.
a42|объекты|1||||total:Star:1|Зажги звезду|
a43|объекты|1|||total:OD:1|total:OD:1|Зажги оранжевого карлика|
a44|объекты|1||||total:YD:1|Зажги жёлтого карлика|
a45|объекты|1|||total:WYD:1|total:WYD:1|Зажги бело-жёлтого карлика|
a46|объекты|1|||total:WS:1|total:WS:1|Зажги белую звезду|
a47|объекты|1||||total:BG:1|Зажги голубого гиганта|
a48|объекты|1|1||total:BG:1|total:SG:1|Зажги сверхгиганта|
a49|объекты|1|1||total:SG:1|total:HG:1|Зажги гипергиганта|
a50|объекты|1||||total:WD:1|Получи белого карлика|
a51|объекты|1||||total:NS:1|Получи нейтронную звезду|
a52|объекты|1|1||total:NS:1|total:MGT:1|Получи магнетар|
a53|объекты|1||||total:BH:1|Получи чёрную дыру|
a54|объекты|1|1||total:BH:1|total:SMBH:1|Создай сверхмассивную чёрную дыру|
a55|объекты|1||||total:N:1|Создай туманность|
a56|объекты|1|1||total:N:1|total:G:1|Создай глобулу|
a57|объекты|1|1||total:G:1|total:STN:1|Создай звёздную колыбель|
a58|объекты|1|1||total:G:1|total:MC:1|Создай молекулярное облако|
a59|объекты|3|1||total:STN:1|total:ASTN:1|Создай активную звёздную колыбель|
a60|объекты|3|1||total:MC:100|total:MC:1E4|Создай 10 000 молекулярных облаков|
a61|объекты|1|1|1|auto:Star:1|auto:Star:10|Создай 10 звёзд с помощью облаков|
a62|объекты|1|1|1|auto:Star:10|auto:Star:1000|Создай 1 000 звёзд с помощью облаков|
a63|объекты|3|1|1|auto:Star:1000|auto:Star:1E6|Создай 1 миллион звёзд с помощью облаков|
a64|объекты|1|1||total:Star:30|total:Star:300|Зажги 300 звёзд|
a65|объекты|1|1||total:OD:1|total:OD:2000|Зажги 2 000 оранжевых карликов|
a66|объекты|1|1||total:WYD:1|total:WYD:500|Зажги 500 бело-жёлтых карликов|
a67|объекты|1|1||total:BD:1|total:BD:1E6|Зажги 1 миллион бурых карликов|
a68|объекты|3|||total:HG:1000|total:HG:1000|Это кому-нибудь нужно?|Зажги 1 000 гипергигантов.
a69|объекты|3|1||total:Star:5000|total:Star:1E7|Зажги 10 миллионов звёзд|
a70|объекты|3|||total:Star:4E9|total:Star:4E11|Зажги 400 миллиардов звёзд|
a71|объекты|3|||total:Star:9E14|total:Star:9E16|Зажги 90 квадриллионов звёзд|
a72|объекты|3|1||total:SMBH:1|total:SMBH:1E4|Сбрось 10 000 вещества в сверхмассивные чёрные дыры|
a73|объекты|3|||total:SMBH:3E5|total:SMBH:4.2E7|Сбрось 42 миллиона вещества в сверхмассивные чёрные дыры|Примерно столько весит Стрелец А со звёздочкой.
a74|объекты|3|||total:SMBH:3E9|total:SMBH:6.6E11|Сбрось 660 миллиардов вещества в сверхмассивные чёрные дыры|Примерно столько весит TON 618.
a75|структуры|1||||total:SC:1|Создай любое звёздное скопление|
a76|структуры|1||||total:SCM:1|Создай среднее звёздное скопление|
a77|структуры|1|1||total:SC:1|total:SCL:1|Создай большое звёздное скопление|
a78|структуры|3|1||total:SCL:1|total:SCG:1|Создай гигантское звёздное скопление|
a79|структуры|3|1||total:SC:10|total:SC:100|Создай 100 звёздных скоплений|
a80|структуры|1||||total:GAL:1|Создай любую галактику|
a81|структуры|3|1|1|total:GAL:1|total:GALS:1|Создай спиральную галактику|
a82|структуры|1||||total:GEM:1|Создай среднюю галактику|
a83|структуры|1|1||total:GAL:1|total:GEL:1|Создай большую галактику|
a84|структуры|3|1||total:GEL:1|total:GEG:1|Создай гигантскую галактику|
a85|структуры|3|1||total:GAL:5|total:GAL:30|Создай 30 галактик|
a86|структуры|1|1||total:GAL:30|total:LGG:1|Создай группу галактик|
a87|структуры|3|1||total:LGG:1|total:LGG:30|Создай 30 групп галактик|
a88|структуры|3|1||total:LGG:30|total:LSC:1|Создай галактическое сверхскопление|
a89|структуры|3|1||total:LSC:1|total:LSC:30|Создай 30 галактических сверхскоплений|
a90|структуры|6|1||total:LSC:30|total:LGF:1|Создай галактическую нить|
a91|структуры|6|1||total:LGF:1|total:LGF:100|Создай 100 галактических нитей|
a92|структуры|6|1||total:LGF:100|total:LGW:1|Создай великую стену|
a93|структуры|6|1||total:LGW:1|total:LGW:100|Создай 100 великих стен|
a94|структуры|6|1||total:LGW:100|total:LUB:1|Создай вселенский пузырь|
a95|структуры|10|1|1|total:LUB:1|total:LUB:1|Это должно было когда-то закончиться!|Создана крупномасштабная структура вселенной.
a96|жизнь|1||1||Planet:1|Создай любую планету|
a97|жизнь|1|1|1|GG:1^AB:1|RP:1|Создай каменистую планету|
a98|жизнь|1|1|1|RP:1|HP:1|Создай обитаемую планету|
a99|жизнь|3||1||EON1:1|Создай жизнь|
a100|жизнь|1|1|1|EON1:1|EON2:1|Вырасти амёб и водоросли|
a101|жизнь|1|1|1|EON2:1|EON3:1|Вырасти трилобитов и рыб|
a102|жизнь|1|1|1|EON3:1|EON4:1|Вырасти динозавров и птиц|
a103|жизнь|1|1|1|EON4:1|EON5:1|Вырасти млекопитающих и приготовься...|
a104|жизнь|1|1|1|Planet:1|Planet:1E5|Создай 100 000 планет|
a105|жизнь|1|1|1|HP:1|HP:1E4|Создай 10 000 обитаемых планет|
a106|жизнь|3|1|1|EON1:1|EON1:1000|Создай жизнь на 1 000 планет|
a107|жизнь|6||1|C0.0:1|C0.0:1|Возник разум!|
a108|жизнь|1|1|1|C0.0:1|C0.5:1|Они умеют лепить глиняные горшки!|Цивилизация достигла уровня 0.5.
a109|жизнь|3|1|1|C0.7:1|C1.0:1|Они освоили свою родную планету!|Цивилизация достигла уровня 1.0.
a110|жизнь|1|1|1|C1.3:1|C1.5:1|Они построили космический город!|Цивилизация достигла уровня 1.5.
a111|жизнь|6|1|1|C1.7:1|C2.0:1|У них есть рой Дайсона!|Цивилизация достигла уровня 2.0.
a112|жизнь|1||1|C2.2:1|C2.2:1|Вроде бы они начали расти!|Цивилизация заселила сотню звёзд.
a113|жизнь|1||1|C2.5:1|C2.5:1|Их почти можно увидеть невооружённым глазом!|Цивилизация заселила среднее звёздное скопление.
a114|жизнь|3||1|C2.8:1|C2.8:1|О, они наконец-то поняли, как управлять гравитацией!|Цивилизация достигла уровня 2.8.
a115|жизнь|6||1|C3.0:1|C3.0:1|Они и правда научились летать!|Цивилизация способна путешествовать между галактиками.
a116|жизнь|3||1|C3.2:1|C3.2:1|Они наверное так гордятся собой!|Цивилизация расселилась на масштабах порядка Туманности Андромеды.
a117|жизнь|3||1|C3.5:1|C3.5:1|Вот это они смекалистые!|Цивилизация использует ресурсы нейтронной звезды.
a118|жизнь|3||1|C3.8:1|C3.8:1|Теперь их и правда легко заметить.|Цивилизация заселила сотни тысяч галактик.
a119|жизнь|6||1|C4.0:1|C4.0:1|Кажется это уже было, нет?|Цивилизация освоила манипуляции со временем.
a120|жизнь|6||1|C4.2:1|C4.2:1|Наконец-то они перестали мелочиться.|Цивилизация использует сверхмассивную чёрную дыру для своих целей.
a121|жизнь|10||1|C4.4:1|C4.4:1|Они справились! Интересно, чем же дальше займутся?|Цивилизация расселилась на масштабах порядка всей видимой вселенной.
a122|жизнь|3|1|1|C1.6:1|SET:1|Они основали колонию у соседней звезды!|
a123|жизнь|6|1|1||GSM:1&C2.8:50|Они похоже основали Галактическую Федерацию или что-то в этом роде!|
a124|жизнь|1||1|||О нет...|Погибла цивилизация 0.8+ уровня.
a125|жизнь|1||1|||Да сколько можно!|Погибло 100 цивилизаций 1.2+ уровня.
a126|жизнь|1||1|||К этому вообще можно привыкнуть?|Погибло 1 000 цивилизаций 1.6+ уровня.
a127|жизнь|3||1|||Да, в какой-то момент становится безразлично.|Погибло 10 000 цивилизаций 2.0+ уровня
a128|жизнь|6||1|||А выглядели крепкими. Что ж поделать.|Погибла цивилизация 3.0+ уровня.
a129|жизнь|10||1|||Моё разочарование неизмеримо.|Погибла цивилизация 4.0+ уровня.
a130|жизнь|6||1|||Что-то это мне напоминает...|На момент 13.8 миллиардов лет существует цивилизация уровня 0.9.
a131|события|3|1|1|высокая плотность|BigCrunch|Большое сжатие!|Вселенная схлопнулась в колоссальную чёрную дыру.
a132|события|3|1|1|низкая плотность|BigRip|Большой разрыв!|Вселенная рассеялась в разные стороны.
a133|события|6|1|1|BigCrunch^BigRip|BigCrunch&BigRip|Двойная неурядица!|Два разных апокалипсиса завершены.
a134|события|10||1|||Изгнание! Вас выгнали из вселенной.|Теперь эта вселенная существует сама по себе, без вашего участия.
a135|события|3||1|||Антитеизм: они отрицают ваше существование.|Может они в чём-то и правы?
a136|события|3||1|||Интеграция: они стремятся к сосуществованию.|Может быть хотя бы у них получится?
a137|события|3||1|||Ксеноцид: их воинственность приводит к ужасным результатам.|Впрочем, ничего нового.
a138|события|3||1|||Реконструкция: они переделывают вселенную.|Это же их вселенная, да? Или нет?
a139|события|3||1|||Духовность: они ищут глубинный смысл.|Что если они узнают правду?
a140|события|3||1|||Исход: они хотят покинуть вселенную.|Что же им здесь не нравится?
a141|события|6||1|||Динамичная вселенная|Все тенденции проявили себя!
a142|события|3||1|||Изгнание: они пытаются выгнать из вселенной вас!|Их определённо что-то не устраивает.
a143|события|3||1|||Федерация: они ищут способы мирного сосуществования.|Наверное это неплохо?
a144|события|3||1|||Просвещение: они помогают отстающим.|Какая прелесть!
a145|события|3||1|||Новый порядок: они закапывают топор войны.|Может оно и к лучшему?
a146|события|3||1|||Великие войны: они устроили эпическое побоище.|Жаль, что отсюда не рассмотреть всех деталей.
a147|события|3||1|||Истребление: они занимаются чисткой потенциальных конкурентов.|Жизнь — это возобновляемый ресурс.
a148|события|3||1|||Опустошение: они разрушают вселенную!|Звёзды превращаются в чёрные дыры.
a149|события|3||1|||Звёздное дробление: они перестраивают вселенную под себя.|Им удобнее жить на малых и стабильных звёздах.
a150|события|3||1|||Звёздная накачка: они создают новые звёзды!|Из облаков и бурых карликов создаются красные карлики.
a151|события|3||1|||Звёздное охлаждение: они стабилизируют звёзды.|Снижается активность звёздных процессов.
a152|события|3||1|||Молитва: они невероятно истово молятся.|Вы даже смогли что-то расслышать.
a153|события|3||1|||Диалог: они ждут ответа!|А отвечать именно вам.
a154|события|3||1|||Живая вселенная|Произошло 10 действий цивилизаций.
a155|события|6||1|||Очень живая вселенная|Произошло 30 действий цивилизаций.
a156|события|10||1|||Невероятно живая вселенная!|Произошло 100 действий цивилизаций.
a157|события|10||1|||Столько всего происходит!|Все действия произошли хотя бы один раз!
a158|силы|1||1|сила получена|сила применена|Научись быстро копать.|Используй лопатин
a159|силы|1||1|сила получена|сила применена|Запусти инфляцию.|Используй надувин
a160|силы|3||1|сила получена|сила применена|Пропусти время.|Используй временит
a161|силы|3||1|сила получена|сила применена|Утилизируй мусор.|Используй столкновит
a162|силы|6||1|сила получена|сила применена|Подели физику на ноль.|Используй первекс
a163|силы|1||1|сила получена|сила применена|Уничтожь пространство.|Используй сингулярит
a164|силы|1||1|сила получена|сила применена|Укрепи структуры.|Используй обновин
a165|силы|3||1|сила получена|сила применена|Разожги звёзды.|Используй вливанит
a166|силы|3||1|сила получена|сила применена|Остуди вселенную.|Используй морозин
a167|силы|6||1|сила получена|сила применена|Избавься от звёзд.|Используй дезвездит
a168|силы|1||1|сила получена|сила применена|Сделай больше планет.|Используй камнекс
a169|силы|1||1|сила получена|сила применена|Осемени вселенную.|Используй панспермин
a170|силы|3||1|сила получена|сила применена|Удобри вселенную.|Используй гайанит
a171|силы|3||1|сила получена|сила применена|Взбодри вселенную.|Используй дарвинит
a172|силы|6||1|сила получена|сила применена|Наполни вселенную благостью.|Используй эдемекс
a173|силы|1||1|сила получена|сила применена|Научи разумных жить в мире.|Используй пацифин
a174|силы|1||1|сила получена|сила применена|Научи разумных сражаться за свою жизнь.|Используй озверин
a175|силы|3||1|сила получена|сила применена|Назначь пророков среди разумных.|Используй моралит
a176|силы|3||1|сила получена|сила применена|Покарай тех разумных, кто не следует твоим планам.|Используй дискриминит
a177|силы|6||1|сила получена|сила применена|Подави тех разумных, которые слишком много о себе возомнили.|Используй репрекс
a178|силы|6||1|одна любая сила получена в 4 градациях|флэш|Потрать силу одного типа по нарастанию градаций.|Зелёная, синяя, фиолетовая, оранжевая
a179|силы|3||1|снижена вручную|Bur=1|Холодная вселенная.|Минимальная горючесть
a180|силы|3||1|аналогично|Bur=19|Звёзды зажигают, чтобы они сгорели!|Максимальная горючесть
a181|силы|3||1|аналогично|Exp=1|Плотный вакуум.|Минимальная взрывчатость
a182|силы|3||1|аналогично|Exp=19|Бада-бим, бада-бум-бум-бум!|Максимальная взрывчатость
a183|силы|3||1|аналогично|Agg=1|Мир и покой.|Минимальная агрессия
a184|силы|3||1|аналогично|Agg=19|Как 4Е4!|Максимальная агрессия
a185|силы|6||1|||Все силы доступны одновременно!|
a186|силы|10||1|||Все силы использованы хотя бы по одному разу в этой вселенной.|
a187|силы|1||1|3 обычные силы одного вида||Улучшите силу до редкой.|
a188|силы|3||1|3 редкие силы одного вида||Улучшите силу до эпической.|
a189|силы|6||1|3 эпические силы одного вида||Улучшите силу до легендарной.|
a190|силы|1||1|улучш 3 силы|5|Улучшите 5 сил.|
a191|силы|1||1|8|13|Улучшите 13 сил.|
a192|силы|3||1|21|34|Улучшите 34 силы.|
a193|силы|3||1|55|89|Улучшите 89 сил.|
a194|силы|6||1|144|233|Улучшите 233 силы.|
a195|силы|6||1|377|610|Улучшите 610 сил.|
a196|силы|10||1|987|1597|Улучшите 1 597 сил.|Продолжайте в том же духе!
a197|силы|1||1|1|3|Улучшите 3 сундука с силами.|
a198|силы|3||1|5|7|Улучшите 7 сундуков с силами.|
a199|силы|6||1|14|20|Улучшите 20 сундуков с силами.|
a200|силы|10||1|38|55|Улучшите 55 сундуков с силами.|Спасибо за поддержку!
"""
#endregion
#region EN
const EN := """
id|Category|Points|progr: 60|Ignore|OpenCondition|AchieveCondition|En1|En2
a1|время|1|||age::1E9|age::1E9|A good start!|The Universe exists for a billion years.
a2|время|3|||age::1E10|age::1E10|Middle age.|The Universe exists for a ten billions years.
a3|время|6|||age::1E11|age::1E11|Impressive term.|The Universe exists for a hundred billions years.
a4|время|10|||age::1E12|age::1E12|Isn't it too long?|The Universe exists for a trillion years.
a5|время|1||1||uptime|The universe is under your control for 13 minutes in a row.|Not bad for a start!
a6|время|3||1||uptime|The universe is under your control for 34 minutes in a row.|It would be worse without you.
a7|время|6||1||uptime|The universe is under your control for 1 hour 29 minutes in a row.|Don't draw unnecessary attention.
a8|время|10||1||uptime|The universe is under your control for 3 hours 53 minutes in a row!|You can also relax, this is the final achievement in this line!
a9|время|1||1||played|You rule the universe for 8 hours.|Its inhabitants should be grateful to you.
a10|время|3||1||played|You rule the universe for 21 hours.|Surely a lot has become clear by now.
a11|время|6||1||played|You rule the universe for 2 days and 7 hours.|Tell your friends about the game!
a12|время|10||1||played|You rule the universe for 6 days.|Share your experience on social networks!
a13|ресурсы|1||||total:H:1000|Get total 1 000 hydrogen|
a14|ресурсы|1|1||total:H:5E4|total:H:1E6|Get total 1 million hydrogen|
a15|ресурсы|1|1||total:H:5E7|total:H:1E9|Get total 1 billion hydrogen|
a16|ресурсы|3|1||total:H:5E10|total:H:1E12|Get total 1 trillion hydrogen|
a17|ресурсы|3|1||total:H:5E13|total:H:1E15|Get total 1 quadrillion hydrogen|
a18|ресурсы|3|1||total:H:5E16|total:H:1E18|Get total 1 quintillion hydrogen|
a19|ресурсы|6|1||total:H:5E19|total:H:1E21|Get total 1 sextillion hydrogen|
a20|ресурсы|6|1||total:H:5E22|total:H:1E24|Get total 1 septillion hydrogen|
a21|ресурсы|1||||total:S:1000|Create 1 000 cubic parsecs of space|
a22|ресурсы|1|||total:S:5E4|total:S:1E6|Create 1 million cubic parsecs of space|
a23|ресурсы|1|||total:S:5E7|total:S:1E9|Create 1 billion cubic parsecs of space|
a24|ресурсы|3|||total:S:5E10|total:S:1E12|Create 1 trillion cubic parsecs of space|
a25|ресурсы|3|||total:S:5E13|total:S:1E15|Create 1 quadrillion cubic parsecs of space|
a26|ресурсы|3|||total:S:5E16|total:S:1E18|Create 1 quintillion cubic parsecs of space|
a27|ресурсы|6|||total:S:5E19|total:S:1E21|Create 1 sextillion cubic parsecs of space|
a28|ресурсы|6|||total:S:5E22|total:S:1E24|Create 1 septillion cubic parsecs of space|
a29|ресурсы|1|||total:He:1E9|total:He:1E9|Obtain 1 billion helium|
a30|ресурсы|3|||total:He:1E15|total:He:1E15|Obtain 1 quadrillion helium|
a31|ресурсы|6|||total:He:1E21|total:He:1E21|Obtain 1 sextilion helium|
a32|ресурсы|1||1|||Use up 1 million helium|
a33|ресурсы|3||1|||Use up 1 trillion helium|
a34|ресурсы|6||1|||Use up 1 quintillion helium|
a35|ресурсы|1||1|total:Voids:5|total:Voids:25|Form 25 voids|
a36|ресурсы|1||1|total:Voids:25|total:Voids:125|Form 125 voids|
a37|ресурсы|1||1|total:Voids:125|total:Voids:625|Form 625 voids|
a38|ресурсы|3||1|total:Voids:625|total:Voids:3125|Form 3125 voids|
a39|ресурсы|3||1|total:Voids:3125|total:Voids:15625|Form 15625 voids|
a40|ресурсы|6||1|total:Voids:15625|total:Voids:78125|Form 78125 voids|Keep up the good work, without voids the universe is doomed!
a41|ресурсы|10||1|energy::1E27|energy::1E27|Out of ammo!|All energy cache is spent.
a42|объекты|1||||total:Star:1|Make a Star|
a43|объекты|1|||total:OD:1|total:OD:1|Make an Orange Dwarf|
a44|объекты|1||||total:YD:1|Make a Yellow Dwarf|
a45|объекты|1|||total:WYD:1|total:WYD:1|Make a White-Yellow Dwarf|
a46|объекты|1|||total:WS:1|total:WS:1|Make a White Star|
a47|объекты|1||||total:BG:1|Make a Blue Giant|
a48|объекты|1|1||total:BG:1|total:SG:1|Make a Supergiant|
a49|объекты|1|1||total:SG:1|total:HG:1|Make a Hypergiant|
a50|объекты|1||||total:WD:1|Get a White Dwarf|
a51|объекты|1||||total:NS:1|Get a Neutron Star|
a52|объекты|1|1||total:NS:1|total:MGT:1|Get a Magnetar|
a53|объекты|1||||total:BH:1|Get a Black Hole|
a54|объекты|1|1||total:BH:1|total:SMBH:1|Make a Supermassive Black Hole|
a55|объекты|1||||total:N:1|Make a Nebula|
a56|объекты|1|1||total:N:1|total:G:1|Make a Globula|
a57|объекты|1|1||total:G:1|total:STN:1|Make a Star Nursery|
a58|объекты|1|1||total:G:1|total:MC:1|Make a Molecular Cloud|
a59|объекты|3|1||total:STN:1|total:ASTN:1|Make an Active Star Nursery|
a60|объекты|3|1||total:MC:100|total:MC:1E4|Make 10 000 Molecular Clouds|
a61|объекты|1|1|1|auto:Star:1|auto:Star:10|Make 10 stars via clouds|
a62|объекты|1|1|1|auto:Star:10|auto:Star:1000|Make 1 000 stars via clouds|
a63|объекты|3|1|1|auto:Star:1000|auto:Star:1E6|Make 1 million stars via clouds|
a64|объекты|1|1||total:Star:30|total:Star:300|Make 300 stars|
a65|объекты|1|1||total:OD:1|total:OD:2000|Make 2 000 orange dwarfs|
a66|объекты|1|1||total:WYD:1|total:WYD:500|Make 500 white-yellow dwarfs|
a67|объекты|1|1||total:BD:1|total:BD:1E6|Make 1 million brown dwarfs|
a68|объекты|3|||total:HG:1000|total:HG:1000|Someone must need them, of course?|Make 1 000 hypergiants.
a69|объекты|3|1||total:Star:5000|total:Star:1E7|Make 10 million stars|
a70|объекты|3|||total:Star:4E9|total:Star:4E11|Make 400 billion stars|
a71|объекты|3|||total:Star:9E14|total:Star:9E16|Make 90 quadrillion stars|
a72|объекты|3|1||total:SMBH:1|total:SMBH:1E4|Drop 10 000 matter into Supermassive Black Holes|
a73|объекты|3|||total:SMBH:3E5|total:SMBH:4.2E7|Drop 42 million matter into Supermassive Black Holes|Sagittarius A* weighs about as much.
a74|объекты|3|||total:SMBH:3E9|total:SMBH:6.6E11|Drop 660 billion matter into Supermassive Black Holes|TON 618 weighs about as much.
a75|структуры|1||||total:SC:1|Make a Star Cluster|
a76|структуры|1||||total:SCM:1|Make a Medium Star Cluster|
a77|структуры|1|1||total:SC:1|total:SCL:1|Make a Large Star Cluster|
a78|структуры|3|1||total:SCL:1|total:SCG:1|Make a Giant Star Cluster|
a79|структуры|3|1||total:SC:10|total:SC:100|Make 100 Star Clusters|
a80|структуры|1||||total:GAL:1|Make a Galaxy|
a81|структуры|3|1|1|total:GAL:1|total:GALS:1|Make a Spiral Galaxy|
a82|структуры|1||||total:GEM:1|Make a Medium Galaxy|
a83|структуры|1|1||total:GAL:1|total:GEL:1|Make a Large Galaxy|
a84|структуры|3|1||total:GEL:1|total:GEG:1|Make a Giant Galaxy|
a85|структуры|3|1||total:GAL:5|total:GAL:30|Make 30 Galaxies|
a86|структуры|1|1||total:GAL:30|total:LGG:1|Make a Galactic Group|
a87|структуры|3|1||total:LGG:1|total:LGG:30|Make 30 Galactic Groups|
a88|структуры|3|1||total:LGG:30|total:LSC:1|Make a Galactic Supercluster|
a89|структуры|3|1||total:LSC:1|total:LSC:30|Make 30 Galactic Superclusters|
a90|структуры|6|1||total:LSC:30|total:LGF:1|Make a Galactic Filament|
a91|структуры|6|1||total:LGF:1|total:LGF:100|Make 100 Galactic Filaments|
a92|структуры|6|1||total:LGF:100|total:LGW:1|Make a Great Wall|
a93|структуры|6|1||total:LGW:1|total:LGW:100|Make 100 Great Walls|
a94|структуры|6|1||total:LGW:100|total:LUB:1|Make an Universe Bubble|
a95|структуры|10|1|1|total:LUB:1|total:LUB:1|It had to end sometime!|The large-scale structure of the universe has been created.
a96|жизнь|1||1||Planet:1|Make a planet|
a97|жизнь|1|1|1|GG:1^AB:1|RP:1|Make a rocky planet|
a98|жизнь|1|1|1|RP:1|HP:1|Make a habitable planet|
a99|жизнь|3||1||EON1:1|Make life|
a100|жизнь|1|1|1|EON1:1|EON2:1|Grow amoebas and algae|
a101|жизнь|1|1|1|EON2:1|EON3:1|Grow trilobites and fishes|
a102|жизнь|1|1|1|EON3:1|EON4:1|Grow dinosaurs and birds|
a103|жизнь|1|1|1|EON4:1|EON5:1|Grow mammals and prepare yourself...|
a104|жизнь|1|1|1|Planet:1|Planet:1E5|Make 100 000 planets|
a105|жизнь|1|1|1|HP:1|HP:1E4|Make 10 000 habitable planets|
a106|жизнь|3|1|1|EON1:1|EON1:1000|Make life at 1 000 planets|
a107|жизнь|6||1|C0.0:1|C0.0:1|Sapience emerged!|
a108|жизнь|1|1|1|C0.0:1|C0.5:1|They can make pots out of clay!|A civilization has reached 0.5 level.
a109|жизнь|3|1|1|C0.7:1|C1.0:1|They have mastered their home planet!|A civilization has reached 1.0 level.
a110|жизнь|1|1|1|C1.3:1|C1.5:1|They have built a space city!|A civilization has reached 1.5 level.
a111|жизнь|6|1|1|C1.7:1|C2.0:1|They have a Dyson Swarm!|A civilization has reached 2.0 level.
a112|жизнь|1||1|C2.2:1|C2.2:1|They seem to grow at last!|A civilization has settled over a hundred of stars.
a113|жизнь|1||1|C2.5:1|C2.5:1|They are almost noticeable by a naked eye!|A civilization has settled over a Medium Star Cluster.
a114|жизнь|3||1|C2.8:1|C2.8:1|Oh, they grasped how to control gravity at last!|A civilization has reached 2.8 level.
a115|жизнь|6||1|C3.0:1|C3.0:1|They really have learned to fly!|A civilization is able to travel between Galaxies.
a116|жизнь|3||1|C3.2:1|C3.2:1|They must be so proud of themselves!|A civilization has settled on a scale of an Andromeda Galaxy.
a117|жизнь|3||1|C3.5:1|C3.5:1|Hey, they are resourceful!|A civilization has utilized a Neutron Star resources.
a118|жизнь|3||1|C3.8:1|C3.8:1|Now they actually easy to notice.|A civilization has settled over hundreds of thousands Galaxies.
a119|жизнь|6||1|C4.0:1|C4.0:1|Feels like this has happened already, no?|A civilization has grasp on temporal manipulations.
a120|жизнь|6||1|C4.2:1|C4.2:1|At last they seem to think big enough.|A civilization has utilized a Supermassive Black Hole for their goals.
a121|жизнь|10||1|C4.4:1|C4.4:1|They've made it! What will they do next, I wonder?|A civilization has expanded on a scale of a whole visible Universe.
a122|жизнь|3|1|1|C1.6:1|SET:1|They have made an extrasolar colony!|
a123|жизнь|6|1|1||GSM:1&C2.8:50|They seem to have found a Galactic Federation or something!|
a124|жизнь|1||1|||Oh no...|A civilization of 0.8+ level extinct.
a125|жизнь|1||1|||Not again!|100 civilizations of 1.2+ level extinct.
a126|жизнь|1||1|||Will I get used to it?|1 000 civilizations of 1.6+ level extinct.
a127|жизнь|3||1|||Yeah, you become numb after a while.|10 000 civilizations of 2.0+ level extinct.
a128|жизнь|6||1|||They seemed to be survivors. Oh well.|A civilization of 3.0+ level extinct.
a129|жизнь|10||1|||My dissapointment is immeasureable.|A civilization of 4.0+ level extinct.
a130|жизнь|6||1|||This reminds me of something...|At the date of 13.8 billions of years there is a civilization of 0.9 level.
a131|события|3|1|1|высокая плотность|BigCrunch|Big Crunch!|The universe collapsed into a colossal black hole.
a132|события|3|1|1|низкая плотность|BigRip|Big Rip!|The universe dissipated in different directions.
a133|события|6|1|1|BigCrunch^BigRip|BigCrunch&BigRip|Double Trouble!|Two different apocalypses are finished.
a134|события|10||1|||Complete banishment! You have been kicked out of the universe.|Now this universe exists by itself, without your participation.
a135|события|3||1|||Antitheism: They deny your existence.|Maybe they are somehow right about that?
a136|события|3||1|||Integration: They strive for coexistence.|Maybe at least they can make it?
a137|события|3||1|||Xenocide: Their bellicosity leads to dire results.|However, nothing is new.
a138|события|3||1|||Reconstruction: They rearrange the universe.|It is their universe, right? Or not?
a139|события|3||1|||Spirituality: They seek deep meaning.|What if they find out the truth?
a140|события|3||1|||Exodus: They want to leave the universe.|What do they not like here?
a141|события|6||1|||Dynamic Universe|All trends have shown themselves!
a142|события|3||1|||Banishment: They are trying to expel you from the universe!|They definitely don't like something.
a143|события|3||1|||Federation: They are looking for ways to peacefully coexist.|Perhaps this is good?
a144|события|3||1|||Enlightenment: They help those who are lagging behind.|How lovely!
a145|события|3||1|||New Order: They bury the hatchet.|Maybe it's for the best?
a146|события|3||1|||Grand Wars: They are engaged into carnage of epic proportions.|It is a pity that it is impossible to discern all the details from here.
a147|события|3||1|||Extermination: They are purging potential competitors.|Life is a renewable resource.
a148|события|3||1|||Devastation: They are destroying the universe!|Stars turn into black holes.
a149|события|3||1|||Stellar Fragmentation: They reconstruct the universe for themselves.|It is more convenient for them to live on small and stable stars.
a150|события|3||1|||Stellar Pumping: They create new stars!|Red dwarfs are created from clouds and brown dwarfs.
a151|события|3||1|||Stellar Cooling: They stabilize the stars.|The activity of stellar processes is decreasing.
a152|события|3||1|||Prayer: They pray incredibly fervently.|You were even able to hear something.
a153|события|3||1|||Dialogue: They are waiting for an answer!|And it is you who should answer.
a154|события|3||1|||Living Universe|10 civilization actions have taken place.
a155|события|6||1|||Very Living Universe|30 civilization actions have taken place.
a156|события|10||1|||Incredibly Alive Universe!|100 civilization actions have taken place.
a157|события|10||1|||So much is happening!|All actions happened at least once!
a158|силы|1||1|сила получена|сила применена|Learn to dig fast.|Use Shovelin
a159|силы|1||1|сила получена|сила применена|Kickstart inflation.|Use Inflatin
a160|силы|3||1|сила получена|сила применена|Pass time.|Use Temporite
a161|силы|3||1|сила получена|сила применена|Dispose of garbage.|Use Collisite
a162|силы|6||1|сила получена|сила применена|Divide physics by zero.|Use Primex
a163|силы|1||1|сила получена|сила применена|Destroy space.|Use Singularite
a164|силы|1||1|сила получена|сила применена|Strengthen structures.|Use Renewin
a165|силы|3||1|сила получена|сила применена|Light up the stars.|Use Infusite
a166|силы|3||1|сила получена|сила применена|Cool down the universe.|Use Frostin
a167|силы|6||1|сила получена|сила применена|Get rid of the stars.|Use Destellarite
a168|силы|1||1|сила получена|сила применена|Make more planets.|Use Rockex
a169|силы|1||1|сила получена|сила применена|Seed the universe.|Use Panspermin
a170|силы|3||1|сила получена|сила применена|Fertilize the universe.|Use Gaianite
a171|силы|3||1|сила получена|сила применена|Invigorate the universe.|Use Darwinite
a172|силы|6||1|сила получена|сила применена|Fill the universe with goodness.|Use Edenex
a173|силы|1||1|сила получена|сила применена|Teach sapients to live in peace.|Use Pacifin
a174|силы|1||1|сила получена|сила применена|Teach sapients to fight for their lives.|Use Enragein
a175|силы|3||1|сила получена|сила применена|Appoint prophets among sapients.|Use Moralite
a176|силы|3||1|сила получена|сила применена|Punish those sapients who do not follow your plans.|Use Discriminite
a177|силы|6||1|сила получена|сила применена|Suppress those sapients who think too much of themselves.|Use Reprex
a178|силы|6||1|одна любая сила получена в 4 градациях|флэш|Use the forces of the one kind by increase of power.|Green, blue, purple, orange
a179|силы|3||1|снижена вручную|Bur=1|Cold universe.|Minimal Burnability
a180|силы|3||1|аналогично|Bur=19|The stars are lit to burn out!|Maximum Burnability
a181|силы|3||1|аналогично|Exp=1|Dense vacuum.|Minimal Explosiveness
a182|силы|3||1|аналогично|Exp=19|Bada-bing, bada-boom-boom-boom!|Maximum Explosiveness
a183|силы|3||1|аналогично|Agg=1|Peace and tranquility.|Minimal Aggression
a184|силы|3||1|аналогично|Agg=19|Like 4E4!|Maximum Aggression
a185|силы|6||1|||All forces are available at once!|
a186|силы|10||1|||All forces are used at least once in this universe.|
a187|силы|1||1|3 обычные силы одного вида||Upgrade a force to rare.|
a188|силы|3||1|3 редкие силы одного вида||Upgrade a force to epic.|
a189|силы|6||1|3 эпические силы одного вида||Upgrade a force to legendary.|
a190|силы|1||1|улучш 3 силы|5|Upgrade 5 forces.|
a191|силы|1||1|8|13|Upgrade 13 forces.|
a192|силы|3||1|21|34|Upgrade 34 forces.|
a193|силы|3||1|55|89|Upgrade 89 forces.|
a194|силы|6||1|144|233|Upgrade 233 forces.|
a195|силы|6||1|377|610|Upgrade 610 forces.|
a196|силы|10||1|987|1597|Upgrade 1 597 forces.|Continue in the same spirit!
a197|силы|1||1|1|3|Upgrade 3 force chests.|
a198|силы|3||1|5|7|Upgrade 7 force chests.|
a199|силы|6||1|14|20|Upgrade 20 force chests.|
a200|силы|10||1|38|55|Upgrade 55 force chests.|Thanks for support!
"""
#endregion
+1
View File
@@ -0,0 +1 @@
uid://bx5onxr8atifn
@@ -1,3 +1,11 @@
class_name OwniverseApocalypseQueueLoader
static func get_data() -> Dictionary:
return CSVLoader.load(DATA)
#region Owniverse Items
const DATA := """
Num|Shortcut|Stage|Screen|Tick|Desc Num|Shortcut|Stage|Screen|Tick|Desc
1|LUB|1|3|50|Вселенский пузырь 1|LUB|1|3|50|Вселенский пузырь
2|LGW|1|3|5|Великая стена 2|LGW|1|3|5|Великая стена
@@ -130,3 +138,7 @@ Num|Shortcut|Stage|Screen|Tick|Desc
129|H|11|1|5|Водород 129|H|11|1|5|Водород
130|SMBH|12|2|5|СМЧД 130|SMBH|12|2|5|СМЧД
131|S|12|1|5|Пространство 131|S|12|1|5|Пространство
"""
#endregion
@@ -0,0 +1 @@
uid://8bh387weuy7a
+6 -6
View File
@@ -1008,7 +1008,7 @@ Trends.Report.Header|[b]Влияние развитых цивилизаций
Int.Fed.Report.Max|[Интеграция] Федерация: [color=#cccc88]+%value0%[/color]. Увеличено число цивилизаций, подверженных воздействию дипломатии.|max|||| Int.Fed.Report.Max|[Интеграция] Федерация: [color=#cccc88]+%value0%[/color]. Увеличено число цивилизаций, подверженных воздействию дипломатии.|max||||
Int.Enl.Report.Max|[Интеграция] Просвещение: [color=#cccc88]+%value0%[/color]. Ускорен прогресс на примитивных стадиях жизни.|max|||| Int.Enl.Report.Max|[Интеграция] Просвещение: [color=#cccc88]+%value0%[/color]. Ускорен прогресс на примитивных стадиях жизни.|max||||
Int.Ord.Report.Max|[Интеграция] Новый порядок: [color=#cccc88]+%value0%[/color]. Оказывается помощь и поддержка цивилизациям, попавшим в кризисные обстоятельства.|max|||| Int.Ord.Report.Max|[Интеграция] Новый порядок: [color=#cccc88]+%value0%[/color]. Оказывается помощь и поддержка цивилизациям, попавшим в кризисные обстоятельства.|max||||
Emerged.Report.Header|[b]Максимальное число возникших сущностей[/b]||||| Emerged.Report.Header|[b]Всего возникло сущностей[/b]|||||
Ctm.Report.Max|Трансцендентная цивилизация мистиков: [color=#cccc88]%value0%[/color]|max|||| Ctm.Report.Max|Трансцендентная цивилизация мистиков: [color=#cccc88]%value0%[/color]|max||||
Ctb.Report.Max|Трансцендентная цивилизация строителей: [color=#cccc88]%value0%[/color]|max|||| Ctb.Report.Max|Трансцендентная цивилизация строителей: [color=#cccc88]%value0%[/color]|max||||
Ctd.Report.Max|Трансцендентная цивилизация дипломатов: [color=#cccc88]%value0%[/color]|max|||| Ctd.Report.Max|Трансцендентная цивилизация дипломатов: [color=#cccc88]%value0%[/color]|max||||
@@ -1075,7 +1075,7 @@ HP.Report.Max|Обитаемая планета: [color=#cccc88]%value0%[/color]
RP.Report.Max|Каменистая планета: [color=#cccc88]%value0%[/color]|max|||| RP.Report.Max|Каменистая планета: [color=#cccc88]%value0%[/color]|max||||
AB.Report.Max|Астероиды: [color=#cccc88]%value0%[/color]|max|||| AB.Report.Max|Астероиды: [color=#cccc88]%value0%[/color]|max||||
GG.Report.Max|Газовый гигант: [color=#cccc88]%value0%[/color]|max|||| GG.Report.Max|Газовый гигант: [color=#cccc88]%value0%[/color]|max||||
Produced.Report.Header|[b]Максимальное число созданных объектов[/b]||||| Produced.Report.Header|[b]Всего создано объектов[/b]|||||
LUB.Report.Max|Вселенский пузырь: [color=#cccc88]%value0%[/color]|max|||| LUB.Report.Max|Вселенский пузырь: [color=#cccc88]%value0%[/color]|max||||
LGW.Report.Max|Великая стена: [color=#cccc88]%value0%[/color]|max|||| LGW.Report.Max|Великая стена: [color=#cccc88]%value0%[/color]|max||||
LGF.Report.Max|Галактическая нить: [color=#cccc88]%value0%[/color]|max|||| LGF.Report.Max|Галактическая нить: [color=#cccc88]%value0%[/color]|max||||
@@ -1116,7 +1116,7 @@ YD.Report.Max|Жёлтый карлик: [color=#cccc88]%value0%[/color]|max||||
OD.Report.Max|Оранжевый карлик: [color=#cccc88]%value0%[/color]|max|||| OD.Report.Max|Оранжевый карлик: [color=#cccc88]%value0%[/color]|max||||
RD.Report.Max|Красный карлик: [color=#cccc88]%value0%[/color]|max|||| RD.Report.Max|Красный карлик: [color=#cccc88]%value0%[/color]|max||||
BD.Report.Max|Бурый карлик: [color=#cccc88]%value0%[/color]|max|||| BD.Report.Max|Бурый карлик: [color=#cccc88]%value0%[/color]|max||||
Obtained.Report.Header|[b]Максимум за время существования вселенной[/b]||||| Obtained.Report.Header|[b]Всего получено ресурсов[/b]|||||
S.Report.Max|Пространство: [color=#cccc88]%value0%[/color]|max|||| S.Report.Max|Пространство: [color=#cccc88]%value0%[/color]|max||||
H.Report.Max|Водород: [color=#cccc88]%value0%[/color]|max|||| H.Report.Max|Водород: [color=#cccc88]%value0%[/color]|max||||
He.Report.Max|Гелий: [color=#cccc88]%value0%[/color]|max|||| He.Report.Max|Гелий: [color=#cccc88]%value0%[/color]|max||||
@@ -2199,7 +2199,7 @@ Trends.Header|[b]Influence of advanced civilizations on the universe[/b]|||||
Int.Fed.Max|[Integration] Federation: [color=#cccc88]+%value0%[/color]. The number of civilizations affected by diplomacy is increased.|max|||| Int.Fed.Max|[Integration] Federation: [color=#cccc88]+%value0%[/color]. The number of civilizations affected by diplomacy is increased.|max||||
Int.Enl.Max|[Integration] Enlightenment: [color=#cccc88]+%value0%[/color]. The progress during primitive stages of life development is accelerated.|max|||| Int.Enl.Max|[Integration] Enlightenment: [color=#cccc88]+%value0%[/color]. The progress during primitive stages of life development is accelerated.|max||||
Int.Ord.Max|[Integration] New Order: [color=#cccc88]+%value0%[/color]. Civilizations in crisis obtain aid and support.|max|||| Int.Ord.Max|[Integration] New Order: [color=#cccc88]+%value0%[/color]. Civilizations in crisis obtain aid and support.|max||||
Emerged.Header|[b]Maximum amount of emerged entities[/b]||||| Emerged.Header|[b]Total entities emerged[/b]|||||
Ctm.Max|Transcendent mystics civilization: [color=#cccc88]%value0%[/color]|max|||| Ctm.Max|Transcendent mystics civilization: [color=#cccc88]%value0%[/color]|max||||
Ctb.Max|Transcendent builders civilization: [color=#cccc88]%value0%[/color]|max|||| Ctb.Max|Transcendent builders civilization: [color=#cccc88]%value0%[/color]|max||||
Ctd.Max|Transcendent diplomats civilization: [color=#cccc88]%value0%[/color]|max|||| Ctd.Max|Transcendent diplomats civilization: [color=#cccc88]%value0%[/color]|max||||
@@ -2266,7 +2266,7 @@ HP.Max|Habitable Planet: [color=#cccc88]%value0%[/color]|max||||
RP.Max|Rocky Planet: [color=#cccc88]%value0%[/color]|max|||| RP.Max|Rocky Planet: [color=#cccc88]%value0%[/color]|max||||
AB.Max|Asteroids: [color=#cccc88]%value0%[/color]|max|||| AB.Max|Asteroids: [color=#cccc88]%value0%[/color]|max||||
GG.Max|Gas Giant: [color=#cccc88]%value0%[/color]|max|||| GG.Max|Gas Giant: [color=#cccc88]%value0%[/color]|max||||
Produced.Header|[b]Maximum amount of created objects[/b]||||| Produced.Header|[b]Total objects created[/b]|||||
LUB.Max|Universe Bubble: [color=#cccc88]%value0%[/color]|max|||| LUB.Max|Universe Bubble: [color=#cccc88]%value0%[/color]|max||||
LGW.Max|Great Wall: [color=#cccc88]%value0%[/color]|max|||| LGW.Max|Great Wall: [color=#cccc88]%value0%[/color]|max||||
LGF.Max|Galactic Filament: [color=#cccc88]%value0%[/color]|max|||| LGF.Max|Galactic Filament: [color=#cccc88]%value0%[/color]|max||||
@@ -2307,7 +2307,7 @@ YD.Max|Yellow Dwarf: [color=#cccc88]%value0%[/color]|max||||
OD.Max|Orange Dwarf: [color=#cccc88]%value0%[/color]|max|||| OD.Max|Orange Dwarf: [color=#cccc88]%value0%[/color]|max||||
RD.Max|Red Dwarf: [color=#cccc88]%value0%[/color]|max|||| RD.Max|Red Dwarf: [color=#cccc88]%value0%[/color]|max||||
BD.Max|Brown Dwarf: [color=#cccc88]%value0%[/color]|max|||| BD.Max|Brown Dwarf: [color=#cccc88]%value0%[/color]|max||||
Obtained.Header|[b]Maximum for all the existence of the universe[/b]||||| Obtained.Header|[b]Total resources obtained[/b]|||||
S.Max|Space: [color=#cccc88]%value0%[/color]|max|||| S.Max|Space: [color=#cccc88]%value0%[/color]|max||||
H.Max|Hydrogen: [color=#cccc88]%value0%[/color]|max|||| H.Max|Hydrogen: [color=#cccc88]%value0%[/color]|max||||
He.Max|Helium: [color=#cccc88]%value0%[/color]|max|||| He.Max|Helium: [color=#cccc88]%value0%[/color]|max||||
+2 -1
View File
@@ -1,6 +1,7 @@
[runnable_presets] [runnable_presets]
"Windows Desktop"="Windows Desktop" "Windows Desktop"="Windows Desktop"
Android="Android"
[preset.0] [preset.0]
@@ -11,7 +12,7 @@ custom_features=""
export_filter="all_resources" export_filter="all_resources"
include_filter="" include_filter=""
exclude_filter="" exclude_filter=""
export_path="../../../Documents/Owad/Owniverse.I/Builds/owniverse.0.7.0-146.console.exe" export_path="../../../Documents/Owad/Owniverse.I/Builds/owniverse.0.7.0-146.exe"
patches=PackedStringArray() patches=PackedStringArray()
patch_delta_encoding=false patch_delta_encoding=false
patch_delta_compression_level_zstd=19 patch_delta_compression_level_zstd=19
+82 -25
View File
@@ -46,9 +46,15 @@ float. Весь порядок E27 сохраняет целые единицы.
### Общие окна рождения и эволюции ### Общие окна рождения и эволюции
Обработка кадров на скоростях 4–6 использует адаптивный шаг 0.1 → 0.2 → 0.5 реальной секунды: накопленное отставание больше двух текущих шагов повышает ступень, меньше половины шага понижает её. Граница ограничивается запрошенным временем, долг не отбрасывается. Скорости 1–3 используют 1/60 секунды. При смене скорости ступень сбрасывается. Сохранённая незавершённая транзакция сначала возобновляется с прежней границей. Кооперативный бюджет кадра остаётся 40 мс. Изменение шага не изменяет ширину временных партий, но может влиять на результаты дискретных расчётов производства и глобальных параметров. На всех скоростях целевая частота запуска — пять полных циклов в секунду. После накопления не менее 0,2 реальной секунды запускается расчёт за весь ожидающий игровой интервал. Время, поступающее во время расчёта, остаётся для следующего цикла. При задержках шаг увеличивается; время не отбрасывается. Смена скорости не сокращает реальный интервал между запусками. Незавершённая транзакция из сохранения возобновляется с прежней границей. Кооперативный бюджет кадра — 40 мс; фактическая частота завершения зависит от стоимости расчёта.
Окно рождения шириной `w = lifespan / 40` при `lifespan > 10`, иначе `w = lifespan`. Окно превращения сдвинуто на `lifespan`. При частичном списании: На скоростях 4–6 укрупнение звёздных партий зависит от размера цикла и прежнего окна рождения. Номинальная ширина сетки партий — `0.2 × 10^(speed - 1)` игровых лет: 200, 2000 и 20000 лет. Если прежнее окно `max(lifespan / 40, временное разрешение)` шире цикла, оно сохраняется: долгоживущие звёзды не дробятся на более мелкую сетку. Если оно не шире цикла, новые звёзды группы `Star` (включая красные гиганты) объединяются по общей границе завершения жизни и переходят целиком.
При обработке существующих партий объединяются только две или более партии одного типа, весь оставшийся интервал превращения которых попадает в один и тот же цикл. Широкие партии и партии, пересекающие границу циклов, остаются без изменений. Количества и накопленные остатки складываются точно; объединённая партия переходит целиком на границе цикла. Повторное укрупнение не меняет результат.
Это приближение времени перехода: задержка узкой партии не превышает одного цикла. Производство учитывает полное исчезновение объединённой партии на выбранной границе. При снижении скорости новые звёзды снова получают постепенные партии; уже созданные партии не разделяются. События SN/GRB сохраняют прежний расчёт.
На скоростях 1–3 звёзды, а также остальные объекты со сроком жизни больше 10 лет используют 40 возрастных групп: окно рождения имеет ширину `w = lifespan / 40`; для коротких событий ширина равна сроку жизни. Окно превращения сдвинуто на `lifespan`. Для облаков и первичной жизни на скоростях 4–6 сохраняется прежнее укрупнение окна по временному разрешению. При частичном списании:
``` ```
expected = remainder + (remaining_count - remainder) * elapsed / remaining_window expected = remainder + (remaining_count - remainder) * elapsed / remaining_window
@@ -56,7 +62,7 @@ converted = floor(expected)
remainder = expected - converted remainder = expected - converted
``` ```
Конец окна списывает остаток. Переход не уничтожает всю партию в начале окна. Начала и длительности сохраняют модель GI, включая годовую длительность событий SN/GRB. Конец постепенного окна списывает остаток. Новые звёздные партии высоких скоростей вместо такого окна имеют одну границу полного перехода. Длительность событий SN/GRB остаётся годовой.
## Планеты ## Планеты
@@ -111,7 +117,7 @@ GI не хранит накопленные счётчики первичной
Прогресс и деградация округляются вниз с сохранением остатков. Если оба накопленных исхода конкурируют за последнюю планету, распределяются только существующие объекты, а неиспользованный остаток переносится на следующую попытку. Деградация распределяется по весам `fall`; знаковые ошибки распределения также сохраняются. Прогресс и деградация округляются вниз с сохранением остатков. Если оба накопленных исхода конкурируют за последнюю планету, распределяются только существующие объекты, а неиспользованный остаток переносится на следующую попытку. Деградация распределяется по весам `fall`; знаковые ошибки распределения также сохраняются.
Для всех шести стадий базовые доли за попытку — `progress=0.00025`, `degradation=0.0025`. Интервалы HP/EON1/EON2/EON3/EON4/EON5: 5 млн, 10 млн, 10 млн, 1 млн, 1 млн, 500 тыс. лет. Используются постепенные окна GI шириной `span / 40`. Случайные множители Unity не применяются. Агрессия, порядок и просвещение изменяют вероятности по формулам из раздела индикаторов ниже. Базовые доли за попытку: для HP `progress=0.00025`, для EON1–EON5 `progress=0.025`; у всех `degradation=0.0025`. Текущие интервалы HP/EON1/EON2/EON3/EON4/EON5: 5 млн, 1 млн, 1 млн, 100 тыс., 100 тыс., 500 тыс. лет. Используются постепенные окна GI шириной `span / 40`. Случайные множители Unity не применяются. Агрессия, порядок и просвещение изменяют вероятности по формулам из раздела индикаторов ниже.
HP деградирует в RP. У эонов возможен откат к предыдущим стадиям по таблице. RP не развивается сам. C0.0 и C0.1 также развиваются: период 2000 лет, прогресс 0.0175, деградация 0.025. Переходы C0.0 → C0.1 → C0.2, откат C0.0 → EON5 и C0.1 → C0.0. Развитие C0.2 и выше подключено через этические правила. Конфликт civ_bonus устранён повторным импортом: бонусы структур совпадают с Unity. Для существующих этических цивилизаций пользователь выбрал равномерное распределение между шестью этиками с детерминированным целым остатком. HP деградирует в RP. У эонов возможен откат к предыдущим стадиям по таблице. RP не развивается сам. C0.0 и C0.1 также развиваются: период 2000 лет, прогресс 0.0175, деградация 0.025. Переходы C0.0 → C0.1 → C0.2, откат C0.0 → EON5 и C0.1 → C0.0. Развитие C0.2 и выше подключено через этические правила. Конфликт civ_bonus устранён повторным импортом: бонусы структур совпадают с Unity. Для существующих этических цивилизаций пользователь выбрал равномерное распределение между шестью этиками с детерминированным целым остатком.
@@ -123,39 +129,37 @@ HP деградирует в RP. У эонов возможен откат к п
`data/owniverse_civilization_rule_loader.gd` содержит 43 стадии по шесть этик из Unity. `OwniverseCivilizationRuleLoader` группирует строки по стадиям, формирует связи между ними и передаёт готовый словарь в `Global.CivilizationRules`. После C4.4 следует Ct: конечная стадия без новых жизненных партий. `data/owniverse_civilization_rule_loader.gd` содержит 43 стадии по шесть этик из Unity. `OwniverseCivilizationRuleLoader` группирует строки по стадиям, формирует связи между ними и передаёт готовый словарь в `Global.CivilizationRules`. После C4.4 следует Ct: конечная стадия без новых жизненных партий.
Состояние `ethics[stage][ethic]` хранит целые количества. Для существующих цивилизаций без этик количество распределяется поровну в порядке m, l, w, d, c, b; остаток распределяется по одному в этом порядке. Существующее распределение сохраняется. При внешних потерях оно пропорционально сокращается. Стадия хранит точное целое количество в физическом реестре и шесть долей `float` в `ethics[stage]`. C0.2–C4.4 не используют возрастные партии. Значение `span` задаёт средний интервал попыток: интенсивности прогресса, деградации, войны и колонизации равны соответствующим коэффициентам таблицы, делённым на эффективный `span`.
Цивилизации с непустым `home` (C2.0–C4.4) освобождаются от прежних привязок без изменения общего количества. Общий механизм переходов больше не переносит планетную привязку на такого преемника. Структуры назначаются отдельно в `civilization_homes`; при возвращении к планетным стадиям цивилизации размещаются у имеющихся карликовых звёзд. `civilization_flow.gd` вычисляет ожидаемые потоки по состоянию начала интервала. Разреженный оператор непрерывных переходов интегрируется рядом Пуассона (uniformization): один интервал может пройти несколько последовательных стадий, включая достижение Ct. Число членов ряда зависит от интенсивности и длительности интервала, а не от количества цивилизаций. Большой интервал полностью обрабатывается частями с параметром Пуассона не более 32. Промежуточные массы и составы потоков — `float`; конечные количества распределяются целыми квотами с сохранением точной общей суммы. Доли квот округляются до 15 десятичных знаков; остатки округления сохраняются между циклами.
Правило среды согласовано: число космических цивилизаций ограничено звёздным ресурсом. Для стадии `i` расход ёмкости — `population[i] * max(1, Stars[i])`. Каждое семейство `Home` имеет общий бюджет для всех использующих его стадий; звёзды внутри структур также учитываются, но повторно не суммируются. SMBH учитывается по суммарной массе в эквиваленте звёзд, согласно решению пользователя. Обычный дрейф этик интегрируется аналитически: уход 2% за средний интервал попыток распределяется между четырьмя не противоположными этиками. После смешивания приходящих потоков дипломатия постепенно смещает доли в соответствии с `diplomacy_rate`. Ct сохраняет этику без дрейфа. Это приближённое разделение процессов: переходы и дрейф используют один интервал, но не чередуются для каждого отдельного поколения.
Переход переиспользует резерв исходной стадии. При нехватке ёмкости неподдержанные переходы остаются на исходной стадии. Лимит применяется к общему механизму выходов партий и ручному созданию; расчёт прогресса C0.2+ использует тот же бюджет. Войны включены в интенсивности переходов. На столкновение ожидается 17/36 успешного прогресса и 31/18 суммарных потерь сторон, разделённых на `Order`; потери распределяются по `fall`. Счётчик столкновений и колониальные попытки интегрируются по времени пребывания на каждой стадии, включая посещённые внутри интервала. Побочный ущерб 40/3 на столкновение применяется к остающейся популяции предыдущей стадии и ограничивается её количеством. Отдельных партий победителей, нападающих и жертв нет.
При загрузке и после эволюционных поколений избыток деградирует последовательно до поддерживаемой стадии. Приоритет получают существующие более развитые цивилизации; остаток бюджета достаётся младшим. Деградация переносит количество и этики, сокращает старые партии и регистрирует новые, не уничтожая цивилизации. C2.0 при избытке становится C1.9 и распределяется по имеющимся звёздам-карликам. Если карликов нет, C1.9 остаётся свободной — отсутствующая звезда не создаётся. Планетные стадии до C2.0 не расходуют отдельную звёздную ёмкость: у одной звезды могут быть несколько планет. Колонизация агрегирует запросы по стадии назначения (на две стадии ниже отправителя), ограничивает их общим запасом GG, AB, RP и HP и заменяет ровно выделенное число планет. Целые колонии сохраняют владельца целевой планеты; состав получает дрейф 960/4/9. Исходное население не расходуется.
Резерв ёмкости логический: звёзды не расходуются и продолжают производить ресурсы. Общий бюджет пересчитывается из физических количеств, отдельная копия бронирований в сохранении не хранится. Применение потоков сначала строит таблицу исходных владельцев. Для каждой исходной стадии готовые целые квоты распределяются по строкам владельцев; последняя строка получает точный остаток. Стадии с единственным свободным запасом применяют готовые квоты напрямую. Поступления объединяются по стадии и владельцу; физическая выдача и статистика создания выполняются один раз на получателя. Ct всегда остаётся свободной от привязок.
При колонизации планеты сначала списываются по типам и владельцам, затем суммарное списание каждого владельца распределяется между стадиями колоний. Каждая стадия получает одну суммарную выдачу. Общие целые квоты, число списанных планет и количество колоний сохраняются точно. Из-за объединения распределений единичные остатки между владельцами могут отличаться от прежнего последовательного порядка; вероятности переходов и формулы этик не меняются.
### Расчёт поколения C0.2–C4.4 В пределах цивилизационной транзакции точные общие количества передаются между фазами. Применение переходов, колонизация и коррекция ёмкости возвращают обновлённый снимок; перемещение между владельцами само по себе общую численность не меняет. Этот снимок используется для периодов, ограничений, нормализации этик и поклонения. После изменения запасов другими подсистемами снимок строится заново. Снимки не сохраняются в файл и не заменяют проверку состояния при внешних изменениях словарей.
1. Из истёкшей части партии пропорционально выбираются представители шести этик. Для каждой этики читаются `progress` и `degradation`; целые исходы получаются с сохранением дробных остатков. Неудавшийся из-за лимита переход возвращается на исходную стадию. Автоматическое создание нескольких типов звёзд также использует один текущий снимок. Успешная пробная операция вычитает фактически израсходованные количества; перенос ресурса в привязку сохраняет общий запас. При отказе пробной операции снимок остаётся неизменным. Новые звёзды выдаются после завершения отбора типов, как и прежде.
2. Прогресс ведёт на следующую стадию; деградация распределяется по весам `fall` конкретной этики. Повторные попытки не увеличивают статистику созданных цивилизаций.
3. При флаге `fights` рассчитываются войны. Веса исходов — 1:2:3:3, доли прогресса победителей — 0:0.25:0.5:0.75; первый исход деградирует атакующих. Потери противников — 2:1:1.5:2 на войну. Два последних исхода дают 10 и 30 единиц побочного ущерба предыдущей стадии. Выбор участников ограничен живой популяцией, включая ещё не сработавшие партии; их партии сокращаются вместе с населением.
4. Для обычного дрейфа веса своей/противоположной/каждой другой этики равны 196/0/1. Дипломатия умножает исходные численности на `diplomacy_rate`, затем нормирует к прежней численности. Флаг дипломатии действует на остающихся на стадии. При переходе в Ct этика сохраняется без дрейфа.
5. При флаге `colonizes` создаются колонии на две стадии ниже отправителя. Цели — существующие GG, AB, RP, HP; колонизация заменяет цель и сохраняет её звёздную привязку. Веса дрейфа колонистов — 960/4/9. Исходная цивилизация не расходуется. Новая партия начинается в текущий игровой момент.
6. После применения переходов пересчитываются этики, поклонение (`Σ population × worship`), бласты и ограничения среды. Число операций зависит от числа типов и партий, а не от числа цивилизаций.
На скоростях 1–3 размер временной партии равен сроку жизни / 40. На скоростях 4–6 он равен максимуму этого значения и 100, 1000, 10000 игровых лет соответственно (0,1 реальной секунды). Близкие существующие партии объединяются при следующем шаге: целые количества и остатки изъятия складываются, временной интервал охватывает исходные партии. Это приближает моменты переходов внутри объединённого интервала. События со сроком жизни до 10 лет сохраняют прежние окна. При снижении скорости ранее объединённые партии не разделяются; новые получают меньшие окна. Шаг публикации на скоростях 4–6 ограничен 0,1 реальной секунды; накопленное отставание сохраняется. Космические стадии C2.0–C4.4 свободны от планетных привязок. Общие бюджеты звёздных семейств и структур учитывают уже занятые резервы. Неподдерживаемый промежуточный переход блокируется в операторе; конечные положительные запросы делят свободную ёмкость одновременно. Отклонённые количества возвращаются к источнику. Освободившаяся при переходах ёмкость доступна следующему шагу. Если внешние изменения уменьшили ёмкость ниже существующего населения, избыток последовательно деградирует с приоритетом старших стадий. При возвращении в планетные стадии используются реальные карликовые звёзды, при отсутствии хозяина население остаётся свободным.
Несколько частей одного типа обрабатываются агрегированно между минимальным и максимальным временем. На скоростях 4–6 новые партии жизни и цивилизаций после перехода создаются на ближайшей следующей границе интервала 100/1000/10000 лет. Начало сетки этих новых партий равно нулю независимо от старого `batch_origins`: округлённое время рождения не попадает обратно в раннюю часть старого окна. Это исключает повторные попытки эволюции той же выходящей популяции внутри её временного интервала; следующие интервалы продолжают обрабатываться. На скоростях 1–3 сохраняются исходные интервалы рождения и последовательные поколения. Звёздные события используют прежнюю схему. Порядок полного шага: производство за интервал → непрерывные потоки существующих цивилизаций → поколения звёзд и биологической жизни и бласты → итоговые ограничения среды → тренды. Возникшие из C0.1 цивилизации C0.2 получают равномерный профиль, смешанный с существующим населением, и начинают непрерывную эволюцию со следующего шага. Разные размеры полного шага могут отличаться из-за этого порядка, ограничений среды и нелинейных эффектов; чистая непрерывная цепочка без ограничений согласована при разделении интервала.
Побочный ущерб неистёкшим партиям применяется в текущий момент. Все остатки округления сохраняются в `civilization_remainders`, счётчики — в `civilization_stats`. Счётчик времени UI показывает запрошенные часы и обновляется с частотой до 10 Гц независимо от тяжёлой транзакции. Состояние мира и его индикаторы публикуются по завершении транзакции; при высокой нагрузке отображаемые часы поэтому могут немного опережать рассчитанные объекты. Тяжёлые циклы адаптивно запускаются с целевым интервалом 0,2–0,5 реальной секунды, каждый использует весь накопленный игровой интервал.
В `civilization_remainders` сохраняются квоты и дробные остатки потоков; в `civilization_stats` — войны, колонии, побочный ущерб и поклонение. Возрастные партии сохраняются для звёзд, HP, эонов, C0.0 и C0.1.
### Структуры и периоды ### Структуры и периоды
Прямой `claim` задаёт структуру стадии и норму 1; каждая промежуточная стадия умножает норму на 10 до следующего `claim`. Начиная с C3.0 требуется соответствующий запас структур, общий для использующих его стадий. При потере запаса избыток деградирует. Этот бюджет действует одновременно со звёздным. Прямой `claim` задаёт структуру стадии и норму 1; каждая промежуточная стадия умножает норму на 10 до следующего `claim`. Начиная с C3.0 требуется соответствующий запас структур, общий для использующих его стадий. При потере запаса избыток деградирует. Этот бюджет действует одновременно со звёздным.
Размещение выбирает структуры в порядке убывания `civ_bonus`, начиная с соответствующего стадии `claim`. Прямое размещение ограничено числом структур; остальные разрешённые структуры при наличии дают общий бонус, как в Unity. Период новых партий равен `span / средневзвешенный civ_bonus`, для неразмещённой части множитель равен 1. Уже назначенные сроки партий не переписываются при изменении бонуса. Размещение выбирает структуры в порядке убывания `civ_bonus`, начиная с соответствующего стадии `claim`. Прямое размещение ограничено числом структур; остальные разрешённые структуры при наличии дают общий бонус, как в Unity. Эффективный средний интервал попыток равен `span / средневзвешенный civ_bonus`, для неразмещённой части множитель равен 1. Бонус входит в оператор следующего расчётного интервала.
Ct не получает часы. Инициализация отсутствующих партий жизни выполняется при загрузке и перед продвижением симуляции. Параметры мира и тенденции описаны ниже; интерфейс применения сил в этот перенос не входит. Ct не получает часы. Инициализация отсутствующих партий жизни выполняется при загрузке и перед продвижением симуляции. Параметры мира и тенденции описаны ниже; интерфейс применения сил в этот перенос не входит.
@@ -173,9 +177,11 @@ Ct не получает часы. Инициализация отсутству
- `1e-5 < density < 0.9`: обычная шкала, `ln(density × 1e5) / ln(9e4)`; - `1e-5 < density < 0.9`: обычная шкала, `ln(density × 1e5) / ln(9e4)`;
- `density ≥ 0.9`: шкала сжатия, `(density − 0.9) × 10`. - `density ≥ 0.9`: шкала сжатия, `(density − 0.9) × 10`.
В Unity в первой формуле было `−7`; это ошибка, не позволяющая указателю двигаться. Здесь исправлено на `+7`. Переключение шкалы меняет и заголовок/описание индикатора. Само наступление апокалипсиса индикатор не запускает. В Unity в первой формуле было `−7`; это ошибка, не позволяющая указателю двигаться. Здесь исправлено на `+7`. Переключение шкалы меняет и заголовок/описание индикатора.
Общая рамка `Background/WarningBorder` получает цвета Unity `WarningUI` в формате RGBA: при плотности ≤1e-5 — `C040407F`, ≤1e-6 — `DF2020BF`, ≤1e-7 — `FF0000FF`; при плотности ≥0,9 — `40A0C07F`, ≥0,97 — `20B0DFBF`, ≥1 — `00C0FFFF`. В нормальном диапазоне рамка прозрачна. Она обновляется вместе с индикаторами, не перехватывает нажатия и снимается при восстановлении плотности. Необратимая последовательность апокалипсиса Unity сюда не входит. Общая рамка `Background/WarningBorder` получает цвета Unity `WarningUI` в формате RGBA: при плотности ≤1e-5 — `C040407F`, ≤1e-6 — `DF2020BF`, ≤1e-7 — `FF0000FF`; при плотности ≥0,9 — `40A0C07F`, ≥0,97 — `20B0DFBF`, ≥1 — `00C0FFFF`. В нормальном диапазоне рамка прозрачна. Она обновляется вместе с индикаторами и не перехватывает нажатия.
Пороговые состояния управляют запуском апокалипсиса: при `density ≤ bigrip3` начинается Big Rip, при `density ≥ bigcrunch3` — Big Crunch. Последовательность берётся из `Global.ApocalypseQueue`, сортируется по `Num` и использует указанные `Stage`, `Screen` и `Tick`. Каждые 0,1 секунды активные объекты выбранной страницы теряют половину количества (для остатков до 5 — по одному). На время последовательности скорость фиксируется на 1, ручное и автоматическое создание блокируется, а экран переключается по очереди. Big Rip пропускает `S`, Big Crunch — `SMBH`; остаток энергии постепенно переводится в соответствующий объект. После 20 тиков с нулевой энергией апокалипсис получает фазу завершения, симуляция останавливает время. Фаза, тип события, предупреждение и позиция в очереди сохраняются в состоянии вселенной.
### Горючесть, взрывчатость и агрессия ### Горючесть, взрывчатость и агрессия
@@ -228,10 +234,61 @@ CSVLoader разбирает строки, колонка `param export` пре
Начальная энергия относится к новым вселенным; запас энергии существующей Начальная энергия относится к новым вселенным; запас энергии существующей
вселенной остаётся частью её сохранения. вселенной остаётся частью её сохранения.
G, C, PI, helium_multiplier, helium_bonus_limit, space_generation, ChatPerTick G, C, space_generation, ChatPerTick и параметры ForceBox загружаются, но пока не
и параметры ForceBox загружаются, но пока не используются в GI: соответствующие используются в GI: соответствующие
формулы и механики старой Unity-версии здесь отсутствуют. Производство объектов формулы и механики старой Unity-версии здесь отсутствуют. Производство объектов
в GI задаётся их собственной таблицей life_product и lifespan. в GI задаётся их собственной таблицей life_product и lifespan.
Для звёзд с прямой стоимостью H доля He в свободной смеси H+He заменяет часть
водородной стоимости. Применяются `helium_multiplier` и `helium_bonus_limit`, а
замещение ограничено 50%, как в Unity. Недостающий He не блокирует создание:
непокрытая часть оплачивается H. При создании таких звёзд срок жизни
умножается на `1 − 1.5 × доля замещения`; тот же коэффициент задаёт сроки их
партий. Автоматическое создание использует свободный He, а бюджет автоматического
H списывает только оставшуюся после замещения водородную часть.
`tests/test_extra_params.gd` проверяет загрузку точных чисел и изменение энергии, `tests/test_extra_params.gd` проверяет загрузку точных чисел и изменение энергии,
плотности, объёма пустот, порогов индикатора и бонуса горючести. плотности, объёма пустот, порогов индикатора и бонуса горючести.
## Достижения и уведомления
`OwniverseAchievementLoader` задаёт локализованные достижения. Строки с
`Ignore=1` исключены из проверок, списка и общего количества. Для включённых
строк поддерживаются `age::порог` и `total:ID:порог`. `total` — накопленные
поступления `stats.total_amount`, включая автоматическое производство и
эволюцию; расход и гибель объектов не уменьшают этот счётчик. Возраст берётся
из завершённого состояния симуляции. Пороги разбираются точной арифметикой.
`ui/achievements/achievement_controller.gd` проверяет условия при смене
ревизии состояния и после создания новой вселенной. Пустое OpenCondition
делает достижение видимым без уведомления; непустое открывает его с серой
иконкой `g`. Выполнение AchieveCondition выдаёт цветную иконку `c`. Если оба
условия выполнены одновременно, показывается только цветное уведомление.
Уже полученные достижения повторно не выдаются.
Состояния видимости и получения сохраняются отдельно в
`user://achievements.cfg` и сохраняются между вселенными. Очередь уведомлений
не сохраняется. Единая панель поверх индикаторов следует за выбранной
страницей: показ 3 секунды, затем затухание 0,3 секунды. Меню приостанавливает
таймер. Клик или касание закрывает текущее уведомление; очередь сохраняется. После показа индикаторы возвращаются. Тексты списка и уведомления
обновляются при смене языка; список прокручивается.
`tests/test_achievements.gd` проверяет все включённые пороги, фильтр Ignore,
открытие и получение, отсутствие повторных уведомлений, паузу в меню,
возврат индикаторов, сохранение прогресса и локализацию. Файл прогресса теста
создаётся отдельно в `/tmp`.
## Отчёт о вселенной
`ui/report/report.gd` форматирует готовое состояние при открытии
экрана отчёта и смене языка. Результат хранится в `Report/ReportBody`.
Периодического пересчёта отчёта и новых счётчиков симуляции нет.
Доли восьми типов звёзд учитывают звёзды в структурах. Строки количеств
используют накопленные `stats.total_amount` с существующими алиасами;
заголовки обозначают суммарное получение, а не исторические максимумы.
Эффекты интеграции показывают прирост готовых параметров Federation,
Enlightenment и Order относительно начального значения 1.
Нулевые строки и пустые разделы скрыты. Количества выводятся полностью,
с пробелами между группами по три цифры. Тексты и заголовок поддерживают RU/EN.
+64 -6
View File
@@ -7,6 +7,7 @@ const SPEED_TEXTURE_NAME_TEMPLATE = "res://ui/topmenu/time&speed/time%s.png"
const ItemDescription = preload("res://world/owniverse/item_description.gd") const ItemDescription = preload("res://world/owniverse/item_description.gd")
const WorldIndicators = preload("res://ui/indicators/world_indicators.gd") const WorldIndicators = preload("res://ui/indicators/world_indicators.gd")
var achievements: Node
var world_indicators: RefCounted var world_indicators: RefCounted
var indicator_elapsed: float = 0.0 var indicator_elapsed: float = 0.0
var indicator_revision: int = -1 var indicator_revision: int = -1
@@ -98,7 +99,14 @@ var g_active_items: Dictionary = {}
var pending_page: int = -1 var pending_page: int = -1
var active_actions_dirty: bool = true var active_actions_dirty: bool = true
var active_actions_elapsed: float = 0.0 var active_actions_elapsed: float = 0.0
var clock_display_elapsed: float = 0.0
var interface_sounds_enabled: bool = false var interface_sounds_enabled: bool = false
var apocalypse_elapsed: float = 0.0
var apocalypse_ui_phase: int = -1
var apocalypse_screen: int = -1
@onready var time_minus_button: TextureButton = $World/TopMenu/TimeMinus
@onready var time_plus_button: TextureButton = $World/TopMenu/TimePlus
@onready var apocalypse_music: OwniverseMusicPlayer = $"../MusicPlayer"
var owniverse:Owniverse = Owniverse.new() var owniverse:Owniverse = Owniverse.new()
var owniverse_items: Dictionary[String, OwniverseItem] = {} var owniverse_items: Dictionary[String, OwniverseItem] = {}
@@ -273,6 +281,7 @@ func _swipe_to_nearest_page() -> void:
func _input(event: InputEvent) -> void: func _input(event: InputEvent) -> void:
if E.eq(owniverse.state.get("apocalypse", {}).get("phase", 0), 1): return
if event is InputEventScreenDrag: if event is InputEventScreenDrag:
#effects_player.play_effect("swipe") #effects_player.play_effect("swipe")
stage_page_scroller.scroll_horizontal = clamp( stage_page_scroller.scroll_horizontal = clamp(
@@ -298,7 +307,7 @@ func _set_active_page_actions() -> void:
#var amount = owniverse.get_available_to_produce(item_id) #var amount = owniverse.get_available_to_produce(item_id)
#var amount = owniverse.get_available_to_produce(entity) #var amount = owniverse.get_available_to_produce(entity)
var amount = owniverse.production.calculate_max_producible(entity) var amount = (0 if E.gt(owniverse.state.get("apocalypse", {}).get("phase", 0), 0) else owniverse.production.calculate_max_producible(entity))
#print(item_id, " : ", amount) #print(item_id, " : ", amount)
if item_id == "SMBH": if item_id == "SMBH":
var preview: Callable = func(requested): var preview: Callable = func(requested):
@@ -315,7 +324,11 @@ func _page_action_pressed(page_id: int, action_amount) -> void:
var item_id: String = g_active_items[page_id].item_id var item_id: String = g_active_items[page_id].item_id
if E.gt(action_amount, 0): if E.gt(action_amount, 0):
owniverse.request_production(item_id, action_amount) if owniverse.request_production(item_id, action_amount):
page_actions[page_id].mark_pending(action_amount)
func _on_owniverse_production_applied(_item_id: String) -> void:
active_actions_dirty = true
#endregion #endregion
@@ -327,6 +340,10 @@ func _ready() -> void:
create_new_universe() create_new_universe()
load_data() load_data()
_refresh_universe_ui() _refresh_universe_ui()
achievements = preload("res://ui/achievements/achievement_controller.gd").new()
add_child(achievements)
achievements.initialize(self)
achievements.update(0.0)
g_counters.swipe.sec = 0.2 g_counters.swipe.sec = 0.2
@@ -345,6 +362,7 @@ func _enable_interface_sounds() -> void:
func create_new_universe() -> void: func create_new_universe() -> void:
var previous: Owniverse = owniverse var previous: Owniverse = owniverse
owniverse = Owniverse.new() owniverse = Owniverse.new()
owniverse.production_applied.connect(_on_owniverse_production_applied)
var inited_result: Dictionary = init_enitities() var inited_result: Dictionary = init_enitities()
locked_items = inited_result.locked_items locked_items = inited_result.locked_items
@@ -384,7 +402,7 @@ func create_new_universe() -> void:
func _refresh_universe_ui() -> void: func _refresh_universe_ui() -> void:
owniverse.presentation.refresh(owniverse.state, owniverse.catalog) owniverse.presentation.refresh(owniverse.state, owniverse.catalog)
_change_speed(0) _change_speed(0)
set_datetime(owniverse.state.time.year) set_datetime(owniverse.display_clock().year)
set_owniverse_item_value() set_owniverse_item_value()
_unlock_items() _unlock_items()
if world_indicators != null: world_indicators.update(owniverse) if world_indicators != null: world_indicators.update(owniverse)
@@ -392,6 +410,12 @@ func _refresh_universe_ui() -> void:
func _process(delta: float) -> void: func _process(delta: float) -> void:
owniverse.evolute_frame(delta, get_tree()) owniverse.evolute_frame(delta, get_tree())
_process_apocalypse(delta)
if achievements != null: achievements.update(delta)
clock_display_elapsed += delta
if clock_display_elapsed >= 0.1:
clock_display_elapsed = fmod(clock_display_elapsed, 0.1)
set_datetime(owniverse.display_clock().year)
indicator_elapsed += delta indicator_elapsed += delta
if indicator_elapsed >= 0.1 and indicator_revision != owniverse.presentation.revision: if indicator_elapsed >= 0.1 and indicator_revision != owniverse.presentation.revision:
indicator_elapsed = 0.0 indicator_elapsed = 0.0
@@ -399,9 +423,9 @@ func _process(delta: float) -> void:
indicator_revision = owniverse.presentation.revision indicator_revision = owniverse.presentation.revision
_update_descriptions(delta) _update_descriptions(delta)
if not owniverse.presentation.pending_amounts.is_empty(): set_owniverse_item_value() if not owniverse.presentation.pending_amounts.is_empty(): set_owniverse_item_value()
if displayed_year == null or E.ne(displayed_year, owniverse.state.time.year): var committed_year: Variant = owniverse.state.time.year
set_datetime(owniverse.state.time.year) if displayed_year == null or E.ne(displayed_year, committed_year):
displayed_year = owniverse.state.time.year displayed_year = committed_year
#owniverse.cycle(delta) #owniverse.cycle(delta)
@@ -421,6 +445,7 @@ func _process(delta: float) -> void:
func _change_speed(delta: int) -> void: func _change_speed(delta: int) -> void:
if E.eq(owniverse.state.get("apocalypse", {}).get("phase", 0), 1) and delta != 0: return
var previous_speed: int = owniverse.speed var previous_speed: int = owniverse.speed
owniverse.speed = owniverse.speed + delta owniverse.speed = owniverse.speed + delta
@@ -448,6 +473,39 @@ func _change_speed(delta: int) -> void:
g_time_plus.texture_normal = load(SPEED_TEXTURE_NAME_TEMPLATE % str(speed + 1)) g_time_plus.texture_normal = load(SPEED_TEXTURE_NAME_TEMPLATE % str(speed + 1))
func _process_apocalypse(delta: float) -> void:
var apocalypse: Dictionary = owniverse.state.get("apocalypse", {})
var phase: int = int(apocalypse.get("phase", 0))
if phase != apocalypse_ui_phase:
apocalypse_ui_phase = phase
if phase == 1:
apocalypse_music.play_apocalypse()
owniverse.speed = 1
_change_speed(0)
elif phase == 2:
owniverse.speed = 1
_change_speed(0)
time_minus_button.disabled = phase == 1
time_plus_button.disabled = phase == 1
active_actions_dirty = true
if phase == 1:
_set_active_page_actions()
if phase != 1:
apocalypse_elapsed = 0.0
return
apocalypse_elapsed += delta
if owniverse.frame_busy or owniverse.production_queue_busy:
return
while apocalypse_elapsed >= OwniverseApocalypseController.TICK_SECONDS and E.eq(owniverse.state.apocalypse.phase, 1):
apocalypse_elapsed -= OwniverseApocalypseController.TICK_SECONDS
var result: Dictionary = owniverse.apocalypse.process_tick(owniverse.state)
if int(result.screen) >= 0 and int(result.screen) != apocalypse_screen:
apocalypse_screen = int(result.screen)
stage_page_scroller.scroll_horizontal = apocalypse_screen * PAGE_WIDTH
if bool(result.finished):
_change_speed(0)
func _on_topmenu_button_pressed(screen_name: String) -> void: func _on_topmenu_button_pressed(screen_name: String) -> void:
var current_screen: String = "" var current_screen: String = ""
for screen_id in screens: for screen_id in screens:
+58 -19
View File
@@ -122,7 +122,41 @@ godot --headless --path Owniverse.GI --script res://tests/test_time.gd
## Согласованные правила эволюции ## Согласованные правила эволюции
Сохраняются постепенные интервальные окна GI: начало преобразования — через `lifespan`, длительность окна — `lifespan / 40` при `lifespan > 10`, иначе `lifespan`. Сохраняются постепенные интервальные окна GI: срок жизни делится на 40 возрастных групп; начало преобразования — через `lifespan`, длительность окна — `lifespan / 40` при `lifespan > 10`, иначе `lifespan`.
На скоростях 4–6 узкие звёздные партии могут объединяться по границе полного
цикла смерти шириной 200/2000/20000 игровых лет. Прежнее окно шире цикла
сохраняется: долгоживущие звёзды не дробятся. Из существующих партий сливаются
только две или более одного типа, чей оставшийся переход полностью попадает
в общий цикл; широкие и пересекающие границу партии сохраняются.
`test_batch_boundaries` проверяет объединение, точные большие количества и
остатки, однократное изъятие, сериализацию, сохранение широких окон,
распределённые рождения и интеграл производства.
`benchmark_large_star_cohorts.gd` создаёт отдельные большие диагностические
сохранения со старой и новой звёздной сеткой при одинаковых количествах и
входных интервалах рождения. Переключатель `batches.cycle_stars` используется
только для сравнительного замера, в игре он включён. Парные прогоны меняют
порядок моделей и сбрасывают исходное состояние перед каждым запуском:
```sh
godot --headless --path Owniverse.GI --script res://tests/benchmark_large_star_cohorts.gd -- cycles=50 repeats=2
```
Сохранения и полные профили записываются в `tests/artifacts/large_star_cohorts_*`;
сохранение игрока не затрагивается. `generate_only` проверяет запись/чтение и
исходное равенство физических количеств без запуска замеров.
`benchmark_many_objects.gd` создаёт новую диагностическую вселенную со всеми
типами объектов и цивилизаций и 4800 синтетическими звёздными партиями,
попадающими группами в общие циклы. Это отдельный стресс-сценарий большого
числа партий; старый и новый расчёты используют одинаковый исходный состав.
```sh
godot --headless --path Owniverse.GI --script res://tests/benchmark_many_objects.gd -- cycles=40 repeats=2
```
Файлы сценария: `tests/artifacts/many_objects_fragmented_stars_*`.
Вероятностные исходы используют накопление дробного остатка GI, без случайного округления. Этот же подход выбран для будущего создания планет. Шансы планет записаны долями (`0.01` = 1%), а альтернативные исходы звёзд — процентами (`%0.1` = 0.1%). Парсер планет сохраняет дроби и поддерживает один элемент без `&`. Вероятностные исходы используют накопление дробного остатка GI, без случайного округления. Этот же подход выбран для будущего создания планет. Шансы планет записаны долями (`0.01` = 1%), а альтернативные исходы звёзд — процентами (`%0.1` = 0.1%). Парсер планет сохраняет дроби и поддерживает один элемент без `&`.
@@ -204,15 +238,33 @@ godot --path Owniverse.GI --script res://tests/benchmark_civilizations.gd -- ui
## Расчёт между кадрами ## Расчёт между кадрами
`test_flow_commit.gd` проверяет консолидированное применение переходов и колонизации: точные квоты получателей, сохранение количества у каждого владельца, профили этик, статистику создания и отсутствие привязок у Ct. Проверки используют несколько владельцев и численность 103, 10^40 + 103 и 10^400 + 103.
Дополнительно проверяется точное совпадение обновляемых общих количеств с полным пересчётом после переходов, колонизации и каскадной деградации при нехватке ёмкости. Входные снимки остаются неизменными.
`test_production_transaction.gd` проверяет, что успешное создание обновляет общий снимок, привязка сохраняет общий ресурс, а отклонённая операция сохраняет и состояние, и снимок.
```sh
godot --headless --path Owniverse.GI --script res://tests/test_flow_commit.gd
```
`godot --headless --path Owniverse.GI --script res://tests/test_frame_evolution.gd` `godot --headless --path Owniverse.GI --script res://tests/test_frame_evolution.gd`
Stage вызывает `evolute_frame`: транзакция получает бюджет 40 мс и уступает Stage вызывает `evolute_frame`: транзакция получает бюджет 40 мс и уступает
следующему кадру внутри создания партий и обработки стадий. Расчёт остаётся следующему кадру внутри создания партий и обработки стадий. Расчёт остаётся
на главном потоке, состояние публикуется только после полного завершения. на главном потоке, состояние публикуется только после полного завершения.
Длительность одной транзакции ограничена обычным шагом 1/60 секунды выбранной Запрошенные игровые часы и счётчик UI обновляются независимо от тяжёлого расчёта;
скорости. Запрошенное время хранится отдельно, поэтому отставание не превращается Stage показывает запрошенное время раз в 0,1 секунды (10 Гц), а опубликованное
в один всё более тяжёлый шаг. При предельной нагрузке время симуляции может состояние мира остаётся на последнем завершённом цикле. Расчёт запускается после
отставать от запрошенного; это не ограничение FPS интерфейса и не удаление событий. накопления выбранного интервала и обрабатывает весь ожидающий интервал. Время,
поступившее во время расчёта, обрабатывается следующим циклом. Контроллер нагрузки
меняет целевой шаг между 0,2, 0,25, 0,333 и 0,5 секунды (5/4/3/2 цикла в секунду):
вниз при EMA расчёта выше 90% текущего шага, вверх после 30 циклов ниже 65%.
Временное разрешение укрупнения партий масштабируется с шагом, исходная точность
сохраняется на 5 Гц. Контроллер не пропускает накопленное игровое время, но 2/с —
нижняя целевая частота, не гарантия при перегрузке.
`benchmark_dynamic_cadence.gd` сравнивает пропускную способность полного цикла на двух диагностических сохранениях. `benchmark_dynamic_cadence_ui.gd` сравнивает 10 секунд работы полной сцены на сохранении со всеми объектами; результаты сохранены в `artifacts/dynamic_cadence_results.json` и `artifacts/dynamic_cadence_ui_results.json`.
Производство во время транзакции выполняется после её завершения. Запрошенное Производство во время транзакции выполняется после её завершения. Запрошенное
время, активный интервал, скорость и очередь производства входят в сохранение; время, активный интервал, скорость и очередь производства входят в сохранение;
@@ -259,20 +311,7 @@ Stage вызывает `evolute_frame`: транзакция получает б
1.057. Замер включает сокращение накопленного отставания сохранения; 1.057. Замер включает сокращение накопленного отставания сохранения;
это результат конкретного сценария на Mac, а не гарантия для Android. это результат конкретного сценария на Mac, а не гарантия для Android.
Адаптивный шаг на скоростях 4–6 выбирает 0.1, 0.2 или 0.5 реальной секунды. Целевая частота запуска — пять циклов в секунду. Размер шага равен всему накопленному интервалу. Checkpoint сохраняет границу активного расчёта и ожидающее реальное время. Тест проверяет все шесть скоростей, смену скорости, задержку запуска и накопление времени во время расчёта.
Долг больше двух шагов повышает ступень, меньше половины шага понижает её.
Шаг ограничен доступным запрошенным временем. Скорости 1–3 сохраняют 1/60;
смена скорости сбрасывает ступень. Возобновляемая транзакция сохраняет точную
границу из checkpoint, адаптация применяется к следующей транзакции.
Бюджет кадра остаётся 40 мс; ширина временных партий не меняется.
`test_frame_evolution` проверяет ступени, гистерезис, сохранение долга,
переключение скорости, возобновление и совпадение с прямым расчётом того же шага.
На свежей диагностической копии сохранения с долгом 407.79 млн лет:
600 кадров реального UI на M4 Pro дали 18.05 FPS (p95 92.91 мс,
максимум 122.45 мс), поступило 3.335 млн лет, обработано 6.480 млн:
пропускная способность 1.943 вместо 0.787 при фиксированном шаге 0.1.
Разбиение времени влияет на дискретные производство и глобальные параметры;
адаптивные шаги не обещают совпадения результата с последовательностью 0.1.
Дипломатический drift использует шесть взвешенных вкладов вместо матрицы Дипломатический drift использует шесть взвешенных вкладов вместо матрицы
пар этик: вес = сумма вкладов + 195 × свой вклад − противоположный вклад. пар этик: вес = сумма вкладов + 195 × свой вклад − противоположный вклад.
+110
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://dkbyhj8ib0eyf
+12
View File
@@ -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")
@@ -0,0 +1,49 @@
{
"date": "2026-10-08",
"machine": "Apple M4 Pro",
"godot": "4.7.2.stable.official.ed1daf0bf",
"renderer": "headless",
"delta_seconds": 0.2,
"profile_enabled": true,
"ui": false,
"adaptation": {
"controller": "runtime-only exponential moving average of full-cycle CPU time; 0.8 previous + 0.2 latest",
"quality_thresholds_ms": {
"quality_1": 200,
"quality_2": 220,
"quality_3": 300,
"quality_2_to_3_when_over_ms": 200,
"recovery_below_ms": 140,
"recovery_cycles": 30
},
"quality_effects": {
"quality_1": "one-transition civilization approximation, at most 3 stellar/life generations per cycle, temporal grouping x2",
"quality_2": "same civilization approximation, at most 1 generation per cycle, temporal grouping x4",
"quality_3": "same civilization approximation, at most 1 generation per cycle, temporal grouping x8, alternate halves of batch types each cycle"
}
},
"inputs": {
"all_objects_speed6.adventure.json": "44bee3b3753fa51615f766d8f800a10cc2b70c8e7fb4ea12b6e18da7f07a7d67",
"many_objects_fragmented_stars_cycle.adventure.json": "1e95ef398923f26b912a305c84f9b3eb502d24cb14e8ae687a4becccf047324a"
},
"runs": [
{"speed": 1, "save": "all_objects_speed6.adventure.json", "cycles": 60, "mean_ms": 165.03665, "min_ms": 151.514, "max_ms": 170.171, "cycles_per_second": 6.05926, "game_years": 12, "validation": "OK"},
{"speed": 1, "save": "many_objects_fragmented_stars_cycle.adventure.json", "cycles": 60, "mean_ms": 128.463817, "min_ms": 124.475, "max_ms": 144.05, "cycles_per_second": 7.784293, "game_years": 12, "validation": "OK"},
{"speed": 2, "save": "all_objects_speed6.adventure.json", "cycles": 60, "mean_ms": 181.431217, "min_ms": 171.92, "max_ms": 196.005, "cycles_per_second": 5.511731, "game_years": 120, "validation": "OK"},
{"speed": 2, "save": "many_objects_fragmented_stars_cycle.adventure.json", "cycles": 60, "mean_ms": 131.78055, "min_ms": 126.963, "max_ms": 148.01, "cycles_per_second": 7.588373, "game_years": 120, "validation": "OK"},
{"speed": 3, "save": "all_objects_speed6.adventure.json", "cycles": 60, "mean_ms": 175.235683, "min_ms": 166.031, "max_ms": 203.456, "cycles_per_second": 5.7066, "game_years": 1200, "validation": "OK"},
{"speed": 3, "save": "many_objects_fragmented_stars_cycle.adventure.json", "cycles": 60, "mean_ms": 145.013367, "min_ms": 135.062, "max_ms": 162.768, "cycles_per_second": 6.895916, "game_years": 1200, "validation": "OK"},
{"speed": 4, "save": "all_objects_speed6.adventure.json", "cycles": 60, "mean_ms": 159.705883, "min_ms": 150.404, "max_ms": 221.101, "cycles_per_second": 6.26151, "game_years": 12000, "validation": "OK"},
{"speed": 4, "save": "many_objects_fragmented_stars_cycle.adventure.json", "cycles": 60, "mean_ms": 177.064483, "min_ms": 170.624, "max_ms": 189.729, "cycles_per_second": 5.64766, "game_years": 12000, "validation": "OK"},
{"speed": 5, "save": "all_objects_speed6.adventure.json", "cycles": 60, "mean_ms": 174.857517, "min_ms": 158.809, "max_ms": 270.078, "cycles_per_second": 5.718942, "game_years": 120000, "validation": "OK"},
{"speed": 5, "save": "many_objects_fragmented_stars_cycle.adventure.json", "cycles": 60, "mean_ms": 185.601233, "min_ms": 131.409, "max_ms": 273.8, "cycles_per_second": 5.387895, "game_years": 120000, "validation": "OK"},
{"speed": 6, "save": "all_objects_speed6.adventure.json", "cycles": 60, "mean_ms": 190.570967, "min_ms": 159.38, "max_ms": 345.149, "cycles_per_second": 5.247389, "game_years": 1200000, "validation": "OK"},
{"speed": 6, "save": "many_objects_fragmented_stars_cycle.adventure.json", "cycles": 60, "mean_ms": 184.426, "min_ms": 149.645, "max_ms": 366.767, "cycles_per_second": 5.422229, "game_years": 1200000, "validation": "OK"}
],
"notes": [
"Each run begins in exact mode and adapts during its 60-cycle series.",
"The mean meets 5 complete cycles/s at all six speeds in these synthetic diagnostic saves on this computer; maxima show occasional cycles still exceed 200 ms at speeds 3-6.",
"No Android device or UI performance was measured.",
"Diagnostic save inputs were not modified."
]
}
Binary file not shown.
File diff suppressed because it is too large Load Diff
@@ -61,3 +61,458 @@ 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 за прогон не найдено.
## Непрерывная модель цивилизаций — 2026-10-06
Сравнены исходная симуляция из коммита `6ee66a6` и новая модель потоков. Оба запуска используют один диагностический JSON, скорость 6, 100 последовательных полных циклов с запросом 0,1 секунды каждый и включённый CPU-профиль. Подготовка каталога и чтение JSON не включены в длительность цикла. Запуски выполнены последовательно, без параллельных тестов. Сохранение игрока не читалось и не изменялось.
| Полный цикл и пропускная способность | До | После |
|---|---:|---:|
| Среднее время полного цикла | 811,292 мс | 401,796 мс |
| Минимум / максимум | 488,914 / 963,141 мс | 323,427 / 435,607 мс |
| Завершённых полных циклов в секунду | 1,233 | 2,489 |
| Обработанных секунд симуляции на секунду CPU | 0,123 | 0,249 |
| Применено игровых лет за 100 циклов | 1 000 000 | 1 000 000 |
Полный цикл ускорился в **2,02 раза**, среднее время сократилось на **50,5%**. Цель **100 мс / 10 полных циклов в секунду не достигнута**: для неё требуется ещё примерно 4,02-кратное ускорение текущей версии. При непрерывном запросе времени на шестой скорости и шаге 0,1 секунды этот стресс-сценарий продолжит накапливать долг. Это измерение полных завершённых шагов симуляции, без показателя FPS.
Текущие внешние интервалы CPU-профиля, по убыванию среднего времени на цикл:
| Расчёт | Среднее за цикл |
|---|---:|
| Поколения звёзд и биологической жизни | 151,6 мс |
| Подготовка непрерывных потоков цивилизаций | 98,5 мс |
| Применение потоков к количествам и владельцам | 53,2 мс |
| Производство за период | 28,7 мс |
| Тренды | 18,1 мс |
| Дрейф, проверка вместимости и поклонение после потоков | 8,8 мс |
| Инициализация жизни | 6,1 мс |
| Ограничение потоков ресурсными бюджетами | 5,8 мс |
| Применение колонизации | 5,5 мс |
Внутри подготовки потоков разреженный оператор занимает 24,5 мс на цикл; этот вложенный интервал нельзя прибавлять к 98,5 мс. Внешние интервалы не покрывают всё время: остальная работа включает копирование транзакции, построение физических снимков и обновление представления.
Модель C0.2–C4.4 теперь использует точные целые количества и шесть долей float без возрастных партий. Оператор учитывает несколько последовательных переходов за интервал. Конечные количества и ресурсные потолки сохраняются точно; пропорции потоков и владельцев приближены float и квантованием долей до 15 десятичных знаков. Изменяется распределение событий во времени, поэтому тесты проверяют законы новой модели, сохранение количества, владение, ресурсные ограничения и воспроизводимое продолжение сохранения.
Регрессия: 27 из 28 наборов прошли. Новые переходы — 1895 проверок; арифметика распределения и кеши — 1889; кадровое выполнение — 24. В `test_descriptions.gd` сохраняются две ранее обнаруженные ошибки обновления выбранного описания и языка; этот UI-код не изменён в данном рефакторинге.
Повторение из папки `Owniverse.GI`:
```sh
godot --headless --path . --script res://tests/benchmark_civilization_flows.gd -- tests/artifacts/all_objects_speed6.adventure.json 0.1 profile cycles=100
```
## Фиксированные пять циклов в секунду — 2026-10-07
Текущая версия после отката агрегации и возврата 40 возрастных групп.
Планировщик использует шаг 0,2 реальной секунды на всех скоростях. Замер:
диагностическое сохранение со всеми объектами и цивилизациями, скорость 6,
100 полных синхронных циклов с CPU-профилем, без отрисовки интерфейса.
Проверка сохранения: OK. Исходное сохранение не изменялось.
| Метрика | Результат |
|---|---:|
| Среднее время полного цикла | 439,228 мс |
| Минимум / максимум | 340,370 / 483,152 мс |
| Завершённых полных циклов в секунду | 2,277 |
| Требуется циклов в секунду | 5 |
| Обработано игровых лет | 2 000 000 |
| Запрошенное время сценария | 20 с |
| Время расчёта 100 циклов | 43,923 с |
| Пропускная способность | 0,455 с симуляции / с расчёта |
Цель 200 мс на полный цикл не достигнута: по среднему времени требуется
ускорение примерно в 2,20 раза. Увеличение шага с 0,1 до 0,2 секунды улучшило
отношение обработанного времени к затратам, но при непрерывном поступлении
времени этот стресс-сценарий продолжит накапливать долг. Это измерение
пропускной способности симуляции, без оценки FPS. Один прогон не задаёт
гарантированную производительность для других сохранений или устройств.
Внешние интервалы CPU-профиля, по убыванию среднего времени на цикл:
| Расчёт | Среднее на цикл |
|---|---:|
| Поколения звёзд и биологической жизни | 168,294 мс |
| Подготовка непрерывных потоков цивилизаций | 111,835 мс |
| Применение потоков к количествам и владельцам | 59,516 мс |
| Производство за период | 29,248 мс |
| Тренды | 17,787 мс |
| Завершение цивилизационных потоков | 8,864 мс |
| Ограничение потоков вместимостью | 6,439 мс |
| Инициализация жизни | 6,172 мс |
| Колонизация | 6,111 мс |
Вложенные интервалы нельзя прибавлять к внешним: построение физических
снимков — 27,845 мс/цикл; изъятие партий — 42,015 мс/цикл; решение оператора
цивилизаций — 31,835 мс/цикл. Проверки кадрового планировщика:
35 проверок, 0 ошибок, включая все шесть скоростей, сохранение долга,
возобновление расчёта и пять завершённых циклов на одну секунду запроса.
```sh
godot --headless --path . --script res://tests/benchmark_civilization_flows.gd -- tests/artifacts/all_objects_speed6.adventure.json 0.2 profile cycles=100
```
## Новые звёздные партии с целым переходом за цикл — 2026-10-07
На скоростях 4–6 новые звёзды одного типа объединяются по общей границе
завершения жизни и переходят целиком. Временная сетка равна одному полному
циклу (200/2000/20000 игровых лет). Старые постепенные партии из сохранения
не преобразуются; SN/GRB сохраняют прежнюю обработку.
Повторён тот же замер: диагностическое сохранение, скорость 6, шаг 0,2 с,
100 полных циклов с CPU-профилем. Проверка сохранения OK.
| Метрика | До | После |
|---|---:|---:|
| Среднее время полного цикла | 439,228 мс | 445,690 мс |
| Минимум / максимум | 340,370 / 483,152 мс | 345,276 / 486,647 мс |
| Полных циклов в секунду | 2,277 | 2,244 |
| Пропускная способность | 0,455 | 0,449 |
| Обработано игровых лет | 2 000 000 | 2 000 000 |
| Поколения, среднее на цикл | 168,294 мс | 171,039 мс |
| Обработка партий новорождённых, среднее на цикл | 7,768 мс | 6,199 мс |
| Вызовы обработки партий новорождённых | 16 147 | 13 885 |
Общего ускорения на этой выборке нет: среднее выросло примерно на 1,5%.
Обработка новорождённых занимает на 20% меньше времени, но это небольшая
доля полного цикла. Замер включает существующие постепенные партии;
он не доказывает ускорения вселенной, целиком созданной по новой схеме.
Цель пяти полных циклов в секунду остаётся недостигнутой.
Проверки: границы партий — 109, звёздные правила — 333, короткие события —
841, укрупнённые поколения — 28, кадровое выполнение — 35; все без ошибок.
Тестируются точные большие количества, целый однократный переход,
распределённые рождения, сохранения и интеграл производства.
## Первоначальный замер больших сохранений до исправления сетки — 2026-10-07
Созданы `large_star_cohorts_legacy.adventure.json` и
`large_star_cohorts_cycle.adventure.json`: по 6×10^60 звёзд каждого из 15
типов (главная последовательность и красные гиганты), остальные объекты,
жизнь и цивилизации взяты из диагностического сценария. Возраст — 10^12 лет.
Для каждого типа заново сформирована одинаковая история рождения длиной
`min(эффективный срок жизни, 2 млн лет)` перед началом завершения жизни.
Исходные физические количества и этические профили совпадают строго.
Сохранения валидируются и проходят точное сравнение состояния после записи
и чтения JSON с полной точностью float. Сохранение игрока не изменялось.
Старая схема измеряется переключателем `batches.cycle_stars = false`, новая
включена по умолчанию. В обоих вариантах одинаковый остальной код симуляции,
скорость 6, шаг 0,2 с и CPU-профиль. Два прогона по 50 полных циклов на модель,
порядок запусков: старая → новая → новая → старая. Перед каждым прогоном
восстанавливается исходное состояние; каждый обрабатывает 1 млн игровых лет.
| Метрика | Старая схема | Новая схема |
|---|---:|---:|
| Среднее первого прогона | 417,265 мс | 423,419 мс |
| Среднее второго прогона | 431,081 мс | 427,011 мс |
| Среднее всех 100 циклов на модель | 424,173 мс | 425,215 мс |
| Полных циклов в секунду | 2,358 | 2,352 |
| Пропускная способность | 0,472 | 0,470 |
| Звёздных партий в исходном сохранении | 118 | 1 300 |
| Пиковое число звёздных партий | 132 | 1 300 |
| Поколения, среднее на цикл | 157,711 мс | 147,906 мс |
| Производство, среднее на цикл | 28,692 мс | 34,704 мс |
| Применение потоков цивилизаций, среднее на цикл | 58,642 мс | 62,160 мс |
| Изъятие партий, вложенное в поколения | 37,015 мс | 41,379 мс |
| Партии новорождённых, вложенное в поколения | 7,648 мс | 4,248 мс |
**Устойчивого выигрыша полного цикла не обнаружено.** Разница средних —
около +0,25% у новой схемы; в первом парном сравнении она медленнее, во втором
быстрее. Поколения занимают примерно на 6,2% меньше времени, но производство
стало дороже примерно на 21%. Пять полных циклов в секунду не достигнуты.
Причина роста числа партий: для долгоживущих звёзд сетка одного цикла
значительно мельче `lifespan / 40`. Например, у RD прежняя ширина — 1 млрд
лет, новая на скорости 6 — 20 тыс. лет. Поэтому изменение не всегда
укрупняет партии: на таком наборе исходных рождений их стало в 11 раз больше.
Итоговое временное распределение переходов у моделей различается; совпадение
их конечных состояний не является условием этого замера.
Для дальнейшего упрощения следует сохранять широкие группы долгоживущих
звёзд и применять целый переход только к партиям, уже укладывающимся в один
цикл. Это предложение в данном замере не реализовано.
```sh
godot --headless --path . --script res://tests/benchmark_large_star_cohorts.gd -- cycles=50 repeats=2
```
Полные результаты первоначального замера: `large_star_cohorts_before_dynamic_fix_results.json`.
Набор с 1300 партиями сохранён в `large_star_cohorts_before_dynamic_fix_cycle.adventure.json`.
Последующие файлы без этого суффикса относятся к исправленной динамической схеме.
## Исправленное динамическое укрупнение без дробления — 2026-10-07
Ошибка первоначальной реализации устранена: циклическая сетка больше не
заменяет широкие возрастные окна долгоживущих звёзд. При создании новая сетка
используется только если прежнее окно не шире цикла. Иначе используется
прежнее широкое окно. Из существующих партий объединяются две или более
одного типа, чей оставшийся интервал завершения целиком лежит в общем цикле.
Партии, пересекающие границу циклов или занимающие несколько циклов, не делятся.
Повторён парный замер на отдельных больших сохранениях, с теми же исходными
количествами и входными интервалами рождения: скорость 6, шаг 0,2 с,
два прогона по 50 циклов на модель в порядке старая → новая → новая → старая.
Каждый прогон обрабатывает 1 млн игровых лет; состояния валидируются до и
после, записанные файлы проходят точное сравнение после чтения.
| Метрика | Старая схема | Исправленная динамическая |
|---|---:|---:|
| Исходных звёздных партий | 118 | 113 |
| Пиковое число партий | 132 | 127 |
| Число партий после 50 циклов | 130 | 125 |
| Среднее первого прогона | 420,388 мс | 424,307 мс |
| Среднее второго прогона | 431,605 мс | 428,498 мс |
| Среднее всех 100 циклов на модель | 425,996 мс | 426,402 мс |
| Полных циклов в секунду | 2,347 | 2,345 |
| Поколения, среднее на цикл | 158,622 мс | 157,606 мс |
| Партии новорождённых, вложенный интервал | 7,679 мс | 6,693 мс |
На этом наборе партиям стало меньше примерно на 4,2%, вместо прежнего
ошибочного роста до 1300. Проверено, что долгая группа RD остаётся одной
широкой партией и её окно не меняется при добавлении новых звёзд.
Новый тест запрещает рост числа исходных партий в диагностическом сценарии.
Устойчивого ускорения полного цикла по этим прогонам нет: разница средних
около +0,10%; знак разницы в отдельных парных сравнениях меняется.
Обработка партий новорождённых занимает примерно на 13% меньше времени,
но её доля недостаточна для заметного выигрыша всего расчёта.
Цель пяти циклов в секунду по-прежнему не достигнута.
Проверки: границы партий — 117, звёздные правила — 333, короткие события —
841, кадровое выполнение — 35; все без ошибок. Дополнительные проверки
охватывают сохранение широких окон, объединение старых партий с точными
количествами и остатками, соседние циклы и повторное укрупнение.
Актуальные сохранения: `large_star_cohorts_legacy.adventure.json` и
`large_star_cohorts_cycle.adventure.json`; результаты: `large_star_cohorts_results.json`.
## Новая вселенная с множеством объектов и мелких звёздных партий — 2026-10-07
Создано отдельное состояние с нуля по текущим таблицам: 150 типов объектов,
все 44 стадии этических цивилизаций (включая Ct), 15 типов живых звёзд.
Запасы большинства объектов — 10^60 каждого типа; планет и цивилизаций —
10^28. Жизнь размещена у четырёх типов карликов, четверть звёзд каждого типа
связана со структурами. Этические профили различаются между стадиями.
Высокий запас пространства сохраняет жизнь при бластах. Интерфейс не отрисовывался.
Для проверки укрупнения заданы валидные синтетические исторические партии:
40 последовательных циклов завершения × 8 узких партий × 15 типов звёзд.
Все восемь партий типа целиком завершаются в общем цикле; это целевой
стресс-сценарий большого числа партий, а не утверждение, что обычное создание
с неизменным временем жизни обязательно порождает такую фрагментацию.
Исходный состав и интервалы одинаковы для обеих моделей. Долгоживущие звёзды
не дробились алгоритмом: ему переданы уже существующие узкие партии.
До укрупнения — 4800 звёздных партий. Старая схема сохраняет 4560,
исправленная динамическая — 600. Созданы два отдельных диагностических файла,
проверены запись/чтение без изменения состояния и валидация до и после каждого
прогона. Сохранение игрока не затрагивалось.
Два прогона по 40 полных циклов на модель; порядок старая → новая → новая →
старая. Скорость 6, фиксированный шаг 0,2 с, CPU-профиль включён.
Каждый прогон восстанавливает исходное состояние и обрабатывает 800 тыс. лет.
| Метрика | Старая схема | Динамическое укрупнение |
|---|---:|---:|
| Исходных звёздных партий после укрупнения | 4 560 | 600 |
| Пиковое число партий | 4 560 | 607 |
| Среднее первого прогона | 612,606 мс | 530,285 мс |
| Среднее второго прогона | 624,625 мс | 532,578 мс |
| Среднее всех 80 циклов на модель | 618,616 мс | 531,431 мс |
| Полных циклов в секунду | 1,617 | 1,882 |
| Обработанных секунд симуляции / секунду расчёта | 0,323 | 0,376 |
| Поколения, среднее на цикл | 211,916 мс | 138,339 мс |
| Производство, среднее на цикл | 51,201 мс | 38,038 мс |
| Подготовка потоков цивилизаций | 113,371 мс | 113,270 мс |
| Применение потоков цивилизаций | 114,343 мс | 113,820 мс |
| Колонизация | 55,284 мс | 55,291 мс |
| Обработка партий новорождённых, вложенная в поколения | 29,464 мс | 2,967 мс |
**Выигрыш полного цикла подтверждён обоими парными сравнениями:** время
сократилось на 14,1%, ускорение около 1,16 раза. Поколения занимают на 34,7%
меньше времени. Механизм даёт эффект при множестве подходящих для объединения
партий; в предыдущем сценарии с небольшим числом широких партий эффект был
незаметен. Изменение моментов переходов допускается моделью: конечные состояния
двух схем не обязаны совпадать, но обе полностью обработали запрошенный интервал.
Пять циклов в секунду не достигнуты: для данного сценария требуется ещё
примерно 2,66-кратное ускорение. Главные оставшиеся затраты — поколения и
подготовка/применение цивилизационных потоков; колонизация также заметна.
Сохранения: `many_objects_fragmented_stars_legacy.adventure.json` и
`many_objects_fragmented_stars_cycle.adventure.json`.
Полные результаты: `many_objects_fragmented_stars_results.json`.
```sh
godot --headless --path . --script res://tests/benchmark_many_objects.gd -- cycles=40 repeats=2
```
## Консолидация применения потоков и колонизации — 2026-10-08
Первый этап: исходное население распределяется по строкам владельцев одной таблицы переходов. Свободные стадии применяют готовые целые квоты напрямую. При колонизации списания планет объединяются по владельцам; каждая стадия колоний получает одну суммарную выдачу. Общие целые количества и статистика сохранены. Изменён порядок распределения единичных остатков между владельцами; полного совпадения состояния с прежним порядком округления не заявляется. Ct остаётся свободной от привязок.
До и после загружались одни и те же диагностические файлы с теми же количествами и партиями. Скорость 6, полный интервал 0,2 секунды (20 000 игровых лет), CPU-профилирование включено, UI отсутствует. По два прогона из исходного состояния, каждый по 20 циклов: до → после → после → до. На каждую модель и сохранение обработано 800 000 игровых лет. Валидация начальных и конечных состояний прошла; SHA-256 входных файлов до и после совпали. Сохранение игрока не использовалось.
| Сохранение | До, мс | После, мс | Сокращение времени | Полных циклов/с после |
|---|---:|---:|---:|---:|
| Все объекты | 385,263 | 377,222 | 2,09% | 2,651 |
| Много объектов и партий | 506,888 | 443,303 | 12,54% | 2,256 |
Для тяжёлого сохранения:
| Участок | До, мс/цикл | После, мс/цикл |
|---|---:|---:|
| Применение переходов | 113,011 | 91,591 |
| Колонизация | 53,487 | 11,520 |
| Подготовка потоков | 114,808 | 114,742 |
| Поколения звёзд и жизни | 116,085 | 115,999 |
Первый этап сокращает полный тяжёлый цикл на 12.54%, но цель пяти циклов в секунду не достигнута. До 200 мс нужно ещё примерно 2.22-кратное ускорение. Следующие крупные затраты — подготовка цивилизационных потоков, поколения и оставшееся применение переходов.
Проверки: целочисленные квоты и владельцы, включая 10^400 + 103 — 1068; цивилизационные потоки — 1895; состояние цивилизаций — 70; кадровый планировщик — 38. Всего 3071 проверка без ошибок. Проверены сохранение населения каждого владельца при планетных переходах, точные поступления по стадиям, замена планет колониями, статистика и Ct.
Полные профили, отдельные прогоны и контрольные суммы: `flow_commit_step1_results.json`. Базовая версия: `413f2be9044090c878c2909dd9d0646f77513a5f`.
Для замера текущей версии на тех же входных файлах:
```sh
godot --headless --path . --script res://tests/benchmark_civilization_flows.gd -- res://tests/artifacts/all_objects_speed6.adventure.json 0.2 profile cycles=20
godot --headless --path . --script res://tests/benchmark_civilization_flows.gd -- res://tests/artifacts/many_objects_fragmented_stars_cycle.adventure.json 0.2 profile cycles=20
```
## Повторное использование общих количеств — 2026-10-08
Второй этап: общий точный снимок передаётся между фазами цивилизационных потоков, колонизации и коррекции ёмкости. Переходы и выдачи обновляют только затронутые количества; перенос между владельцами сохраняет общую численность. Периоды стадий, дрейф и поклонение используют актуальные количества этого интервала. Автоматическое создание разных типов звёзд также использует общий снимок: успешная пробная операция вычитает расходы, отказ сохраняет исходный снимок. При внешних изменениях используется обычный пересчёт. Снимки не записываются в сохранения.
Сравнение с версией после первого этапа. На двух одинаковых диагностических сохранениях выполнены пары до → после → после → до: по 20 циклов на прогон, по 40 циклов на модель и сохранение. Скорость 6, интервал 0,2 секунды, CPU-профиль включён, UI отсутствует. Каждый прогон полностью обработал 400 000 игровых лет. Состояния валидны, входные SHA-256 не изменились.
| Сохранение | До, мс | После, мс | Сокращение | Полных циклов/с после |
|---|---:|---:|---:|---:|
| Все объекты | 377,185 | 367,326 | 2,61% | 2,722 |
| Много объектов и партий | 443,163 | 422,973 | 4,56% | 2,364 |
Тяжёлое сохранение:
| Метрика | До | После |
|---|---:|---:|
| Полных сборок общих количеств за цикл | 15 | 7 |
| Сборка общих количеств, мс/цикл | 37,498 | 17,767 |
| Производство, мс/цикл | 37,554 | 20,316 |
| Применение потоков, мс/цикл | 91,626 | 89,259 |
| Завершение потоков, мс/цикл | 9,821 | 6,881 |
Время сборки общих количеств вложено в остальные фазы и не прибавляется к ним повторно. Проверка цивилизационной части отдельно дала небольшой выигрыш; основную экономию обеспечивает исключение повторных сборок при автоматическом создании звёзд.
Полные состояния обеих версий совпали после 10 последовательных сравнений: по пять на каждое сохранение, интервалы 0,2 / 0,53 / 0,31 / 0,2 / 0,2 секунды. Дополнительные проверки сравнивают снимки с полным пересчётом, контролируют неизменность входных снимков, отказ производства и каскадную деградацию. Выполнено 4321 целевая проверка без ошибок; полная регрессия всех наборов не запускалась.
Пять полных циклов в секунду пока не достигнуты: тяжёлый цикл составляет 423.0 мс; до 200 мс нужно ещё около 2.11-кратного ускорения. Следующая крупная статья — подготовка потоков цивилизаций, около 115.2 мс/цикл.
Полные результаты и контрольные суммы: `physical_totals_step2_results.json`. Базовая версия: `9357f757a95db96274d4e115c7e498b58eca6902`. Сохранение игрока не использовалось.
## Упрощение расчёта наград потоков цивилизаций — 2026-10-08
Третий этап убирает промежуточное динамическое число при умножении точного целого населения на float-ставку награды. Частное и дробный остаток вычисляются напрямую, а дробный перенос сохраняет прежнее округление. Это применяется к наградам войны, урона и колонизации; размеры населения остаются точными целыми.
Сравнение с коммитом `c429403` (после второго этапа): те же два диагностических сохранения, скорость 6, интервал 0,2 секунды, CPU-профиль включён. Порядок пар — до → после → после → до; два прогона по 20 циклов на версию и сохранение. Каждый прогон обработал 400 000 игровых лет, валидация сохранений прошла, SHA-256 входов совпали. Сохранение игрока не использовалось.
| Сохранение | До, мс | После, мс | Сокращение времени | Полных циклов/с после |
|---|---:|---:|---:|---:|
| Все объекты | 380,579 | 357,026 | 6,19% | 2,801 |
| Много объектов и партий | 432,912 | 409,268 | 5,46% | 2,443 |
Подготовка потоков уменьшилась с 111,980 до 87,832 мс на сохранении со всеми объектами и со 117,004 до 92,100 мс на тяжёлом сохранении — примерно на 21% в обоих случаях. Остальные основные участки остались в пределах погрешности замера.
Полные состояния до и после совпали после пяти последовательных интервалов на каждом сохранении (включая дробные интервалы). Тест оптимизаций прошёл: 1889 проверок без ошибок. Цель пяти полных циклов в секунду ещё не достигнута: тяжёлому циклу требуется ускорение примерно в 2,05 раза, чтобы уложиться в 200 мс.
Полные профили и SHA-256 входов: `civilization_flow_prepare_step3_results.json`.
## Проверка оптимизации частичного изъятия партий — 2026-10-08
Перенос связанного содержимого уже агрегируется по типам внутри поколения. Проверен вариант частичного изъятия, исключающий промежуточное сложение квоты с дробным переносом и последующее вычитание целой части. Возрастные интервалы и порядок поколений сохранялись.
Парный замер на тех же сохранениях: скорость 6, интервал 0,2 секунды, CPU-профиль, без UI; до → после → после → до, по 20 циклов на прогон (40 циклов на версию и сохранение).
| Сохранение | До, мс/цикл | После, мс/цикл |
|---|---:|---:|
| Все объекты | 350,516 | 349,835 |
| Много объектов и партий | 404,255 | 404,390 |
Изъятие партий на тяжёлом сохранении: 40,919 → 40,837 мс/цикл. Изменения полного цикла меньше 0,2%; подтверждённого выигрыша нет. Вариант **откачен**, код симуляции оставлен прежним. Полные состояния совпали в 10 последовательных сравнениях; границы партий — 117 проверок без ошибок. Входные файлы не изменены.
Результаты отклонённого варианта: `batch_withdrawal_step4_results.json`. Этот замер не доказывает достижение пяти полных циклов в секунду.
## Адаптивное упрощение симуляции — 2026-10-08
Добавлен runtime-контроллер качества, который измеряет сглаженное CPU-время полного цикла. Он повышает уровень упрощения при превышении бюджета 200 мс и снимает его после 30 циклов с запасом. Режим не сохраняется в файл: после загрузки точность постепенно подстраивается заново.
При нагрузке цивилизационный поток заменяется приближением не более чем с одним переходом на стадию за интервал. На наиболее грубом уровне типы возрастных партий делятся на две чередующиеся группы: каждая обрабатывается раз в два цикла. Временные группы при этом расширяются. Это сохраняет количества, но сдвигает сроки отдельных переходов и исключает несколько последовательных переходов цивилизации за один интервал.
Проверены два синтетических сохранения на Apple M4 Pro, Godot 4.7.2, headless, с CPU-профилем и интервалом 0,2 секунды. Каждый замер — 60 последовательных полных циклов; каждый запуск начинается в точном режиме и адаптируется по ходу серии.
| Скорость | Все объекты, мс/цикл (циклов/с) | Много объектов и партий, мс/цикл (циклов/с) |
|---:|---:|---:|
| 1 | 165,0 (6,06) | 128,5 (7,78) |
| 2 | 181,4 (5,51) | 131,8 (7,59) |
| 3 | 175,2 (5,71) | 145,0 (6,90) |
| 4 | 159,7 (6,26) | 177,1 (5,65) |
| 5 | 174,9 (5,72) | 185,6 (5,39) |
| 6 | 190,6 (5,25) | 184,4 (5,42) |
Среднее время на всех прогонах ниже 200 мс. Отдельные циклы всё ещё бывают длиннее: максимумы составили 203–367 мс на скоростях 3–6, включая первые циклы до срабатывания адаптации. Значит, замеры подтверждают требуемую среднюю пропускную способность на этом компьютере, но не гарантируют отсутствие кратковременных задержек. Android и работа с UI отдельно не проверялись.
Проверки: адаптивное качество — 18, хронология поколений — 28, границы партий — 117, оптимизации симуляции — 1889, планировщик кадров — 39; ошибок нет. Все 12 конечных диагностических состояний прошли валидацию. Хеши входных файлов: `44bee3b3753fa51615f766d8f800a10cc2b70c8e7fb4ea12b6e18da7f07a7d67` и `1e95ef398923f26b912a305c84f9b3eb502d24cb14e8ae687a4becccf047324a`.
Детали уровней, минимумы/максимумы и воспроизводимые результаты: `adaptive_quality_results.json`. Штатный benchmark принимает параметр `speed=1..6`.
## Адаптивная частота циклов 5 → 4 → 3 — 2026-10-17
Частота запуска полного цикла теперь снижается при устойчивой нагрузке: пороги шага — 0,2, 0,25 и 0,333 секунды. EMA времени расчёта переключает частоту вниз, когда цикл занимает больше 90% текущего интервала; возврат на одну ступень происходит после 30 циклов быстрее 65% интервала. Уровень частоты временный и после загрузки начинается с 5 Гц. Игровые часы продолжают накапливать весь прошедший интервал. Временное объединение партий масштабируется с выбранным интервалом, при этом при 5 Гц прежняя точность остаётся прежней.
Проверка сцены на диагностическом сохранении `all_objects_speed6.adventure.json`, скорость 6, Godot 4.7.2, Mac16,8. Сравнивались фиксированные 5 Гц и адаптивные 5/4/3 Гц, два прогона каждого режима в чередующемся порядке, по 10 секунд реального времени. Последняя транзакция каждого прогона завершалась до снятия результата.
| Метрика | Фиксированные 5 Гц | Адаптивно, установилось 3 Гц | Изменение |
|---|---:|---:|---:|
| Завершённые циклы/с | 4,64 | 2,96 | −36% |
| Средний FPS сцены | 22,60 | 64,35 | ×2,85 |
| p95 времени кадра | 115,6 мс | 66,5 мс | −42% |
| Средний максимум кадра | 227 мс | 205 мс | −10% |
| Опубликованные игровые годы/с | 99 017 | 98 049 | −1,0% |
На этих замерах реальное игровое время почти не замедлилось: симуляция завершала около трёх более крупных циклов вместо примерно 4,6 меньших. Средний FPS заметно вырос, но отдельные кадры всё ещё превышали 200 мс; это не гарантия отсутствия коротких задержек. UI-замер выполнен в headless-сцене на компьютере, поэтому его FPS не переносится напрямую на телефон.
Отдельно прогнан синхронный benchmark полного CPU-цикла на тех же двух тяжёлых сохранениях (скорость 6, 30 циклов × 2 повтора). При фиксированном шаге 0,2 секунды и адаптивном шаге итоговая обработка игрового времени за CPU-секунду выросла с 1,099 до 1,656 на сохранении со всеми объектами (+50,7%) и с 1,036 до 1,697 на сохранении со множеством объектов и партий (+63,9%). В каждом адаптивном прогоне частоты установились на 3 Гц; интервалы игровых часов сверены с запрошенными, все конечные состояния прошли проверку сохранения. Синхронный показатель показывает пропускную способность расчёта, а UI-прогон — фактический темп транзакций сцены.
Повторяемые результаты: `dynamic_cadence_results.json` (CPU-прогоны) и `dynamic_cadence_ui_results.json` (сцена и UI). Запуски выполняются скриптами `benchmark_dynamic_cadence.gd` и `benchmark_dynamic_cadence_ui.gd`; исходные диагностические сохранения не изменялись.
@@ -0,0 +1,646 @@
{
"retained": false,
"conclusion": "No measurable full-cycle improvement; implementation reverted",
"delta_seconds": 0.2,
"exact_state_differential": true,
"input_sha256": {
"all_objects_speed6": "44bee3b3753fa51615f766d8f800a10cc2b70c8e7fb4ea12b6e18da7f07a7d67",
"many_objects_fragmented_stars_cycle": "1e95ef398923f26b912a305c84f9b3eb502d24cb14e8ae687a4becccf047324a"
},
"order": "before-after-after-before",
"runs": [
{
"cycles": 20,
"max_ms": 382.054,
"mean_ms": 347.474,
"min_ms": 291.952,
"model": "before",
"profile": {
"assign_homes": [
23851,
80
],
"civilization_flow_capacity": [
121403,
20
],
"civilization_flow_colonies": [
52415,
20
],
"civilization_flow_finish": [
135271,
20
],
"civilization_flow_ledger": [
880672,
20
],
"civilization_flow_plan": [
1718363,
20
],
"civilization_operator_build": [
61955,
20
],
"civilization_operator_solve": [
673740,
860
],
"enforce_capacity": [
316094,
80
],
"generation_products": [
190666,
40
],
"generation_withdrawal": [
545822,
60
],
"generations": [
2727124,
20
],
"initialize": [
113986,
20
],
"newborn_batches": [
121039,
3016
],
"physical_amounts_build": [
285729,
200
],
"production": [
373910,
20
],
"trends": [
358046,
20
]
},
"run": 1,
"save": "all_objects_speed6"
},
{
"cycles": 20,
"max_ms": 382.156,
"mean_ms": 349.53305,
"min_ms": 297.266,
"model": "after",
"profile": {
"assign_homes": [
23877,
80
],
"civilization_flow_capacity": [
121854,
20
],
"civilization_flow_colonies": [
53021,
20
],
"civilization_flow_finish": [
137725,
20
],
"civilization_flow_ledger": [
883114,
20
],
"civilization_flow_plan": [
1733356,
20
],
"civilization_operator_build": [
62368,
20
],
"civilization_operator_solve": [
680005,
860
],
"enforce_capacity": [
319290,
80
],
"generation_products": [
192497,
40
],
"generation_withdrawal": [
548252,
60
],
"generations": [
2737148,
20
],
"initialize": [
115930,
20
],
"newborn_batches": [
121459,
3016
],
"physical_amounts_build": [
287621,
200
],
"production": [
376359,
20
],
"trends": [
359971,
20
]
},
"run": 1,
"save": "all_objects_speed6"
},
{
"cycles": 20,
"max_ms": 387.438,
"mean_ms": 350.1367,
"min_ms": 298.254,
"model": "after",
"profile": {
"assign_homes": [
24181,
80
],
"civilization_flow_capacity": [
122008,
20
],
"civilization_flow_colonies": [
53315,
20
],
"civilization_flow_finish": [
137169,
20
],
"civilization_flow_ledger": [
885851,
20
],
"civilization_flow_plan": [
1736978,
20
],
"civilization_operator_build": [
62264,
20
],
"civilization_operator_solve": [
680485,
860
],
"enforce_capacity": [
319558,
80
],
"generation_products": [
193735,
40
],
"generation_withdrawal": [
547473,
60
],
"generations": [
2743162,
20
],
"initialize": [
115720,
20
],
"newborn_batches": [
121401,
3016
],
"physical_amounts_build": [
287832,
200
],
"production": [
376295,
20
],
"trends": [
360758,
20
]
},
"run": 2,
"save": "all_objects_speed6"
},
{
"cycles": 20,
"max_ms": 391.136,
"mean_ms": 353.55755,
"min_ms": 295.752,
"model": "before",
"profile": {
"assign_homes": [
23999,
80
],
"civilization_flow_capacity": [
125136,
20
],
"civilization_flow_colonies": [
53681,
20
],
"civilization_flow_finish": [
137614,
20
],
"civilization_flow_ledger": [
893234,
20
],
"civilization_flow_plan": [
1749507,
20
],
"civilization_operator_build": [
63052,
20
],
"civilization_operator_solve": [
683642,
860
],
"enforce_capacity": [
319168,
80
],
"generation_products": [
193901,
40
],
"generation_withdrawal": [
550377,
60
],
"generations": [
2768999,
20
],
"initialize": [
115976,
20
],
"newborn_batches": [
122734,
3016
],
"physical_amounts_build": [
292064,
200
],
"production": [
382112,
20
],
"trends": [
363057,
20
]
},
"run": 2,
"save": "all_objects_speed6"
},
{
"cycles": 20,
"max_ms": 418.952,
"mean_ms": 402.88125,
"min_ms": 362.784,
"model": "before",
"profile": {
"assign_homes": [
18182,
60
],
"civilization_flow_capacity": [
138911,
20
],
"civilization_flow_colonies": [
235353,
20
],
"civilization_flow_finish": [
139454,
20
],
"civilization_flow_ledger": [
1808094,
20
],
"civilization_flow_plan": [
1823967,
20
],
"civilization_operator_build": [
63710,
20
],
"civilization_operator_solve": [
684725,
860
],
"enforce_capacity": [
241433,
60
],
"generation_products": [
79394,
20
],
"generation_withdrawal": [
814774,
40
],
"generations": [
2390414,
20
],
"initialize": [
142892,
20
],
"newborn_batches": [
55241,
1490
],
"physical_amounts_build": [
359891,
140
],
"production": [
410388,
20
],
"trends": [
362721,
20
]
},
"run": 1,
"save": "many_objects_fragmented_stars_cycle"
},
{
"cycles": 20,
"max_ms": 425.202,
"mean_ms": 404.3497,
"min_ms": 355.885,
"model": "after",
"profile": {
"assign_homes": [
18056,
60
],
"civilization_flow_capacity": [
139522,
20
],
"civilization_flow_colonies": [
236599,
20
],
"civilization_flow_finish": [
140701,
20
],
"civilization_flow_ledger": [
1809113,
20
],
"civilization_flow_plan": [
1824567,
20
],
"civilization_operator_build": [
63452,
20
],
"civilization_operator_solve": [
684152,
860
],
"enforce_capacity": [
242790,
60
],
"generation_products": [
80500,
20
],
"generation_withdrawal": [
817704,
40
],
"generations": [
2406807,
20
],
"initialize": [
143427,
20
],
"newborn_batches": [
55885,
1490
],
"physical_amounts_build": [
363175,
140
],
"production": [
412428,
20
],
"trends": [
365215,
20
]
},
"run": 1,
"save": "many_objects_fragmented_stars_cycle"
},
{
"cycles": 20,
"max_ms": 418.858,
"mean_ms": 404.4306,
"min_ms": 357.106,
"model": "after",
"profile": {
"assign_homes": [
18133,
60
],
"civilization_flow_capacity": [
139021,
20
],
"civilization_flow_colonies": [
237100,
20
],
"civilization_flow_finish": [
139480,
20
],
"civilization_flow_ledger": [
1815717,
20
],
"civilization_flow_plan": [
1825515,
20
],
"civilization_operator_build": [
63298,
20
],
"civilization_operator_solve": [
685950,
860
],
"enforce_capacity": [
242932,
60
],
"generation_products": [
79477,
20
],
"generation_withdrawal": [
815781,
40
],
"generations": [
2400821,
20
],
"initialize": [
144101,
20
],
"newborn_batches": [
56140,
1490
],
"physical_amounts_build": [
362394,
140
],
"production": [
412986,
20
],
"trends": [
364511,
20
]
},
"run": 2,
"save": "many_objects_fragmented_stars_cycle"
},
{
"cycles": 20,
"max_ms": 422.492,
"mean_ms": 405.6284,
"min_ms": 355.699,
"model": "before",
"profile": {
"assign_homes": [
18208,
60
],
"civilization_flow_capacity": [
139305,
20
],
"civilization_flow_colonies": [
236973,
20
],
"civilization_flow_finish": [
141048,
20
],
"civilization_flow_ledger": [
1818465,
20
],
"civilization_flow_plan": [
1832211,
20
],
"civilization_operator_build": [
63519,
20
],
"civilization_operator_solve": [
688184,
860
],
"enforce_capacity": [
243601,
60
],
"generation_products": [
79839,
20
],
"generation_withdrawal": [
822003,
40
],
"generations": [
2410757,
20
],
"initialize": [
144424,
20
],
"newborn_batches": [
55500,
1490
],
"physical_amounts_build": [
363454,
140
],
"production": [
412711,
20
],
"trends": [
367008,
20
]
},
"run": 2,
"save": "many_objects_fragmented_stars_cycle"
}
],
"speed": 6
}
@@ -0,0 +1,645 @@
{
"reference_commit": "c429403",
"delta_seconds": 0.2,
"exact_state_differential": true,
"input_sha256": {
"all_objects_speed6": "44bee3b3753fa51615f766d8f800a10cc2b70c8e7fb4ea12b6e18da7f07a7d67",
"many_objects_fragmented_stars_cycle": "1e95ef398923f26b912a305c84f9b3eb502d24cb14e8ae687a4becccf047324a"
},
"order": "before-after-after-before",
"runs": [
{
"cycles": 20,
"max_ms": 417.124,
"mean_ms": 381.20745,
"min_ms": 328.622,
"model": "before",
"profile": {
"assign_homes": [
24482,
80
],
"civilization_flow_capacity": [
127513,
20
],
"civilization_flow_colonies": [
54148,
20
],
"civilization_flow_finish": [
140918,
20
],
"civilization_flow_ledger": [
905665,
20
],
"civilization_flow_plan": [
2240897,
20
],
"civilization_operator_build": [
63936,
20
],
"civilization_operator_solve": [
685115,
860
],
"enforce_capacity": [
323853,
80
],
"generation_products": [
195550,
40
],
"generation_withdrawal": [
565054,
60
],
"generations": [
2802836,
20
],
"initialize": [
117049,
20
],
"newborn_batches": [
123941,
3016
],
"physical_amounts_build": [
294599,
200
],
"production": [
385593,
20
],
"trends": [
363466,
20
]
},
"run": 1,
"save": "all_objects_speed6"
},
{
"cycles": 20,
"max_ms": 390.389,
"mean_ms": 357.03875,
"min_ms": 302.117,
"model": "after",
"profile": {
"assign_homes": [
24490,
80
],
"civilization_flow_capacity": [
125647,
20
],
"civilization_flow_colonies": [
54325,
20
],
"civilization_flow_finish": [
141339,
20
],
"civilization_flow_ledger": [
902399,
20
],
"civilization_flow_plan": [
1755403,
20
],
"civilization_operator_build": [
63055,
20
],
"civilization_operator_solve": [
683933,
860
],
"enforce_capacity": [
324284,
80
],
"generation_products": [
197296,
40
],
"generation_withdrawal": [
562515,
60
],
"generations": [
2804447,
20
],
"initialize": [
117904,
20
],
"newborn_batches": [
123892,
3016
],
"physical_amounts_build": [
296839,
200
],
"production": [
389361,
20
],
"trends": [
364246,
20
]
},
"run": 1,
"save": "all_objects_speed6"
},
{
"cycles": 20,
"max_ms": 390.735,
"mean_ms": 357.0124,
"min_ms": 304.896,
"model": "after",
"profile": {
"assign_homes": [
24460,
80
],
"civilization_flow_capacity": [
125447,
20
],
"civilization_flow_colonies": [
54216,
20
],
"civilization_flow_finish": [
139778,
20
],
"civilization_flow_ledger": [
906760,
20
],
"civilization_flow_plan": [
1757858,
20
],
"civilization_operator_build": [
63411,
20
],
"civilization_operator_solve": [
687170,
860
],
"enforce_capacity": [
325360,
80
],
"generation_products": [
196450,
40
],
"generation_withdrawal": [
558924,
60
],
"generations": [
2799057,
20
],
"initialize": [
117825,
20
],
"newborn_batches": [
123788,
3016
],
"physical_amounts_build": [
295498,
200
],
"production": [
385114,
20
],
"trends": [
367268,
20
]
},
"run": 2,
"save": "all_objects_speed6"
},
{
"cycles": 20,
"max_ms": 410.865,
"mean_ms": 379.9508,
"min_ms": 328.074,
"model": "before",
"profile": {
"assign_homes": [
24504,
80
],
"civilization_flow_capacity": [
125523,
20
],
"civilization_flow_colonies": [
54072,
20
],
"civilization_flow_finish": [
138884,
20
],
"civilization_flow_ledger": [
900719,
20
],
"civilization_flow_plan": [
2238292,
20
],
"civilization_operator_build": [
63477,
20
],
"civilization_operator_solve": [
687863,
860
],
"enforce_capacity": [
323929,
80
],
"generation_products": [
196146,
40
],
"generation_withdrawal": [
557728,
60
],
"generations": [
2793469,
20
],
"initialize": [
117168,
20
],
"newborn_batches": [
123830,
3016
],
"physical_amounts_build": [
294159,
200
],
"production": [
382811,
20
],
"trends": [
366676,
20
]
},
"run": 2,
"save": "all_objects_speed6"
},
{
"cycles": 20,
"max_ms": 454.17,
"mean_ms": 431.32255,
"min_ms": 380.165,
"model": "before",
"profile": {
"assign_homes": [
18277,
60
],
"civilization_flow_capacity": [
138886,
20
],
"civilization_flow_colonies": [
239194,
20
],
"civilization_flow_finish": [
140538,
20
],
"civilization_flow_ledger": [
1823769,
20
],
"civilization_flow_plan": [
2332907,
20
],
"civilization_operator_build": [
63920,
20
],
"civilization_operator_solve": [
689691,
860
],
"enforce_capacity": [
244580,
60
],
"generation_products": [
80081,
20
],
"generation_withdrawal": [
823351,
40
],
"generations": [
2417052,
20
],
"initialize": [
145160,
20
],
"newborn_batches": [
55841,
1490
],
"physical_amounts_build": [
364671,
140
],
"production": [
414028,
20
],
"trends": [
365376,
20
]
},
"run": 1,
"save": "many_objects_fragmented_stars_cycle"
},
{
"cycles": 20,
"max_ms": 437.315,
"mean_ms": 409.44945,
"min_ms": 360.224,
"model": "after",
"profile": {
"assign_homes": [
18338,
60
],
"civilization_flow_capacity": [
139951,
20
],
"civilization_flow_colonies": [
238906,
20
],
"civilization_flow_finish": [
141435,
20
],
"civilization_flow_ledger": [
1834486,
20
],
"civilization_flow_plan": [
1841420,
20
],
"civilization_operator_build": [
64315,
20
],
"civilization_operator_solve": [
691338,
860
],
"enforce_capacity": [
245861,
60
],
"generation_products": [
79738,
20
],
"generation_withdrawal": [
825947,
40
],
"generations": [
2423500,
20
],
"initialize": [
145491,
20
],
"newborn_batches": [
56085,
1490
],
"physical_amounts_build": [
371874,
140
],
"production": [
441338,
20
],
"trends": [
366919,
20
]
},
"run": 1,
"save": "many_objects_fragmented_stars_cycle"
},
{
"cycles": 20,
"max_ms": 424.959,
"mean_ms": 409.08685,
"min_ms": 359.281,
"model": "after",
"profile": {
"assign_homes": [
18404,
60
],
"civilization_flow_capacity": [
140108,
20
],
"civilization_flow_colonies": [
239591,
20
],
"civilization_flow_finish": [
141361,
20
],
"civilization_flow_ledger": [
1836691,
20
],
"civilization_flow_plan": [
1842577,
20
],
"civilization_operator_build": [
64001,
20
],
"civilization_operator_solve": [
692117,
860
],
"enforce_capacity": [
245631,
60
],
"generation_products": [
80486,
20
],
"generation_withdrawal": [
829783,
40
],
"generations": [
2438245,
20
],
"initialize": [
145228,
20
],
"newborn_batches": [
56248,
1490
],
"physical_amounts_build": [
369443,
140
],
"production": [
416359,
20
],
"trends": [
367683,
20
]
},
"run": 2,
"save": "many_objects_fragmented_stars_cycle"
},
{
"cycles": 20,
"max_ms": 452.639,
"mean_ms": 434.50095,
"min_ms": 387.05,
"model": "before",
"profile": {
"assign_homes": [
18271,
60
],
"civilization_flow_capacity": [
140778,
20
],
"civilization_flow_colonies": [
239800,
20
],
"civilization_flow_finish": [
141233,
20
],
"civilization_flow_ledger": [
1836252,
20
],
"civilization_flow_plan": [
2347237,
20
],
"civilization_operator_build": [
64454,
20
],
"civilization_operator_solve": [
693176,
860
],
"enforce_capacity": [
244735,
60
],
"generation_products": [
80563,
20
],
"generation_withdrawal": [
830888,
40
],
"generations": [
2436028,
20
],
"initialize": [
145261,
20
],
"newborn_batches": [
56321,
1490
],
"physical_amounts_build": [
369324,
140
],
"production": [
417541,
20
],
"trends": [
369444,
20
]
},
"run": 2,
"save": "many_objects_fragmented_stars_cycle"
}
],
"speed": 6
}
@@ -0,0 +1,172 @@
{
"cycles_per_run": 30,
"godot": {
"build": "official",
"hash": "ed1daf0bf001b61586d9930840f2f1394092c079",
"hex": 263938,
"major": 4,
"minor": 7,
"patch": 2,
"status": "stable",
"string": "4.7.2-stable (official)",
"timestamp": 0
},
"input_saves_unchanged": true,
"machine": "Mac16,8",
"repeats": 2,
"results": [
{
"applied_game_years": "600000",
"cadence_steps": {
"0.2": 30
},
"cpu_cycles_per_second": 5.5818824005294605,
"cycles": 30,
"final_adaptive_quality": 3,
"final_cadence_hz": 5.0,
"max_ms": 351.156,
"mean_ms_per_cycle": 179.15103333333334,
"mode": "fixed_5",
"p95_ms": 188.985,
"repeat": 0,
"requested_simulation_seconds": 6.000000000000003,
"save": "all_objects_speed6.adventure.json",
"simulated_seconds_per_cpu_second": 1.1163764801058926
},
{
"applied_game_years": "978333",
"cadence_steps": {
"0.2": 1,
"0.25": 1,
"0.33333333333333": 28
},
"cpu_cycles_per_second": 5.126867760649103,
"cycles": 30,
"final_adaptive_quality": 3,
"final_cadence_hz": 3.0,
"max_ms": 349.806,
"mean_ms_per_cycle": 195.05086666666668,
"mode": "adaptive_5_4_3",
"p95_ms": 214.441,
"repeat": 0,
"requested_simulation_seconds": 9.783333333333333,
"save": "all_objects_speed6.adventure.json",
"simulated_seconds_per_cpu_second": 1.671928541945013
},
{
"applied_game_years": "978333",
"cadence_steps": {
"0.2": 1,
"0.25": 1,
"0.33333333333333": 28
},
"cpu_cycles_per_second": 5.029407785556781,
"cycles": 30,
"final_adaptive_quality": 3,
"final_cadence_hz": 3.0,
"max_ms": 340.747,
"mean_ms_per_cycle": 198.83056666666667,
"mode": "adaptive_5_4_3",
"p95_ms": 233.775,
"repeat": 1,
"requested_simulation_seconds": 9.783333333333333,
"save": "all_objects_speed6.adventure.json",
"simulated_seconds_per_cpu_second": 1.6401457611787948
},
{
"applied_game_years": "600000",
"cadence_steps": {
"0.2": 30
},
"cpu_cycles_per_second": 5.403485464263869,
"cycles": 30,
"final_adaptive_quality": 3,
"final_cadence_hz": 5.0,
"max_ms": 351.698,
"mean_ms_per_cycle": 185.06573333333333,
"mode": "fixed_5",
"p95_ms": 195.396,
"repeat": 1,
"requested_simulation_seconds": 6.000000000000003,
"save": "all_objects_speed6.adventure.json",
"simulated_seconds_per_cpu_second": 1.0806970928527742
},
{
"applied_game_years": "600000",
"cadence_steps": {
"0.2": 30
},
"cpu_cycles_per_second": 5.218331511555821,
"cycles": 30,
"final_adaptive_quality": 3,
"final_cadence_hz": 5.0,
"max_ms": 373.477,
"mean_ms_per_cycle": 191.63213333333334,
"mode": "fixed_5",
"p95_ms": 250.5,
"repeat": 0,
"requested_simulation_seconds": 6.000000000000003,
"save": "many_objects_fragmented_stars_cycle.adventure.json",
"simulated_seconds_per_cpu_second": 1.0436663023111648
},
{
"applied_game_years": "978333",
"cadence_steps": {
"0.2": 1,
"0.25": 1,
"0.33333333333333": 28
},
"cpu_cycles_per_second": 5.21296979935453,
"cycles": 30,
"final_adaptive_quality": 3,
"final_cadence_hz": 3.0,
"max_ms": 373.168,
"mean_ms_per_cycle": 191.82923333333332,
"mode": "adaptive_5_4_3",
"p95_ms": 258.047,
"repeat": 0,
"requested_simulation_seconds": 9.783333333333333,
"save": "many_objects_fragmented_stars_cycle.adventure.json",
"simulated_seconds_per_cpu_second": 1.7000073734561718
},
{
"applied_game_years": "978333",
"cadence_steps": {
"0.2": 1,
"0.25": 1,
"0.33333333333333": 28
},
"cpu_cycles_per_second": 5.195109254879333,
"cycles": 30,
"final_adaptive_quality": 3,
"final_cadence_hz": 3.0,
"max_ms": 372.878,
"mean_ms_per_cycle": 192.48873333333333,
"mode": "adaptive_5_4_3",
"p95_ms": 254.848,
"repeat": 1,
"requested_simulation_seconds": 9.783333333333333,
"save": "many_objects_fragmented_stars_cycle.adventure.json",
"simulated_seconds_per_cpu_second": 1.694182851452316
},
{
"applied_game_years": "600000",
"cadence_steps": {
"0.2": 30
},
"cpu_cycles_per_second": 5.137478056546851,
"cycles": 30,
"final_adaptive_quality": 3,
"final_cadence_hz": 5.0,
"max_ms": 375.784,
"mean_ms_per_cycle": 194.64803333333333,
"mode": "fixed_5",
"p95_ms": 255.446,
"repeat": 1,
"requested_simulation_seconds": 6.000000000000003,
"save": "many_objects_fragmented_stars_cycle.adventure.json",
"simulated_seconds_per_cpu_second": 1.0274956113093707
}
],
"speed": 6
}
@@ -0,0 +1,359 @@
{
"godot": {
"build": "official",
"hash": "ed1daf0bf001b61586d9930840f2f1394092c079",
"hex": 263938,
"major": 4,
"minor": 7,
"patch": 2,
"status": "stable",
"string": "4.7.2-stable (official)",
"timestamp": 0
},
"machine": "Mac16,8",
"results": [
{
"commits": 48,
"completed_cycles_per_second": 4.743091908967023,
"cpu_profile_usec_calls": {
"assign_homes": [
51095,
170
],
"civilization_flow_capacity": [
111005,
48
],
"civilization_flow_colonies": [
131724,
48
],
"civilization_flow_finish": [
325933,
48
],
"civilization_flow_ledger": [
928271,
48
],
"civilization_flow_plan": [
825706,
48
],
"civilization_operator_build": [
148485,
48
],
"civilization_operator_solve": [
104924,
2064
],
"enforce_capacity": [
626179,
170
],
"generation_products": [
235626,
74
],
"generation_withdrawal": [
455348,
97
],
"generations": [
3654469,
48
],
"initialize": [
215278,
48
],
"newborn_batches": [
237070,
3088
],
"physical_amounts_build": [
630353,
437
],
"production": [
1115470,
48
],
"trends": [
865132,
48
]
},
"elapsed_seconds": 10.119981,
"final_adaptive_quality": 3,
"final_cadence_hz": 5.0,
"frames": 246,
"mean_render_fps": 24.584772192104204,
"mode": "fixed_5",
"published_to_requested": 0.9943600000000001,
"published_years": "1006667",
"render_max_ms": 237.132,
"render_p95_ms": 111.449,
"requested_years": "1012372"
},
{
"commits": 21,
"completed_cycles_per_second": 2.085573876935847,
"cpu_profile_usec_calls": {
"assign_homes": [
30276,
75
],
"civilization_flow_capacity": [
51694,
21
],
"civilization_flow_colonies": [
52773,
21
],
"civilization_flow_finish": [
142952,
21
],
"civilization_flow_ledger": [
309711,
21
],
"civilization_flow_plan": [
435139,
21
],
"civilization_operator_build": [
74385,
21
],
"civilization_operator_solve": [
87284,
903
],
"enforce_capacity": [
414865,
75
],
"generation_products": [
105449,
33
],
"generation_withdrawal": [
152279,
43
],
"generations": [
1503309,
21
],
"initialize": [
255626,
21
],
"newborn_batches": [
114834,
1633
],
"physical_amounts_build": [
274341,
193
],
"production": [
685165,
21
],
"trends": [
377582,
21
]
},
"elapsed_seconds": 10.069171,
"final_adaptive_quality": 3,
"final_cadence_hz": 2.0,
"frames": 879,
"mean_render_fps": 87.80117977216634,
"mode": "adaptive_5_4_3_2",
"published_to_requested": 0.9889800000000001,
"published_years": "989844",
"render_max_ms": 203.26,
"render_p95_ms": 42.712,
"requested_years": "1000874"
},
{
"commits": 21,
"completed_cycles_per_second": 2.0775335521668676,
"cpu_profile_usec_calls": {
"assign_homes": [
31004,
76
],
"civilization_flow_capacity": [
51172,
21
],
"civilization_flow_colonies": [
52782,
21
],
"civilization_flow_finish": [
142851,
21
],
"civilization_flow_ledger": [
309607,
21
],
"civilization_flow_plan": [
431321,
21
],
"civilization_operator_build": [
74854,
21
],
"civilization_operator_solve": [
86348,
903
],
"enforce_capacity": [
423638,
76
],
"generation_products": [
105537,
34
],
"generation_withdrawal": [
151611,
43
],
"generations": [
1516916,
21
],
"initialize": [
261806,
21
],
"newborn_batches": [
112492,
1621
],
"physical_amounts_build": [
278871,
195
],
"production": [
696352,
21
],
"trends": [
377969,
21
]
},
"elapsed_seconds": 10.10814,
"final_adaptive_quality": 3,
"final_cadence_hz": 2.0,
"frames": 880,
"mean_render_fps": 88.00051040296032,
"mode": "adaptive_5_4_3_2",
"published_to_requested": 0.9933700000000001,
"published_years": "994002",
"render_max_ms": 201.104,
"render_p95_ms": 42.207,
"requested_years": "1000640"
},
{
"commits": 47,
"completed_cycles_per_second": 4.646829581875308,
"cpu_profile_usec_calls": {
"assign_homes": [
50139,
165
],
"civilization_flow_capacity": [
108674,
47
],
"civilization_flow_colonies": [
129977,
47
],
"civilization_flow_finish": [
323458,
47
],
"civilization_flow_ledger": [
911542,
47
],
"civilization_flow_plan": [
807459,
47
],
"civilization_operator_build": [
147856,
47
],
"civilization_operator_solve": [
96154,
2021
],
"enforce_capacity": [
619397,
165
],
"generation_products": [
234744,
71
],
"generation_withdrawal": [
451797,
95
],
"generations": [
3665554,
47
],
"initialize": [
217827,
47
],
"newborn_batches": [
265766,
3178
],
"physical_amounts_build": [
623211,
428
],
"production": [
1142731,
47
],
"trends": [
856364,
47
]
},
"elapsed_seconds": 10.114423,
"final_adaptive_quality": 3,
"final_cadence_hz": 5.0,
"frames": 239,
"mean_render_fps": 23.87480014844331,
"mode": "fixed_5",
"published_to_requested": 0.9904200000000001,
"published_years": "992039",
"render_max_ms": 201.438,
"render_p95_ms": 114.211,
"requested_years": "1001635"
}
],
"save": "all_objects_speed6.adventure.json",
"speed": 6,
"wall_seconds_per_run": 10.0
}
@@ -0,0 +1,763 @@
{
"delta_seconds": 0.2,
"input_sha256": {
"all_objects_speed6": "44bee3b3753fa51615f766d8f800a10cc2b70c8e7fb4ea12b6e18da7f07a7d67",
"many_objects_fragmented_stars_cycle": "1e95ef398923f26b912a305c84f9b3eb502d24cb14e8ae687a4becccf047324a"
},
"order": "before-after-after-before",
"profile_enabled": true,
"runs": [
{
"cycles": 20,
"max_ms": 407.447,
"mean_ms": 382.1863,
"min_ms": 338.878,
"model": "before",
"profile": {
"assign_homes": [
23607,
80
],
"civilization_flow_capacity": [
119827,
20
],
"civilization_flow_colonies": [
100257,
20
],
"civilization_flow_finish": [
167710,
20
],
"civilization_flow_ledger": [
1031564,
20
],
"civilization_flow_plan": [
2148765,
20
],
"civilization_operator_build": [
60291,
20
],
"civilization_operator_solve": [
665230,
860
],
"enforce_capacity": [
338749,
80
],
"generation_products": [
187536,
40
],
"generation_withdrawal": [
520136,
60
],
"generations": [
2614358,
20
],
"initialize": [
112213,
20
],
"newborn_batches": [
118460,
3016
],
"physical_amounts_build": [
478063,
360
],
"production": [
537724,
20
],
"trends": [
349106,
20
]
},
"run": 1,
"save": "all_objects_speed6",
"validation": "",
"years": 400000
},
{
"cycles": 20,
"max_ms": 406.895,
"mean_ms": 376.23395,
"min_ms": 324.14,
"model": "after",
"profile": {
"assign_homes": [
23735,
80
],
"civilization_flow_capacity": [
120538,
20
],
"civilization_flow_colonies": [
50071,
20
],
"civilization_flow_finish": [
169132,
20
],
"civilization_flow_ledger": [
892018,
20
],
"civilization_flow_plan": [
2165803,
20
],
"civilization_operator_build": [
60679,
20
],
"civilization_operator_solve": [
670541,
860
],
"enforce_capacity": [
340289,
80
],
"generation_products": [
188663,
40
],
"generation_withdrawal": [
534195,
60
],
"generations": [
2649493,
20
],
"initialize": [
113482,
20
],
"newborn_batches": [
118902,
3016
],
"physical_amounts_build": [
489793,
360
],
"production": [
548119,
20
],
"trends": [
350991,
20
]
},
"run": 1,
"save": "all_objects_speed6",
"validation": "",
"years": 400000
},
{
"cycles": 20,
"max_ms": 413.636,
"mean_ms": 378.2097,
"min_ms": 326.044,
"model": "after",
"profile": {
"assign_homes": [
23901,
80
],
"civilization_flow_capacity": [
121116,
20
],
"civilization_flow_colonies": [
50174,
20
],
"civilization_flow_finish": [
170232,
20
],
"civilization_flow_ledger": [
899208,
20
],
"civilization_flow_plan": [
2173515,
20
],
"civilization_operator_build": [
61133,
20
],
"civilization_operator_solve": [
672190,
860
],
"enforce_capacity": [
341889,
80
],
"generation_products": [
190021,
40
],
"generation_withdrawal": [
537089,
60
],
"generations": [
2668071,
20
],
"initialize": [
113520,
20
],
"newborn_batches": [
120186,
3016
],
"physical_amounts_build": [
491086,
360
],
"production": [
549548,
20
],
"trends": [
352056,
20
]
},
"run": 2,
"save": "all_objects_speed6",
"validation": "",
"years": 400000
},
{
"cycles": 20,
"max_ms": 410.851,
"mean_ms": 388.3392,
"min_ms": 346.936,
"model": "before",
"profile": {
"assign_homes": [
23780,
80
],
"civilization_flow_capacity": [
121258,
20
],
"civilization_flow_colonies": [
101386,
20
],
"civilization_flow_finish": [
171098,
20
],
"civilization_flow_ledger": [
1048869,
20
],
"civilization_flow_plan": [
2182234,
20
],
"civilization_operator_build": [
61215,
20
],
"civilization_operator_solve": [
676192,
860
],
"enforce_capacity": [
343586,
80
],
"generation_products": [
191018,
40
],
"generation_withdrawal": [
528628,
60
],
"generations": [
2654320,
20
],
"initialize": [
113911,
20
],
"newborn_batches": [
120518,
3016
],
"physical_amounts_build": [
485368,
360
],
"production": [
550201,
20
],
"trends": [
354035,
20
]
},
"run": 2,
"save": "all_objects_speed6",
"validation": "",
"years": 400000
},
{
"cycles": 20,
"max_ms": 540.275,
"mean_ms": 504.8664,
"min_ms": 445.592,
"model": "before",
"profile": {
"assign_homes": [
17920,
60
],
"civilization_flow_capacity": [
136151,
20
],
"civilization_flow_colonies": [
1064307,
20
],
"civilization_flow_finish": [
195983,
20
],
"civilization_flow_ledger": [
2251097,
20
],
"civilization_flow_plan": [
2284423,
20
],
"civilization_operator_build": [
62124,
20
],
"civilization_operator_solve": [
678656,
860
],
"enforce_capacity": [
288140,
60
],
"generation_products": [
77749,
20
],
"generation_withdrawal": [
795116,
40
],
"generations": [
2321187,
20
],
"initialize": [
140958,
20
],
"newborn_batches": [
54878,
1490
],
"physical_amounts_build": [
746203,
300
],
"production": [
744868,
20
],
"trends": [
355994,
20
]
},
"run": 1,
"save": "many_objects_fragmented_stars_cycle",
"validation": "",
"years": 400000
},
{
"cycles": 20,
"max_ms": 462.134,
"mean_ms": 441.9819,
"min_ms": 389.761,
"model": "after",
"profile": {
"assign_homes": [
17865,
60
],
"civilization_flow_capacity": [
137136,
20
],
"civilization_flow_colonies": [
229748,
20
],
"civilization_flow_finish": [
196611,
20
],
"civilization_flow_ledger": [
1828223,
20
],
"civilization_flow_plan": [
2286989,
20
],
"civilization_operator_build": [
62177,
20
],
"civilization_operator_solve": [
678566,
860
],
"enforce_capacity": [
288805,
60
],
"generation_products": [
78203,
20
],
"generation_withdrawal": [
805143,
40
],
"generations": [
2312257,
20
],
"initialize": [
141951,
20
],
"newborn_batches": [
55057,
1490
],
"physical_amounts_build": [
746690,
300
],
"production": [
749636,
20
],
"trends": [
355174,
20
]
},
"run": 1,
"save": "many_objects_fragmented_stars_cycle",
"validation": "",
"years": 400000
},
{
"cycles": 20,
"max_ms": 467.425,
"mean_ms": 444.6246,
"min_ms": 389.196,
"model": "after",
"profile": {
"assign_homes": [
18039,
60
],
"civilization_flow_capacity": [
137286,
20
],
"civilization_flow_colonies": [
231059,
20
],
"civilization_flow_finish": [
197868,
20
],
"civilization_flow_ledger": [
1835420,
20
],
"civilization_flow_plan": [
2302705,
20
],
"civilization_operator_build": [
62638,
20
],
"civilization_operator_solve": [
680863,
860
],
"enforce_capacity": [
289856,
60
],
"generation_products": [
78541,
20
],
"generation_withdrawal": [
808144,
40
],
"generations": [
2327718,
20
],
"initialize": [
142049,
20
],
"newborn_batches": [
55184,
1490
],
"physical_amounts_build": [
752516,
300
],
"production": [
752648,
20
],
"trends": [
358218,
20
]
},
"run": 2,
"save": "many_objects_fragmented_stars_cycle",
"validation": "",
"years": 400000
},
{
"cycles": 20,
"max_ms": 532.382,
"mean_ms": 508.9093,
"min_ms": 451.042,
"model": "before",
"profile": {
"assign_homes": [
18208,
60
],
"civilization_flow_capacity": [
137370,
20
],
"civilization_flow_colonies": [
1075170,
20
],
"civilization_flow_finish": [
198272,
20
],
"civilization_flow_ledger": [
2269355,
20
],
"civilization_flow_plan": [
2307886,
20
],
"civilization_operator_build": [
62940,
20
],
"civilization_operator_solve": [
683671,
860
],
"enforce_capacity": [
291171,
60
],
"generation_products": [
78498,
20
],
"generation_withdrawal": [
807187,
40
],
"generations": [
2322215,
20
],
"initialize": [
143015,
20
],
"newborn_batches": [
55362,
1490
],
"physical_amounts_build": [
749843,
300
],
"production": [
753597,
20
],
"trends": [
360333,
20
]
},
"run": 2,
"save": "many_objects_fragmented_stars_cycle",
"validation": "",
"years": 400000
}
],
"speed": 6,
"reference_commit": "413f2be9044090c878c2909dd9d0646f77513a5f",
"reference_script_sha256": "b2a04cc50ad1e77b45762264846cb546b013b09f4c204ba22e6472a38728e5b7",
"candidate_script_sha256": "cbc65eaf4ab0e67fb9d6d5d7ad11757bdeb5f0b1e0ffb2f2631bfe3dcd9e962e",
"summary": {
"all_objects_speed6": {
"before": {
"mean_ms": 385.26275,
"cycles_per_second": 2.5956311634073113,
"profile_ms": {
"assign_homes": 1.184675,
"civilization_flow_capacity": 6.027125,
"civilization_flow_colonies": 5.041075,
"civilization_flow_finish": 8.4702,
"civilization_flow_ledger": 52.010825,
"civilization_flow_plan": 108.27497500000001,
"civilization_operator_build": 3.03765,
"civilization_operator_solve": 33.53555,
"enforce_capacity": 17.058375,
"generation_products": 9.46385,
"generation_withdrawal": 26.219099999999997,
"generations": 131.71695000000003,
"initialize": 5.6531,
"newborn_batches": 5.97445,
"physical_amounts_build": 24.085775,
"production": 27.198125,
"trends": 17.578525000000003
}
},
"after": {
"mean_ms": 377.22182499999997,
"cycles_per_second": 2.6509600816442687,
"profile_ms": {
"assign_homes": 1.1909,
"civilization_flow_capacity": 6.04135,
"civilization_flow_colonies": 2.506125,
"civilization_flow_finish": 8.4841,
"civilization_flow_ledger": 44.78065,
"civilization_flow_plan": 108.48295,
"civilization_operator_build": 3.0453,
"civilization_operator_solve": 33.568275,
"enforce_capacity": 17.05445,
"generation_products": 9.4671,
"generation_withdrawal": 26.7821,
"generations": 132.9391,
"initialize": 5.675050000000001,
"newborn_batches": 5.9772,
"physical_amounts_build": 24.521974999999998,
"production": 27.441675,
"trends": 17.576175
}
},
"reduction_percent": 2.0871275512621024
},
"many_objects_fragmented_stars_cycle": {
"before": {
"mean_ms": 506.88784999999996,
"cycles_per_second": 1.9728229824407906,
"profile_ms": {
"assign_homes": 0.9032,
"civilization_flow_capacity": 6.838025,
"civilization_flow_colonies": 53.486925,
"civilization_flow_finish": 9.856375,
"civilization_flow_ledger": 113.0113,
"civilization_flow_plan": 114.807725,
"civilization_operator_build": 3.1266,
"civilization_operator_solve": 34.058175000000006,
"enforce_capacity": 14.482775,
"generation_products": 3.906175,
"generation_withdrawal": 40.057575,
"generations": 116.08505000000001,
"initialize": 7.099324999999999,
"newborn_batches": 2.756,
"physical_amounts_build": 37.40115,
"production": 37.461625,
"trends": 17.908175
}
},
"after": {
"mean_ms": 443.30325,
"cycles_per_second": 2.255792169355853,
"profile_ms": {
"assign_homes": 0.8976000000000001,
"civilization_flow_capacity": 6.86055,
"civilization_flow_colonies": 11.520175,
"civilization_flow_finish": 9.861975000000001,
"civilization_flow_ledger": 91.591075,
"civilization_flow_plan": 114.74235,
"civilization_operator_build": 3.120375,
"civilization_operator_solve": 33.985724999999995,
"enforce_capacity": 14.466524999999999,
"generation_products": 3.9186,
"generation_withdrawal": 40.332175,
"generations": 115.999375,
"initialize": 7.1,
"newborn_batches": 2.756025,
"physical_amounts_build": 37.48015,
"production": 37.5571,
"trends": 17.834799999999998
}
},
"reduction_percent": 12.544116020930463
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,345 @@
{
"cycles_per_run": 50,
"delta_seconds": 0.2,
"profile_enabled": true,
"repeats": 2,
"runs": [
{
"cycles": 50,
"cycles_per_second": 2.39655612966922,
"final_stellar_cohorts": 130,
"game_years": "1000000",
"max_ms": 454.421,
"mean_ms": 417.26542,
"min_ms": 349.968,
"model": "legacy",
"p95_ms": 448.31,
"peak_stellar_cohorts": 132,
"profile": {
"assign_homes": [
58334,
200
],
"civilization_flow_capacity": [
308485,
50
],
"civilization_flow_colonies": [
272661,
50
],
"civilization_flow_finish": [
432935,
50
],
"civilization_flow_ledger": [
2884631,
50
],
"civilization_flow_plan": [
5425115,
50
],
"civilization_operator_build": [
151687,
50
],
"civilization_operator_solve": [
1609348,
2150
],
"enforce_capacity": [
871224,
200
],
"generation_products": [
542267,
100
],
"generation_withdrawal": [
1820057,
150
],
"generations": [
7751170,
50
],
"initialize": [
297786,
50
],
"newborn_batches": [
375307,
8322
],
"physical_amounts_build": [
1351719,
900
],
"production": [
1410303,
50
],
"trends": [
879262,
50
]
},
"repeat": 1,
"throughput": 0.479311225933843
},
{
"cycles": 50,
"cycles_per_second": 2.36172713483247,
"final_stellar_cohorts": 1300,
"game_years": "1000000",
"max_ms": 460.373,
"mean_ms": 423.41894,
"min_ms": 350.862,
"model": "cycle",
"p95_ms": 458.712,
"peak_stellar_cohorts": 1300,
"profile": {
"assign_homes": [
52297,
176
],
"civilization_flow_capacity": [
314752,
50
],
"civilization_flow_colonies": [
277416,
50
],
"civilization_flow_finish": [
442436,
50
],
"civilization_flow_ledger": [
3097867,
50
],
"civilization_flow_plan": [
5500302,
50
],
"civilization_operator_build": [
156156,
50
],
"civilization_operator_solve": [
1624350,
2150
],
"enforce_capacity": [
841074,
176
],
"generation_products": [
441260,
76
],
"generation_withdrawal": [
2058882,
126
],
"generations": [
7358962,
50
],
"initialize": [
304251,
50
],
"newborn_batches": [
211077,
5960
],
"physical_amounts_build": [
1293092,
826
],
"production": [
1725690,
50
],
"trends": [
899707,
50
]
},
"repeat": 1,
"throughput": 0.472345426966493
},
{
"cycles": 50,
"cycles_per_second": 2.34186004575995,
"final_stellar_cohorts": 1300,
"game_years": "1000000",
"max_ms": 465.408,
"mean_ms": 427.011,
"min_ms": 351.168,
"model": "cycle",
"p95_ms": 460.645,
"peak_stellar_cohorts": 1300,
"profile": {
"assign_homes": [
52872,
176
],
"civilization_flow_capacity": [
317075,
50
],
"civilization_flow_colonies": [
279484,
50
],
"civilization_flow_finish": [
446195,
50
],
"civilization_flow_ledger": [
3118109,
50
],
"civilization_flow_plan": [
5537625,
50
],
"civilization_operator_build": [
158082,
50
],
"civilization_operator_solve": [
1637405,
2150
],
"enforce_capacity": [
854481,
176
],
"generation_products": [
445328,
76
],
"generation_withdrawal": [
2078977,
126
],
"generations": [
7431634,
50
],
"initialize": [
308950,
50
],
"newborn_batches": [
213741,
5960
],
"physical_amounts_build": [
1308770,
826
],
"production": [
1744677,
50
],
"trends": [
903605,
50
]
},
"repeat": 2,
"throughput": 0.468372009151989
},
{
"cycles": 50,
"cycles_per_second": 2.31974771537327,
"final_stellar_cohorts": 130,
"game_years": "1000000",
"max_ms": 462.587,
"mean_ms": 431.08136,
"min_ms": 364.143,
"model": "legacy",
"p95_ms": 461.998,
"peak_stellar_cohorts": 132,
"profile": {
"assign_homes": [
60468,
200
],
"civilization_flow_capacity": [
318914,
50
],
"civilization_flow_colonies": [
281067,
50
],
"civilization_flow_finish": [
446947,
50
],
"civilization_flow_ledger": [
2979604,
50
],
"civilization_flow_plan": [
5570636,
50
],
"civilization_operator_build": [
158593,
50
],
"civilization_operator_solve": [
1643754,
2150
],
"enforce_capacity": [
904863,
200
],
"generation_products": [
564253,
100
],
"generation_withdrawal": [
1881469,
150
],
"generations": [
8019908,
50
],
"initialize": [
312899,
50
],
"newborn_batches": [
389499,
8322
],
"physical_amounts_build": [
1396769,
900
],
"production": [
1458932,
50
],
"trends": [
909402,
50
]
},
"repeat": 2,
"throughput": 0.463949543074653
}
],
"speed": 6
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,345 @@
{
"cycles_per_run": 50,
"delta_seconds": 0.2,
"profile_enabled": true,
"repeats": 2,
"runs": [
{
"cycles": 50,
"cycles_per_second": 2.3787548645537,
"final_stellar_cohorts": 130,
"game_years": "1000000",
"max_ms": 453.742,
"mean_ms": 420.388,
"min_ms": 354.113,
"model": "legacy",
"p95_ms": 452.604,
"peak_stellar_cohorts": 132,
"profile": {
"assign_homes": [
59021,
200
],
"civilization_flow_capacity": [
310616,
50
],
"civilization_flow_colonies": [
276485,
50
],
"civilization_flow_finish": [
436890,
50
],
"civilization_flow_ledger": [
2908982,
50
],
"civilization_flow_plan": [
5444724,
50
],
"civilization_operator_build": [
152980,
50
],
"civilization_operator_solve": [
1604917,
2150
],
"enforce_capacity": [
879602,
200
],
"generation_products": [
550010,
100
],
"generation_withdrawal": [
1834842,
150
],
"generations": [
7827667,
50
],
"initialize": [
300448,
50
],
"newborn_batches": [
379245,
8322
],
"physical_amounts_build": [
1360704,
900
],
"production": [
1417179,
50
],
"trends": [
884554,
50
]
},
"repeat": 1,
"throughput": 0.47575097291074
},
{
"cycles": 50,
"cycles_per_second": 2.3567861205283,
"final_stellar_cohorts": 125,
"game_years": "1000000",
"max_ms": 461.919,
"mean_ms": 424.30664,
"min_ms": 355.62,
"model": "cycle",
"p95_ms": 459.138,
"peak_stellar_cohorts": 127,
"profile": {
"assign_homes": [
59817,
200
],
"civilization_flow_capacity": [
314882,
50
],
"civilization_flow_colonies": [
279378,
50
],
"civilization_flow_finish": [
442443,
50
],
"civilization_flow_ledger": [
2950938,
50
],
"civilization_flow_plan": [
5516132,
50
],
"civilization_operator_build": [
156869,
50
],
"civilization_operator_solve": [
1618413,
2150
],
"enforce_capacity": [
892637,
200
],
"generation_products": [
529169,
100
],
"generation_withdrawal": [
1861386,
150
],
"generations": [
7833909,
50
],
"initialize": [
304443,
50
],
"newborn_batches": [
332349,
7522
],
"physical_amounts_build": [
1382887,
900
],
"production": [
1444907,
50
],
"trends": [
896615,
50
]
},
"repeat": 1,
"throughput": 0.471357224105661
},
{
"cycles": 50,
"cycles_per_second": 2.33373492243879,
"final_stellar_cohorts": 125,
"game_years": "1000000",
"max_ms": 471.891,
"mean_ms": 428.49768,
"min_ms": 363.562,
"model": "cycle",
"p95_ms": 458.377,
"peak_stellar_cohorts": 127,
"profile": {
"assign_homes": [
60419,
200
],
"civilization_flow_capacity": [
317546,
50
],
"civilization_flow_colonies": [
281187,
50
],
"civilization_flow_finish": [
447446,
50
],
"civilization_flow_ledger": [
2973320,
50
],
"civilization_flow_plan": [
5561963,
50
],
"civilization_operator_build": [
157327,
50
],
"civilization_operator_solve": [
1631869,
2150
],
"enforce_capacity": [
900410,
200
],
"generation_products": [
541714,
100
],
"generation_withdrawal": [
1875079,
150
],
"generations": [
7926735,
50
],
"initialize": [
307215,
50
],
"newborn_batches": [
336903,
7522
],
"physical_amounts_build": [
1400661,
900
],
"production": [
1457871,
50
],
"trends": [
908259,
50
]
},
"repeat": 2,
"throughput": 0.466746984487757
},
{
"cycles": 50,
"cycles_per_second": 2.31693373652923,
"final_stellar_cohorts": 130,
"game_years": "1000000",
"max_ms": 466.374,
"mean_ms": 431.60492,
"min_ms": 366.359,
"model": "legacy",
"p95_ms": 465.621,
"peak_stellar_cohorts": 132,
"profile": {
"assign_homes": [
60684,
200
],
"civilization_flow_capacity": [
318805,
50
],
"civilization_flow_colonies": [
282225,
50
],
"civilization_flow_finish": [
448335,
50
],
"civilization_flow_ledger": [
2980134,
50
],
"civilization_flow_plan": [
5579243,
50
],
"civilization_operator_build": [
157141,
50
],
"civilization_operator_solve": [
1638102,
2150
],
"enforce_capacity": [
904255,
200
],
"generation_products": [
565486,
100
],
"generation_withdrawal": [
1885470,
150
],
"generations": [
8034541,
50
],
"initialize": [
308996,
50
],
"newborn_batches": [
388637,
8322
],
"physical_amounts_build": [
1400258,
900
],
"production": [
1465783,
50
],
"trends": [
913581,
50
]
},
"repeat": 2,
"throughput": 0.463386747305846
}
],
"speed": 6
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,345 @@
{
"cycles_per_run": 40,
"delta_seconds": 0.2,
"profile_enabled": true,
"repeats": 2,
"runs": [
{
"cycles": 40,
"cycles_per_second": 1.6323701614814,
"final_stellar_cohorts": 105,
"game_years": "800000",
"max_ms": 628.888,
"mean_ms": 612.60615,
"min_ms": 564.108,
"model": "legacy",
"p95_ms": 624.987,
"peak_stellar_cohorts": 4560,
"profile": {
"assign_homes": [
48558,
160
],
"civilization_flow_capacity": [
263456,
40
],
"civilization_flow_colonies": [
2192836,
40
],
"civilization_flow_finish": [
401558,
40
],
"civilization_flow_ledger": [
4523753,
40
],
"civilization_flow_plan": [
4491368,
40
],
"civilization_operator_build": [
128185,
40
],
"civilization_operator_solve": [
1314272,
1720
],
"enforce_capacity": [
755412,
160
],
"generation_products": [
621550,
80
],
"generation_withdrawal": [
2524452,
120
],
"generations": [
8393123,
40
],
"initialize": [
293736,
40
],
"newborn_batches": [
1166060,
19424
],
"physical_amounts_build": [
1707597,
675
],
"production": [
2026863,
40
],
"trends": [
716666,
40
]
},
"repeat": 1,
"throughput": 0.32647403229628
},
{
"cycles": 40,
"cycles_per_second": 1.88577938012452,
"final_stellar_cohorts": 99,
"game_years": "800000",
"max_ms": 564.379,
"mean_ms": 530.284725,
"min_ms": 448.092,
"model": "cycle",
"p95_ms": 560.287,
"peak_stellar_cohorts": 607,
"profile": {
"assign_homes": [
39924,
132
],
"civilization_flow_capacity": [
265954,
40
],
"civilization_flow_colonies": [
2204837,
40
],
"civilization_flow_finish": [
404568,
40
],
"civilization_flow_ledger": [
4540902,
40
],
"civilization_flow_plan": [
4517593,
40
],
"civilization_operator_build": [
128322,
40
],
"civilization_operator_solve": [
1324328,
1720
],
"enforce_capacity": [
672871,
132
],
"generation_products": [
184761,
52
],
"generation_withdrawal": [
2150050,
92
],
"generations": [
5519639,
40
],
"initialize": [
295834,
40
],
"newborn_batches": [
118202,
3115
],
"physical_amounts_build": [
1594387,
612
],
"production": [
1526313,
40
],
"trends": [
723713,
40
]
},
"repeat": 1,
"throughput": 0.377155876024903
},
{
"cycles": 40,
"cycles_per_second": 1.87765994001252,
"final_stellar_cohorts": 99,
"game_years": "800000",
"max_ms": 566.428,
"mean_ms": 532.5778,
"min_ms": 456.191,
"model": "cycle",
"p95_ms": 565.345,
"peak_stellar_cohorts": 607,
"profile": {
"assign_homes": [
40238,
132
],
"civilization_flow_capacity": [
264957,
40
],
"civilization_flow_colonies": [
2218466,
40
],
"civilization_flow_finish": [
405624,
40
],
"civilization_flow_ledger": [
4564735,
40
],
"civilization_flow_plan": [
4543992,
40
],
"civilization_operator_build": [
127427,
40
],
"civilization_operator_solve": [
1338172,
1720
],
"enforce_capacity": [
672773,
132
],
"generation_products": [
185633,
52
],
"generation_withdrawal": [
2159985,
92
],
"generations": [
5547513,
40
],
"initialize": [
297516,
40
],
"newborn_batches": [
119187,
3115
],
"physical_amounts_build": [
1590461,
612
],
"production": [
1516716,
40
],
"trends": [
731042,
40
]
},
"repeat": 2,
"throughput": 0.375531988002504
},
{
"cycles": 40,
"cycles_per_second": 1.60095961519335,
"final_stellar_cohorts": 105,
"game_years": "800000",
"max_ms": 660.704,
"mean_ms": 624.625375,
"min_ms": 562.732,
"model": "legacy",
"p95_ms": 638.671,
"peak_stellar_cohorts": 4560,
"profile": {
"assign_homes": [
49183,
160
],
"civilization_flow_capacity": [
268901,
40
],
"civilization_flow_colonies": [
2229856,
40
],
"civilization_flow_finish": [
406135,
40
],
"civilization_flow_ledger": [
4623708,
40
],
"civilization_flow_plan": [
4578330,
40
],
"civilization_operator_build": [
130249,
40
],
"civilization_operator_solve": [
1340466,
1720
],
"enforce_capacity": [
765788,
160
],
"generation_products": [
635386,
80
],
"generation_withdrawal": [
2573324,
120
],
"generations": [
8560126,
40
],
"initialize": [
297396,
40
],
"newborn_batches": [
1191081,
19424
],
"physical_amounts_build": [
1738195,
675
],
"production": [
2069255,
40
],
"trends": [
735591,
40
]
},
"repeat": 2,
"throughput": 0.320191923038669
}
],
"speed": 6
}
@@ -0,0 +1,870 @@
{
"delta_seconds": 0.2,
"input_sha256": {
"all_objects_speed6": "44bee3b3753fa51615f766d8f800a10cc2b70c8e7fb4ea12b6e18da7f07a7d67",
"many_objects_fragmented_stars_cycle": "1e95ef398923f26b912a305c84f9b3eb502d24cb14e8ae687a4becccf047324a"
},
"order": "before-after-after-before",
"profile_enabled": true,
"runs": [
{
"cycles": 20,
"max_ms": 409.924,
"mean_ms": 374.42165,
"min_ms": 321.955,
"model": "before",
"profile": {
"assign_homes": [
23286,
80
],
"civilization_flow_capacity": [
120325,
20
],
"civilization_flow_colonies": [
49609,
20
],
"civilization_flow_finish": [
167957,
20
],
"civilization_flow_ledger": [
890864,
20
],
"civilization_flow_plan": [
2156571,
20
],
"civilization_operator_build": [
59939,
20
],
"civilization_operator_solve": [
667995,
860
],
"enforce_capacity": [
338483,
80
],
"generation_products": [
186561,
40
],
"generation_withdrawal": [
530120,
60
],
"generations": [
2635468,
20
],
"initialize": [
112680,
20
],
"newborn_batches": [
118343,
3016
],
"physical_amounts_build": [
488368,
360
],
"production": [
543976,
20
],
"trends": [
348509,
20
]
},
"run": 1,
"save": "all_objects_speed6",
"validation": "",
"years": 400000
},
{
"cycles": 20,
"max_ms": 396.498,
"mean_ms": 366.8874,
"min_ms": 316.948,
"model": "after",
"profile": {
"assign_homes": [
23679,
80
],
"civilization_flow_capacity": [
121355,
20
],
"civilization_flow_colonies": [
52357,
20
],
"civilization_flow_finish": [
134122,
20
],
"civilization_flow_ledger": [
867762,
20
],
"civilization_flow_plan": [
2171014,
20
],
"civilization_operator_build": [
60589,
20
],
"civilization_operator_solve": [
672927,
860
],
"enforce_capacity": [
313233,
80
],
"generation_products": [
188758,
40
],
"generation_withdrawal": [
537217,
60
],
"generations": [
2691969,
20
],
"initialize": [
113057,
20
],
"newborn_batches": [
119560,
3016
],
"physical_amounts_build": [
283727,
200
],
"production": [
371874,
20
],
"trends": [
352124,
20
]
},
"run": 1,
"save": "all_objects_speed6",
"validation": "",
"years": 400000
},
{
"cycles": 20,
"max_ms": 400.433,
"mean_ms": 367.76455,
"min_ms": 317.346,
"model": "after",
"profile": {
"assign_homes": [
23679,
80
],
"civilization_flow_capacity": [
120585,
20
],
"civilization_flow_colonies": [
52508,
20
],
"civilization_flow_finish": [
133714,
20
],
"civilization_flow_ledger": [
873014,
20
],
"civilization_flow_plan": [
2178049,
20
],
"civilization_operator_build": [
60872,
20
],
"civilization_operator_solve": [
674461,
860
],
"enforce_capacity": [
312963,
80
],
"generation_products": [
189817,
40
],
"generation_withdrawal": [
538355,
60
],
"generations": [
2697406,
20
],
"initialize": [
113617,
20
],
"newborn_batches": [
119833,
3016
],
"physical_amounts_build": [
283294,
200
],
"production": [
370613,
20
],
"trends": [
354430,
20
]
},
"run": 2,
"save": "all_objects_speed6",
"validation": "",
"years": 400000
},
{
"cycles": 20,
"max_ms": 412.947,
"mean_ms": 379.94895,
"min_ms": 327.79,
"model": "before",
"profile": {
"assign_homes": [
23817,
80
],
"civilization_flow_capacity": [
120810,
20
],
"civilization_flow_colonies": [
50245,
20
],
"civilization_flow_finish": [
170562,
20
],
"civilization_flow_ledger": [
901208,
20
],
"civilization_flow_plan": [
2186592,
20
],
"civilization_operator_build": [
61551,
20
],
"civilization_operator_solve": [
676571,
860
],
"enforce_capacity": [
342898,
80
],
"generation_products": [
190410,
40
],
"generation_withdrawal": [
538869,
60
],
"generations": [
2675372,
20
],
"initialize": [
114216,
20
],
"newborn_batches": [
120558,
3016
],
"physical_amounts_build": [
495969,
360
],
"production": [
554663,
20
],
"trends": [
353982,
20
]
},
"run": 2,
"save": "all_objects_speed6",
"validation": "",
"years": 400000
},
{
"cycles": 20,
"max_ms": 463.763,
"mean_ms": 441.44965,
"min_ms": 386.031,
"model": "before",
"profile": {
"assign_homes": [
17806,
60
],
"civilization_flow_capacity": [
136652,
20
],
"civilization_flow_colonies": [
230107,
20
],
"civilization_flow_finish": [
196342,
20
],
"civilization_flow_ledger": [
1825154,
20
],
"civilization_flow_plan": [
2292865,
20
],
"civilization_operator_build": [
62146,
20
],
"civilization_operator_solve": [
680101,
860
],
"enforce_capacity": [
288185,
60
],
"generation_products": [
77590,
20
],
"generation_withdrawal": [
800537,
40
],
"generations": [
2298719,
20
],
"initialize": [
141855,
20
],
"newborn_batches": [
54592,
1490
],
"physical_amounts_build": [
745858,
300
],
"production": [
747261,
20
],
"trends": [
356732,
20
]
},
"run": 1,
"save": "many_objects_fragmented_stars_cycle",
"validation": "",
"years": 400000
},
{
"cycles": 20,
"max_ms": 445.055,
"mean_ms": 423.0315,
"min_ms": 374.104,
"model": "after",
"profile": {
"assign_homes": [
17840,
60
],
"civilization_flow_capacity": [
137440,
20
],
"civilization_flow_colonies": [
231537,
20
],
"civilization_flow_finish": [
137928,
20
],
"civilization_flow_ledger": [
1782223,
20
],
"civilization_flow_plan": [
2310391,
20
],
"civilization_operator_build": [
61962,
20
],
"civilization_operator_solve": [
687903,
860
],
"enforce_capacity": [
238360,
60
],
"generation_products": [
77666,
20
],
"generation_withdrawal": [
802637,
40
],
"generations": [
2360346,
20
],
"initialize": [
141317,
20
],
"newborn_batches": [
54930,
1490
],
"physical_amounts_build": [
356207,
140
],
"production": [
405364,
20
],
"trends": [
357407,
20
]
},
"run": 1,
"save": "many_objects_fragmented_stars_cycle",
"validation": "",
"years": 400000
},
{
"cycles": 20,
"max_ms": 441.891,
"mean_ms": 422.9136,
"min_ms": 376.819,
"model": "after",
"profile": {
"assign_homes": [
17821,
60
],
"civilization_flow_capacity": [
136949,
20
],
"civilization_flow_colonies": [
231992,
20
],
"civilization_flow_finish": [
137312,
20
],
"civilization_flow_ledger": [
1788156,
20
],
"civilization_flow_plan": [
2297486,
20
],
"civilization_operator_build": [
61993,
20
],
"civilization_operator_solve": [
683317,
860
],
"enforce_capacity": [
238430,
60
],
"generation_products": [
78170,
20
],
"generation_withdrawal": [
804912,
40
],
"generations": [
2364256,
20
],
"initialize": [
141451,
20
],
"newborn_batches": [
54900,
1490
],
"physical_amounts_build": [
354476,
140
],
"production": [
407287,
20
],
"trends": [
358298,
20
]
},
"run": 2,
"save": "many_objects_fragmented_stars_cycle",
"validation": "",
"years": 400000
},
{
"cycles": 20,
"max_ms": 465.049,
"mean_ms": 444.87595,
"min_ms": 392.884,
"model": "before",
"profile": {
"assign_homes": [
17906,
60
],
"civilization_flow_capacity": [
136963,
20
],
"civilization_flow_colonies": [
229784,
20
],
"civilization_flow_finish": [
196495,
20
],
"civilization_flow_ledger": [
1839904,
20
],
"civilization_flow_plan": [
2302987,
20
],
"civilization_operator_build": [
62518,
20
],
"civilization_operator_solve": [
683660,
860
],
"enforce_capacity": [
289846,
60
],
"generation_products": [
78882,
20
],
"generation_withdrawal": [
808021,
40
],
"generations": [
2328372,
20
],
"initialize": [
142755,
20
],
"newborn_batches": [
54963,
1490
],
"physical_amounts_build": [
754056,
300
],
"production": [
754898,
20
],
"trends": [
358634,
20
]
},
"run": 2,
"save": "many_objects_fragmented_stars_cycle",
"validation": "",
"years": 400000
}
],
"speed": 6,
"reference_commit": "9357f757a95db96274d4e115c7e498b58eca6902",
"exact_differential": {
"cycles": 10,
"inputs": [
"all_objects_speed6",
"many_objects_fragmented_stars_cycle"
],
"delta_seconds": [
0.2,
0.53,
0.31,
0.2,
0.2
],
"equal_full_states": true
},
"checks": {
"flow_commit": 1971,
"civilization_flows": 1895,
"civilization_state": 70,
"frame_evolution": 38,
"production_transaction": 14,
"stellar_rules": 333
},
"summary": {
"all_objects_speed6": {
"before": {
"mean_ms": 377.1853,
"cycles_per_second": 2.651216789201488,
"profile_ms": {
"assign_homes": 1.177575,
"civilization_flow_capacity": 6.028375,
"civilization_flow_colonies": 2.49635,
"civilization_flow_finish": 8.462975,
"civilization_flow_ledger": 44.8018,
"civilization_flow_plan": 108.579075,
"civilization_operator_build": 3.03725,
"civilization_operator_solve": 33.61415,
"enforce_capacity": 17.034525000000002,
"generation_products": 9.424275,
"generation_withdrawal": 26.724725,
"generations": 132.771,
"initialize": 5.6724,
"newborn_batches": 5.972524999999999,
"physical_amounts_build": 24.608425,
"production": 27.465975,
"trends": 17.562275000000003
},
"profile_calls_per_cycle": {
"assign_homes": 4.0,
"civilization_flow_capacity": 1.0,
"civilization_flow_colonies": 1.0,
"civilization_flow_finish": 1.0,
"civilization_flow_ledger": 1.0,
"civilization_flow_plan": 1.0,
"civilization_operator_build": 1.0,
"civilization_operator_solve": 43.0,
"enforce_capacity": 4.0,
"generation_products": 2.0,
"generation_withdrawal": 3.0,
"generations": 1.0,
"initialize": 1.0,
"newborn_batches": 150.8,
"physical_amounts_build": 18.0,
"production": 1.0,
"trends": 1.0
}
},
"after": {
"mean_ms": 367.32597499999997,
"cycles_per_second": 2.7223775830173733,
"profile_ms": {
"assign_homes": 1.18395,
"civilization_flow_capacity": 6.0485,
"civilization_flow_colonies": 2.621625,
"civilization_flow_finish": 6.6959,
"civilization_flow_ledger": 43.519400000000005,
"civilization_flow_plan": 108.726575,
"civilization_operator_build": 3.036525,
"civilization_operator_solve": 33.6847,
"enforce_capacity": 15.6549,
"generation_products": 9.464375,
"generation_withdrawal": 26.8893,
"generations": 134.734375,
"initialize": 5.66685,
"newborn_batches": 5.984825,
"physical_amounts_build": 14.175525,
"production": 18.562175,
"trends": 17.66385
},
"profile_calls_per_cycle": {
"assign_homes": 4.0,
"civilization_flow_capacity": 1.0,
"civilization_flow_colonies": 1.0,
"civilization_flow_finish": 1.0,
"civilization_flow_ledger": 1.0,
"civilization_flow_plan": 1.0,
"civilization_operator_build": 1.0,
"civilization_operator_solve": 43.0,
"enforce_capacity": 4.0,
"generation_products": 2.0,
"generation_withdrawal": 3.0,
"generations": 1.0,
"initialize": 1.0,
"newborn_batches": 150.8,
"physical_amounts_build": 10.0,
"production": 1.0,
"trends": 1.0
}
},
"reduction_percent": 2.613920797019398
},
"many_objects_fragmented_stars_cycle": {
"before": {
"mean_ms": 443.1628,
"cycles_per_second": 2.2565070894939736,
"profile_ms": {
"assign_homes": 0.8927999999999999,
"civilization_flow_capacity": 6.840375,
"civilization_flow_colonies": 11.497275,
"civilization_flow_finish": 9.820924999999999,
"civilization_flow_ledger": 91.62644999999999,
"civilization_flow_plan": 114.8963,
"civilization_operator_build": 3.1166,
"civilization_operator_solve": 34.094025,
"enforce_capacity": 14.450775,
"generation_products": 3.9118000000000004,
"generation_withdrawal": 40.21395,
"generations": 115.677275,
"initialize": 7.11525,
"newborn_batches": 2.738875,
"physical_amounts_build": 37.49785,
"production": 37.553975,
"trends": 17.88415
},
"profile_calls_per_cycle": {
"assign_homes": 3.0,
"civilization_flow_capacity": 1.0,
"civilization_flow_colonies": 1.0,
"civilization_flow_finish": 1.0,
"civilization_flow_ledger": 1.0,
"civilization_flow_plan": 1.0,
"civilization_operator_build": 1.0,
"civilization_operator_solve": 43.0,
"enforce_capacity": 3.0,
"generation_products": 1.0,
"generation_withdrawal": 2.0,
"generations": 1.0,
"initialize": 1.0,
"newborn_batches": 74.5,
"physical_amounts_build": 15.0,
"production": 1.0,
"trends": 1.0
}
},
"after": {
"mean_ms": 422.97254999999996,
"cycles_per_second": 2.364219616615783,
"profile_ms": {
"assign_homes": 0.891525,
"civilization_flow_capacity": 6.859725,
"civilization_flow_colonies": 11.588225,
"civilization_flow_finish": 6.881,
"civilization_flow_ledger": 89.25947500000001,
"civilization_flow_plan": 115.19692500000001,
"civilization_operator_build": 3.098875,
"civilization_operator_solve": 34.2805,
"enforce_capacity": 11.91975,
"generation_products": 3.8959,
"generation_withdrawal": 40.188725,
"generations": 118.11505,
"initialize": 7.0691999999999995,
"newborn_batches": 2.74575,
"physical_amounts_build": 17.767075000000002,
"production": 20.316275,
"trends": 17.892625
},
"profile_calls_per_cycle": {
"assign_homes": 3.0,
"civilization_flow_capacity": 1.0,
"civilization_flow_colonies": 1.0,
"civilization_flow_finish": 1.0,
"civilization_flow_ledger": 1.0,
"civilization_flow_plan": 1.0,
"civilization_operator_build": 1.0,
"civilization_operator_solve": 43.0,
"enforce_capacity": 3.0,
"generation_products": 1.0,
"generation_withdrawal": 2.0,
"generations": 1.0,
"initialize": 1.0,
"newborn_batches": 74.5,
"physical_amounts_build": 7.0,
"production": 1.0,
"trends": 1.0
}
},
"reduction_percent": 4.5559442263655825
}
},
"candidate_script_sha256": {
"civilization_flow.gd": "5d2b5f5a90fc9182ac1f933d473441457be28b47800a0081b1409614dd28aab5",
"civilization_state.gd": "ae0832531c7ce244e6939793007b3d7be5bd32246be6c209a75fdb5e3e6ffe7a",
"civilization_evolution.gd": "24efc14eedee5e8a28422262e37e6c54e1a0c403a164bd8f4c96d749f89ac61f",
"evolution_coordinator.gd": "0b8cec7e53f047698545e9b97c986b99e0ea5bfc61a6b0177335aedb442e6048",
"object_ledger.gd": "f7fbfeb4be905b525ad91648167ed1e32c4d3b77553549c69c3ebb808ac99b4b",
"manual_production.gd": "138a94d3bb7d63470f2ca2eb841badef910ec27f3e247c5c9ed8e925c0971fa4",
"automatic_production.gd": "dba9f98cbecc90ad9935653c39f7fcfad53edb3ca2670bbce4e1594b58b399a5",
"universe_environment.gd": "e31c87b38a4ec0052f3d43f7be2af51ee7612a48fadcf0b5072ba678107e576e"
}
}
+67
View File
@@ -0,0 +1,67 @@
extends SceneTree
## Full-cycle throughput benchmark; diagnostic input is never modified.
const E = preload("res://world/owniverse/math/exact.gd")
func _initialize():
call_deferred("run")
func restore_json_integers(value):
# JSON parses ordinary integer tokens as float. Large integers are $exact.
if value is float and value == floorf(value) and absf(value) <= 9007199254740991:
return int(value)
if value is Dictionary:
for key in value: value[key] = restore_json_integers(value[key])
elif value is Array:
for i in value.size(): value[i] = restore_json_integers(value[i])
return value
func run():
var args = OS.get_cmdline_user_args()
if args.is_empty():
print("Usage: --script tests/benchmark_civilization_flows.gd -- /path/to/save.adventure.json [delta_seconds] [profile] [cycles=100] [speed=1..6]")
quit(1)
return
var data = JSON.parse_string(FileAccess.get_file_as_string(args[0]))
if not data is Dictionary or data.get("format_version") != 6:
push_error("Expected diagnostic JSON format 6")
quit(1)
return
var initial: Dictionary = E.decode(restore_json_integers(data)).state
var validation: String = root.get_node("SaveManager").validate_data(E.decode(restore_json_integers(data)))
print("Save validation: ", "OK" if validation.is_empty() else validation)
var delta: float = args[1].to_float() if args.size() > 1 else 1.0 / 60.0
if not is_finite(delta) or delta <= 0:
quit(1)
return
var selected_speed: int = 6
for arg in args:
if arg.begins_with("speed="):
selected_speed = clampi(arg.trim_prefix("speed=").to_int(), 1, root.get_node("Global").SPEED_LIMIT)
for speed in [selected_speed]:
var world = load("res://world/owniverse/owniverse.gd").new()
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
world.state = initial.duplicate(true)
world.speed = speed
world.profile_enabled = args.has("profile")
var elapsed: Array = []
var cycles: int = 100
for arg in args:
if arg.begins_with("cycles="): cycles = maxi(1, arg.trim_prefix("cycles=").to_int())
var initial_clock: Variant = world.state.time.datetime
for cycle in cycles:
var start: int = Time.get_ticks_usec()
world.evolute(delta)
elapsed.append((Time.get_ticks_usec() - start) / 1000.0)
print("Speed %d, %d complete %.3f-second steps" % [speed, cycles, delta])
print("Min/max ms: ", elapsed.min(), "/", elapsed.max())
print("Game years applied: ", E.fixed(E.subtract(world.state.time.datetime, initial_clock)))
var total_ms: float = 0.0
for sample in elapsed: total_ms += sample
var throughput: float = float(cycles) * delta / (total_ms / 1000.0)
print("Mean complete cycle ms: ", total_ms / cycles)
print("Full cycles/s: ", float(cycles) * 1000.0 / total_ms)
print("Simulation throughput: %.3f real-time seconds processed per CPU second; needs >= 1.0 to keep up" % throughput)
if world.profile_enabled: print("CPU profile (usec, calls): ", world.profile)
for entity in world.catalog.entities.values(): entity.free()
world.free()
quit()
@@ -0,0 +1 @@
uid://biwpwcsboh0s7
+121
View File
@@ -0,0 +1,121 @@
extends SceneTree
## Paired, read-only throughput benchmark for adaptive 5/4/3/2 Hz cadence.
const E = preload("res://world/owniverse/math/exact.gd")
const SAVE_PATHS: Array[String] = [
"res://tests/artifacts/all_objects_speed6.adventure.json",
"res://tests/artifacts/many_objects_fragmented_stars_cycle.adventure.json"
]
const RESULT_PATH := "res://tests/artifacts/dynamic_cadence_results.json"
var cycles: int = 40
var repeats: int = 2
func _initialize() -> void:
call_deferred("run")
func restore_json_integers(value):
if value is float and value == floorf(value) and absf(value) <= 9007199254740991:
return int(value)
if value is Dictionary:
for key in value: value[key] = restore_json_integers(value[key])
elif value is Array:
for i in value.size(): value[i] = restore_json_integers(value[i])
return value
func create_world(initial: Dictionary, use_adaptive_cadence: bool):
var world = load("res://world/owniverse/owniverse.gd").new()
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
world.state = initial.duplicate(true)
world.speed = 6
world.adaptive_cadence = use_adaptive_cadence
world.frame_step_seconds = 0.2
world.cadence_level = 0
return world
func dispose(world) -> void:
for entity in world.catalog.entities.values(): entity.free()
world.free()
func measure(initial: Dictionary, adaptive: bool, save_name: String, repeat_index: int) -> Dictionary:
var world = create_world(initial, adaptive)
var elapsed_usec: int = 0
var requested_seconds: float = 0.0
var step_counts: Dictionary = {}
var samples: Array[float] = []
var initial_clock: Variant = world.state.time.datetime
for i in cycles:
var step: float = world.frame_step_seconds
step_counts[str(step)] = int(step_counts.get(str(step), 0)) + 1
requested_seconds += step
var started: int = Time.get_ticks_usec()
world.evolute(step)
var usec: int = Time.get_ticks_usec() - started
elapsed_usec += usec
samples.append(float(usec) / 1000.0)
var applied_years: Variant = E.subtract(world.state.time.datetime, initial_clock)
var expected_years: float = requested_seconds * 100000.0
assert(absf(E.approx(applied_years) - expected_years) <= maxf(1.0, expected_years * 1e-10), "Clock advance differs from accumulated cadence interval")
var validation: String = root.get_node("SaveManager").validate_data({
"format_version": 6,
"app_version": "0.7.0",
"ui": {"current_page": 0},
"state": world.state
})
assert(validation.is_empty(), validation)
samples.sort()
var result: Dictionary = {
"save": save_name,
"mode": "adaptive_5_4_3_2" if adaptive else "fixed_5",
"repeat": repeat_index,
"cycles": cycles,
"mean_ms_per_cycle": float(elapsed_usec) / (cycles * 1000.0),
"p95_ms": samples[int(ceil(samples.size() * 0.95)) - 1],
"max_ms": samples.back(),
"cpu_cycles_per_second": float(cycles) * 1000000.0 / elapsed_usec,
"simulated_seconds_per_cpu_second": requested_seconds * 1000000.0 / elapsed_usec,
"requested_simulation_seconds": requested_seconds,
"applied_game_years": E.fixed(applied_years),
"cadence_steps": step_counts,
"final_cadence_hz": 1.0 / world.frame_step_seconds,
"final_adaptive_quality": world.adaptive_quality
}
print("CADENCE_RESULT ", JSON.stringify(result))
dispose(world)
return result
func run() -> void:
for arg in OS.get_cmdline_user_args():
if arg.begins_with("cycles="): cycles = maxi(1, arg.trim_prefix("cycles=").to_int())
if arg.begins_with("repeats="): repeats = maxi(1, arg.trim_prefix("repeats=").to_int())
var inputs: Dictionary = {}
for path in SAVE_PATHS:
var parsed = JSON.parse_string(FileAccess.get_file_as_string(path))
assert(parsed is Dictionary and parsed.get("format_version") == 6, "Invalid benchmark save: " + path)
var payload: Dictionary = E.decode(restore_json_integers(parsed))
var validation: String = root.get_node("SaveManager").validate_data(payload)
assert(validation.is_empty(), validation)
inputs[path.get_file()] = payload.state
var results: Array[Dictionary] = []
for save_name in inputs:
var initial: Dictionary = inputs[save_name]
# Alternate order to limit machine warm-up and thermal bias.
for repeat_index in repeats:
if repeat_index % 2 == 0:
results.append(measure(initial, false, save_name, repeat_index))
results.append(measure(initial, true, save_name, repeat_index))
else:
results.append(measure(initial, true, save_name, repeat_index))
results.append(measure(initial, false, save_name, repeat_index))
var output := FileAccess.open(RESULT_PATH, FileAccess.WRITE)
assert(output != null)
output.store_string(JSON.stringify({
"machine": OS.get_model_name(),
"godot": Engine.get_version_info(),
"speed": 6,
"cycles_per_run": cycles,
"repeats": repeats,
"input_saves_unchanged": true,
"results": results
}, "\t", true, true))
output.close()
print("Wrote paired cadence results: ", RESULT_PATH)
quit()
+1
View File
@@ -0,0 +1 @@
uid://3fogff2lfmy5
+116
View File
@@ -0,0 +1,116 @@
extends SceneTree
## Measures real Stage scheduling on the full-load diagnostic save.
const E = preload("res://world/owniverse/math/exact.gd")
const RUN_SECONDS: float = 10.0
const RESULT_PATH := "res://tests/artifacts/dynamic_cadence_ui_results.json"
func _initialize() -> void:
call_deferred("run")
func restore(value):
if value is float and value == floorf(value) and absf(value) <= 9007199254740991: return int(value)
if value is Dictionary:
for key in value: value[key] = restore(value[key])
elif value is Array:
for i in value.size(): value[i] = restore(value[i])
return value
func prepare(stage, state: Dictionary, adaptive: bool) -> void:
var world = stage.owniverse
world.state = state.duplicate(true)
world.speed = 6
world.adaptive_cadence = adaptive
world.adaptive_cycle_ema_ms = 0.0
world.adaptive_quality = 0
world.adaptive_recovery_cycles = 0
world.cadence_level = 0
world.cadence_recovery_cycles = 0
world.frame_step_seconds = 0.2
world.frame_elapsed_seconds = 0.0
world.frame_clock = world.state.time.duplicate()
world.frame_active_clock = {}
world._resume_clock = {}
world._queued_productions.clear()
world.production_queue_busy = false
world.frame_busy = false
world.frame_commits = 0
world.profile.clear()
stage.frame_samples.clear()
stage.process_samples.clear()
stage.previous_frame = 0
stage.measuring = false
world.presentation.refresh(world.state, world.catalog)
stage.set_owniverse_item_value()
func run_measurement(stage, initial: Dictionary, adaptive: bool) -> Dictionary:
prepare(stage, initial, adaptive)
var world = stage.owniverse
var start_usec: int = Time.get_ticks_usec()
var requested_start: Variant = world.frame_clock.datetime
var published_start: Variant = world.state.time.datetime
stage.measuring = true
while float(Time.get_ticks_usec() - start_usec) / 1000000.0 < RUN_SECONDS:
await process_frame
stage.measuring = false
var measured_frames: int = stage.frame_samples.size()
# Let the final atomic transaction publish before starting the paired run.
while world.frame_busy or world.production_queue_busy:
await process_frame
var elapsed_usec: int = Time.get_ticks_usec() - start_usec
var requested: Variant = E.subtract(world.frame_clock.datetime, requested_start)
var published: Variant = E.subtract(world.state.time.datetime, published_start)
var samples: Array = stage.frame_samples.duplicate()
var total_frame_ms: float = 0.0
for value in samples: total_frame_ms += value
samples.sort()
var result: Dictionary = {
"mode": "adaptive_5_4_3_2" if adaptive else "fixed_5",
"frames": measured_frames,
"elapsed_seconds": float(elapsed_usec) / 1000000.0,
"commits": world.frame_commits,
"completed_cycles_per_second": float(world.frame_commits) * 1000000.0 / elapsed_usec,
"requested_years": E.fixed(requested),
"published_years": E.fixed(published),
"published_to_requested": snappedf(E.approx(E.divide(published, requested)), 0.00001),
"mean_render_fps": float(samples.size()) * 1000.0 / total_frame_ms if total_frame_ms > 0.0 else 0.0,
"render_p95_ms": samples[int(ceil(samples.size() * 0.95)) - 1] if not samples.is_empty() else 0.0,
"render_max_ms": samples.back() if not samples.is_empty() else 0.0,
"final_cadence_hz": 1.0 / world.frame_step_seconds,
"final_adaptive_quality": world.adaptive_quality,
"cpu_profile_usec_calls": world.profile.duplicate(true)
}
print("UI_CADENCE_RESULT ", JSON.stringify(result))
return result
func run() -> void:
var raw = JSON.parse_string(FileAccess.get_file_as_string("res://tests/artifacts/all_objects_speed6.adventure.json"))
assert(raw is Dictionary and raw.get("format_version") == 6)
var payload: Dictionary = E.decode(restore(raw))
var validation: String = root.get_node("SaveManager").validate_data(payload)
assert(validation.is_empty(), validation)
var scene = load("res://world/world.tscn").instantiate()
var stage = scene.get_node("Stage")
stage.owniverse.free()
stage.set_script(load("res://tests/fixtures/benchmark_ui_stage.gd"))
root.add_child(scene)
await process_frame
stage.owniverse.profile_enabled = true
var results: Array[Dictionary] = []
for repeat_index in 2:
if repeat_index % 2 == 0:
results.append(await run_measurement(stage, payload.state, false))
results.append(await run_measurement(stage, payload.state, true))
else:
results.append(await run_measurement(stage, payload.state, true))
results.append(await run_measurement(stage, payload.state, false))
var report: Dictionary = {"machine": OS.get_model_name(), "godot": Engine.get_version_info(), "speed": 6, "wall_seconds_per_run": RUN_SECONDS, "save": "all_objects_speed6.adventure.json", "results": results}
var output := FileAccess.open(RESULT_PATH, FileAccess.WRITE)
assert(output != null)
output.store_string(JSON.stringify(report, "\t", true, true))
output.close()
print("UI_CADENCE_RESULTS ", JSON.stringify(report))
print("Wrote paired UI cadence results: ", RESULT_PATH)
for entity in stage.owniverse.catalog.entities.values(): entity.free()
stage.owniverse.free()
scene.free()
quit()
@@ -0,0 +1 @@
uid://olp81bbycovp
+1 -1
View File
@@ -40,7 +40,7 @@ func run():
for id in stage.owniverse.catalog.entities: for id in stage.owniverse.catalog.entities:
if E.gt(stage.owniverse.ledger.physical_amount(stage.owniverse.state, id), 0): remaining_objects += 1 if E.gt(stage.owniverse.ledger.physical_amount(stage.owniverse.state, id), 0): remaining_objects += 1
print("Remaining objects: ", remaining_objects) print("Remaining objects: ", remaining_objects)
print("Final adaptive step: ", stage.owniverse.frame_step_seconds) print("Simulation step: ", stage.owniverse.frame_step_seconds)
for entity in stage.owniverse.catalog.entities.values(): entity.free() for entity in stage.owniverse.catalog.entities.values(): entity.free()
stage.owniverse.free() stage.owniverse.free()
scene.free() scene.free()
+130
View File
@@ -0,0 +1,130 @@
extends SceneTree
## Paired old/new cohort timing on separate generated large diagnostic saves.
const E = preload("res://world/owniverse/math/exact.gd")
var cycles: int = 50
var repeats: int = 2
var artifact_prefix: String = "large_star_cohorts"
func _initialize(): call_deferred("run")
func restore_json_integers(value):
if value is float and value == floorf(value) and absf(value) <= 9007199254740991: return int(value)
if value is Dictionary:
for key in value: value[key] = restore_json_integers(value[key])
elif value is Array:
for i in value.size(): value[i] = restore_json_integers(value[i])
return value
func dispose(world):
for entity in world.catalog.entities.values(): entity.free()
world.free()
func world_from(state: Dictionary, use_cycles: bool):
var world = load("res://world/owniverse/owniverse.gd").new()
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
world.state = state.duplicate(true)
world.speed = 6
world.batches.cycle_stars = use_cycles
return world
func cohort_count(world) -> int:
var total: int = 0
for id in world.state.batches:
if world.catalog.entities[id].group_as == "Star": total += world.state.batches[id].size()
return total
func make_state(initial: Dictionary, use_cycles: bool) -> Dictionary:
var world = world_from(initial, use_cycles)
var anchor: Variant = E.of("1e12")
var shift: Variant = E.subtract(anchor, world.state.time.datetime)
for id in world.state.batches:
for batch: Dictionary in world.state.batches[id]:
for field in ["created_from", "created_to", "start_year", "end_year", "processed_until"]:
if batch.has(field): batch[field] = E.add(batch[field], shift)
for id in world.state.batch_origins:
world.state.batch_origins[id] = E.add(world.state.batch_origins[id], shift)
world.state.time = {"datetime": anchor, "year": anchor}
var physical: Dictionary = world.ledger.physical_amounts(world.state)
var populations: Dictionary = {}
for id in world.catalog.entities:
var entity = world.catalog.entities[id]
if entity.group_as != "Star" or E.le(entity.lifespan, 0) or E.le(physical.get(id, 0), 0): continue
world.state.batches[id] = []
world.state.batch_origins.erase(id)
var lifespan: Variant = world.batches.effective_lifespan(world.state, id)
var birth_from: Variant = E.subtract(anchor, lifespan)
var duration: Variant = E.minimum(lifespan, 2000000)
world.batches.append_batch(world.state, id, physical[id], birth_from, E.add(birth_from, duration))
populations[id] = E.fixed(physical[id])
var payload: Dictionary = {"format_version": 6, "app_version": "0.7.0", "ui": {"current_page": 0}, "state": world.state,
"simulation": {"active_clock": {}, "requested_clock": {}, "productions": [], "speed": 6}}
var error: String = root.get_node("SaveManager").validate_data(payload)
assert(error.is_empty(), error)
var suffix: String = "cycle" if use_cycles else "legacy"
var path: String = "res://tests/artifacts/%s_%s.adventure.json" % [artifact_prefix, suffix]
var output = FileAccess.open(path, FileAccess.WRITE)
assert(output != null)
output.store_string(JSON.stringify(E.encode(payload), " ", true, true))
output.close()
print("Generated %s: star_types=%d stellar_cohorts=%d populations=%s" % [suffix, populations.size(), cohort_count(world), str(populations)])
var restored: Dictionary = E.decode(restore_json_integers(JSON.parse_string(FileAccess.get_file_as_string(path))))
assert(E.eq(restored.state, world.state), "Diagnostic file roundtrip changed initial state")
dispose(world)
return restored.state
func measure(initial: Dictionary, use_cycles: bool, run_index: int) -> Dictionary:
var world = world_from(initial, use_cycles)
world.profile_enabled = true
var samples: Array = []
var total_usec: int = 0
var initial_year: Variant = world.state.time.datetime
var peak_cohorts: int = cohort_count(world)
for cycle in cycles:
var started: int = Time.get_ticks_usec()
world.evolute(0.2)
var elapsed: int = Time.get_ticks_usec() - started
total_usec += elapsed
samples.append(elapsed / 1000.0)
peak_cohorts = maxi(peak_cohorts, cohort_count(world))
var applied: Variant = E.subtract(world.state.time.datetime, initial_year)
assert(E.eq(applied, cycles * 20000))
var error: String = root.get_node("SaveManager").validate_data({"format_version": 6, "app_version": "0.7.0", "ui": {"current_page": 0}, "state": world.state})
assert(error.is_empty(), error)
samples.sort()
var result: Dictionary = {"model": "cycle" if use_cycles else "legacy", "repeat": run_index, "cycles": cycles,
"mean_ms": total_usec / (1000.0 * cycles), "min_ms": samples.front(), "max_ms": samples.back(), "p95_ms": samples[int(ceil(samples.size() * 0.95)) - 1],
"cycles_per_second": cycles * 1000000.0 / total_usec, "throughput": cycles * 200000.0 / total_usec,
"game_years": E.fixed(applied), "peak_stellar_cohorts": peak_cohorts, "final_stellar_cohorts": cohort_count(world), "profile": world.profile.duplicate(true)}
print("PAIRED_RESULT ", JSON.stringify(result))
dispose(world)
return result
func run():
for arg in OS.get_cmdline_user_args():
if arg.begins_with("cycles="): cycles = maxi(1, arg.trim_prefix("cycles=").to_int())
if arg.begins_with("repeats="): repeats = maxi(1, arg.trim_prefix("repeats=").to_int())
var raw = JSON.parse_string(FileAccess.get_file_as_string("res://tests/artifacts/all_objects_speed6.adventure.json"))
var payload: Dictionary = E.decode(restore_json_integers(raw))
assert(root.get_node("SaveManager").validate_data(payload).is_empty())
var legacy: Dictionary = make_state(payload.state, false)
var current: Dictionary = make_state(payload.state, true)
var reference = world_from(legacy, false)
var candidate = world_from(current, true)
assert(E.eq(reference.ledger.physical_amounts(reference.state), candidate.ledger.physical_amounts(candidate.state)))
assert(E.eq(reference.state.ethics, candidate.state.ethics))
assert(cohort_count(candidate) <= cohort_count(reference), "Dynamic widening increased the fixture cohort count")
dispose(reference)
dispose(candidate)
if OS.get_cmdline_user_args().has("generate_only"):
print("Generated states validate and survive disk roundtrip; physical amounts and ethics match")
quit()
return
var results: Array = []
for repeat_index in repeats:
var order: Array = [false, true] if repeat_index % 2 == 0 else [true, false]
for use_cycles in order:
results.append(measure(current if use_cycles else legacy, use_cycles, repeat_index + 1))
var output = FileAccess.open("res://tests/artifacts/%s_results.json" % artifact_prefix, FileAccess.WRITE)
output.store_string(JSON.stringify({"delta_seconds": 0.2, "speed": 6, "cycles_per_run": cycles, "repeats": repeats, "profile_enabled": true, "runs": results}, " "))
output.close()
quit()
@@ -0,0 +1 @@
uid://hl20vbt440nb
+111
View File
@@ -0,0 +1,111 @@
extends "res://tests/benchmark_large_star_cohorts.gd"
## Fresh all-object stress world with many valid cohorts sharing death cycles.
const Fixture = preload("res://tests/fixtures/civilization_benchmark.gd")
var death_cycles: int = 40
var fragments_per_cycle: int = 8
func _initialize():
artifact_prefix = "many_objects_fragmented_stars"
cycles = 40
call_deferred("run")
func make_state(_initial: Dictionary, use_cycles: bool) -> Dictionary:
var world = load("res://world/owniverse/owniverse.gd").new()
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
var global: Node = root.get_node("Global")
Fixture.seed(world, global)
world.batches.cycle_stars = use_cycles
var anchor: Variant = E.of("1e12")
var stock: Variant = E.of("1e60")
var planets: Variant = E.of("1e28")
world.state.time = {"datetime": anchor, "year": anchor}
world.state.energy = E.of("1e100")
world.state.batches = {}
world.state.batch_origins = {}
world.state.binds = {}
world.state.ethics = {}
for id in world.catalog.entities:
world.state.amount[id] = 0 if world.catalog.is_group(id) else (planets if world.catalog.entities[id].group_as == "Planet" else stock)
world.state.stats.total_amount[id] = world.state.amount[id]
# Keep the synthetic universe sparse enough for blast damage to coexist
# with all life stages instead of sterilizing them on the first cycle.
world.state.amount.S = E.of("1e300")
world.state.amount.H = E.of("1e80")
world.state.amount.He = E.of("1e79")
world.state.amount.Met = E.of("1e79")
var dwarfs: Array = ["RD", "OD", "YD", "WYD"]
var stages: int = 0
for id in global.CivilizationRules:
world.state.amount[id] = planets
var weights: Array = [0.35, 0.2, 0.15, 0.12, 0.1, 0.08]
var profile: Dictionary = {}
var ethics: Array = ["m", "l", "w", "d", "c", "b"]
for e in 6: profile[ethics[e]] = weights[(e + stages) % 6]
world.state.ethics[id] = profile
stages += 1
# Planetary stages are hosted by all four kinds of dwarf stars.
for id in world.catalog.entities:
if world.catalog.entities[id].group_as != "Planet" or world.catalog.is_group(id): continue
if global.CivilizationRules.has(id) and str(global.CivilizationRules[id].home) != "": continue
var held: Variant = E.divide(world.state.amount[id], 4)
world.state.amount[id] = 0
for dwarf in dwarfs:
if not world.state.binds.has(dwarf): world.state.binds[dwarf] = {}
world.state.binds[dwarf][id] = held
# Bound stars remain physical and continue producing inside structures.
var structures: Array = ["SCD", "SCM", "SCG", "GEM", "GSD"]
var stellar_types: int = 0
for id in world.catalog.entities:
if world.catalog.entities[id].group_as != "Star" or E.le(world.catalog.entities[id].lifespan, 0): continue
var held: Variant = E.divide(stock, 4)
var owner: String = structures[stellar_types % structures.size()]
if not world.state.binds.has(owner): world.state.binds[owner] = {}
world.state.binds[owner][id] = held
world.state.amount[id] = E.subtract(stock, held)
stellar_types += 1
world.ledger.refresh_groups(world.state)
var physical: Dictionary = world.ledger.physical_amounts(world.state)
var active_types: int = 0
for id in world.catalog.entities:
if world.catalog.is_group(id) or E.le(physical.get(id, 0), 0): continue
active_types += 1
world.state.stats.total_amount[id] = physical[id]
var entity = world.catalog.entities[id]
if E.le(entity.lifespan, 0) or global.CivilizationRules.has(id): continue
if entity.group_as != "Star":
world.batches.append_batch(world.state, id, physical[id], anchor, anchor)
continue
# Valid but deliberately fragmented historical age cohorts. The test
# stresses merge eligibility, rather than claiming these are produced
# by the ordinary steady-state creation path with one fixed origin.
var span: Variant = world.batches.effective_lifespan(world.state, id)
var count: int = death_cycles * fragments_per_cycle
var assigned: Variant = 0
world.state.batches[id] = []
for n in count:
var cycle: int = n / fragments_per_cycle
var fragment: int = n % fragments_per_cycle
var start: Variant = E.add(anchor, cycle * 20000 + (fragment + 1) * 1800)
var end: Variant = E.add(start, 500)
var quota: Variant = E.floor_value(E.mul_div(physical[id], n + 1, count))
world.state.batches[id].append({"amount": E.subtract(quota, assigned), "created_from": E.subtract(start, span), "created_to": E.subtract(end, span), "start_year": start, "end_year": end})
assigned = quota
assert(E.eq(assigned, physical[id]))
world.state.batch_origins[id] = world.state.batches[id][0].created_from
var raw_cohorts: int = cohort_count(world)
world.batches.coarsen(world.state)
var payload: Dictionary = {"format_version": 6, "app_version": "0.7.0", "ui": {"current_page": 0}, "state": world.state,
"simulation": {"active_clock": {}, "requested_clock": {}, "productions": [], "speed": 6}}
var error: String = root.get_node("SaveManager").validate_data(payload)
assert(error.is_empty(), error)
var suffix: String = "cycle" if use_cycles else "legacy"
var path: String = "res://tests/artifacts/%s_%s.adventure.json" % [artifact_prefix, suffix]
var output = FileAccess.open(path, FileAccess.WRITE)
assert(output != null)
output.store_string(JSON.stringify(E.encode(payload), " ", true, true))
output.close()
var restored: Dictionary = E.decode(restore_json_integers(JSON.parse_string(FileAccess.get_file_as_string(path))))
assert(E.eq(restored.state, world.state), "Save roundtrip changed the stress state")
print("Generated %s: active_types=%d civilization_stages=%d stellar_types=%d raw_cohorts=%d stored_stellar_cohorts=%d" % [suffix, active_types, stages, stellar_types, raw_cohorts, cohort_count(world)])
dispose(world)
return restored.state
+1
View File
@@ -0,0 +1 @@
uid://d0plagt7blmcf
+129
View File
@@ -0,0 +1,129 @@
extends SceneTree
const E = preload("res://world/owniverse/math/exact.gd")
const Controller = preload("res://ui/achievements/achievement_controller.gd")
var failures := 0
class MockWorld extends RefCounted:
var state := {"time": {"year": 0}, "stats": {"total_amount": {}}}
var presentation := {"revision": 0}
class MockStage extends Control:
const PAGE_WIDTH = 1080
var owniverse = MockWorld.new()
var screens: Dictionary
var stage_page_scroller: ScrollContainer
func _initialize() -> void:
call_deferred("run")
func check(value: bool, message: String) -> void:
if not value:
failures += 1
push_error(message)
func node(parent: Node, child: Node, label: String) -> Node:
child.name = label
parent.add_child(child)
return child
func run() -> void:
check(load("res://stage/stage.gd") != null, "Stage integration compiles")
var scene: PackedScene = load("res://world/world.tscn")
var scene_state: SceneState = scene.get_state()
for language in ["English", "Russian"]:
var connected := false
for i in scene_state.get_connection_count():
if str(scene_state.get_connection_source(i)) == "Stage/MainMenu/Language/" + language:
connected = scene_state.get_connection_signal(i) == &"pressed" and str(scene_state.get_connection_target(i)) == "Stage/MainMenu" and scene_state.get_connection_method(i) == &"_on_change_locale_pressed" and scene_state.get_connection_binds(i) == ["en" if language == "English" else "ru"]
check(connected, language + " button connects to locale handler")
var stage := MockStage.new()
root.add_child(stage)
var world = node(stage, Control.new(), "World")
stage.screens = {"World": world}
stage.stage_page_scroller = node(world, ScrollContainer.new(), "PageScroller")
var pages = node(stage.stage_page_scroller, HBoxContainer.new(), "HBoxContainer")
for names in [["ForcePage", "ForceHeader"], ["ResourcePage", "ResourceHeader"], ["StarPage", "StarHeader"], ["StructurePage", "StructureHeader"], ["LifePage", "LifeHeader"]]:
node(node(pages, VBoxContainer.new(), names[0]), HBoxContainer.new(), names[1])
var screen = node(stage, Control.new(), "Achievements")
var title = node(node(screen, Control.new(), "Header"), TextureRect.new(), "Title")
node(node(screen, ScrollContainer.new(), "AchievementList"), VBoxContainer.new(), "AchievementItems")
var controller = Controller.new()
controller.progress_path = "/tmp/owniverse-achievements-test-%s.cfg" % OS.get_process_id()
stage.add_child(controller)
controller.initialize(stage)
check(controller.definitions.size() == 72, "All 72 enabled achievements loaded")
check(not controller.definitions.has("a95"), "Ignored achievement excluded")
controller.update(0.0)
check(controller.queue.is_empty() and controller.current.is_empty(), "Initially visible achievements do not notify")
stage.owniverse.state.stats.total_amount.H = E.of("5E4")
stage.owniverse.presentation.revision += 1
controller.update(0.0)
check(controller.progress.a13.unlocked, "Cumulative hydrogen threshold unlocks")
check(controller.progress.a14.visible and not controller.progress.a14.unlocked, "Opening threshold reveals next achievement")
controller.queue.clear()
controller._enqueue("a14", "g")
controller._enqueue("a14", "c")
check(controller.queue.size() == 1 and controller.queue[0].type == "c", "Pending reveal upgrades to completion")
controller.queue.clear()
controller.current = {}
stage.owniverse.state.time.year = E.of("1E12")
stage.owniverse.state.stats.total_amount.H = E.of("1E24")
stage.owniverse.state.stats.total_amount.Star = E.of("9E16")
stage.owniverse.presentation.revision += 1
controller.update(0.0)
check(controller.progress.a4.unlocked and controller.progress.a20.unlocked and controller.progress.a71.unlocked, "Exact large thresholds unlock")
check(controller.current.type == "c", "Immediate completion emits only completed notification")
var count: int = controller.queue.size()
controller.update(0.0)
check(controller.queue.size() == count, "Repeated update does not duplicate events")
world.hide()
var time: float = controller.elapsed
controller.update(5.0)
check(controller.elapsed == time, "Menu pauses notification")
world.show()
controller.update(3.4)
check(controller.current.is_empty() and not controller.panel.visible, "Notification expires")
for header in controller.headers: check(header.modulate.a == 1.0, "Indicators restored")
controller.update(0.0)
var remaining: int = controller.queue.size()
var click := InputEventMouseButton.new()
click.button_index = MOUSE_BUTTON_LEFT
click.pressed = true
controller._on_notification_input(click)
check(controller.current.is_empty() and not controller.panel.visible, "Click dismisses notification")
check(controller.queue.size() == remaining, "Click preserves pending notifications")
for header in controller.headers: check(header.modulate.a == 1.0, "Click restores indicators")
stage.owniverse = MockWorld.new()
controller.update(0.0)
check(controller.progress.a71.unlocked, "New universe preserves unlocked achievements")
controller.progress.clear()
controller._load_progress()
check(controller.progress.a71.unlocked, "Progress survives reload")
# Every enabled definition must be reachable through its actual compiled rule.
for id in controller.definitions:
var rule: Dictionary = controller.conditions[id].achieve
var source := {"time": {"year": 0}, "stats": {"total_amount": {}}}
if rule.type == "age": source.time.year = E.subtract(rule.amount, 1)
else: source.stats.total_amount[rule.id] = E.subtract(rule.amount, 1)
check(not controller._matches(rule, source), id + " stays locked below threshold")
if rule.type == "age": source.time.year = rule.amount
else: source.stats.total_amount[rule.id] = rule.amount
check(controller._matches(rule, source), id + " completes at exact threshold")
var global: Node = root.get_node("Global")
var previous_locale: String = global.Locale
global.Locale = "ru"
controller.update(0.0)
check(controller.status.text.begins_with("Открыто"), "Status localizes")
check(title.texture == Controller.TITLES.ru, "Russian title selected")
check(controller.items.a71.event_label.text.contains("90 квадриллионов"), "Russian achievement text selected")
global.Locale = "en"
controller.update(0.0)
check(controller.status.text.begins_with("Unlocked"), "English status selected")
check(title.texture == Controller.TITLES.en, "English title selected")
check(controller.items.a71.event_label.text.contains("90 quadrillion"), "English achievement text selected")
controller.current = {"id": "a71", "type": "c"}
controller._display_event(controller.current)
check(controller.notification_item.event_label.text.contains("90 quadrillion"), "English notification text selected")
global.Locale = "ru"
controller.update(0.0)
check(controller.notification_item.event_label.text.contains("90 квадриллионов"), "Active notification changes language")
global.Locale = previous_locale
var path: String = controller.progress_path
stage.free()
DirAccess.remove_absolute(path)
print("Achievement checks: ", "PASS" if failures == 0 else "FAIL")
quit(1 if failures else 0)
+1
View File
@@ -0,0 +1 @@
uid://uslcyxm8vpcn
+4
View File
@@ -38,6 +38,10 @@ func run() -> void:
actions.set_actions("step", 123, func ( request ) : return E.multiply(E.floor_value(E.divide(request, 5)), 5)) actions.set_actions("step", 123, func ( request ) : return E.multiply(E.floor_value(E.divide(request, 5)), 5))
check(E.eq(actions.action_values.values(), [10, 100, 123, 0, 0]), "Unproducible small requests omitted") check(E.eq(actions.action_values.values(), [10, 100, 123, 0, 0]), "Unproducible small requests omitted")
check(E.eq(actions.action_labels[2].text, "120"), "Recipe preview displayed while original request retained") check(E.eq(actions.action_labels[2].text, "120"), "Recipe preview displayed while original request retained")
actions.mark_pending(123)
check(actions.action_labels[2].text == "…" and actions.action_labels[2].get_parent().get_node("TextureButton").disabled, "Queued action is visibly pending and cannot be duplicated")
actions.set_actions("step", 123, func ( request ) : return E.multiply(E.floor_value(E.divide(request, 5)), 5))
check(actions.action_labels[2].text == "120" and not actions.action_labels[2].get_parent().get_node("TextureButton").disabled, "Action refresh clears pending state")
var emitted: Array = [] var emitted: Array = []
actions.action_pressed.connect(func ( _page , amount ) : emitted.append(amount)) actions.action_pressed.connect(func ( _page , amount ) : emitted.append(amount))
actions._on_action_button_pressed(2) actions._on_action_button_pressed(2)
+57
View File
@@ -0,0 +1,57 @@
extends SceneTree
var checks: int = 0
var failures: int = 0
func _initialize() -> void:
call_deferred("run")
func check(condition: bool, message: String) -> void:
checks += 1
if not condition:
failures += 1
push_error(message)
func run() -> void:
var world = load("res://world/owniverse/owniverse.gd").new()
world.adaptive_cadence = false
world.update_adaptive_quality(250000)
check(world.adaptive_quality == 2, "A slow cycle selects the medium approximation")
world.update_adaptive_quality(250000)
check(world.adaptive_quality == 3, "A slow medium-approximation cycle selects the coarse mode")
check(world.adaptive_generation_phase == 1, "Coarse generation phase alternates")
for i in 36: world.update_adaptive_quality(100000)
check(world.adaptive_quality == 2, "Thirty consecutive recovery cycles lower quality by one level")
for i in 30: world.update_adaptive_quality(100000)
check(world.adaptive_quality == 1, "Sustained headroom lowers quality gradually")
for i in 30: world.update_adaptive_quality(100000)
check(world.adaptive_quality == 0, "The exact mode returns after sustained headroom")
var cadence_world = load("res://world/owniverse/owniverse.gd").new()
for i in 3: cadence_world.update_adaptive_quality(350000)
check(cadence_world.cadence_level == 3, "Sustained 350 ms cycles select the 2 Hz cadence")
check(is_equal_approx(cadence_world.frame_step_seconds, 0.5), "The lowest cadence uses a 0.5 second frame step")
cadence_world.free()
var rows: Array = []
for node in 12:
var edges: Dictionary = {}
var outgoing: float = 0.0
if node < 6:
edges[node + 6] = 0.25
outgoing = 0.25
rows.append({"edges": edges, "outgoing": outgoing})
var generator: Dictionary = {"rows": rows, "index": {"source": 0}}
var solved: Dictionary = world.civilization_flow._solve_single_step(generator, "source", 2.0)
for ethic in 6:
var mass: float = 0.0
var occupation: float = 0.0
for node in range(ethic, 12, 6):
mass += solved.mass[node]
occupation += solved.occupation[node]
check(absf(mass - 1.0) < 1e-12, "Approximate transition conserves ethic mass %d" % ethic)
check(absf(occupation - 2.0) < 1e-12, "Approximate transition conserves integrated time %d" % ethic)
world.free()
print("Adaptive quality: %d checks, %d failures" % [checks, failures])
quit(1 if failures else 0)
+1
View File
@@ -0,0 +1 @@
uid://ct356urggcgal
+98
View File
@@ -109,5 +109,103 @@ func run() -> void:
world.batches.withdraw_batches(world.state, "N", 1) world.batches.withdraw_batches(world.state, "N", 1)
check(world.state.batches.N.is_empty(), "Scaling invalidates readiness and removes zero cohorts") check(world.state.batches.N.is_empty(), "Scaling invalidates readiness and removes zero cohorts")
dispose(world) dispose(world)
for selected_speed in [4, 5, 6]:
world = fixture()
world.speed = selected_speed
world.state.time = {"datetime": 37, "year": 37}
var cycle_width: Variant = E.multiply(E.power(10, selected_speed - 1), E.of("0.2"))
world.catalog.entities.HG.lifespan = E.multiply(cycle_width, 3)
world.state.batches = {}
var life: Variant = world.batches.effective_lifespan(world.state, "HG")
var deadline: Variant = E.add(37, E.multiply(cycle_width, 4))
world.batches.append_batch(world.state, "HG", huge, E.add(37, 1), E.add(37, 1))
world.batches.append_batch(world.state, "HG", 7, E.add(37, 2), E.add(37, 2))
check(world.state.batches.HG.size() == 1 and E.eq(world.state.batches.HG[0].amount, E.add(huge, 7)), "Births in the same death cycle merge exactly")
var point: Dictionary = world.state.batches.HG[0]
check(E.eq(point.start_year, deadline) and E.eq(point.end_year, deadline), "High-speed stellar cohort transitions at one cycle boundary")
var born: Variant = E.add(37, 1)
var delay: Variant = E.subtract(deadline, E.add(born, life))
check(E.ge(delay, 0) and E.le(delay, cycle_width), "Transition is delayed by at most one cycle")
var encoded: Dictionary = E.decode(E.encode(world.state))
check(E.eq(encoded.batches, world.state.batches), "Cycle cohorts survive exact save roundtrip")
check(world.batches.withdraw_batches(world.state, "HG", E.subtract(deadline, 1)).is_empty(), "Cycle cohort is not withdrawn partially")
var expired: Array = world.batches.withdraw_batches(world.state, "HG", deadline)
check(expired.size() == 1 and E.eq(expired[0].amount, E.add(huge, 7)) and world.state.batches.HG.is_empty(), "Boundary withdraws the exact whole cohort once")
check(world.batches.withdraw_batches(world.state, "HG", deadline).is_empty(), "Repeated boundary cannot withdraw twice")
world.batches.append_batch(world.state, "HG", 3, E.add(37, 1), E.add(37, 1))
check(E.eq(world.state.batches.HG[0].amount, 3), "Withdrawal invalidates cycle insertion references")
world.state.batches.HG = []
world.batches.append_batch(world.state, "HG", 11, 37, E.add(37, E.multiply(cycle_width, 3)))
var total: Variant = 0
for batch: Dictionary in world.state.batches.HG:
total = E.add(total, batch.amount)
check(E.eq(batch.start_year, batch.end_year), "Distributed births create point cohorts")
check(E.eq(total, 11) and world.state.batches.HG.size() == 3, "Distributed births retain all integers across death cycles")
var before_coarsen: Dictionary = world.state.duplicate(true)
world.batches.coarsen(world.state)
check(E.eq(before_coarsen, world.state), "Coarsening cannot smear point stellar transitions")
world.state.batches.HG = []
var completed: Dictionary = {}
var cutoff: Variant = E.add(37, E.multiply(cycle_width, 10))
var integral: Variant = world.batches.append_batch(world.state, "HG", 11, 37, E.add(37, E.multiply(cycle_width, 3)), completed, cutoff)
var reference_integral: Variant = 0
total = 0
for part: Dictionary in completed.HG:
total = E.add(total, part.amount)
reference_integral = E.add(reference_integral, E.multiply(part.amount, E.subtract(cutoff, part.to)))
reference_integral = E.subtract(E.multiply(11, E.subtract(cutoff, E.add(37, E.multiply(cycle_width, E.of("1.5"))))), reference_integral)
check(E.eq(total, 11) and world.state.batches.HG.is_empty(), "Already completed stars retain counts without stored batches")
check(E.eq(integral, reference_integral), "Lifetime production agrees with point transitions")
world.speed = 1
world.state.batches.HG = []
world.batches.append_batch(world.state, "HG", 10, 100, 100)
check(E.gt(world.state.batches.HG[0].end_year, world.state.batches.HG[0].start_year), "Low speed retains gradual stellar transitions")
dispose(world)
world = fixture()
world.speed = 6
world.state.amount.RHG = 5
world.batches.append_batch(world.state, "RHG", 5, 0, 0)
world.ledger.refresh_groups(world.state)
var validation: String = root.get_node("SaveManager").validate_data({"format_version": 6, "app_version": "", "ui": {"current_page": 0}, "state": world.state})
check(validation.is_empty(), "Cycle-star state born at zero validates: " + validation)
var lifespan: Variant = world.batches.effective_lifespan(world.state, "RHG")
check(E.eq(world.state.batches.RHG[0].start_year, lifespan), "Birth exactly aligned with a death boundary adds no extra cycle")
dispose(world)
world = fixture()
world.speed = 6
world.state.batches.RD = []
world.batches.append_batch(world.state, "RD", huge, 0, 2000000)
check(world.state.batches.RD.size() == 1, "Long-lived stars are not subdivided into cycle buckets")
var long_cohort: Dictionary = world.state.batches.RD[0].duplicate(true)
world.batches.append_batch(world.state, "RD", 7, 10000, 20000)
check(world.state.batches.RD.size() == 1 and E.eq(world.state.batches.RD[0].amount, E.add(huge, 7)), "New births still merge into their original wide stellar group")
check(E.eq(world.state.batches.RD[0].start_year, long_cohort.start_year) and E.eq(world.state.batches.RD[0].end_year, long_cohort.end_year), "Long-lived transition interval is preserved")
world.state.batches.RD = [
{"amount": huge, "start_year": 1000, "end_year": 2000, "withdraw_remainder": 0.25},
{"amount": 7, "start_year": 3000, "end_year": 4000, "withdraw_remainder": 0.5},
{"amount": 9, "start_year": 19000, "end_year": 21000},
{"amount": 11, "start_year": 22000, "end_year": 1000000},
{"amount": 13, "start_year": 21000, "end_year": 22000},
]
var spanning: Dictionary = world.state.batches.RD[2].duplicate(true)
var wide: Dictionary = world.state.batches.RD[3].duplicate(true)
world.batches.coarsen(world.state)
check(world.state.batches.RD.size() == 4, "Only cohorts ending entirely within the same cycle are merged")
var merged: Dictionary = {}
var broad_kept: bool = false
var crossing_kept: bool = false
for batch: Dictionary in world.state.batches.RD:
if E.eq(batch.start_year, 20000) and E.eq(batch.end_year, 20000): merged = batch
if E.eq(batch, wide): broad_kept = true
if E.eq(batch, spanning): crossing_kept = true
check(not merged.is_empty() and E.eq(merged.amount, E.add(huge, 7)) and E.eq(merged.withdraw_remainder, 0.75), "Merged stellar cohort retains exact population and carries")
check(broad_kept and crossing_kept, "Wide and cycle-crossing cohorts remain unchanged")
var once: Dictionary = world.state.duplicate(true)
world.batches.coarsen(world.state)
check(E.eq(world.state, once), "Dynamic stellar merging is idempotent")
world.state.batches.HG = [{"amount": 3, "start_year": 20000, "end_year": 20000}, {"amount": 4, "start_year": 20001, "end_year": 21000}]
world.batches.coarsen(world.state)
check(world.state.batches.HG.size() == 2, "An exact boundary belongs to the preceding cycle")
dispose(world)
print(("Batch boundaries: %d checks, %d failures" % [checks, failures])) print(("Batch boundaries: %d checks, %d failures" % [checks, failures]))
quit((0 if E.eq(failures, 0) else 1)) quit((0 if E.eq(failures, 0) else 1))
+95 -124
View File
@@ -1,165 +1,136 @@
extends SceneTree extends SceneTree
const E = preload("res://world/owniverse/math/exact.gd") const E = preload("res://world/owniverse/math/exact.gd")
const Clock = preload("res://world/owniverse/simulation_time.gd") const State = preload("res://world/owniverse/civilization_state.gd")
var checks: int = 0 var checks: int = 0
var failures: int = 0 var failures: int = 0
func _initialize() -> void: func _initialize(): call_deferred("run")
call_deferred("run") func check(value: bool, message: String) -> void:
checks += 1
func check(condition: bool, message: String) -> void: if not value:
checks = E.add(checks, 1) failures += 1
if (not condition):
failures = E.add(failures, 1)
push_error(message) push_error(message)
func fixture(): func fixture():
var world = load("res://world/owniverse/owniverse.gd").new() var world = load("res://world/owniverse/owniverse.gd").new()
var global: Node = root.get_node("Global") world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
for id in global.OwniverseEntities: for id in world.catalog.entities:
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
world.catalog.entities[id] = entity
world.state.amount[id] = 0 world.state.amount[id] = 0
world.state.stats.total_amount[id] = 0 world.state.stats.total_amount[id] = 0
if E.ne(entity.group_as, ""): for id in ["RD", "OD", "YD", "WYD", "BG", "TWD", "SNS", "MGT", "BH25", "SMBH", "SCD", "SCS", "SCM", "SCL", "SCG", "GED", "GES", "GEM", "GEL", "GEG", "LGG", "LSC", "LGF", "LGW", "LUB"]:
world.state.amount[entity.group_as] = 0 if world.state.amount.has(id): world.state.amount[id] = E.of("1e60")
world.state.stats.total_amount[entity.group_as] = 0
world.catalog.entities["N"].life_product = {}
world.catalog.entities["N"].product_per_year_type = "H"
world.catalog.entities["N"].product_per_year_amount = 10
world.catalog.entities["N"].auto = 0
world.state.amount.N = 1
return world return world
func dispose(world) -> void: func dispose(world):
for entity in world.catalog.entities.values(): for entity in world.catalog.entities.values(): entity.free()
entity.free()
world.free() world.free()
func seed_population(world, id: String, count: Variant, time: Variant = 1.0) -> void: func population(world) -> Variant:
world.state.amount.N = 0
world.state.amount[id] = count
world.state.batches[id] = [{"amount": count, "start_year": time, "end_year": time}]
world.evolution.initialize_life_batches()
func sum_ethics(world, id: String) -> Variant:
var result: Variant = 0 var result: Variant = 0
for n in world.state.ethics.get(id, {}).values(): result = E.add(result, n) for id in world.ledger.physical_amounts(world.state):
if root.get_node("Global").CivilizationRules.has(id) or world.catalog.entities[id].group_as == "Planet":
result = E.add(result, world.ledger.physical_amount(world.state, id))
return result 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 physical:
check(E.ge(physical[id], 0) and E.eq(physical[id], E.floor_value(physical[id])), "Physical counts remain nonnegative whole: " + id)
for id in world.state.ethics: 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 value 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(is_finite(value) and value >= 0.0 and value <= 1.0, "Finite normalized ethic shares")
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Civilization state validates") total += value
check(absf(total - 1.0) < 1e-12, "Ethic shares sum to one: " + id)
func run() -> void: func configure(rules: Dictionary, progress: float, degradation: float, span: int = 100) -> void:
for id in rules:
if rules[id].ethics.is_empty(): continue
rules[id].span = span
rules[id].fights = false
rules[id].colonizes = false
rules[id].diplomacy = false
for row in rules[id].ethics.values():
row.progress = progress
row.degradation = degradation
row.war = 0
row.colonization = 0
row.fall = rules[id].previous + ":1"
func run():
var global: Node = root.get_node("Global") var global: Node = root.get_node("Global")
var baseline: Dictionary = global.CivilizationRules.duplicate(true) var baseline: Dictionary = global.CivilizationRules.duplicate(true)
configure(global.CivilizationRules, 1.0, 0.0)
var world = fixture() var world = fixture()
seed_population(world, "C0.2", 60000) var count: Variant = E.of("10000000000000000000000000000000000000007")
world.evolution.evolve_batches(world.state, 1) world.state.amount["C0.2"] = count
check(E.gt(world.state.amount["C0.3"], 0), "C0.2 actually progresses") world.state.ethics["C0.2"] = State.empty_profile()
check(E.gt(world.state.amount["C0.1"], 0), "C0.2 actually degrades") world.civilization_flow.evolve(world.state, 0, 100)
check(E.eq(world.state.civilization_stats.get("wars", 0), 0), "Table disables early wars") check(E.eq(population(world), count), "Continuous chain conserves arbitrary-size whole population")
check(E.eq(E.add(E.add(world.state.amount["C0.2"], world.state.amount["C0.3"]), world.state.amount["C0.1"]), 60000), "Ordinary evolution conserves population") check(absf(E.approx(E.divide(world.state.amount["C0.2"], count)) - exp(-1.0)) < 1e-12, "Survival follows the exponential interval law")
check(E.eq(world.state.stats.total_amount["C0.2"], 0), "Survivors do not count as new civilizations") check(E.gt(world.state.amount["C0.4"], 0), "One interval includes multiple successive stages")
check(E.gt(world.state.civilization_stats.worship, 0), "Worship derived from surviving ethics") check(not world.state.batches.has("C0.2") and not world.state.batches.has("C0.3"), "Civilizations do not create age batches")
invariants(world) invariants(world)
var restored = fixture() var half = fixture()
restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state)))) half.state.amount["C0.2"] = count
world.evolution.evolve_batches(world.state, 2100) half.state.ethics["C0.2"] = State.empty_profile()
restored.evolution.evolve_batches(restored.state, 2100) half.civilization_flow.evolve(half.state, 0, 50)
check(E.eq(world.state, restored.state), "Save resumes deterministic ethical evolution") half.civilization_flow.evolve(half.state, 50, 100)
invariants(world) for id in global.CivilizationRules:
dispose(restored) check(E.approx(E.divide(E.absolute(E.subtract(world.state.amount.get(id, 0), half.state.amount.get(id, 0))), count)) < 1e-12, "Splitting the interval preserves continuous dynamics: " + id)
dispose(half)
dispose(world) dispose(world)
configure(global.CivilizationRules, 0.0, 0.0)
world = fixture() world = fixture()
world.state.amount.RD = 100000000 world.state.amount["C0.2"] = 10001
seed_population(world, "C2.0", 60000) world.state.ethics["C0.2"] = {"m": 1.0, "l": 0.0, "w": 0.0, "d": 0.0, "c": 0.0, "b": 0.0}
world.state.amount["C1.9"] = 10000 world.civilization_flow.evolve(world.state, 0, 100)
world.evolution.initialize_life_batches() check(E.eq(world.state.amount["C0.2"], 10001), "Disabled stage movement retains every individual")
world.evolution.evolve_batches(world.state, 1) check(world.state.ethics["C0.2"].m < 1.0 and world.state.ethics["C0.2"].m > 0.9, "Analytical drift changes shares without materializing populations")
check(E.gt(world.state.civilization_stats.wars, 0), "Wars execute when enabled")
check(E.gt(world.state.civilization_stats.collateral, 0), "Wars affect previous-stage populations")
check(E.gt(world.state.amount["C2.1"], 0), "Winners and peaceful progress reach next stage")
invariants(world) invariants(world)
dispose(world) dispose(world)
configure(global.CivilizationRules, 1.0, 0.0)
world = fixture() world = fixture()
world.state.amount.RD = 1 for id in ["RD", "OD", "YD", "WYD"]: world.state.amount[id] = 0
world.state.binds.RD = {"HP": 5, "GG": 5, "AB": 5, "RP": 5} world.state.amount["C1.9"] = 101
seed_population(world, "C1.6", 60000) world.civilization_flow.evolve(world.state, 0, 100)
for row : Dictionary in global.CivilizationRules["C1.6"].ethics.values(): row.degradation = "0" check(E.eq(world.state.amount["C1.9"], 101), "Unfunded interstellar flows return to their source")
world.evolution.evolve_batches(world.state, 1) check(E.eq(population(world), 101), "Resource admission conserves exact population")
var after: Dictionary = world.ledger.physical_amounts(world.state) dispose(world)
check(E.eq(world.state.civilization_stats.colonies, 20), "Colonization limited to real targets")
check(E.eq(E.add(E.add(E.add(after.HP, after.GG), after.AB), after.RP), 0), "Colonized objects consumed once") configure(global.CivilizationRules, 0.0, 0.0, 1)
check(E.eq(world.state.binds.RD.get("C1.4", 0), 20), "Colonies retain target host and use two-stages-earlier level") world = fixture()
var totals: Variant = 0 world.state.amount["C1.6"] = 60000
for id in after: world.state.binds.RD = {"HP": 5, "RP": 5, "GG": 5, "AB": 5}
if E.eq(world.catalog.entities[id].group_as, "Planet"): totals = E.add(totals, after[id]) global.CivilizationRules["C1.6"].colonizes = true
check(E.eq(totals, 60020), "Colonization replaces planets, does not create extra worlds") for row in global.CivilizationRules["C1.6"].ethics.values(): row.colonization = 1
world.civilization_flow.evolve(world.state, 0, 1)
check(E.eq(world.state.civilization_stats.colonies, 20), "Integrated colonization is limited by real planet stock")
check(E.eq(world.state.binds.RD.get("C1.4", 0), 20), "Colonies retain the target stellar owner")
check(E.eq(population(world), 60020), "Colonization creates precisely the consumed number of occupied worlds")
var physical: Dictionary = world.ledger.physical_amounts(world.state)
for resource in ["HP", "RP", "GG", "AB"]: check(E.eq(physical[resource], 0), "Planet stock consumed once: " + resource)
invariants(world) invariants(world)
dispose(world) dispose(world)
global.CivilizationRules = baseline.duplicate(true) global.CivilizationRules = baseline.duplicate(true)
world = fixture() world = fixture()
world.state.amount.RD = E.of("1e25") world.state.amount["C2.0"] = 60000
world.state.amount.BG = E.of("1e25") world.state.amount["C1.9"] = 10000
world.state.amount.TWD = E.of("1e25") world.civilization_flow.evolve(world.state, 0, 20000)
world.state.amount.SNS = E.of("1e25") check(E.gt(world.state.civilization_stats.wars, 0), "War rewards integrate encounters during the interval")
world.state.amount.MGT = E.of("1e25") check(E.gt(world.state.civilization_stats.collateral, 0), "Collateral damage reaches previous stages")
world.state.amount.BH25 = E.of("1e25") check(E.eq(population(world), 70000), "War outcomes and collateral conserve occupied-world counts")
world.state.amount.SMBH = E.of("1e26")
for id in world.catalog.entities:
if E.ne(world.catalog.entities[id].claim, ""): world.state.amount[id] = E.of("1e12")
# Accelerated deterministic fixture traverses every enabled stage, including Ct.
for id in global.CivilizationRules:
if E.eq(id, "Ct"): continue
global.CivilizationRules[id].fights = false
global.CivilizationRules[id].colonizes = false
global.CivilizationRules[id].diplomacy = false
for row : Dictionary in global.CivilizationRules[id].ethics.values():
row.progress = "1"
row.degradation = "0"
world.catalog.entities[id].lifespan = 1
world.catalog.entities[id].lifespan_delta = 1
global.CivilizationRules[id].span = 1
seed_population(world, "C0.2", 6)
world.evolution.evolve_batches(world.state, 1000)
check(E.eq(world.state.amount.Ct, 6), "Complete C0.2 -> C4.4 -> Ct chain runs")
check((not world.state.batches.has("Ct")), "Transcendence is terminal")
invariants(world) invariants(world)
var restored = fixture()
restored.state = E.decode(E.encode(world.state))
world.civilization_flow.evolve(world.state, 20000, 30000)
restored.civilization_flow.evolve(restored.state, 20000, 30000)
check(E.eq(world.state, restored.state), "Save resume reproduces flow populations and carries")
dispose(restored)
dispose(world) dispose(world)
global.CivilizationRules = baseline global.CivilizationRules = baseline
print("Civilization flows: %d checks, %d failures" % [checks, failures])
world = fixture() quit(1 if failures else 0)
world.state.amount.RD = 10000
world.state.amount.SCS = 1
world.state.amount["C2.4"] = 1
world.evolution.initialize_life_batches()
var span: Variant = world.state.batches["C2.4"][0].start_year
check(E.near(span, E.divide(20000, E.of("3.6"))), "Structure bonus shortens newly registered lifespan")
dispose(world)
var evolution = load("res://world/owniverse/civilization_evolution.gd")
var balanced: Dictionary = {"m": 10000, "l": 10000, "w": 10000, "d": 10000, "c": 10000, "b": 10000}
var neutral: Dictionary = evolution.drift({}, "test", balanced, global.CivilizationRules["C2.0"].ethics)
var diplomatic: Dictionary = evolution.drift({}, "test", balanced, global.CivilizationRules["C2.0"].ethics, true)
check(E.eq(neutral, balanced), "Ordinary drift preserves balanced ethics")
check(E.gt(diplomatic.d, diplomatic.w), "Diplomacy uses per-ethic influence")
check(E.eq(evolution.total(diplomatic), 60000), "Diplomacy conserves population")
world = fixture()
world.state.amount.RD = E.of("1e15")
world.state.amount.BG = E.of("1e15")
world.state.amount["C3.0"] = 6
world.evolution.initialize_life_batches()
check((E.eq(world.state.amount["C3.0"], 0) and E.eq(world.state.amount["C2.9"], 6)), "Missing mandatory structures degrade existing civilizations")
invariants(world)
dispose(world)
print(("Civilization evolution: %d checks, %d failures" % [checks, failures]))
quit((0 if E.eq(failures, 0) else 1))
+15 -9
View File
@@ -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()
@@ -74,7 +79,8 @@ func run() -> void:
row.colonization = "0" row.colonization = "0"
world.state.binds.RD = {"C1.9": 10} world.state.binds.RD = {"C1.9": 10}
world.state.batches["C1.9"] = [{"amount": 10, "start_year": 1, "end_year": 1}] world.state.batches["C1.9"] = [{"amount": 10, "start_year": 1, "end_year": 1}]
world.evolution.evolve_batches(world.state, 1) global.CivilizationRules["C1.9"].span = E.of("0.001")
world.civilization_flow.evolve(world.state, 0, 1)
check((E.eq(world.state.amount["C2.0"], 10) and E.eq(world.state.binds.RD.get("C2.0", 0), 0)), "Space successor does not inherit planetary binding") check((E.eq(world.state.amount["C2.0"], 10) and E.eq(world.state.binds.RD.get("C2.0", 0), 0)), "Space successor does not inherit planetary binding")
dispose(world) dispose(world)
var capacity = load("res://world/owniverse/civilization_state.gd") var capacity = load("res://world/owniverse/civilization_state.gd")
@@ -117,7 +123,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()
@@ -128,7 +134,7 @@ func run() -> void:
row.colonization = "0" row.colonization = "0"
world.state.binds.RD = {"C1.9": 5} world.state.binds.RD = {"C1.9": 5}
world.state.batches["C1.9"] = [{"amount": 5, "start_year": 1, "end_year": 1}] world.state.batches["C1.9"] = [{"amount": 5, "start_year": 1, "end_year": 1}]
world.evolution.evolve_batches(world.state, 1) world.civilization_flow.evolve(world.state, 0, 1)
check((E.eq(world.state.amount["C2.0"], 2) and E.eq(world.state.binds.RD["C1.9"], 3)), "Unfunded transition retains the original planetary civilization") check((E.eq(world.state.amount["C2.0"], 2) and E.eq(world.state.binds.RD["C1.9"], 3)), "Unfunded transition retains the original planetary civilization")
dispose(world) dispose(world)
print(("Civilization state: %d checks, %d failures" % [checks, failures])) print(("Civilization state: %d checks, %d failures" % [checks, failures]))
+2 -2
View File
@@ -53,11 +53,11 @@ func run():
check(E.eq(physical.get("C0.1", 0), 38300), "C0.1 attempts evolution once") check(E.eq(physical.get("C0.1", 0), 38300), "C0.1 attempts evolution once")
check(E.eq(physical.get("C0.2", 0), 700), "C0.1 successor does not immediately attempt again") check(E.eq(physical.get("C0.2", 0), 700), "C0.1 successor does not immediately attempt again")
else: else:
check(world.profile.civilization_transitions[1] == 1, "Ethical civilization is processed only once per birth bucket") check(world.profile.civilization_flow_plan[1] == 1, "Ethical civilization uses one continuous interval plan")
var before: Dictionary = world.state.duplicate(true) 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"]:
+1 -1
View File
@@ -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])
+78
View File
@@ -0,0 +1,78 @@
extends "res://tests/test_civilization_evolution.gd"
func run():
var rules: Dictionary = root.get_node("Global").CivilizationRules
for count in [103, E.of("10000000000000000000000000000000000000103"), E.add(E.of("1e400"), 103)]:
var world = fixture()
var third: Variant = E.floor_value(E.divide(count, 3))
var free: Variant = E.subtract(count, E.multiply(third, 2))
world.state.amount["C0.2"] = free
world.state.binds.RD = {"C0.2": third}
world.state.binds.OD = {"C0.2": third}
var counts: Dictionary = {"C0.2": third, "C0.3": third, "HP": free}
var profile: Dictionary = {"m": 0.4, "l": 0.2, "w": 0.1, "d": 0.1, "c": 0.1, "b": 0.1}
var profiles: Dictionary = {}
for id in counts: profiles[id] = profile
var physical: Dictionary = world.ledger.physical_amounts(world.state)
var initial_physical: Dictionary = physical.duplicate()
var committed: Dictionary = world.civilization_flow._commit(world.state, physical, {"C0.2": {"counts": counts, "profiles": profiles}}, rules, 100)
check(E.eq(committed, world.ledger.physical_amounts(world.state)), "Commit snapshot matches a full physical rebuild")
check(E.eq(physical, initial_physical), "Commit does not mutate its input snapshot")
for id in counts:
check(E.eq(world.ledger.physical_amount(world.state, id), counts[id]), "Commit preserves exact destination quota: " + id)
for owner in ["RD", "OD"]:
var held: Variant = 0
for id in counts: held = E.add(held, world.state.binds[owner].get(id, 0))
check(E.eq(held, third), "Commit preserves each owner's exact population")
for id in ["C0.2", "C0.3"]:
for ethic in profile: check(absf(world.state.ethics[id][ethic] - profile[ethic]) < 1e-12, "Commit preserves incoming ethical profile")
invariants(world)
dispose(world)
world = fixture()
world.state.binds.RD = {"C4.4": count}
var terminal: Dictionary = {"Ct": State.empty_profile()}
world.civilization_flow._commit(world.state, world.ledger.physical_amounts(world.state), {"C4.4": {"counts": {"Ct": count}, "profiles": terminal}}, rules, 100)
check(E.eq(world.state.amount.Ct, count), "Transcendent population is entirely free")
check(E.eq(world.state.binds.RD.get("Ct", 0), 0), "Transcendent population cannot retain a stellar owner")
dispose(world)
world = fixture()
world.state.amount["C2.0"] = count
var initial_capacity: Dictionary = world.ledger.physical_amounts(world.state)
var frozen_capacity: Dictionary = initial_capacity.duplicate()
var constrained: Dictionary = State.enforce_capacity(world, world.state, rules, 100, initial_capacity)
check(E.eq(constrained, world.ledger.physical_amounts(world.state)), "Cascading capacity changes maintain exact physical totals")
check(E.eq(initial_capacity, frozen_capacity), "Capacity correction does not mutate the previous snapshot")
dispose(world)
world = fixture()
var original_owners: Dictionary = {}
for owner in ["RD", "OD", "YD", "WYD"]:
world.state.binds[owner] = {}
for resource in ["GG", "AB", "RP", "HP"]:
world.state.binds[owner][resource] = count
original_owners[owner] = E.multiply(count, 4)
var colonies: Dictionary = {
"C1.4": {"counts": {"C1.4": E.multiply(count, 3)}, "profiles": {"C1.4": profile}},
"C1.5": {"counts": {"C1.5": E.multiply(count, 5)}, "profiles": {"C1.5": profile}}
}
var physical_before: Dictionary = world.ledger.physical_amounts(world.state)
var untouched: Dictionary = physical_before.duplicate()
var colonized: Dictionary = world.civilization_flow._colonize(world.state, colonies, 100, rules, physical_before)
check(E.eq(colonized, world.ledger.physical_amounts(world.state)), "Colonization snapshot matches a full physical rebuild")
check(E.eq(physical_before, untouched), "Colonization does not mutate its input snapshot")
var capacity: Dictionary = State.enforce_capacity(world, world.state, rules, 100, colonized)
check(E.eq(capacity, world.ledger.physical_amounts(world.state)), "Capacity snapshot matches a full physical rebuild")
check(E.eq(world.ledger.physical_amount(world.state, "C1.4"), E.multiply(count, 3)), "Colonization credits exact first quota")
check(E.eq(world.ledger.physical_amount(world.state, "C1.5"), E.multiply(count, 5)), "Colonization credits exact second quota")
check(E.eq(world.state.stats.total_amount["C1.4"], E.multiply(count, 3)), "Consolidated credit preserves creation statistics")
check(E.eq(world.state.civilization_stats.colonies, E.multiply(count, 8)), "Colonization statistics equal exact granted total")
for owner in original_owners:
var held: Variant = 0
for id in ["GG", "AB", "RP", "HP", "C1.4", "C1.5"]: held = E.add(held, world.state.binds[owner].get(id, 0))
check(E.eq(held, original_owners[owner]), "Colonization preserves each owner's occupied worlds")
var remaining_planets: Variant = 0
for id in ["GG", "AB", "RP", "HP"]: remaining_planets = E.add(remaining_planets, world.ledger.physical_amount(world.state, id))
check(E.eq(remaining_planets, E.multiply(count, 8)), "Planet loss equals colonies created")
invariants(world)
dispose(world)
print("Flow commit: %d checks, %d failures" % [checks, failures])
quit(1 if failures else 0)
+1
View File
@@ -0,0 +1 @@
uid://bfgdsxoycr20e
+48 -29
View File
@@ -35,24 +35,28 @@ func run():
direct = fixture() direct = fixture()
sliced = fixture() sliced = fixture()
var initial_h: Variant = sliced.state.amount.H var initial_h: Variant = sliced.state.amount.H
sliced.evolute_frame(1.0 / 60.0, self) sliced.evolute_frame(0.2, self)
check(sliced.frame_busy, "Heavy transaction yields before publishing") check(sliced.frame_busy, "Heavy transaction yields before publishing")
check(E.eq(sliced.state.time.year, 0), "Published clock stays at completed transaction") check(E.eq(sliced.state.time.year, 0), "Published clock stays at completed transaction")
sliced.request_production("H", 1, true) check(E.eq(sliced.display_clock().year, Clock.advance({"datetime": 0, "year": 0}, 0.2, sliced.speed).year), "Display clock advances independently of the heavy transaction")
check(sliced.request_production("H", 1, true), "Production requested during a frame is queued")
check(sliced.frame_checkpoint().productions.size() == 1, "Queued production remains part of the save checkpoint")
check(E.eq(sliced.state.amount.H, initial_h), "Player action waits for transaction commit") check(E.eq(sliced.state.amount.H, initial_h), "Player action waits for transaction commit")
sliced.evolute_frame(0.01, self) sliced.evolute_frame(0.01, self)
var requested: Dictionary = sliced.frame_clock.duplicate() var requested: Dictionary = sliced.frame_clock.duplicate()
sliced.speed = 3 sliced.speed = 3
check(sliced.speed == 3 and sliced.state.speed == 6, "Speed change is visible and cannot mutate an unfinished transaction") check(sliced.speed == 3 and sliced.state.speed == 6, "Speed change is visible and cannot mutate an unfinished transaction")
while sliced.frame_busy: await process_frame while sliced.frame_busy or sliced.production_queue_busy: await process_frame
direct.evolute(1.0 / 60.0) direct.evolute(0.2)
direct.production.produce_hs("H", 1) direct.production.produce_hs("H", 1)
direct.speed = 3 direct.speed = 3
check(E.eq(direct.state, sliced.state), "Queued action and speed change survive publication") check(E.eq(direct.state, sliced.state), "Queued action and speed change survive publication")
sliced.evolute_frame(0.001, self) sliced.evolute_frame(0.001, self)
var second_clock: Dictionary = Clock.advance(requested, 0.001, 3) check(not sliced.frame_busy, "Speed change does not bypass real-time cadence")
sliced.evolute_frame(0.189, self)
var second_clock: Dictionary = Clock.advance(Clock.advance(requested, 0.001, 3), 0.189, 3)
var active: Dictionary = sliced.frame_active_clock.duplicate() var active: Dictionary = sliced.frame_active_clock.duplicate()
while sliced.frame_busy: await process_frame while sliced.frame_busy or sliced.production_queue_busy: await process_frame
direct.evolution.evolute_to_datetime(active.datetime, active) direct.evolution.evolute_to_datetime(active.datetime, active)
check(E.eq(direct.state, sliced.state), "Bounded frame transaction matches its exact target clock") check(E.eq(direct.state, sliced.state), "Bounded frame transaction matches its exact target clock")
check(E.eq(sliced.frame_clock, second_clock), "All elapsed time during a busy frame remains in the requested clock") check(E.eq(sliced.frame_clock, second_clock), "All elapsed time during a busy frame remains in the requested clock")
@@ -60,7 +64,7 @@ func run():
dispose(direct) dispose(direct)
dispose(sliced) dispose(sliced)
var original = fixture() var original = fixture()
original.evolute_frame(1.0 / 60.0, self) original.evolute_frame(0.2, self)
original.request_production("H", 123, true) original.request_production("H", 123, true)
original.evolute_frame(0.01, self) original.evolute_frame(0.01, self)
original.speed = 3 original.speed = 3
@@ -75,7 +79,7 @@ func run():
expected.state = published.duplicate(true) expected.state = published.duplicate(true)
resumed.restore_frame_checkpoint(checkpoint) resumed.restore_frame_checkpoint(checkpoint)
resumed.evolute_frame(0.0, self) resumed.evolute_frame(0.0, self)
while resumed.frame_busy: await process_frame while resumed.frame_busy or resumed.production_queue_busy: await process_frame
expected.evolution.evolute_to_datetime(expected_clock.datetime, expected_clock) expected.evolution.evolute_to_datetime(expected_clock.datetime, expected_clock)
expected.production.produce_hs("H", 123) expected.production.produce_hs("H", 123)
expected.speed = 3 expected.speed = 3
@@ -84,7 +88,7 @@ func run():
dispose(resumed) dispose(resumed)
dispose(expected) dispose(expected)
var cancelled = fixture() var cancelled = fixture()
cancelled.evolute_frame(0.133333333333, self) cancelled.evolute_frame(0.2, self)
check(cancelled.frame_busy, "Cancellation fixture has suspended work") check(cancelled.frame_busy, "Cancellation fixture has suspended work")
var budget_ref: RefCounted = cancelled.frame_budget var budget_ref: RefCounted = cancelled.frame_budget
dispose(cancelled) dispose(cancelled)
@@ -96,25 +100,40 @@ func run():
invalid.evolute(1e308) invalid.evolute(1e308)
check(not invalid.frame_busy and invalid.frame_clock.is_empty() and E.eq(before, invalid.state), "Overflowing elapsed float cannot enter either clock API") check(not invalid.frame_busy and invalid.frame_clock.is_empty() and E.eq(before, invalid.state), "Overflowing elapsed float cannot enter either clock API")
dispose(invalid) dispose(invalid)
var adaptive = fixture() for selected_speed in range(1, 7):
adaptive.frame_clock = Clock.advance(adaptive.state.time, 10.0, 6) var scheduled = fixture()
var debt_clock: Dictionary = adaptive.frame_clock.duplicate(true) scheduled.speed = selected_speed
check(adaptive.update_frame_step() == 0.2, "Growing debt increases step to 0.2 seconds") scheduled.evolute_frame(0.1, self)
check(adaptive.update_frame_step() == 0.5, "Large debt increases step to bounded 0.5 seconds") check(not scheduled.frame_busy and scheduled.frame_commits == 0, "Wait for a full step at speed %d" % selected_speed)
check(adaptive.update_frame_step() == 0.5 and E.eq(adaptive.frame_clock, debt_clock), "Maximum step preserves requested time") scheduled.evolute_frame(0.1, self)
adaptive.frame_clock = Clock.advance(adaptive.state.time, 0.3, 6) while scheduled.frame_busy: await process_frame
check(adaptive.update_frame_step() == 0.5, "Hysteresis keeps step between thresholds") var cadence_clock: Dictionary = Clock.advance({"datetime": 0, "year": 0}, 0.2, selected_speed)
adaptive.frame_clock = Clock.advance(adaptive.state.time, 0.05, 6) check(scheduled.frame_commits == 1 and E.eq(scheduled.state.time, cadence_clock), "One complete 0.2 second cycle at speed %d" % selected_speed)
check(adaptive.update_frame_step() == 0.2, "Small debt reduces step") scheduled.evolute_frame(0.0, self)
check(adaptive.update_frame_step() == 0.1, "Caught-up simulation returns to base step") check(not scheduled.frame_busy and scheduled.frame_commits == 1, "No extra cycle without elapsed time")
adaptive.frame_clock = Clock.advance(adaptive.state.time, 0.15, 6) dispose(scheduled)
adaptive._frame_cost_ratio = 1.1 var behind = fixture()
check(adaptive.update_frame_step() == 0.2, "Measured slow transactions increase step before two-step debt") behind.evolute_frame(1.0, self)
adaptive.frame_clock = Clock.advance(adaptive.state.time, 0.05, 6) while behind.frame_busy: await process_frame
adaptive.speed = 3 check(E.eq(behind.state.time.datetime, 100000) and E.eq(behind.state.time, behind.frame_clock), "Delayed start consumes all elapsed time in one cycle")
check(adaptive.update_frame_step() == 1.0 / 60.0, "Low speeds retain 60 Hz scheduling") behind.evolute_frame(0.0, self)
adaptive.speed = 6 check(behind.frame_commits == 1 and not behind.frame_busy, "Consumed interval cannot be processed twice")
check(adaptive.update_frame_step() == 0.1, "Speed switch resets adaptive step") var next_interval: float = behind.frame_step_seconds + 0.03
dispose(adaptive) behind.evolute_frame(next_interval, self)
var remained_busy: bool = behind.frame_busy
if remained_busy: behind.evolute_frame(0.53, self)
var pending_clock: Dictionary = behind.frame_clock.duplicate()
if remained_busy:
var expected_active: Dictionary = Clock.advance({"datetime": 100000, "year": 100000}, next_interval, behind.speed)
check(E.eq(behind.frame_active_clock, expected_active), "Busy transaction reserves its original target")
while behind.frame_busy: await process_frame
if remained_busy:
check(E.eq(behind.state.time, behind.frame_active_clock), "Busy transaction keeps its original target")
behind.evolute_frame(0.0, self)
while behind.frame_busy: await process_frame
check(E.eq(behind.state.time, pending_clock), "Next cycle consumes all time accumulated during computation")
else:
check(E.eq(behind.state.time, pending_clock), "Completed transaction publishes the full requested interval")
dispose(behind)
print("Frame evolution: %d checks, %d failures" % [checks, failures]) print("Frame evolution: %d checks, %d failures" % [checks, failures])
quit(1 if failures else 0) quit(1 if failures else 0)
+4 -4
View File
@@ -46,7 +46,7 @@ func run() -> void:
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), "Initialization is idempotent") check(E.eq(world.state, before), "Initialization is idempotent")
world.evolution.evolve_batches(world.state, E.add(5000000, E.divide(5000000, 40))) world.evolution.evolve_batches(world.state, E.add(5000000, E.divide(5000000, root.get_node("Global").batch_count)))
check(E.eq(world.state.amount.EON1, 10), "HP abiogenesis follows source probability") check(E.eq(world.state.amount.EON1, 10), "HP abiogenesis follows source probability")
check(E.eq(world.state.amount.RP, 100), "HP degradation produces RP") check(E.eq(world.state.amount.RP, 100), "HP degradation produces RP")
check(E.eq(world.state.amount.HP, 39890), "Unchanged planets retry, not disappear") check(E.eq(world.state.amount.HP, 39890), "Unchanged planets retry, not disappear")
@@ -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)
@@ -69,7 +69,7 @@ func run() -> void:
# Test host preservation with a fixed rate, independent of game balance. # Test host preservation with a fixed rate, independent of game balance.
world.catalog.entities.EON5.life_rule.progress = "0.00025" world.catalog.entities.EON5.life_rule.progress = "0.00025"
world.evolution.initialize_life_batches() world.evolution.initialize_life_batches()
world.evolution.evolve_batches(world.state, E.add(500000, E.divide(500000, 40))) world.evolution.evolve_batches(world.state, E.add(500000, E.divide(500000, root.get_node("Global").batch_count)))
check(E.eq(world.state.binds.RD["C0.0"], 10), "Intelligence emerges at the same host star") check(E.eq(world.state.binds.RD["C0.0"], 10), "Intelligence emerges at the same host star")
check(E.eq(world.state.amount["C0.0"], 0), "Life transition keeps planet bound") check(E.eq(world.state.amount["C0.0"], 0), "Life transition keeps planet bound")
check(world.state.batches.has("C0.0"), "New C0.0 starts civilization evolution") check(world.state.batches.has("C0.0"), "New C0.0 starts civilization evolution")
@@ -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))
+1 -1
View File
@@ -17,7 +17,7 @@ func run() -> void:
var World = load("res://world/owniverse/owniverse.gd") var World = load("res://world/owniverse/owniverse.gd")
var first = World.new() var first = World.new()
var second = World.new() var second = World.new()
var names: Array = ["batches", "ledger", "production", "automatic", "stellar", "life", "environment", "evolution"] var names: Array = ["batches", "ledger", "production", "automatic", "stellar", "life", "environment", "evolution", "civilization_flow"]
var references: Array = [] var references: Array = []
for name in names: for name in names:
check(E.ne(first.get(name), second.get(name)), E.add("World has its own ", name)) check(E.ne(first.get(name), second.get(name)), E.add("World has its own ", name))
+15 -2
View File
@@ -59,14 +59,27 @@ func run() -> void:
world.state.auto_resources = {"H": 10} world.state.auto_resources = {"H": 10}
world.state.batches.H = [{"amount": 10, "start_year": 1, "end_year": 2}] world.state.batches.H = [{"amount": 10, "start_year": 1, "end_year": 2}]
var before: Dictionary = world.state.duplicate(true) var before: Dictionary = world.state.duplicate(true)
check((not world.production.try_production(world.state, world.catalog.entities.N, 1, true)), "Binding failure rejects production") var physical: Dictionary = world.ledger.physical_amounts(world.state)
var frozen_physical: Dictionary = physical.duplicate()
check((not world.production.try_production(world.state, world.catalog.entities.N, 1, true, physical)), "Binding failure rejects production")
check(E.eq(physical, frozen_physical), "Failed production preserves the shared quantity snapshot")
check(E.eq(world.state, before), "Failure preserves costs, batches and budget") check(E.eq(world.state, before), "Failure preserves costs, batches and budget")
world.state.amount.S = 10 world.state.amount.S = 10
physical = world.ledger.physical_amounts(world.state)
var old_batches: Array = world.state.batches.H var old_batches: Array = world.state.batches.H
check(world.production.try_production(world.state, world.catalog.entities.N, 2, true), "Funded production succeeds") check(world.production.try_production(world.state, world.catalog.entities.N, 2, true, physical), "Funded production succeeds")
check(E.eq(physical, world.ledger.physical_amounts(world.state)), "Successful production updates exact totals without changing bound-resource totals")
check((E.eq(world.state.amount.H, 6) and E.eq(world.state.amount.S, 4)), "Exact cost and binding") check((E.eq(world.state.amount.H, 6) and E.eq(world.state.amount.S, 4)), "Exact cost and binding")
check(E.eq(world.state.auto_resources.H, 6), "Exact automatic budget") check(E.eq(world.state.auto_resources.H, 6), "Exact automatic budget")
check((E.eq(old_batches[0].amount, 10) and E.eq(world.state.batches.H[0].amount, 6)), "Consumed batches isolated") check((E.eq(old_batches[0].amount, 10) and E.eq(world.state.batches.H[0].amount, 6)), "Consumed batches isolated")
for member in world.catalog.members("Star"): world.state.amount[member] = 0
var star_stocks: Array = ["1234567890123456789", "876543210987654321", "3141592653589793238", "2718281828459045235", "1414213562373095048", "1732050807568877293", "2236067977499789696", "2449489742783178098", "3162277660168379331"]
for i in star_stocks.size(): world.state.amount[world.catalog.members("Star")[i]] = E.of(star_stocks[i])
var scd_request: Variant = E.of("1e15")
var scd_created: Variant = world.production.produce("SCD", scd_request)
check(E.eq(scd_created, scd_request), "Large SCD production distributes a whole star quota exactly")
check(E.eq(world.state.amount.SCD, scd_request), "Large SCD creation is committed")
print(("Transaction: %d checks, %d failures" % [checks, failures])) print(("Transaction: %d checks, %d failures" % [checks, failures]))
dispose(world) dispose(world)
quit((1 if failures else 0)) quit((1 if failures else 0))
+56 -57
View File
@@ -1,6 +1,7 @@
extends SceneTree extends SceneTree
const E = preload("res://world/owniverse/math/exact.gd") const E = preload("res://world/owniverse/math/exact.gd")
const Civ = preload("res://world/owniverse/civilization_evolution.gd") const Civ = preload("res://world/owniverse/civilization_evolution.gd")
const Q = preload("res://world/owniverse/quantity_math.gd")
const Fixture = preload("res://tests/fixtures/civilization_benchmark.gd") const Fixture = preload("res://tests/fixtures/civilization_benchmark.gd")
var checks: int = 0 var checks: int = 0
var failures: int = 0 var failures: int = 0
@@ -11,52 +12,36 @@ func check(value: bool, message: String):
failures += 1 failures += 1
push_error(message) push_error(message)
func reference_drift(target: Dictionary, pop: Dictionary, rows: Dictionary, diplomacy: bool, colony: bool) -> Dictionary:
var weights: Dictionary = {}
for other in Civ.ETHICS: weights[other] = 0
for ethic in pop:
var contribution: Variant = E.multiply(pop[ethic], E.of(rows[ethic].diplomacy_rate) if diplomacy else 1)
if E.eq(contribution, 0): continue
for other in Civ.ETHICS:
var weight: int = 9 if colony else 1
if other == ethic: weight = 960 if colony else 196
elif other == Civ.OPPOSITE[ethic]: weight = 4 if colony else 0
weights[other] = E.add(weights[other], E.multiply(contribution, weight))
if E.le(Civ.total(weights), 0): return pop.duplicate()
return Civ.split(target, "test", Civ.total(pop), weights)
func run(): func run():
var rows: Dictionary = {} for count in [0, 1, 7, 999999999, E.of("1e40"), E.of("1e400")]:
for ethic in Civ.ETHICS: rows[ethic] = {"diplomacy_rate": "0.125" if ethic == "m" else "1.75"} var carry: Dictionary = {}
for count in [0, 1, 7, 999999999, E.of("1e40")]: for round_index in 20:
for diplomacy in [false, true]: var weights: Dictionary = {"m": round_index + 1.0, "l": 0.0, "w": 9.0, "d": 4.0, "c": 137.0, "b": 2.0}
for colony in [false, true]: var profile: Dictionary = Civ.State.normalize_profile(weights)
var actual: Dictionary = {} var shares: Array = []
var reference: Dictionary = {} for ethic in Civ.ETHICS: shares.append(profile[ethic])
for round_index in 5: var result: Dictionary = Q.split_profile(carry, "profile", count, Civ.ETHICS, shares)
var pop: Dictionary = {"m": count, "l": round_index, "w": 9, "d": 0, "c": 137, "b": 2} check(E.eq(Q.sum(result), count), "Profile quotas conserve arbitrary-size whole totals")
var result: Dictionary = Civ.drift(actual, "test", pop, rows, diplomacy, colony) for value in result.values(): check(E.ge(value, 0) and E.eq(value, E.floor_value(value)), "Profile quotas stay nonnegative whole")
if diplomacy: for value in carry.values(): check(is_finite(value) and value >= -1.0 and value <= 1.0, "Signed profile carries stay bounded")
check(E.eq(result, reference_drift(reference, pop, rows, diplomacy, colony)), "Diplomatic drift preserves allocation") check(absf(E.ratio_approx(E.of("1e400"), E.of("3e400")) - 1.0 / 3.0) < 1e-15, "Float profile ratio handles values beyond float range")
var carries_valid: bool = true for count in [1, 101, E.of("1e40"), E.of("1e400")]:
for value in actual.get("civilization_remainders", {}).values(): var stocks: Dictionary = {"a": count, "b": E.add(count, 1), "c": 0}
carries_valid = carries_valid and E.ge(value, -1) and E.le(value, 1) var total: Variant = Q.sum(stocks)
check(carries_valid, "Diplomatic drift carries stay bounded") for limit in [0, count, E.subtract(total, 1), total]:
else: var allocated: Dictionary = Q.allocate_profiled(stocks, limit)
check(E.eq(Civ.total(result), Civ.total(pop)), "Integer flow drift conserves population") check(E.eq(Q.sum(allocated), limit), "Profiled stock allocation retains the exact granted total")
var valid_whole: bool = true for id in stocks: check(E.ge(allocated[id], 0) and E.le(allocated[id], stocks[id]), "Profiled stock allocation respects every owner ceiling")
for ethic in Civ.ETHICS: for probability in [0.1, 1.0 / 3.0, 1.23456789]:
valid_whole = valid_whole and E.ge(result[ethic], 0) and E.eq(result[ethic], E.floor_value(result[ethic])) var carries: Dictionary = {}
check(valid_whole, "Integer flow drift has nonnegative whole counts") var old_carry: Variant = 0
var decimal_rows: Dictionary = {} for iteration in 20:
for ethic in Civ.ETHICS: decimal_rows[ethic] = {"diplomacy_rate": "0.333333333333" if ethic == "m" else "0.017"} var expected: Variant = E.add(E.multiply(count, probability), old_carry)
var decimal_target: Dictionary = {} var whole: Variant = E.floor_value(expected)
for round_index in 20: old_carry = E.maximum(0, E.subtract(expected, whole))
var pop: Dictionary = {"m": E.of("1e40"), "l": round_index, "w": 17, "d": 4, "c": 91, "b": 2} var actual: Variant = Q.accumulate_rate(carries, "rate", count, probability)
var result: Dictionary = Civ.drift(decimal_target, "decimal", pop, decimal_rows, true) check(E.eq(actual, whole), "Fast float carries preserve the whole result of rate accumulation")
check(E.eq(Civ.total(result), Civ.total(pop)), "Decimal diplomatic drift preserves whole population") check(E.near(carries.rate, old_carry), "Fast float carries preserve the fractional remainder")
for ethic in result:
check(E.ge(result[ethic], 0) and E.eq(result[ethic], E.floor_value(result[ethic])), "Decimal drift returns nonnegative whole counts")
for speed in [1, 4, 6]: for speed in [1, 4, 6]:
var world = load("res://world/owniverse/owniverse.gd").new() var world = load("res://world/owniverse/owniverse.gd").new()
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities) world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
@@ -70,39 +55,53 @@ func run():
var row: Dictionary = rules[id].ethics.m var row: Dictionary = rules[id].ethics.m
var p: Variant = E.maximum(0, E.multiply(E.of(row.progress), 2)) var p: Variant = E.maximum(0, E.multiply(E.of(row.progress), 2))
var d: Variant = E.maximum(0, E.of(row.degradation)) var d: Variant = E.maximum(0, E.of(row.degradation))
check(E.eq(changed.m.pro, E.divide(p, E.maximum(1, E.add(p, d)))), "Coefficient cache follows modifiers") check(E.near(changed.m.pro, E.divide(p, E.maximum(1, E.add(p, d)))), "Coefficient cache follows modifiers")
modifiers.progress = 1 modifiers.progress = 1
check(E.eq(Civ.coefficients(world, rules, id, modifiers), original), "Coefficient cache restores original rates") check(E.eq(Civ.coefficients(world, rules, id, modifiers), original), "Coefficient cache restores original rates")
var local_rules: Dictionary = rules.duplicate(true) var local_rules: Dictionary = rules.duplicate(true)
local_rules[id].ethics.m.progress = "0.375" local_rules[id].ethics.m.progress = "0.375"
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.near(changed.m.pro, E.divide(E.of("0.375"), E.maximum(1, E.add(E.of("0.375"), d)))), "Coefficient cache follows rule edits")
var target: Dictionary = {"ethics": {}} 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)
# Civilizations no longer trigger batch generations. Seed an actual
# stellar completion so the selective-rounding path is exercised.
world.state.batches.BG = [{"amount": world.state.amount.BG, "start_year": 1, "end_year": 1}]
world.speed = speed 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)
check(world.ledger.checks > 0 and world.ledger.failures == 0, "Selective and full rounding agree at speed %d" % speed) check(world.ledger.checks > 0 and world.ledger.failures == 0, "Selective and full rounding agree at speed %d" % speed)
+4 -4
View File
@@ -70,7 +70,7 @@ func run() -> void:
check(E.eq(world.stellar.evolution_products(world.state, world.catalog.entities.HRG, 37), restored.stellar.evolution_products(restored.state, restored.catalog.entities.HRG, 37)), "Evolution carry survives serialization") check(E.eq(world.stellar.evolution_products(world.state, world.catalog.entities.HRG, 37), restored.stellar.evolution_products(restored.state, restored.catalog.entities.HRG, 37)), "Evolution carry survives serialization")
dispose(restored) dispose(restored)
# Real RD lifespan, four stars spread over the existing lifespan/40 window. # Real RD lifespan, four stars spread over one age-group window.
world.state.amount.N = 0 world.state.amount.N = 0
world.state.amount.RD = 4 world.state.amount.RD = 4
world.batches.append_batch(world.state, "RD", 4, 0, 0) world.batches.append_batch(world.state, "RD", 4, 0, 0)
@@ -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
View File
@@ -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]:
+36
View File
@@ -0,0 +1,36 @@
extends SceneTree
const E = preload("res://world/owniverse/math/exact.gd")
const Report = preload("res://ui/report/report.gd")
class Ledger extends RefCounted:
func physical_amounts(_source: Dictionary) -> Dictionary:
return {"RD": 3, "HG": 1}
class Catalog extends RefCounted:
var entities: Dictionary = {}
func is_group(_id: String) -> bool: return false
class World extends RefCounted:
var catalog = Catalog.new()
var ledger = Ledger.new()
var state = {"amount": {}, "stats": {"total_amount": {"H": 1234}}, "world_parameters": {"Federation": 1, "Enlightenment": 1, "Order": 1}}
func _initialize() -> void:
call_deferred("run")
func run() -> void:
var global: Node = root.get_node("Global")
var world = World.new()
world.state.stats.total_amount.H = E.of("1234567890123456789012345")
var before: Dictionary = E.encode(world.state)
global.Locale = "en"
var text: String = Report.build(world, global)
assert(text.contains("75.00%") and text.contains("25.00%"), text)
assert(text.contains("1 234 567 890 123 456 789 012 345"), text)
assert(text.contains("Total resources obtained") and not text.contains("Total entities emerged"), text)
world.state.world_parameters.Federation = E.of("1.3")
text = Report.build(world, global)
assert(text.contains("Federation:") and text.contains("0.30"), text)
world.state.world_parameters.Federation = 1
global.Locale = "ru"
text = Report.build(world, global)
assert(text.contains("Всего получено ресурсов") and text.contains("Красные карлики"), text)
assert(E.encode(world.state) == before, "Report mutated simulation")
assert(load("res://stage/stage.gd") != null)
print("Universe report checks: PASS")
quit()
+1
View File
@@ -0,0 +1 @@
uid://brb04x12elvqt
+15 -9
View File
@@ -1,7 +1,6 @@
extends SceneTree extends SceneTree
const E = preload("res://world/owniverse/math/exact.gd") const E = preload("res://world/owniverse/math/exact.gd")
const CivilizationState = preload("res://world/owniverse/civilization_state.gd") const CivilizationState = preload("res://world/owniverse/civilization_state.gd")
const CivilizationEvolution = preload("res://world/owniverse/civilization_evolution.gd")
const Parameters = preload("res://world/owniverse/world_parameters.gd") const Parameters = preload("res://world/owniverse/world_parameters.gd")
const Descriptions = preload("res://world/owniverse/item_description.gd") const Descriptions = preload("res://world/owniverse/item_description.gd")
@@ -91,24 +90,24 @@ 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
var next_stage: String = global.CivilizationRules["C0.2"].next var next_stage: String = global.CivilizationRules["C0.2"].next
var outcomes: Array = [] var outcomes: Array = []
for target in [baseline, advanced]: for target in [baseline, advanced]:
var expired: Dictionary = {"C0.2": [{"amount": 1000000, "from": 0, "to": 1}]} world.civilization_flow.evolve(target, 0, 2000)
var budget: Dictionary = CivilizationState.stellar_budget(world, target, global.CivilizationRules) outcomes.append(target.amount.get(next_stage, 0))
CivilizationEvolution.prepare(world, target, expired, 1, budget, global.CivilizationRules)
outcomes.append(expired["C0.2"][0].products.get(next_stage, 0))
check(E.gt(outcomes[1], outcomes[0]), "Aggression changes actual ethical civilization transitions") check(E.gt(outcomes[1], outcomes[0]), "Aggression changes actual ethical civilization transitions")
dispose(world) dispose(world)
# 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 +116,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 +139,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)
+206
View File
@@ -0,0 +1,206 @@
extends Node
## Player-wide achievement progress and the shared in-game notification queue.
const E = preload("res://world/owniverse/math/exact.gd")
const ITEM = preload("res://ui/achievements/achievement_item.tscn")
const TITLES = {
"en": preload("res://ui/achievements/achievements.en.png"),
"ru": preload("res://ui/achievements/achievements.ru.png"),
}
var progress_path: String = "user://achievements.cfg"
const HOLD_SECONDS = 3.0
const FADE_SECONDS = 0.3
var global: Node
var stage: Control
var definitions: Dictionary = {}
var conditions: Dictionary = {}
var progress: Dictionary = {}
var items: Dictionary = {}
var queue: Array[Dictionary] = []
var current: Dictionary = {}
var elapsed: float = 0.0
var last_revision: int = -1
var last_world: Object
var locale: String = ""
var panel: PanelContainer
var notification_item: Control
var headers: Array[Control] = []
var status: Label
var list: VBoxContainer
var storage_available: bool = true
func initialize(owner_stage: Control) -> void:
stage = owner_stage
global = get_tree().root.get_node("Global")
locale = global.Locale
for id in global.Achievements.en:
var row: Dictionary = global.Achievements.en[id]
if str(row.Ignore) == "1": continue
definitions[id] = row
conditions[id] = {
"open": _parse_condition(str(row.OpenCondition)),
"achieve": _parse_condition(str(row.AchieveCondition)),
}
_load_progress()
list = stage.get_node("Achievements/AchievementList/AchievementItems")
for child in list.get_children():
list.remove_child(child)
child.queue_free()
status = Label.new()
status.add_theme_font_size_override("font_size", 32)
list.add_child(status)
var pages: Node = stage.get_node("World/PageScroller/HBoxContainer")
for path in ["ForcePage/ForceHeader", "ResourcePage/ResourceHeader", "StarPage/StarHeader", "StructurePage/StructureHeader", "LifePage/LifeHeader"]:
headers.append(pages.get_node(path))
panel = PanelContainer.new()
panel.name = "AchievementNotification"
panel.set_as_top_level(true)
panel.mouse_filter = Control.MOUSE_FILTER_STOP
panel.gui_input.connect(_on_notification_input)
panel.z_index = 20
stage.get_node("World").add_child(panel)
notification_item = ITEM.instantiate()
panel.add_child(notification_item)
_ignore_notification_children(notification_item)
panel.hide()
_rebuild_list()
func _parse_condition(expression: String) -> Dictionary:
if expression.is_empty(): return {}
var parts := expression.split(":")
assert(parts.size() == 3 and parts[0] in ["age", "total"], "Unsupported achievement condition: " + expression)
return {"type": parts[0], "id": parts[1], "amount": E.of(parts[2])}
func _matches(condition: Dictionary, source: Dictionary) -> bool:
if condition.is_empty(): return true
var amount: Variant = source.time.year if condition.type == "age" else source.stats.total_amount.get(condition.id, 0)
return E.ge(amount, condition.amount)
func update(delta: float) -> void:
if global.Locale != locale:
locale = global.Locale
_rebuild_list()
if not current.is_empty(): _display_event(current)
var world = stage.owniverse
if world != last_world or world.presentation.revision != last_revision:
last_world = world
last_revision = world.presentation.revision
_check(world.state)
# Menu time does not consume a notification's display time.
if not stage.screens.World.visible: return
if current.is_empty() and not queue.is_empty():
current = queue.pop_front()
elapsed = 0.0
_display_event(current)
if current.is_empty(): return
elapsed += delta
var page: int = clampi(roundi(float(stage.stage_page_scroller.scroll_horizontal) / stage.PAGE_WIDTH), 0, headers.size() - 1)
for i in headers.size():
headers[i].modulate.a = 0.0 if i == page else 1.0
panel.global_position = headers[page].global_position
panel.size = Vector2(headers[page].size.x, maxf(128.0, headers[page].size.y))
panel.modulate.a = clampf(1.0 - (elapsed - HOLD_SECONDS) / FADE_SECONDS, 0.0, 1.0)
if elapsed >= HOLD_SECONDS + FADE_SECONDS:
dismiss_notification()
func _ignore_notification_children(node: Node) -> void:
if node is Control: node.mouse_filter = Control.MOUSE_FILTER_IGNORE
for child in node.get_children(): _ignore_notification_children(child)
func _on_notification_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
panel.accept_event()
dismiss_notification()
elif event is InputEventScreenTouch and event.pressed:
panel.accept_event()
dismiss_notification()
func dismiss_notification() -> void:
panel.hide()
current = {}
elapsed = 0.0
for header in headers: header.modulate.a = 1.0
func _check(source: Dictionary) -> void:
var changed := false
for id in definitions:
var saved: Dictionary = progress.get(id, {"visible": false, "unlocked": false})
if bool(saved.unlocked): continue
var rule: Dictionary = conditions[id]
if _matches(rule.achieve, source):
saved.visible = true
saved.unlocked = true
_enqueue(id, "c")
changed = true
elif not bool(saved.visible) and _matches(rule.open, source):
saved.visible = true
# An empty opening condition makes an item visible without an alert.
if not rule.open.is_empty(): _enqueue(id, "g")
changed = true
progress[id] = saved
if changed:
_save_progress()
_rebuild_list()
func _enqueue(id: String, event_type: String) -> void:
for event in queue:
if event.id == id:
event.type = event_type
return
queue.append({"id": id, "type": event_type})
func _text(id: String) -> String:
var row: Dictionary = global.Achievements[locale][id]
var prefix: String = "Ru" if locale == "ru" else "En"
var title: String = str(row[prefix + "1"])
var description: String = str(row[prefix + "2"])
return "[b]" + title + "[/b]" + ("\n" + description if not description.is_empty() else "")
func _display_event(event: Dictionary) -> void:
notification_item.open(int(definitions[event.id].Points), event.type, _text(event.id))
panel.modulate.a = 1.0
panel.show()
func _rebuild_list() -> void:
var title: TextureRect = stage.get_node_or_null("Achievements/Header/Title")
if title != null: title.texture = TITLES[locale]
var unlocked := 0
var position := 1
for id in definitions:
var saved: Dictionary = progress.get(id, {})
if not bool(saved.get("visible", false)) and not bool(saved.get("unlocked", false)): continue
if not items.has(id):
var item: Control = ITEM.instantiate()
list.add_child(item)
items[id] = item
list.move_child(items[id], position)
position += 1
var completed: bool = bool(saved.get("unlocked", false))
if completed: unlocked += 1
items[id].open(int(definitions[id].Points), "c" if completed else "g", _text(id))
var row: Dictionary = global.get_template("Achievement-Status-Template", "Interface", "Header")
status.text = global.render_template(row, {"count": unlocked, "total": definitions.size()})
func _load_progress() -> void:
var config := ConfigFile.new()
var error := config.load(progress_path)
if error == ERR_FILE_NOT_FOUND: return
if error != OK:
storage_available = false
push_error("Cannot load achievement progress: " + error_string(error))
return
for id in definitions:
progress[id] = {"visible": bool(config.get_value(id, "visible", false)), "unlocked": bool(config.get_value(id, "unlocked", false))}
func _save_progress() -> void:
if not storage_available: return
var config := ConfigFile.new()
# Preserve entries for achievements temporarily disabled in the catalog.
config.load(progress_path)
for id in progress:
config.set_value(id, "visible", bool(progress[id].visible))
config.set_value(id, "unlocked", bool(progress[id].unlocked))
var error := config.save(progress_path + ".tmp")
if error == OK:
error = DirAccess.rename_absolute(progress_path + ".tmp", progress_path)
if error != OK: push_error("Cannot save achievement progress: " + error_string(error))
@@ -0,0 +1 @@
uid://c1x220s0rloxc
+28
View File
@@ -0,0 +1,28 @@
extends Control
@onready var icons = {
"1c": $Row/Icon1c,
"1g": $Row/Icon1g,
"3c": $Row/Icon3c,
"3g": $Row/Icon3g,
"6c": $Row/Icon6c,
"6g": $Row/Icon6g,
"10c": $Row/Icon10c,
"10g": $Row/Icon10g,
}
@onready var event_label: RichTextLabel = $Row/Label
## Shows this achievement event with the matching points/state icon.
## event_type is "c" for completed or "g" for revealed but not completed.
func open(level: int, event_type: String, event_text: String) -> void:
var icon_key := str(level) + event_type.to_lower()
if not icons.has(icon_key):
push_warning("Unknown achievement icon: %s" % icon_key)
return
for key: String in icons:
icons[key].visible = key == icon_key
event_label.text = event_text
visible = true
+1
View File
@@ -0,0 +1 @@
uid://b6sqyd1tpljr3
+83
View File
@@ -0,0 +1,83 @@
[gd_scene format=3 uid="uid://cs8a5u3ecf51t"]
[ext_resource type="Script" uid="uid://b6sqyd1tpljr3" path="res://ui/achievements/achievement_item.gd" id="1_w8f20"]
[ext_resource type="Texture2D" uid="uid://cfohmmlwh84gk" path="res://ui/achievements/icons/1c.png" id="2_7ntr3"]
[ext_resource type="Texture2D" uid="uid://k6dqaewr30qe" path="res://ui/achievements/icons/1g.png" id="3_1b655"]
[ext_resource type="Texture2D" uid="uid://bccs26gkwsio3" path="res://ui/achievements/icons/3c.png" id="4_xgo6w"]
[ext_resource type="Texture2D" uid="uid://c7regmaoq0dr5" path="res://ui/achievements/icons/3g.png" id="5_7pkbt"]
[ext_resource type="Texture2D" uid="uid://bl7dr5fapao0x" path="res://ui/achievements/icons/6c.png" id="6_aart1"]
[ext_resource type="Texture2D" uid="uid://bk5morikm4k5o" path="res://ui/achievements/icons/6g.png" id="7_ndc4d"]
[ext_resource type="Texture2D" uid="uid://bs54rppydwlvy" path="res://ui/achievements/icons/10c.png" id="8_20e5y"]
[ext_resource type="Texture2D" uid="uid://bmp7e6lmtaex0" path="res://ui/achievements/icons/10g.png" id="9_3xyrb"]
[node name="AchievementItem" type="Control" unique_id=1273881744]
custom_minimum_size = Vector2(1080, 128)
layout_mode = 3
anchors_preset = 0
size_flags_horizontal = 3
script = ExtResource("1_w8f20")
[node name="Row" type="HBoxContainer" parent="." unique_id=2025386077]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
theme_override_constants/separation = 16
[node name="Icon1c" type="TextureRect" parent="Row" unique_id=2146478339]
visible = false
custom_minimum_size = Vector2(128, 128)
layout_mode = 2
texture = ExtResource("2_7ntr3")
[node name="Icon1g" type="TextureRect" parent="Row" unique_id=2089044466]
visible = false
custom_minimum_size = Vector2(128, 128)
layout_mode = 2
texture = ExtResource("3_1b655")
[node name="Icon3c" type="TextureRect" parent="Row" unique_id=501141005]
visible = false
custom_minimum_size = Vector2(128, 128)
layout_mode = 2
texture = ExtResource("4_xgo6w")
[node name="Icon3g" type="TextureRect" parent="Row" unique_id=844167957]
visible = false
custom_minimum_size = Vector2(128, 128)
layout_mode = 2
texture = ExtResource("5_7pkbt")
[node name="Icon6c" type="TextureRect" parent="Row" unique_id=2137074157]
visible = false
custom_minimum_size = Vector2(128, 128)
layout_mode = 2
texture = ExtResource("6_aart1")
[node name="Icon6g" type="TextureRect" parent="Row" unique_id=616115687]
visible = false
custom_minimum_size = Vector2(128, 128)
layout_mode = 2
texture = ExtResource("7_ndc4d")
[node name="Icon10c" type="TextureRect" parent="Row" unique_id=418038490]
visible = false
custom_minimum_size = Vector2(128, 128)
layout_mode = 2
texture = ExtResource("8_20e5y")
[node name="Icon10g" type="TextureRect" parent="Row" unique_id=1352890506]
visible = false
custom_minimum_size = Vector2(128, 128)
layout_mode = 2
texture = ExtResource("9_3xyrb")
[node name="Label" type="RichTextLabel" parent="Row" unique_id=230945261]
layout_mode = 2
size_flags_horizontal = 3
theme_override_font_sizes/normal_font_size = 30
theme_override_font_sizes/bold_font_size = 30
bbcode_enabled = true
text = "123"
fit_content = true
scroll_active = false
vertical_alignment = 1
Binary file not shown.
Binary file not shown.
+7
View File
@@ -58,6 +58,13 @@ func _set_action_values(values: Array, displayed: Array) -> void:
action_labels[ind].text = text action_labels[ind].text = text
action_labels[ind].get_parent().get_node("TextureButton").disabled = E.le(values[ind], 0) action_labels[ind].get_parent().get_node("TextureButton").disabled = E.le(values[ind], 0)
func mark_pending(amount: Variant) -> void:
for ind in range(5):
if not E.eq(action_values[ind], amount): continue
action_labels[ind].text = "…"
action_labels[ind].get_parent().get_node("TextureButton").disabled = true
return
func _format_action_value(value: Variant) -> String: func _format_action_value(value: Variant) -> String:
if ((E.lt(value, 1) or (not E.finite(value))) or E.ne(value, E.floor_value(value))): if ((E.lt(value, 1) or (not E.finite(value))) or E.ne(value, E.floor_value(value))):
BIN
View File
Binary file not shown.
+63
View File
@@ -0,0 +1,63 @@
extends VBoxContainer
const E = preload("res://world/owniverse/math/exact.gd")
const Presentation = preload("res://world/owniverse/item_presentation.gd")
const STARS = ["RD", "OD", "YD", "WYD", "WS", "BG", "SG", "HG"]
const EFFECTS = {"Int.Fed": "Federation", "Int.Enl": "Enlightenment", "Int.Ord": "Order"}
var report_locale: String = ""
@onready var report_body: RichTextLabel = $ReportBody
@onready var report_header: TextureRect = $Header/Header
func _ready() -> void:
visibility_changed.connect(_on_visibility_changed)
func _process(_delta: float) -> void:
if is_visible_in_tree() and report_locale != get_node("/root/Global").Locale:
refresh()
func _on_visibility_changed() -> void:
if is_node_ready() and is_visible_in_tree():
refresh()
report_body.scroll_to_line(0)
func refresh() -> void:
var global: Node = get_node("/root/Global")
report_locale = global.Locale
report_body.text = build(get_parent().owniverse, global)
report_header.texture = load("res://ui/report/" + report_locale + "/report.png")
## Formats existing committed state only. No historical counters or simulation work.
static func build(world, global: Node) -> String:
var source: Dictionary = world.state
var totals: Dictionary = Presentation.amounts(source, world.catalog, true)
var physical: Dictionary = world.ledger.physical_amounts(source)
var star_total: Variant = 0
for id in STARS: star_total = E.add(star_total, physical.get(id, 0))
var blocks: PackedStringArray = []
var lines: PackedStringArray = []
var header: String = ""
# Russian keys provide the common order; get_template resolves both key formats.
for key in global.Templates.ru:
if not str(key).contains(".Report."): continue
var parts: PackedStringArray = str(key).rsplit(".Report.", true, 1)
var id: String = parts[0]
var kind: String = parts[1]
var row: Dictionary = global.get_template(id, "Report", kind)
if kind == "Header":
if not lines.is_empty(): blocks.append(header + "\n\n" + "\n".join(lines))
lines.clear()
header = global.render_template(row, {})
elif kind == "Fraction":
var amount: Variant = physical.get(id, 0)
if E.gt(amount, 0) and E.gt(star_total, 0):
var percent: String = E.grouped(E.mul_div(amount, 100, star_total), 2, false) + "%"
lines.append(global.render_template(row, {"proportion": percent}))
elif kind == "Max":
var amount: Variant = totals.get(id, 0)
if EFFECTS.has(id):
amount = E.subtract(source.world_parameters.get(EFFECTS[id], 1), 1)
if E.gt(amount, 0):
lines.append(global.render_template(row, {"max": E.grouped(amount, 2 if EFFECTS.has(id) else 0, false)}))
if not lines.is_empty(): blocks.append(header + "\n\n" + "\n".join(lines))
return "\n\n".join(blocks)
+1
View File
@@ -0,0 +1 @@
uid://v8h4ov33opyf
BIN
View File
Binary file not shown.
+2 -2
View File
@@ -35,7 +35,7 @@ Stage обращается непосредственно к `world.presentation
| `automatic_production.gd` / `automatic` | Выпуск за период и жизнь новорождённых объектов, разделение ресурсов на обычные и автоматические, энергия, квоты звёзд и бурых карликов | | `automatic_production.gd` / `automatic` | Выпуск за период и жизнь новорождённых объектов, разделение ресурсов на обычные и автоматические, энергия, квоты звёзд и бурых карликов |
| `stellar_rules.gd` / `stellar` | Продукты звёздной эволюции, создание планет, выбор звёздных преемников, два уровня разрушения и глобальные бласты | | `stellar_rules.gd` / `stellar` | Продукты звёздной эволюции, создание планет, выбор звёздных преемников, два уровня разрушения и глобальные бласты |
| `life_evolution.gd` / `life` | Прогресс, деградация и повторная попытка планетарной жизни | | `life_evolution.gd` / `life` | Прогресс, деградация и повторная попытка планетарной жизни |
| `civilization_evolution.gd`, `civilization_state.gd` | Существующие правила цивилизаций: этики, войны, колонизация, лимиты и места обитания | | `civilization_flow.gd`, `civilization_state.gd` | Непрерывные потоки цивилизаций, доли этик, целые квоты, лимиты и места обитания |
| `universe_environment.gd` / `environment` | Плотность, металличность, доля попаданий бластов, изменение параметров мира с сохранением возраста звёзд | | `universe_environment.gd` / `environment` | Плотность, металличность, доля попаданий бластов, изменение параметров мира с сохранением возраста звёзд |
| `evolution_coordinator.gd` / `evolution` | Порядок фаз шага, последовательные поколения и публикация нового состояния | | `evolution_coordinator.gd` / `evolution` | Порядок фаз шага, последовательные поколения и публикация нового состояния |
@@ -50,7 +50,7 @@ Stage обращается непосредственно к `world.presentation
1. Определить обработанную десятую года. При продвижении времени восстановить недостающие партии жизни и проверить вместимость. 1. Определить обработанную десятую года. При продвижении времени восстановить недостающие партии жизни и проверить вместимость.
2. Скопировать состояние, посчитать физические количества и производство за период. 2. Скопировать состояние, посчитать физические количества и производство за период.
3. Обработать поколения до конца интервала. Для каждого поколения: локальные разрушения → выбытие партий всех типов → план переходов → резервирование передачи привязок → зачисление преемников и их партий → производство новорождённых → применение привязок и округление → завершение переходов цивилизаций → глобальные бласты → проверка вместимости. 3. Интегрировать непрерывные потоки цивилизаций за весь интервал, затем обработать поколения звёзд и биологической жизни до конца интервала. Для каждого поколения: локальные разрушения → выбытие партий всех типов → план переходов → резервирование передачи привязок → зачисление преемников и их партий → производство новорождённых → применение привязок и округление → глобальные бласты → проверка вместимости.
4. Обновить групповые количества, ограничить автоматические ресурсы и продвинуть параметры мира. 4. Обновить групповые количества, ограничить автоматические ресурсы и продвинуть параметры мира.
5. Опубликовать новое состояние и обновить представление количеств. 5. Опубликовать новое состояние и обновить представление количеств.
+169
View File
@@ -0,0 +1,169 @@
extends "res://world/owniverse/simulation_module.gd"
class_name OwniverseApocalypseController
const E = preload("res://world/owniverse/math/exact.gd")
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
const TICK_SECONDS: float = 0.1
const END_WAIT_TICKS: int = 20
var _schedule: Array = []
func defaults() -> Dictionary:
return {"phase": 0, "type": 0, "warning": 0, "stage": 1, "tick": 0, "end_ticks": 0}
func check_density(target: Dictionary) -> void:
var apocalypse: Dictionary = target.get("apocalypse", defaults())
if E.gt(apocalypse.get("phase", 0), 0): return
var density: Variant = world.environment.stellar_environment(target).density
var parameters: Dictionary = WorldParameters.extra_params()
var warning: int = 0
if E.ge(density, parameters.bigcrunch3): warning = 3
elif E.ge(density, parameters.bigcrunch2): warning = 2
elif E.ge(density, parameters.bigcrunch1): warning = 1
elif E.le(density, parameters.bigrip3): warning = -3
elif E.le(density, parameters.bigrip2): warning = -2
elif E.le(density, parameters.bigrip1): warning = -1
apocalypse.warning = warning
target.apocalypse = apocalypse
if E.eq(warning, 3):
start(target, 3)
elif E.eq(warning, -3):
start(target, -3)
func start(target: Dictionary, event_type: int) -> void:
var apocalypse: Dictionary = target.get("apocalypse", defaults()).duplicate(true)
if E.gt(apocalypse.phase, 0): return
apocalypse.phase = 1
apocalypse.type = event_type
apocalypse.stage = 1
apocalypse.tick = 0
apocalypse.end_ticks = 0
target.apocalypse = apocalypse
target.speed = 1
target.speed_in_year = 1
_schedule = _make_schedule("SMBH" if event_type > 0 else "S")
func process_tick(target: Dictionary) -> Dictionary:
var result: Dictionary = {"screen": -1, "finished": false}
var apocalypse: Dictionary = target.get("apocalypse", defaults())
if E.ne(apocalypse.get("phase", 0), 1): return result
if _schedule.is_empty(): _schedule = _make_schedule("SMBH" if E.gt(apocalypse.type, 0) else "S")
apocalypse.tick = E.add(apocalypse.tick, 1)
var stage_rows: Array = []
for row: Dictionary in _schedule:
if E.eq(row.stage, apocalypse.stage): stage_rows.append(row)
if stage_rows.is_empty():
var next_stage: Variant = INF
for row: Dictionary in _schedule:
if E.gt(row.stage, apocalypse.stage): next_stage = E.minimum(next_stage, row.stage)
if E.finite(next_stage):
apocalypse.stage = E.integer(next_stage)
apocalypse.tick = 0
else:
_process_final_energy(target, apocalypse)
if E.eq(apocalypse.end_ticks, END_WAIT_TICKS):
apocalypse.phase = 2
result.finished = true
target.apocalypse = apocalypse
return result
var stage_complete: bool = true
for row: Dictionary in stage_rows:
if E.gt(row.tick, apocalypse.tick):
stage_complete = false
continue
var id: String = row.id
if not world.catalog.entities.has(id): continue
if E.gt(world.ledger.physical_amount(target, id), 0):
stage_complete = false
result.screen = int(row.screen)
_destroy_fraction(target, id)
if stage_complete:
var next_stage: Variant = INF
for row: Dictionary in _schedule:
if E.gt(row.stage, apocalypse.stage): next_stage = E.minimum(next_stage, row.stage)
if E.finite(next_stage):
apocalypse.stage = E.integer(next_stage)
apocalypse.tick = 0
else:
_process_final_energy(target, apocalypse)
if E.eq(apocalypse.end_ticks, END_WAIT_TICKS):
apocalypse.phase = 2
result.finished = true
target.apocalypse = apocalypse
world.ledger.refresh_groups(target)
world.ledger.clamp_auto_resources(target)
world.presentation.refresh(target, world.catalog)
return result
func _make_schedule(skipped_id: String) -> Array:
var rows: Array = Global.ApocalypseQueue.values()
rows.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return int(a.Num) < int(b.Num))
var result: Array = []
var stage: int = -1
var tick: int = 0
for source: Dictionary in rows:
if source.Shortcut == skipped_id: continue
var row_stage: int = int(source.Stage)
if row_stage != stage:
stage = row_stage
tick = 0
tick += int(source.Tick)
result.append({"id": str(source.Shortcut), "stage": row_stage, "screen": int(source.Screen), "tick": tick})
return result
func _destroy_fraction(target: Dictionary, id: String) -> void:
var entity: OwniverseEntity = world.catalog.entities[id]
var physical: Variant = world.ledger.physical_amount(target, id)
if E.le(physical, 0): return
var amount: Variant = E.round_value(E.divide(physical, 2)) if E.gt(physical, 5) else (1 if E.ge(physical, 1) else physical)
amount = E.minimum(physical, E.maximum(0, amount))
if E.le(amount, 0): return
var fraction: Variant = E.clamp_value(E.divide(amount, physical), 0, 1)
if E.gt(entity.destroy_level, 0): world.stellar.destroy_planets(target, id, fraction, entity.destroy_level, "apocalypse")
var products: Dictionary = world.stellar.evolution_products(target, entity, amount, {"explosiveness": WorldParameters.effective(target, "Exp")})
var normalized: Dictionary = {}
for output in products:
normalized[output] = E.divide(products[output], amount)
var incoming: Dictionary = world.ledger.transfer_evolution(target, {id: fraction}, {id: normalized})
world.batches.scale_batches(target, id, E.subtract(1, fraction))
var terminal: String = "SMBH" if E.gt(target.apocalypse.type, 0) else "S"
var terminal_yield: Variant = 0
var new_owners: Dictionary = {}
if normalized.is_empty():
terminal_yield = E.multiply(amount, E.add(1, E.maximum(0, entity.mass))) if E.contains(["Star", "BH"], entity.group_as) else amount
for output in normalized:
if E.eq(output, "blast"):
var environment: Dictionary = world.environment.stellar_environment(target)
world.stellar.apply_blast(target, E.multiply(normalized[output], amount), environment)
continue
if not world.catalog.entities.has(output): continue
var output_amount: Variant = E.maximum(0, products[output])
if E.le(output_amount, 0): continue
output_amount = world.ledger.credit(target, output, output_amount)
var output_entity: OwniverseEntity = world.catalog.entities[output]
if E.gt(output_entity.lifespan, 0) or not output_entity.life_rule.is_empty():
world.batches.append_batch(target, output, output_amount, target.time.datetime, target.time.datetime)
new_owners[output] = E.add(new_owners.get(output, 0), output_amount)
world.ledger.accept_transfers(target, incoming)
world.ledger.bind_new_owners(target, new_owners)
if E.gt(terminal_yield, 0): world.ledger.credit(target, terminal, terminal_yield)
world.ledger.round_object_allocations(target)
func _process_final_energy(target: Dictionary, apocalypse: Dictionary) -> void:
var energy: Variant = E.maximum(0, target.energy)
if E.gt(energy, 0):
var amount: Variant = E.round_value(E.divide(energy, 2)) if E.gt(energy, 5) else (1 if E.ge(energy, 1) else energy)
amount = E.minimum(energy, amount)
target.energy = E.maximum(0, E.subtract(energy, amount))
world.ledger.credit(target, "SMBH" if E.gt(apocalypse.type, 0) else "S", amount)
apocalypse.end_ticks = 0
else:
apocalypse.end_ticks = int(apocalypse.end_ticks) + 1
@@ -0,0 +1 @@
uid://u5hs757qtn8c
+25 -8
View File
@@ -14,6 +14,7 @@ func yearly_products(entity: OwniverseEntity) -> Dictionary:
func produce_for_period(previous: Dictionary, target: Dictionary, quantities: Dictionary, distribution: Dictionary, from_year: Variant, to_year: Variant) -> void: func produce_for_period(previous: Dictionary, target: Dictionary, quantities: Dictionary, distribution: Dictionary, from_year: Variant, to_year: Variant) -> void:
if E.gt(target.get("apocalypse", {}).get("phase", 0), 0): return
var basic: Dictionary = {} var basic: Dictionary = {}
var automatic: Dictionary = {} var automatic: Dictionary = {}
for id in quantities: for id in quantities:
@@ -72,6 +73,7 @@ func produce_for_period(previous: Dictionary, target: Dictionary, quantities: Di
# One common multiplier preserves the initial count ratios, even under scarcity. # One common multiplier preserves the initial count ratios, even under scarcity.
var candidates: Dictionary = {} var candidates: Dictionary = {}
var helium_share: Variant = world.production.stellar_helium_share(previous)
for id in distribution: for id in distribution:
var entity: OwniverseEntity = world.catalog.entities[id] var entity: OwniverseEntity = world.catalog.entities[id]
if E.ne(entity.group_as, "Star"): if E.ne(entity.group_as, "Star"):
@@ -87,11 +89,16 @@ func produce_for_period(previous: Dictionary, target: Dictionary, quantities: Di
return return
var costs: Dictionary = {} var costs: Dictionary = {}
var needs: Dictionary = {} var needs: Dictionary = {}
var stellar_hydrogen_cost: Variant = 0
for id in candidates: for id in candidates:
if E.le(auto_capacity(target, world.catalog.entities[id], budget), 0): var entity: OwniverseEntity = world.catalog.entities[id]
if E.le(auto_capacity(target, entity, budget, helium_share), 0):
continue continue
for resource in world.catalog.entities[id].cost: for resource in entity.cost:
var cost: Variant = E.multiply(E.maximum(0, world.catalog.entities[id].cost[resource]), candidates[id]) var cost: Variant = E.multiply(E.maximum(0, entity.cost[resource]), candidates[id])
if E.eq(resource, "H") and world.production.uses_stellar_helium(entity):
stellar_hydrogen_cost = E.add(stellar_hydrogen_cost, cost)
continue
costs[resource] = E.add(costs.get(resource, 0), cost) costs[resource] = E.add(costs.get(resource, 0), cost)
needs[resource] = E.add(needs.get(resource, 0), cost) needs[resource] = E.add(needs.get(resource, 0), cost)
for resource in world.catalog.entities[id].bind: for resource in world.catalog.entities[id].bind:
@@ -100,6 +107,9 @@ func produce_for_period(previous: Dictionary, target: Dictionary, quantities: Di
for resource in costs: for resource in costs:
if E.gt(costs[resource], 0): if E.gt(costs[resource], 0):
scale = E.minimum(scale, E.divide(budget.get(resource, 0), costs[resource])) scale = E.minimum(scale, E.divide(budget.get(resource, 0), costs[resource]))
if E.gt(stellar_hydrogen_cost, 0):
var hydrogen_budget: Variant = E.minimum(budget.get("H", 0), world.ledger.available(target, "H"))
scale = E.minimum(scale, world.production.stellar_hydrogen_capacity(stellar_hydrogen_cost, hydrogen_budget, world.ledger.available(target, "He"), helium_share))
for resource in needs: for resource in needs:
if E.gt(needs[resource], 0): if E.gt(needs[resource], 0):
scale = E.minimum(scale, E.divide(world.ledger.available(target, resource), needs[resource])) scale = E.minimum(scale, E.divide(world.ledger.available(target, resource), needs[resource]))
@@ -116,7 +126,8 @@ func produce_for_period(previous: Dictionary, target: Dictionary, quantities: Di
var order: Array = candidates.keys() var order: Array = candidates.keys()
order.sort_custom(func ( a : String , b : String ) -> bool : return (E.lt(candidates[a], candidates[b]) if E.ne(candidates[a], candidates[b]) else E.lt(a, b))) order.sort_custom(func ( a : String , b : String ) -> bool : return (E.lt(candidates[a], candidates[b]) if E.ne(candidates[a], candidates[b]) else E.lt(a, b)))
var created: Dictionary = {} var created: Dictionary = {}
var metallicity: Variant = world.environment.stellar_environment(target).metallicity var current_quantities: Dictionary = world.ledger.physical_amounts(target)
var metallicity: Variant = world.environment.stellar_environment(target, current_quantities).metallicity
var total_created: Variant = 0 var total_created: Variant = 0
for id in order: for id in order:
var entity: OwniverseEntity = world.catalog.entities[id] var entity: OwniverseEntity = world.catalog.entities[id]
@@ -125,10 +136,10 @@ func produce_for_period(previous: Dictionary, target: Dictionary, quantities: Di
continue continue
var quota: Variant = E.maximum(0, E.add(E.multiply(candidates[id], scale), deficits.get(id, 0))) var quota: Variant = E.maximum(0, E.add(E.multiply(candidates[id], scale), deficits.get(id, 0)))
var desired: Variant = E.multiply(E.floor_value(E.divide(quota, step)), step) var desired: Variant = E.multiply(E.floor_value(E.divide(quota, step)), step)
var actual: Variant = E.minimum(desired, auto_capacity(target, entity, budget)) var actual: Variant = E.minimum(desired, auto_capacity(target, entity, budget, helium_share))
if E.le(actual, 0): if E.le(actual, 0):
continue continue
if (not world.production.try_production(target, entity, actual, true)): if (not world.production.try_production(target, entity, actual, true, current_quantities, helium_share)):
continue continue
budget = ordinary_star_budget(target) budget = ordinary_star_budget(target)
created[id] = actual created[id] = actual
@@ -138,7 +149,8 @@ func produce_for_period(previous: Dictionary, target: Dictionary, quantities: Di
deficits[id] = E.subtract(E.add(deficits.get(id, 0), E.multiply(candidates[id], total_created)), created.get(id, 0)) deficits[id] = E.subtract(E.add(deficits.get(id, 0), E.multiply(candidates[id], total_created)), created.get(id, 0))
for id in created: for id in created:
world.ledger.credit(target, id, created[id]) world.ledger.credit(target, id, created[id])
world.batches.append_batch(target, id, created[id], from_year, to_year) var lifespan_multiplier: Variant = world.production.stellar_lifespan_multiplier(world.catalog.entities[id], helium_share)
world.batches.append_batch(target, id, created[id], from_year, to_year, {}, -INF, null, null, NAN, lifespan_multiplier)
world.stellar.create_planets(target, id, created[id], from_year, to_year, metallicity) world.stellar.create_planets(target, id, created[id], from_year, to_year, metallicity)
@@ -182,11 +194,16 @@ func ordinary_star_budget(target: Dictionary) -> Dictionary:
return budget return budget
func auto_capacity(source: Dictionary, entity: OwniverseEntity, budget: Dictionary) -> Variant: func auto_capacity(source: Dictionary, entity: OwniverseEntity, budget: Dictionary, helium_share_override: Variant = NAN) -> Variant:
var capacity: Variant = INF var capacity: Variant = INF
var needs: Dictionary = {} var needs: Dictionary = {}
var helium_share: Variant = world.production.stellar_helium_share(source) if not E.finite(helium_share_override) else helium_share_override
for resource in entity.cost: for resource in entity.cost:
var cost: Variant = E.maximum(0, entity.cost[resource]) var cost: Variant = E.maximum(0, entity.cost[resource])
if E.eq(resource, "H") and world.production.uses_stellar_helium(entity):
var hydrogen_budget: Variant = E.minimum(budget.get("H", 0), world.ledger.available(source, "H"))
capacity = E.minimum(capacity, world.production.stellar_hydrogen_capacity(cost, hydrogen_budget, world.ledger.available(source, "He"), helium_share))
continue
needs[resource] = cost needs[resource] = cost
if E.gt(cost, 0): if E.gt(cost, 0):
capacity = E.minimum(capacity, E.floor_value(E.divide(budget.get(resource, 0), cost))) capacity = E.minimum(capacity, E.floor_value(E.divide(budget.get(resource, 0), cost)))
+17 -399
View File
@@ -1,205 +1,27 @@
extends RefCounted extends RefCounted
## Shared civilization coefficients and worship derived from float profiles.
const E = preload("res://world/owniverse/math/exact.gd") const E = preload("res://world/owniverse/math/exact.gd")
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
const State = preload("res://world/owniverse/civilization_state.gd") const State = preload("res://world/owniverse/civilization_state.gd")
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
const ETHICS = State.ETHICS const ETHICS = State.ETHICS
const OPPOSITE = {"m": "l", "l": "m", "w": "d", "d": "w", "c": "b", "b": "c"} const OPPOSITE = {"m": "l", "l": "m", "w": "d", "d": "w", "c": "b", "b": "c"}
const DIPLOMACY_SCALE: int = 1000000000
static var _diplomacy_rates: Dictionary = {}
static func total(pop: Dictionary) -> Variant: # Cache the weighted float worship rate multiplied by the exact population.
return QuantityMath.sum(pop) static func worship(world, target: Dictionary, rules: Dictionary, physical: Dictionary = {}) -> Variant:
static func add(to: Dictionary, from: Dictionary) -> void:
for key in from: to[key] = E.add(to.get(key, 0), from[key])
static func take(world, pop: Dictionary, count: Variant, population_total: Variant = null) -> Dictionary:
var taken: Dictionary = QuantityMath.allocate_whole_with_total(pop, count, population_total) if population_total != null else QuantityMath.allocate_whole(pop, count)
for key in taken: pop[key] = E.subtract(pop[key], taken[key])
return taken
static func rate(target: Dictionary, key: String, count: Variant, probability: Variant) -> Variant:
if (not target.has("civilization_remainders")): target.civilization_remainders = {}
return QuantityMath.accumulate_rate(target.civilization_remainders, key, count, probability)
# Fixed-size largest-remainder apportionment with saved signed rounding errors.
# Runtime depends on the number of branches, never the number of civilizations.
static func split(target: Dictionary, key: String, count: Variant, weights: Dictionary) -> Dictionary:
if E.le(count, 0): return {}
if (not target.has("civilization_remainders")): target.civilization_remainders = {}
return QuantityMath.split(target.civilization_remainders, key, count, weights)
static func drift(target: Dictionary, key: String, pop: Dictionary, rows: Dictionary, diplomacy: bool = false, colony: bool = false, population: Variant = null) -> Dictionary:
if population == null: population = total(pop)
if E.le(population, 0): return pop.duplicate()
if not diplomacy and E.eq(population, E.floor_value(population)):
return direct_drift(target, key, pop, colony, population)
var weights: Array = []
if not diplomacy:
# Integer populations: exact algebraic reduction of the 6 x 6 matrix.
var common: Variant = E.multiply(population, 9) if colony else population
for ethic in ETHICS:
weights.append(E.subtract(E.add(common, E.multiply(pop.get(ethic, 0), 951 if colony else 195)), E.multiply(pop.get(OPPOSITE[ethic], 0), 5 if colony else 1)))
else:
# Algebraic reduction of the diplomatic 6 x 6 matrix.
var rates: Array = diplomacy_rates(rows)
var contributions: Array = [0, 0, 0, 0, 0, 0]
var combined: Variant = 0
for ethic in pop:
var index: int = ETHICS.find(ethic)
var contribution: Variant = E.multiply(pop[ethic], rates[index])
contributions[index] = contribution
combined = E.add(combined, contribution)
var common: Variant = E.multiply(combined, 9) if colony else combined
for ethic in ETHICS:
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 not target.has("civilization_remainders"): target.civilization_remainders = {}
return QuantityMath.split_arrays(target.civilization_remainders, key, population, ETHICS, weights, 0)
static func diplomacy_rates(rows: Dictionary) -> Array:
var signature: String = ""
for ethic in ETHICS: signature += str(rows[ethic].diplomacy_rate) + ";"
if _diplomacy_rates.has(signature): return _diplomacy_rates[signature]
var rates: Array = []
for ethic in ETHICS:
var exact_rate: Variant = E.of(rows[ethic].diplomacy_rate)
rates.append(E.integer(E.round_value(E.multiply(exact_rate, DIPLOMACY_SCALE))))
if _diplomacy_rates.size() >= 256: _diplomacy_rates.clear()
rates.make_read_only()
_diplomacy_rates[signature] = rates
return rates
static func direct_drift(target: Dictionary, key: String, pop: Dictionary, colony: bool, population: Variant) -> Dictionary:
if not target.has("civilization_remainders"): target.civilization_remainders = {}
var carries: Dictionary = target.civilization_remainders
var result: Dictionary = {}
for ethic in ETHICS: result[ethic] = 0
for ethic in ETHICS:
if E.ne(pop.get(ethic, 0), E.floor_value(pop.get(ethic, 0))): return fractional_direct_fallback(target, key, pop, colony, population)
var divisor: int = 25 if colony else 50
for source in ETHICS:
var count: Variant = pop.get(source, 0)
if E.le(count, 0): continue
var whole: Variant = E.floor_value(E.divide(count, divisor))
var count_remainder: int = E.integer(E.subtract(count, E.multiply(whole, divisor)))
var remainder_key: String = key + ":flow:" + source
var pending: int = count_remainder + int(carries.get(remainder_key, 0))
var outgoing: Variant = E.add(whole, pending / divisor)
carries[remainder_key] = pending % divisor
result[source] = E.add(result[source], E.subtract(count, outgoing))
if E.le(outgoing, 0): continue
if not colony:
var neighbor_base: Variant = E.floor_value(E.divide(outgoing, 4))
var extras: int = E.integer(E.subtract(outgoing, E.multiply(neighbor_base, 4)))
var neighbors: Array = []
for destination in ETHICS:
if destination != source and destination != OPPOSITE[source]: neighbors.append(destination)
var cursor_key: String = key + ":flow_cursor:" + source
var cursor: int = int(carries.get(cursor_key, 0))
for neighbor_index in 4:
var destination: String = neighbors[neighbor_index]
var add: Variant = E.add(neighbor_base, 1 if ((neighbor_index - cursor + 4) % 4) < extras else 0)
result[destination] = E.add(result[destination], add)
carries[cursor_key] = (cursor + extras) % 4
else:
distribute_colonial_flow(result, carries, key, source, outgoing)
assert(E.eq(total(result), population), "Direct ethic drift must conserve population")
return result
static func distribute_colonial_flow(result: Dictionary, carries: Dictionary, key: String, source: String, outgoing: Variant) -> void:
# Colonial flow is 9:9:9:9:4 among the other ethics (total weight 40).
var base: Variant = E.floor_value(E.divide(outgoing, 40))
var tail: int = E.integer(E.subtract(outgoing, E.multiply(base, 40)))
var destinations: Array = []
var weights: Array = []
for destination in ETHICS:
if destination == source: continue
destinations.append(destination)
weights.append(4 if destination == OPPOSITE[source] else 9)
var assigned: Variant = 0
var rests: Array = []
var quotas: Array = []
for i in destinations.size():
var carry_key: String = key + ":col_flow:" + source + ":" + destinations[i]
var numerator: int = tail * weights[i] + int(carries.get(carry_key, 0))
var extra: int = floori(float(numerator) / 40.0)
var rest: int = numerator - extra * 40
quotas.append(E.add(E.multiply(base, weights[i]), extra))
rests.append(rest)
assigned = E.add(assigned, quotas[i])
carries[carry_key] = rest
var missing: int = E.integer(E.subtract(outgoing, assigned))
var order: Array = [0, 1, 2, 3, 4]
order.sort_custom(func(a: int, b: int) -> bool: return rests[a] > rests[b] if rests[a] != rests[b] else destinations[a] < destinations[b])
for i in order:
if missing <= 0: break
quotas[i] = E.add(quotas[i], 1)
var carry_key: String = key + ":col_flow:" + source + ":" + destinations[i]
carries[carry_key] = int(carries[carry_key]) - 40
missing -= 1
for i in destinations.size(): result[destinations[i]] = E.add(result[destinations[i]], quotas[i])
static func fractional_direct_fallback(target: Dictionary, key: String, pop: Dictionary, colony: bool, population: Variant) -> Dictionary:
var weights: Dictionary = {}
for ethic in ETHICS:
var common: Variant = E.multiply(population, 9) if colony else population
weights[ethic] = E.subtract(E.add(common, E.multiply(pop.get(ethic, 0), 951 if colony else 195)), E.multiply(pop.get(OPPOSITE[ethic], 0), 5 if colony else 1))
if not target.has("civilization_remainders"): target.civilization_remainders = {}
return QuantityMath.split(target.civilization_remainders, key, population, weights)
static func emit(plan: Dictionary, destination: String, pop: Dictionary) -> void:
if (not plan.outputs.has(destination)): plan.outputs[destination] = {}
add(plan.outputs[destination], pop)
static func fall(target: Dictionary, plan: Dictionary, rules: Dictionary, pop: Dictionary) -> void:
for ethic in pop:
if E.le(pop[ethic], 0): continue
var weights: Dictionary = QuantityMath.rule_weights(str(rules[plan.id].ethics[ethic].fall))
if weights.is_empty(): weights = {rules[plan.id].previous: 1}
var shares: Dictionary = split(target, E.add(E.add(E.add("fall:", plan.id), ":"), ethic), pop[ethic], weights)
for stage in shares: emit(plan, stage, {ethic: shares[stage]})
static func progress(world, target: Dictionary, rules: Dictionary, budget: Dictionary, plan: Dictionary, pop: Dictionary, population_total: Variant = null) -> Dictionary:
var successor: String = rules[plan.id].next
if population_total == null: population_total = total(pop)
var admitted: Variant = State.admit_transition(budget, rules, plan.id, successor, population_total)
var accepted: Dictionary = take(world, pop, admitted, population_total)
emit(plan, successor, accepted)
return pop
static func new_plan(id: String, year: Variant) -> Dictionary:
return {"id": id, "from": year, "to": E.neg(INF), "amount": 0, "outputs": {}}
static func consume_untriggered(world, target: Dictionary, plan: Dictionary, pop: Dictionary, count: Variant, year: Variant, population_total: Variant = null) -> Dictionary:
var old_total: Variant = total(pop) if population_total == null else population_total
var actual: Variant = E.floor_value(E.minimum(E.maximum(0, count), old_total))
var removed: Dictionary = take(world, pop, actual, old_total)
if E.gt(actual, 0):
world.batches.scale_batches(target, plan.id, E.divide((E.subtract(old_total, actual)), old_total))
plan.amount = E.add(plan.amount, actual)
plan.to = E.maximum(plan.to, year)
return removed
# Cache products per ethic, then sum them in the original stage/ethic order.
# Keeping the summation order preserves float fractional rounding exactly.
static func worship(world, target: Dictionary, rules: Dictionary) -> Variant:
var result: Variant = 0 var result: Variant = 0
if physical.is_empty(): physical = 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:
@@ -215,215 +37,11 @@ static func coefficients(world, rules: Dictionary, id: String, modifiers: Dictio
var rates: Dictionary = {} var rates: Dictionary = {}
for ethic in rules[id].ethics: for ethic in rules[id].ethics:
var row: Dictionary = rules[id].ethics[ethic] var row: Dictionary = rules[id].ethics[ethic]
var p: Variant = E.maximum(0, E.multiply(E.of(row.progress), modifiers.progress)) var p: float = maxf(0.0, E.approx(E.of(row.progress)) * E.approx(modifiers.progress))
var d: Variant = E.maximum(0, E.multiply(E.of(row.degradation), modifiers.degradation)) var d: float = maxf(0.0, E.approx(E.of(row.degradation)) * E.approx(modifiers.degradation))
var scale: Variant = E.maximum(1, E.add(p, d)) var scale: float = maxf(1.0, p + d)
rates[ethic] = {"pro": E.divide(p, scale), "deg": E.divide(d, scale), rates[ethic] = {"pro": p / scale, "deg": d / scale,
"war": E.minimum(1, E.multiply(E.of(row.war), modifiers.war)), "war": clampf(E.approx(E.of(row.war)) * E.approx(modifiers.war), 0.0, 1.0),
"col": E.minimum(1, E.multiply(E.of(row.colonization), modifiers.colonization))} "col": clampf(E.approx(E.of(row.colonization)) * E.approx(modifiers.colonization), 0.0, 1.0)}
world.ledger.civilization_coefficients[id] = {"signature": signature, "rates": rates} world.ledger.civilization_coefficients[id] = {"signature": signature, "rates": rates}
return rates return rates
static func prepare(world, target: Dictionary, expired: Dictionary, year: Variant, budget: Dictionary, rules: Dictionary, physical: Dictionary = {}, budget_timer: RefCounted = null, expired_totals: Dictionary = {}) -> Dictionary:
var profile_started: int = world.profile_start()
var ids: Array = []
for id in expired:
if rules.has(id) and not expired[id].is_empty(): ids.append(id)
if ids.is_empty():
world.profile_end("civilization_prepare", profile_started)
return {"active": false}
var reconcile_ids: Array = []
for id in ids:
reconcile_ids.append(id)
if rules[id].fights and not reconcile_ids.has(rules[id].previous): reconcile_ids.append(rules[id].previous)
State.reconcile(world, target, rules, physical, reconcile_ids)
var remaining: Dictionary = target.ethics.duplicate()
var copied: Dictionary = {}
var plans: Dictionary = {}
var colonies: Array = []
var collateral: Dictionary = {}
var modifiers: Dictionary = WorldParameters.life_modifiers(target)
var order: Variant = WorldParameters.value(target, "Order")
if (not target.has("civilization_stats")): target.civilization_stats = {}
ids.sort()
for id in ids:
if budget_timer != null and not await budget_timer.pause(): return {}
if ((not rules.has(id)) or expired[id].is_empty()): continue
var plan: Dictionary = new_plan(id, year)
plans[id] = plan
plan.amount = expired_totals.get(id, 0)
for part : Dictionary in expired[id]:
if not expired_totals.has(id): plan.amount = E.add(plan.amount, part.amount)
plan.from = E.minimum(plan.from, part.from)
plan.to = E.maximum(plan.to, part.to)
remaining[id] = remaining[id].duplicate()
copied[id] = true
var triggered: Dictionary = take(world, remaining[id], plan.amount)
var successes: Dictionary = {}
var failures: Dictionary = {}
var warriors: Dictionary = {}
var settlers: Dictionary = {}
var stage_rates: Dictionary = coefficients(world, rules, id, modifiers)
for ethic in triggered:
var probability: Dictionary = stage_rates[ethic]
var prefix: String = id + ":" + ethic
var pro: Variant = rate(target, prefix + ":pro", triggered[ethic], probability.pro)
var deg: Variant = rate(target, prefix + ":deg", triggered[ethic], probability.deg)
var bounded: Dictionary = QuantityMath.allocate_whole({"pro": pro, "deg": deg}, triggered[ethic])
if rules[id].fights: warriors[ethic] = rate(target, prefix + ":war", triggered[ethic], probability.war)
if rules[id].colonizes: settlers[ethic] = rate(target, prefix + ":col", triggered[ethic], probability.col)
successes[ethic] = bounded.pro
failures[ethic] = bounded.deg
triggered[ethic] = E.subtract(triggered[ethic], E.add(bounded.pro, bounded.deg))
var profile_transitions: int = world.profile_start()
add(triggered, progress(world, target, rules, budget, plan, successes))
fall(target, plan, rules, failures)
world.profile_end("civilization_transitions", profile_transitions)
if rules[id].fights:
var profile_wars: int = world.profile_start()
for ethic in warriors: warriors[ethic] = E.minimum(warriors[ethic], triggered[ethic])
var warriors_total: Variant = total(warriors)
var triggered_total: Variant = total(triggered)
var remaining_total: Variant = total(remaining[id])
var wars: Variant = E.minimum(warriors_total, E.floor_value(E.divide(E.add(triggered_total, remaining_total), 2)))
warriors = QuantityMath.allocate_whole_with_total(warriors, wars, warriors_total)
for ethic in warriors: triggered[ethic] = E.subtract(triggered[ethic], warriors[ethic])
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})
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")))
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))
var wins_requested: Variant = rate(target, E.add(id, ":wins"), win_expected, 1)
var winners: Dictionary = take(world, warriors, 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)
var losses_requested: Variant = rate(target, E.add(id, ":attackers_lost"), E.divide(outcomes.get("deg", 0), order), 1)
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))
warriors_total = E.subtract(warriors_total, attackers_lost_total)
add(triggered, progress(world, target, rules, budget, plan, winners, winner_total))
triggered_total = total(triggered)
fall(target, plan, rules, attackers_lost)
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 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)
triggered_total = E.subtract(triggered_total, sides.due)
add(victims, consume_untriggered(world, target, plan, remaining[id], sides.later, year, remaining_total))
fall(target, plan, rules, victims)
add(triggered, warriors)
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)
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)
world.profile_end("wars", profile_wars)
emit(plan, id, triggered)
if E.gt(total(settlers), 0): colonies.append({"stage": rules[id].colony, "source": id, "population": settlers})
# Collateral is applied after all stages planned their own attempts.
for id in collateral:
if (not rules.has(id)): continue
if not copied.has(id):
remaining[id] = remaining[id].duplicate()
copied[id] = true
if (not plans.has(id)): plans[id] = new_plan(id, year)
var plan: Dictionary = plans[id]
var retry: Dictionary = plan.outputs.get(id, {})
var retry_total: Variant = total(retry)
var remaining_total: Variant = total(remaining[id])
var amount: Variant = E.minimum(collateral[id], 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)
add(victims, consume_untriggered(world, target, plan, remaining[id], sides.later, year, remaining_total))
fall(target, plan, rules, victims)
target.civilization_stats.collateral = E.add(target.civilization_stats.get("collateral", 0), total(victims))
var incoming: Dictionary = {}
var planetary: Dictionary = {}
for id in plans:
if budget_timer != null and not await budget_timer.pause(): return {}
var plan: Dictionary = plans[id]
var products: Dictionary = {}
for destination in plan.outputs:
var pop: Dictionary = plan.outputs[destination]
var count: Variant = total(pop)
if rules.has(destination):
if E.ne(destination, "Ct") and E.gt(count, 0):
var profile_drift: int = world.profile_start()
pop = drift(target, E.add(E.add(E.add(id, ":"), destination), ":drift"), pop, rules[id].ethics, (E.eq(destination, id) and rules[id].diplomacy), false, count)
world.profile_end("ethics_drift", profile_drift)
if (not incoming.has(destination)): incoming[destination] = {}
add(incoming[destination], pop)
products[destination] = count
if (State.is_space_civilization(id, rules) and E.eq(world.catalog.entities[destination].group_as, "Planet")):
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}]
world.profile_end("civilization_prepare", profile_started)
return {"active": true, "remaining": remaining, "incoming": incoming, "planetary": planetary, "colonies": colonies}
static func finish(world, target: Dictionary, transaction: Dictionary, year: Variant, rules: Dictionary, budget_timer: RefCounted = null) -> void:
var profile_started: int = world.profile_start()
if not transaction.get("active", true):
State.reconcile(world, target, rules)
if not target.has("civilization_stats"): target.civilization_stats = {}
target.civilization_stats.worship = worship(world, target, rules)
world.profile_end("civilization_finish", profile_started)
return
target.ethics = transaction.remaining
for id in transaction.incoming:
if (not target.ethics.has(id)): target.ethics[id] = {}
add(target.ethics[id], transaction.incoming[id])
# Space civilizations that fall back to planetary stages need a stellar host.
var physical: Dictionary = world.ledger.physical_amounts(target)
var hosts: Dictionary = {}
for id in physical:
if (E.eq(world.catalog.entities[id].event_group, "DwStar") and E.gt(physical[id], 0)): hosts[id] = physical[id]
for id in transaction.planetary:
var shares: Dictionary = split(target, E.add("return:", id), E.minimum(transaction.planetary[id], target.amount.get(id, 0)), hosts)
for owner in shares:
if (not target.binds.has(owner)): target.binds[owner] = {}
target.binds[owner][id] = E.add(target.binds[owner].get(id, 0), shares[owner])
target.amount[id] = E.subtract(target.amount[id], shares[owner])
if budget_timer != null:
await colonize(world, target, transaction.colonies, year, rules, physical, budget_timer)
if budget_timer.cancelled: return
else:
colonize(world, target, transaction.colonies, year, rules, physical)
State.reconcile(world, target, rules, physical if transaction.colonies.is_empty() else {})
target.civilization_stats.worship = worship(world, target, rules)
world.profile_end("civilization_finish", profile_started)
static func colonize(world, target: Dictionary, colonies: Array, year: Variant, rules: Dictionary, physical: Dictionary = {}, budget_timer: RefCounted = null) -> void:
if colonies.is_empty(): return
var profile_colonies: int = world.profile_start()
if physical.is_empty(): physical = world.ledger.physical_amounts(target)
var stock: Dictionary = {}
for id in ["GG", "AB", "RP", "HP"]: stock[id] = physical.get(id, 0)
var requests: Dictionary = {}
for i in colonies.size(): requests[str(i)] = total(colonies[i].population)
var granted: Dictionary = QuantityMath.allocate_whole(requests, total(stock))
for i in colonies.size():
if budget_timer != null and not await budget_timer.pause(): return
var colony: Dictionary = colonies[i]
var population: Dictionary = QuantityMath.allocate_whole(colony.population, granted[str(i)])
population = drift(target, E.add(colony.source, ":colonies"), population, rules[colony.source].ethics, false, true)
var count: Variant = total(population)
var targets: Dictionary = QuantityMath.allocate_whole(stock, count)
for id in targets:
var amount: Variant = targets[id]
if E.le(amount, 0): continue
var slots: Dictionary = {"": target.amount.get(id, 0)}
for owner in target.binds: slots[owner] = target.binds[owner].get(id, 0)
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)
for owner in taken:
if E.le(taken[owner], 0): continue
if E.eq(owner, ""): target.amount[id] = E.subtract(target.amount[id], taken[owner])
else: target.binds[owner][id] = E.subtract(target.binds[owner][id], taken[owner])
var created: Variant = world.ledger.credit(target, colony.stage, taken[owner])
if E.ne(owner, ""):
target.amount[colony.stage] = E.subtract(target.amount[colony.stage], created)
target.binds[owner][colony.stage] = E.add(target.binds[owner].get(colony.stage, 0), created)
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)
world.profile_end("colonization", profile_colonies)
+568
View File
@@ -0,0 +1,568 @@
extends "res://world/owniverse/simulation_module.gd"
## Continuous expected flows. Only ledger quantities and rounding carries are exact.
const E = preload("res://world/owniverse/math/exact.gd")
const State = preload("res://world/owniverse/civilization_state.gd")
const Q = preload("res://world/owniverse/quantity_math.gd")
const Civ = preload("res://world/owniverse/civilization_evolution.gd")
const Parameters = preload("res://world/owniverse/world_parameters.gd")
const ETHICS = State.ETHICS
var _generator_key: String = ""
var _generator: Dictionary = {}
var _solutions: Dictionary = {}
func _edges(target: Dictionary, rules: Dictionary, physical: Dictionary = {}) -> Dictionary:
var modifiers: Dictionary = Parameters.life_modifiers(target)
var order: float = maxf(1e-12, E.approx(Parameters.value(target, "Order")))
var stages: Array = rules.keys()
stages.sort()
var rates: Dictionary = {}
var spans: Dictionary = {}
var progression: Dictionary = {}
var budget: Dictionary = State.stellar_budget(world, target, rules, physical)
var signature: String = str(modifiers) + ":" + str(order)
for id in stages.duplicate():
if rules[id].ethics.is_empty(): continue
rates[id] = Civ.coefficients(world, rules, id, modifiers)
progression[id] = _can_progress(id, rules[id].next, budget, rules)
spans[id] = maxf(1e-12, E.approx(State.effective_span(world, target, id, rules, physical)))
signature += id + str(spans[id]) + str(rates[id]) + str(rules[id]) + str(progression[id])
for row: Dictionary in rules[id].ethics.values():
for destination in Q.rule_weights(str(row.fall)):
if not stages.has(destination): stages.append(destination)
if signature == _generator_key: return _generator
var build_started: int = world.profile_start()
var index: Dictionary = {}
for i in stages.size(): index[stages[i]] = i
var rows: Array = []
var rewards: Array = []
var maximum: float = 0.0
for id in stages:
for e in 6:
var edges: Dictionary = {}
var reward: Dictionary = {"war": 0.0, "col": 0.0, "colony": "", "previous": ""}
if rates.has(id):
var ethic: String = ETHICS[e]
var r: Dictionary = rates[id][ethic]
var span: float = spans[id]
var war: float = minf(0.5, E.approx(r.war)) if rules[id].fights else 0.0
var progress: float = (E.approx(r.pro) + war * 17.0 / 36.0) / span if progression[id] else 0.0
var degradation: float = (E.approx(r.deg) + war * 31.0 / 18.0 / order) / span
var next_node: int = index[rules[id].next] * 6 + e
edges[next_node] = progress
var falls: Dictionary = Q.rule_weights(str(rules[id].ethics[ethic].fall))
if falls.is_empty(): falls = {rules[id].previous: 1}
var fall_total: float = 0.0
for weight in falls.values(): fall_total += E.approx(weight)
for destination in falls:
var node: int = index[destination] * 6 + e
edges[node] = float(edges.get(node, 0.0)) + degradation * E.approx(falls[destination]) / fall_total
reward = {"war": war / span, "col": E.approx(r.col) / span if rules[id].colonizes else 0.0,
"colony": rules[id].colony, "previous": rules[id].previous}
var outgoing: float = 0.0
var self_node: int = index[id] * 6 + e
edges.erase(self_node)
for rate_value in edges.values(): outgoing += rate_value
maximum = maxf(maximum, outgoing)
rows.append({"edges": edges, "outgoing": outgoing})
rewards.append(reward)
_generator_key = signature
_solutions.clear()
var targets: Array = []
var probabilities: Array = []
for node in rows.size():
var destinations: PackedInt32Array = []
var weights: PackedFloat64Array = []
destinations.append(node)
weights.append(maxf(0.0, 1.0 - rows[node].outgoing / maximum) if maximum > 0.0 else 1.0)
for destination in rows[node].edges:
destinations.append(destination)
weights.append(rows[node].edges[destination] / maximum if maximum > 0.0 else 0.0)
targets.append(destinations)
probabilities.append(weights)
_generator = {"stages": stages, "index": index, "rows": rows, "rewards": rewards, "lambda": maximum, "order": order, "targets": targets, "probabilities": probabilities}
world.profile_end("civilization_operator_build", build_started)
return _generator
func _can_progress(source: String, destination: String, budget: Dictionary, rules: Dictionary) -> bool:
var home: String = rules[destination].home
if home != "":
var old_cost: Variant = E.maximum(1, rules[source].stars) if rules[source].home == home else 0
var net: Variant = E.subtract(E.maximum(1, rules[destination].stars), old_cost)
if E.gt(net, 0) and E.lt(budget.free.get(home, 0), net): return false
var new_req: Dictionary = budget.habitats.requirements.get(destination, {})
var old_req: Dictionary = budget.habitats.requirements.get(source, {})
if not new_req.is_empty():
var net: Variant = E.subtract(new_req.units, old_req.get("units", 0) if old_req.get("id", "") == new_req.id else 0)
var free: Variant = E.subtract(budget.habitats.capacity.get(new_req.id, 0), budget.habitats.requested.get(new_req.id, 0))
if E.gt(net, 0) and E.lt(free, net): return false
return true
## Uniformization computes exp(Q*t) through a Poisson series of sparse steps.
## Absorbing biological stages and Ct retain their mass. No one-transition cap.
func _solve(generator: Dictionary, source: String, profile: Dictionary, years: float) -> Dictionary:
var approximation: int = world.adaptive_quality
var key: String = source + ":" + str(years) + ":" + str(approximation)
var basis: Dictionary = _solutions.get(key, {})
if basis.is_empty():
var started: int = world.profile_start()
basis = _solve_single_step(generator, source, years) if approximation > 0 else _solve_basis(generator, source, years)
world.profile_end("civilization_operator_solve", started)
if _solutions.size() >= 512: _solutions.clear()
_solutions[key] = basis
var mass: PackedFloat64Array = basis.mass.duplicate()
var occupation: PackedFloat64Array = basis.occupation.duplicate()
var shares: PackedFloat64Array = []
for ethic in ETHICS: shares.append(profile[ethic])
for node in mass.size():
mass[node] *= shares[node % 6]
occupation[node] *= shares[node % 6]
return {"mass": mass, "occupation": occupation, "occupation_scale": 1.0}
## Load-shedding approximation: each ethic cohort can make at most one
## transition during this interval. The exact Markov solve remains the default.
func _solve_single_step(generator: Dictionary, source: String, years: float) -> Dictionary:
var size: int = generator.rows.size()
var mass: PackedFloat64Array = []
var occupation: PackedFloat64Array = []
mass.resize(size)
occupation.resize(size)
var offset: int = generator.index[source] * 6
for ethic in 6:
var node: int = offset + ethic
var row: Dictionary = generator.rows[node]
var outgoing: float = row.outgoing
if outgoing <= 0.0:
mass[node] = 1.0
occupation[node] = years
continue
var moved: float = clampf(1.0 - exp(-outgoing * years), 0.0, 1.0)
var stayed: float = 1.0 - moved
mass[node] += stayed
occupation[node] += years * (1.0 - moved * 0.5)
for destination in row.edges:
var rate: float = row.edges[destination]
if rate <= 0.0: continue
var share: float = moved * rate / outgoing
mass[destination] += share
occupation[destination] += years * share * 0.5
return {"mass": mass, "occupation": occupation}
func _solve_basis(generator: Dictionary, source: String, years: float) -> Dictionary:
var size: int = generator.rows.size()
var values: PackedFloat64Array = []
values.resize(size)
var active: PackedInt32Array = []
var offset: int = generator.index[source] * 6
for e in 6:
values[offset + e] = 1.0
active.append(offset + e)
var mass: PackedFloat64Array = []
var occupation: PackedFloat64Array = []
mass.resize(size)
occupation.resize(size)
var frequency: float = generator.lambda
if frequency <= 0.0:
mass = values.duplicate()
for node in active: occupation[node] = years
return {"mass": mass, "occupation": occupation}
var mean: float = frequency * years
var weight: float = exp(-mean)
var cumulative: float = 0.0
var limit: int = maxi(32, ceili(mean + 12.0 * sqrt(mean) + 32.0))
for step in limit:
cumulative = minf(1.0, cumulative + weight)
var time_weight: float = maxf(0.0, 1.0 - cumulative) / frequency
for node in active:
mass[node] += weight * values[node]
occupation[node] += time_weight * values[node]
if step > mean and weight < 1e-18: break
var next: PackedFloat64Array = []
next.resize(size)
var next_active: PackedInt32Array = []
for node in active:
var destinations: PackedInt32Array = generator.targets[node]
var probabilities: PackedFloat64Array = generator.probabilities[node]
for i in destinations.size():
var contribution: float = values[node] * probabilities[i]
if contribution <= 0.0: continue
var destination: int = destinations[i]
if next[destination] == 0.0: next_active.append(destination)
next[destination] += contribution
values = next
active = next_active
weight *= mean / float(step + 1)
var integrated: PackedFloat64Array = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
for node in size: integrated[node % 6] += occupation[node]
for e in 6:
if integrated[e] <= 0.0:
occupation[offset + e] = years
else:
var factor: float = years / integrated[e]
for node in range(e, size, 6): occupation[node] *= factor
return {"mass": mass, "occupation": occupation}
func _drift(target: Dictionary, rules: Dictionary, generator: Dictionary, years: float, physical: Dictionary = {}) -> void:
for id in rules:
if rules[id].ethics.is_empty() or not target.ethics.has(id): continue
var profile: Dictionary = State.normalize_profile(target.ethics[id])
var span: float = maxf(1e-12, E.approx(State.effective_span(world, target, id, rules, physical)))
var duration: float = years / span
var pair_decay: float = exp(-0.03 * duration)
var opposite_decay: float = exp(-0.02 * duration)
var result: Dictionary = {}
for ethic in ETHICS:
var opposite: float = profile[Civ.OPPOSITE[ethic]]
result[ethic] = 1.0 / 6.0 + pair_decay * ((profile[ethic] + opposite) * 0.5 - 1.0 / 6.0) + opposite_decay * (profile[ethic] - opposite) * 0.5
if rules[id].diplomacy:
var weighted: Dictionary = {}
for ethic in ETHICS: weighted[ethic] = result[ethic] * maxf(0.0, E.approx(E.of(rules[id].ethics[ethic].diplomacy_rate)))
weighted = State.normalize_profile(weighted)
var fraction: float = 1.0 - exp(-duration)
for ethic in ETHICS: result[ethic] = lerpf(result[ethic], weighted[ethic], fraction)
target.ethics[id] = State.normalize_profile(result)
func _profile(vector: Array) -> Dictionary:
var result: Dictionary = {}
for e in 6: result[ETHICS[e]] = vector[e]
return State.normalize_profile(result)
func _merge(plan: Dictionary, destination: String, count: Variant, profile: Dictionary) -> void:
if E.le(count, 0): return
var previous: Variant = plan.counts.get(destination, 0)
plan.profiles[destination] = State.merge_profiles(plan.profiles.get(destination, {}), previous, profile, count)
plan.counts[destination] = E.add(previous, count)
func _plans(target: Dictionary, physical: Dictionary, rules: Dictionary, generator: Dictionary, years: float) -> Dictionary:
var plans: Dictionary = {}
var colonies: Dictionary = {}
var damages: Dictionary = {}
var wars: Variant = 0
for source in rules:
var count: Variant = physical.get(source, 0)
if E.le(count, 0) or rules[source].ethics.is_empty(): continue
var solved: Dictionary = _solve(generator, source, State.normalize_profile(target.ethics.get(source, {})), years)
var vectors: Dictionary = {}
for node in solved.mass.size():
if solved.mass[node] <= 0.0: continue
var destination: String = generator.stages[int(node) / 6]
if not vectors.has(destination): vectors[destination] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
vectors[destination][int(node) % 6] += solved.mass[node]
var ids: Array = []
var shares: Array = []
var profiles: Dictionary = {}
var retained: float = 0.0
for destination in vectors:
var share: float = 0.0
for value in vectors[destination]: share += value
profiles[destination] = _profile(vectors[destination])
if destination == source or share < 0.5e-15:
retained += share
else:
ids.append(destination)
shares.append(share)
ids.append(source)
shares.append(retained)
if not profiles.has(source): profiles[source] = State.normalize_profile(target.ethics.get(source, {}))
var counts: Dictionary = Q.split_profile(target.civilization_remainders, "flow:" + source, count, ids, shares)
plans[source] = {"counts": counts, "profiles": profiles}
var colony_vectors: Dictionary = {}
var damage: Dictionary = {}
var war_reward: float = 0.0
for node in solved.occupation.size():
if solved.occupation[node] <= 0.0: continue
var reward: Dictionary = generator.rewards[node]
var duration: float = solved.occupation[node] * solved.occupation_scale
war_reward += duration * reward.war
if reward.war > 0.0:
damage[reward.previous] = float(damage.get(reward.previous, 0.0)) + duration * reward.war * 40.0 / 3.0 / generator.order
if reward.col <= 0.0: continue
if not colony_vectors.has(reward.colony): colony_vectors[reward.colony] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
colony_vectors[reward.colony][int(node) % 6] += duration * reward.col
wars = E.add(wars, Q.accumulate_rate(target.civilization_remainders, "flow:wars:" + source, count, war_reward))
for destination in damage:
damages[destination] = E.add(damages.get(destination, 0), Q.accumulate_rate(target.civilization_remainders, "flow:damage:" + source + ":" + destination, count, damage[destination]))
for destination in colony_vectors:
var reward: float = 0.0
for value in colony_vectors[destination]: reward += value
var quantity: Variant = Q.accumulate_rate(target.civilization_remainders, "flow:colony:" + source + ":" + destination, count, reward)
if not colonies.has(destination): colonies[destination] = {"counts": {}, "profiles": {}}
_merge(colonies[destination], destination, quantity, _profile(colony_vectors[destination]))
return {"plans": plans, "colonies": colonies, "damages": damages, "wars": wars}
## Positive resource costs share the same frozen free budget. Rejected movement
## stays at its source. Releases become available after this transaction commits.
func _limit(plans: Dictionary, budget: Dictionary, rules: Dictionary) -> void:
var demands: Dictionary = {}
var costs: Dictionary = {}
for source in plans:
costs[source] = {}
for destination in plans[source].counts:
if destination == source or not rules.has(destination): continue
var amount: Variant = plans[source].counts[destination]
if E.le(amount, 0): continue
var resource_costs: Dictionary = {}
var home: String = rules[destination].home
if home != "":
var old_cost: Variant = rules[source].stars if rules[source].home == home else 0
var net: Variant = E.maximum(0, E.subtract(E.maximum(1, rules[destination].stars), old_cost))
if E.gt(net, 0): resource_costs["star:" + home] = net
var housing: Dictionary = budget.habitats
var new_req: Dictionary = housing.requirements.get(destination, {})
var old_req: Dictionary = housing.requirements.get(source, {})
if not new_req.is_empty():
var net: Variant = E.maximum(0, E.subtract(new_req.units, old_req.get("units", 0) if old_req.get("id", "") == new_req.id else 0))
if E.gt(net, 0): resource_costs["house:" + new_req.id] = net
costs[source][destination] = resource_costs
for resource in resource_costs:
demands[resource] = E.add(demands.get(resource, 0), E.multiply(amount, resource_costs[resource]))
var factors: Dictionary = {}
for resource in demands:
var available: Variant
if resource.begins_with("star:"):
available = budget.free.get(resource.trim_prefix("star:"), 0)
else:
var id: String = resource.trim_prefix("house:")
available = E.maximum(0, E.subtract(budget.habitats.capacity.get(id, 0), budget.habitats.requested.get(id, 0)))
factors[resource] = [available, demands[resource]]
for source in costs:
for destination in costs[source]:
var requested: Variant = plans[source].counts[destination]
var allowed: Variant = requested
for resource in costs[source][destination]:
var ratio: Array = factors[resource]
if E.lt(ratio[0], ratio[1]): allowed = E.minimum(allowed, E.floor_value(E.mul_div(requested, ratio[0], ratio[1])))
var rejected: Variant = E.subtract(requested, allowed)
if E.le(rejected, 0): continue
plans[source].counts[destination] = allowed
_merge(plans[source], source, rejected, plans[source].profiles[destination])
## Apply one conserved source-by-owner table, then credit each destination once.
func _commit(target: Dictionary, physical: Dictionary, plans: Dictionary, rules: Dictionary, year: Variant) -> Dictionary:
var credits: Dictionary = {}
var contributions: Dictionary = {}
var births: Dictionary = {}
var ownership: Dictionary = {}
# Each source quota replaces its old physical population. Transfers between
# owners do not change these exact totals, and credits are applied below.
var final: Dictionary = physical.duplicate()
for source in plans:
if final.has(source): final[source] = plans[source].counts.get(source, 0)
var space_stages: Dictionary = {"Ct": true}
for id in rules: space_stages[id] = id == "Ct" or str(rules[id].home) != ""
for source in plans:
ownership[source] = {}
var free: Variant = target.amount.get(source, 0)
if E.gt(free, 0): ownership[source][""] = free
for owner in target.binds:
for source in target.binds[owner]:
if not ownership.has(source): continue
var held: Variant = target.binds[owner][source]
if E.gt(held, 0): ownership[source][owner] = held
for source in plans:
var plan: Dictionary = plans[source]
var remaining: Dictionary = {}
for destination in plan.counts:
var amount: Variant = plan.counts[destination]
if E.le(amount, 0): continue
remaining[destination] = amount
if not contributions.has(destination): contributions[destination] = []
contributions[destination].append({"amount": amount, "profile": plan.profiles[destination]})
var slots: Dictionary = ownership[source]
target.amount[source] = 0
var owners: Array = slots.keys()
if owners.size() == 1 and owners[0] == "":
target.amount[source] = remaining.get(source, 0)
for destination in remaining:
if destination == source: continue
var host: String = "@host" if space_stages.get(source, false) and not space_stages.get(destination, false) else ""
if not credits.has(destination): credits[destination] = {}
credits[destination][host] = E.add(credits[destination].get(host, 0), remaining[destination])
continue
for i in owners.size():
var owner: String = owners[i]
# Last row receives the remaining exact quotas; the free-only path
# therefore needs no proportional allocation or repeated subtraction.
var allocation: Dictionary = remaining if i == owners.size() - 1 else Q.allocate_profiled(remaining, slots[owner])
for destination in allocation:
var amount: Variant = allocation[destination]
if E.le(amount, 0): continue
if i != owners.size() - 1:
remaining[destination] = E.subtract(remaining[destination], amount)
if destination == source:
if owner == "": target.amount[source] = amount
else: target.binds[owner][source] = amount
continue
var host: String = owner
if space_stages.get(destination, false): host = ""
elif owner == "" and space_stages.get(source, false): host = "@host"
if not credits.has(destination): credits[destination] = {}
credits[destination][host] = E.add(credits[destination].get(host, 0), amount)
if owner != "" and E.le(allocation.get(source, 0), 0): target.binds[owner][source] = 0
var hosts: Dictionary = {}
for id in physical:
if world.catalog.entities[id].event_group == "DwStar" and E.gt(physical[id], 0): hosts[id] = physical[id]
var host_shares: Array = _host_shares(hosts) if not hosts.is_empty() else []
for destination in credits:
var requested: Variant = Q.sum(credits[destination])
var created: Variant = world.ledger.credit(target, destination, requested)
if final.has(destination): final[destination] = E.add(final[destination], created)
var bound: Variant = 0
for owner in credits[destination]:
var count: Variant = credits[destination][owner]
if owner == "" or E.le(count, 0): continue
if owner == "@host":
if hosts.is_empty(): continue
var distributed: Dictionary = Q.split_profile(target.civilization_remainders, "flow:hosts:" + destination, count, hosts.keys(), host_shares)
for host in distributed:
if E.le(distributed[host], 0): continue
if not target.binds.has(host): target.binds[host] = {}
target.binds[host][destination] = E.add(target.binds[host].get(destination, 0), distributed[host])
else:
target.binds[owner][destination] = E.add(target.binds[owner].get(destination, 0), count)
bound = E.add(bound, count)
target.amount[destination] = E.subtract(target.amount[destination], bound)
if not rules.has(destination): births[destination] = created
for destination in contributions:
if not rules.has(destination) or E.le(final.get(destination, 0), 0): continue
var profile: Dictionary = {}
for ethic in ETHICS: profile[ethic] = 0.0
var existing: Variant = physical.get(destination, 0) if not plans.has(destination) else 0
if E.gt(existing, 0):
var original: Dictionary = State.normalize_profile(target.ethics.get(destination, {}))
var ratio: float = E.ratio_approx(existing, final[destination])
for ethic in ETHICS: profile[ethic] += original[ethic] * ratio
for entry: Dictionary in contributions[destination]:
var ratio: float = E.ratio_approx(entry.amount, final[destination])
for ethic in ETHICS: profile[ethic] += entry.profile[ethic] * ratio
target.ethics[destination] = State.normalize_profile(profile)
for destination in births: world.batches.append_batch(target, destination, births[destination], year, year)
return final
func _host_shares(hosts: Dictionary) -> Array:
var total: Variant = Q.sum(hosts)
var result: Array = []
for value in hosts.values(): result.append(E.ratio_approx(value, total))
return result
func _colonize(target: Dictionary, colonies: Dictionary, year: Variant, rules: Dictionary, physical: Dictionary = {}) -> Dictionary:
if physical.is_empty(): physical = world.ledger.physical_amounts(target)
if colonies.is_empty(): return physical
var stocks: Dictionary = {}
for id in ["GG", "AB", "RP", "HP"]: stocks[id] = physical.get(id, 0)
var requests: Dictionary = {}
for id in colonies: requests[id] = colonies[id].counts.get(id, 0)
var granted: Dictionary = Q.allocate_profiled(requests, Q.sum(stocks))
var total: Variant = Q.sum(granted)
if E.le(total, 0): return physical
var taken_resources: Dictionary = Q.allocate_profiled(stocks, total)
var removed_by_owner: Dictionary = {}
for resource in taken_resources:
var removed: Variant = taken_resources[resource]
if E.le(removed, 0): continue
var owners: Dictionary = {"": target.amount.get(resource, 0)}
for owner in target.binds:
if E.gt(target.binds[owner].get(resource, 0), 0): owners[owner] = target.binds[owner][resource]
var taken: Dictionary = Q.allocate_profiled(owners, removed)
world.batches.scale_batches(target, resource, E.subtract(1, E.divide(removed, stocks[resource])))
for owner in taken:
var amount: Variant = taken[owner]
if E.le(amount, 0): continue
if owner == "": target.amount[resource] = E.subtract(target.amount[resource], amount)
else: target.binds[owner][resource] = E.subtract(target.binds[owner][resource], amount)
removed_by_owner[owner] = E.add(removed_by_owner.get(owner, 0), amount)
var remaining: Dictionary = granted.duplicate()
var placements: Dictionary = {}
var final: Dictionary = physical.duplicate()
var owners: Array = removed_by_owner.keys()
for i in owners.size():
var owner: String = owners[i]
var allocation: Dictionary = remaining if i == owners.size() - 1 else Q.allocate_profiled(remaining, removed_by_owner[owner])
for destination in allocation:
var count: Variant = allocation[destination]
if E.le(count, 0): continue
if i != owners.size() - 1: remaining[destination] = E.subtract(remaining[destination], count)
if owner == "" or State.is_space_civilization(destination, rules): continue
if not placements.has(destination): placements[destination] = {}
placements[destination][owner] = count
for destination in granted:
var count: Variant = granted[destination]
if E.le(count, 0): continue
var created: Variant = world.ledger.credit(target, destination, count)
if final.has(destination): final[destination] = E.add(final[destination], created)
var bound: Variant = 0
for owner in placements.get(destination, {}):
var held: Variant = placements[destination][owner]
target.binds[owner][destination] = E.add(target.binds[owner].get(destination, 0), held)
bound = E.add(bound, held)
target.amount[destination] = E.subtract(target.amount[destination], bound)
for id in granted:
if E.le(granted[id], 0): continue
var profile: Dictionary = colonies[id].profiles[id]
var drifted: Dictionary = {}
for ethic in ETHICS:
drifted[ethic] = 0.0
for source in ETHICS:
drifted[ethic] += profile[source] * (0.96 if source == ethic else (0.004 if Civ.OPPOSITE[source] == ethic else 0.009))
target.ethics[id] = State.merge_profiles(target.ethics.get(id, {}), physical.get(id, 0), drifted, granted[id])
target.civilization_stats.colonies = E.add(target.civilization_stats.get("colonies", 0), total)
for resource in taken_resources:
final[resource] = E.subtract(final.get(resource, 0), taken_resources[resource])
return final
func evolve(target: Dictionary, from_year: Variant, to_year: Variant, timer: RefCounted = null) -> void:
var keep_alive: RefCounted = self
var years: float = E.approx(E.subtract(to_year, from_year))
if years <= 0.0: return
var rules: Dictionary = Global.CivilizationRules
var generator: Dictionary = {}
# Bound the Poisson mean, processing the entire requested interval.
var maximum_years: float = years
var cursor: Variant = from_year
var physical: Dictionary = world.ledger.physical_amounts(target)
while E.lt(cursor, to_year):
if timer != null and not await timer.pause(): return
generator = _edges(target, rules, physical)
maximum_years = 32.0 / generator.lambda if generator.lambda > 0.0 else years
var end: Variant = E.minimum(to_year, E.add(cursor, E.of(maximum_years)))
var started: int = world.profile_start()
var transaction: Dictionary = _plans(target, physical, rules, generator, E.approx(E.subtract(end, cursor)))
world.profile_end("civilization_flow_plan", started)
if timer != null and not await timer.pause(): return
var plans: Dictionary = transaction.plans
var damage_total: Variant = 0
for source in transaction.damages:
if not plans.has(source): continue
var retained: Variant = plans[source].counts.get(source, 0)
var amount: Variant = E.minimum(retained, transaction.damages[source])
if E.le(amount, 0): continue
var profile: Dictionary = plans[source].profiles[source]
var weights: Dictionary = {}
for ethic in ETHICS:
var falls: Dictionary = Q.rule_weights(str(rules[source].ethics[ethic].fall))
if falls.is_empty(): falls = {rules[source].previous: 1}
var sum: float = 0.0
for weight in falls.values(): sum += E.approx(weight)
for destination in falls: weights[destination] = float(weights.get(destination, 0.0)) + profile[ethic] * E.approx(falls[destination]) / sum
var split: Dictionary = Q.split_profile(target.civilization_remainders, "flow:collateral:" + source, amount, weights.keys(), weights.values())
plans[source].counts[source] = E.subtract(retained, amount)
for destination in split: _merge(plans[source], destination, split[destination], profile)
damage_total = E.add(damage_total, amount)
started = world.profile_start()
_limit(plans, State.stellar_budget(world, target, rules, physical), rules)
world.profile_end("civilization_flow_capacity", started)
started = world.profile_start()
physical = _commit(target, physical, plans, rules, end)
world.profile_end("civilization_flow_ledger", started)
if timer != null and not await timer.pause(): return
started = world.profile_start()
physical = _colonize(target, transaction.colonies, end, rules, physical)
world.profile_end("civilization_flow_colonies", started)
started = world.profile_start()
_drift(target, rules, generator, E.approx(E.subtract(end, cursor)), physical)
target.civilization_stats.wars = E.add(target.civilization_stats.get("wars", 0), transaction.wars)
target.civilization_stats.collateral = E.add(target.civilization_stats.get("collateral", 0), damage_total)
physical = State.enforce_capacity(world, target, rules, end, physical)
State.reconcile(world, target, rules, physical)
target.civilization_stats.worship = Civ.worship(world, target, rules, physical)
world.profile_end("civilization_flow_finish", started)
cursor = end
+1
View File
@@ -0,0 +1 @@
uid://clon18ixi6yqx
+59 -31
View File
@@ -5,11 +5,46 @@ 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 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.ratio_approx(base_count, total) if E.gt(base_count, 0) else 0.0
var incoming_ratio: float = E.ratio_approx(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; flow calculations use shares directly.
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 +54,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()}
@@ -110,11 +133,13 @@ static func admit_transition(budget: Dictionary, rules: Dictionary, source: Stri
# Resolve shortages from the most advanced stages downwards. This gives existing # Resolve shortages from the most advanced stages downwards. This gives existing
# advanced populations priority and lets a demotion cascade in the same cycle. # advanced populations priority and lets a demotion cascade in the same cycle.
static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year: Variant) -> void: static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year: Variant, physical: Dictionary = {}) -> Dictionary:
var profile_started: int = world.profile_start() var profile_started: int = world.profile_start()
detach_space_civilizations(target, rules) detach_space_civilizations(target, rules)
var physical: Dictionary = world.ledger.physical_amounts(target) if physical.is_empty(): physical = world.ledger.physical_amounts(target)
else: physical = physical.duplicate()
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()
@@ -139,17 +164,20 @@ static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year:
var previous: String = (stages[E.subtract(index, 1)] if E.gt(index, 0) else "C1.9") var previous: String = (stages[E.subtract(index, 1)] if E.gt(index, 0) else "C1.9")
world.batches.scale_batches(target, id, E.divide(supported, count)) world.batches.scale_batches(target, id, E.divide(supported, count))
target.amount[id] = supported target.amount[id] = supported
world.ledger.credit(target, previous, excess) var credited: Variant = world.ledger.credit(target, previous, excess)
physical[id] = E.maximum(0, supported)
physical[previous] = E.add(physical.get(previous, 0), credited)
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 = physical
for star in current_physical: for star in current_physical:
if (E.eq(world.catalog.entities[star].event_group, "DwStar") and E.gt(current_physical[star], 0)): if (E.eq(world.catalog.entities[star].event_group, "DwStar") and E.gt(current_physical[star], 0)):
stars[star] = current_physical[star] stars[star] = current_physical[star]
@@ -167,10 +195,10 @@ static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year:
target.binds[owner][previous] = E.add(target.binds[owner].get(previous, 0), amount) target.binds[owner][previous] = E.add(target.binds[owner].get(previous, 0), amount)
target.amount[previous] = E.subtract(target.amount[previous], amount) target.amount[previous] = E.subtract(target.amount[previous], amount)
if changed: if changed:
physical = world.ledger.physical_amounts(target)
reconcile(world, target, rules, physical) reconcile(world, target, rules, physical)
assign_homes(world, target, rules, physical) assign_homes(world, target, rules, physical)
world.profile_end("enforce_capacity", profile_started) world.profile_end("enforce_capacity", profile_started)
return physical
# Unity's structure chain: direct Claim resets the required structure count; # Unity's structure chain: direct Claim resets the required structure count;
# intervening stages require ten times more units of the preceding structure. # intervening stages require ten times more units of the preceding structure.
@@ -222,15 +250,15 @@ static func assign_homes(world, target: Dictionary, rules: Dictionary, physical:
world.profile_end("assign_homes", profile_started) world.profile_end("assign_homes", profile_started)
static func effective_span(world, target: Dictionary, id: String, rules: Dictionary) -> Variant: static func effective_span(world, target: Dictionary, id: String, rules: Dictionary, physical: Dictionary = {}) -> Variant:
if (not rules.has(id)): return world.catalog.entities[id].lifespan if (not rules.has(id)): return world.catalog.entities[id].lifespan
var base: Variant = rules[id].span var base: Variant = rules[id].span
if (not is_space_civilization(id, rules)): return base if (not is_space_civilization(id, rules)): return base
var population: Variant = target.amount.get(id, 0) var population: Variant = target.amount.get(id, 0)
if E.le(population, 0): return base if E.le(population, 0): return base
var homes: Dictionary = stage_homes(world, target, id, world.catalog.civilization_layout(rules)) var homes: Dictionary = stage_homes(world, target, id, world.catalog.civilization_layout(rules), physical)
target.civilization_homes[id] = homes target.civilization_homes[id] = homes
var weighted: Variant = population var multiplier: float = 1.0
for home in homes: for home in homes:
weighted = E.add(weighted, E.multiply(homes[home], (E.subtract(E.maximum(0, world.catalog.entities[home].civ_bonus), 1)))) multiplier += E.ratio_approx(homes[home], population) * (maxf(0.0, E.approx(world.catalog.entities[home].civ_bonus)) - 1.0)
return (E.mul_div(base, population, weighted) if E.gt(weighted, 0) else base) return E.approx(base) / multiplier if multiplier > 0.0 else base
+65 -37
View File
@@ -1,15 +1,16 @@
extends "res://world/owniverse/simulation_module.gd" extends "res://world/owniverse/simulation_module.gd"
const E = preload("res://world/owniverse/math/exact.gd") const E = preload("res://world/owniverse/math/exact.gd")
const CivilizationEvolution = preload("res://world/owniverse/civilization_evolution.gd")
const CivilizationState = preload("res://world/owniverse/civilization_state.gd") const CivilizationState = preload("res://world/owniverse/civilization_state.gd")
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 QuantityMath = preload("res://world/owniverse/quantity_math.gd") const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
func initialize_life_batches() -> void: func initialize_life_batches() -> Dictionary:
for id in Global.CivilizationRules:
world.state.batches.erase(id)
world.state.batch_origins.erase(id)
CivilizationState.detach_space_civilizations(world.state, Global.CivilizationRules) CivilizationState.detach_space_civilizations(world.state, Global.CivilizationRules)
CivilizationState.enforce_capacity(world, world.state, Global.CivilizationRules, world.state.time.datetime) var physical: Dictionary = CivilizationState.enforce_capacity(world, world.state, Global.CivilizationRules, world.state.time.datetime)
var physical: Dictionary = world.ledger.physical_amounts(world.state)
for id in world.catalog.entities: for id in world.catalog.entities:
if world.catalog.entities[id].life_rule.is_empty(): if world.catalog.entities[id].life_rule.is_empty():
continue continue
@@ -17,6 +18,7 @@ func initialize_life_batches() -> void:
for batch : Dictionary in world.state.batches.get(id, []): for batch : Dictionary in world.state.batches.get(id, []):
tracked = E.add(tracked, batch.amount) tracked = E.add(tracked, batch.amount)
world.batches.append_batch(world.state, id, E.maximum(0, E.subtract(physical.get(id, 0), tracked)), world.state.time.datetime, world.state.time.datetime) world.batches.append_batch(world.state, id, E.maximum(0, E.subtract(physical.get(id, 0), tracked)), world.state.time.datetime, world.state.time.datetime)
return physical
func run_frame(clock: Dictionary, budget: RefCounted) -> void: func run_frame(clock: Dictionary, budget: RefCounted) -> void:
@@ -26,14 +28,18 @@ func run_frame(clock: Dictionary, budget: RefCounted) -> void:
func evolute_to_datetime(datetime: Variant, clock: Dictionary = {}, budget: RefCounted = null) -> void: func evolute_to_datetime(datetime: Variant, clock: Dictionary = {}, budget: RefCounted = null) -> void:
var _keep_alive: RefCounted = self var _keep_alive: RefCounted = self
var adaptive_started: int = world.adaptive_timer_usec()
# Reconcile only when simulation advances. # Reconcile only when simulation advances.
# 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)
var quantities: Dictionary = {}
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() quantities = 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)
@@ -42,13 +48,18 @@ func evolute_to_datetime(datetime: Variant, clock: Dictionary = {}, budget: RefC
var from_year: Variant = previous.time.year var from_year: Variant = previous.time.year
var to_year: Variant = processed_year var to_year: Variant = processed_year
if E.gt(to_year, from_year): if E.gt(to_year, from_year):
var quantities: Dictionary = world.ledger.physical_amounts(previous)
var distribution: Dictionary = QuantityMath.normalize_distribution(quantities) var distribution: Dictionary = QuantityMath.normalize_distribution(quantities)
next.distribution = distribution next.distribution = distribution
var profile_production: int = world.profile_start() var profile_production: int = world.profile_start()
world.automatic.produce_for_period(previous, next, quantities, distribution, from_year, to_year) world.automatic.produce_for_period(previous, next, quantities, distribution, from_year, to_year)
world.profile_end("production", profile_production) world.profile_end("production", profile_production)
if budget != null:
budget.call("set_phase", "civilization_flows")
await world.civilization_flow.evolve(next, from_year, to_year, budget)
if budget.cancelled: return
else:
world.civilization_flow.evolve(next, from_year, to_year)
var profile_generations: int = world.profile_start() var profile_generations: int = world.profile_start()
if budget != null: if budget != null:
await evolve_batches(next, to_year, budget) await evolve_batches(next, to_year, budget)
@@ -61,12 +72,14 @@ func evolute_to_datetime(datetime: Variant, clock: Dictionary = {}, budget: RefC
var profile_trends: int = world.profile_start() var profile_trends: int = world.profile_start()
WorldParameters.advance(previous, next, Global.CivilizationRules, E.subtract(to_year, from_year)) WorldParameters.advance(previous, next, Global.CivilizationRules, E.subtract(to_year, from_year))
world.profile_end("trends", profile_trends) world.profile_end("trends", profile_trends)
world.apocalypse.check_density(next)
next.time = clock if not clock.is_empty() else {"datetime": datetime, "year": to_year} next.time = clock if not clock.is_empty() else {"datetime": datetime, "year": to_year}
# Publish only after every phase has finished; previous stays untouched. # Publish only after every phase has finished; previous stays untouched.
world.state = E.bound_state(next) world.state = E.bound_state(next)
if E.gt(to_year, from_year): if E.gt(to_year, from_year):
world.presentation.refresh(world.state, world.catalog) world.presentation.refresh(world.state, world.catalog)
world.update_adaptive_quality(world.adaptive_timer_usec() - adaptive_started)
func evolve_batches(target: Dictionary, to_year: Variant, budget: RefCounted = null) -> void: func evolve_batches(target: Dictionary, to_year: Variant, budget: RefCounted = null) -> void:
@@ -78,15 +91,24 @@ func evolve_batches(target: Dictionary, to_year: Variant, budget: RefCounted = n
"enlightenment": WorldParameters.value(target, "Enlightenment"), "enlightenment": WorldParameters.value(target, "Enlightenment"),
"explosiveness": WorldParameters.effective(target, "Exp") "explosiveness": WorldParameters.effective(target, "Exp")
} }
var generations: int = 0
var generation_limit: int = 0
if world.adaptive_quality == 1: generation_limit = 3
elif world.adaptive_quality >= 2: generation_limit = 1
while await evolve_batch_generation(target, to_year, completed, budget, context): while await evolve_batch_generation(target, to_year, completed, budget, context):
generations += 1
# Direct terminal stellar events queued during a generation still run
# immediately, preserving their blast and matter outputs.
if generation_limit > 0 and generations >= generation_limit and completed.is_empty(): break
if budget != null and not await budget.pause(): return if budget != null and not await budget.pause(): return
func evolve_batch_generation(target: Dictionary, to_year: Variant, completed: Dictionary = { }, budget: RefCounted = null, context: Dictionary = {}) -> bool: func evolve_batch_generation(target: Dictionary, to_year: Variant, completed: Dictionary = { }, budget: RefCounted = null, context: Dictionary = {}) -> bool:
var _keep_alive: RefCounted = self var _keep_alive: RefCounted = self
# Apply direct completions at the original next-generation boundary, so # Apply direct completions at their original generation boundaries, so
# binding transfers, the blast environment and capacity checks keep order. # binding transfers, 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():
@@ -94,7 +116,19 @@ func evolve_batch_generation(target: Dictionary, to_year: Variant, completed: Di
var quantities: Dictionary = world.ledger.physical_amounts(target) var quantities: Dictionary = world.ledger.physical_amounts(target)
var expired: Dictionary = {} var expired: Dictionary = {}
var withdrawn: Dictionary = {} var withdrawn: Dictionary = {}
var batch_ids: Array = target.batches.keys() var batch_ids: Array = []
for id in target.batches:
if not Global.CivilizationRules.has(id): batch_ids.append(id)
if world.adaptive_quality >= 3:
batch_ids.sort()
var selected_ids: Array = []
for i in batch_ids.size():
if i % 2 == world.adaptive_generation_phase or completed.has(batch_ids[i]):
selected_ids.append(batch_ids[i])
for id in completed:
if not Global.CivilizationRules.has(id) and not selected_ids.has(id):
selected_ids.append(id)
batch_ids = selected_ids
var planets_changed: bool = false var planets_changed: bool = false
var profile_withdraw: int = world.profile_start() var profile_withdraw: int = world.profile_start()
for id in batch_ids: for id in batch_ids:
@@ -122,8 +156,8 @@ func evolve_batch_generation(target: Dictionary, to_year: Variant, completed: Di
break break
if (not has_expired): if (not has_expired):
return false return false
# This per-generation summary is valid until the next transition mutates # Aggregate quantities are used to calculate the type-level transfer ratio;
# the cohort set. Reuse it for civilization planning and transfer ratios. # transition products remain calculated for each expiring cohort.
var expired_totals: Dictionary = {} var expired_totals: Dictionary = {}
for id in expired: for id in expired:
var total: Variant = 0 var total: Variant = 0
@@ -132,29 +166,28 @@ 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)
var civilization_transaction: Dictionary = await CivilizationEvolution.prepare(world, target, expired, to_year, star_budget, Global.CivilizationRules, current_quantities, budget, expired_totals) if budget != null: budget.call("set_phase", "civilization_prepare")
if budget != null and budget.cancelled: return false
var profile_products: int = world.profile_start() var profile_products: int = world.profile_start()
for id in expired: for id in expired:
if budget != null and not await budget.pause(): return false if budget != null and not await budget.pause(): return false
var total: Variant = expired_totals[id] var total: Variant = expired_totals[id]
fractions[id] = (E.clamp_value(E.divide(total, quantities[id]), 0, 1) if E.gt(quantities.get(id, 0), 0) else 0) fractions[id] = (E.clamp_value(E.divide(total, quantities[id]), 0, 1) if E.gt(quantities.get(id, 0), 0) else 0)
outputs[id] = {}
var combined: Dictionary = {} var combined: Dictionary = {}
for part : Dictionary in expired[id]: for part: Dictionary in expired[id]:
if (not part.has("products")): if not part.has("products"):
part.products = world.stellar.evolution_products(target, world.catalog.entities[id], part.amount, context) part.products = world.stellar.evolution_products(target, world.catalog.entities[id], part.amount, context)
for successor in part.products.keys(): for successor in part.products.keys():
if Global.CivilizationRules.has(id): if Global.CivilizationRules.has(id): continue
continue
var requested: Variant = part.products[successor] var requested: Variant = part.products[successor]
var admitted: Variant = CivilizationState.admit_transition(star_budget, Global.CivilizationRules, id, successor, requested) var admitted: Variant = CivilizationState.admit_transition(star_budget, Global.CivilizationRules, id, successor, requested)
part.products[successor] = admitted part.products[successor] = admitted
if E.lt(admitted, requested): if E.lt(admitted, requested):
part.products[id] = E.subtract(E.add(part.products.get(id, 0), requested), admitted) part.products[id] = E.add(part.products.get(id, 0), E.subtract(requested, admitted))
for successor in part.products: for successor in part.products:
combined[successor] = E.add(combined.get(successor, 0), part.products[successor]) combined[successor] = E.add(combined.get(successor, 0), part.products[successor])
outputs[id] = {}
if E.gt(total, 0): if E.gt(total, 0):
for successor in combined: for successor in combined:
outputs[id][successor] = E.divide(combined[successor], total) outputs[id][successor] = E.divide(combined[successor], total)
@@ -166,40 +199,34 @@ 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 = {}
var born_civilizations: Dictionary = {}
var blast_power: Variant = 0 var blast_power: Variant = 0
for parts: Array in expired.values(): for parts: Array in expired.values():
for part: Dictionary in parts: for part: Dictionary in parts:
blast_power = E.add(blast_power, part.products.get("blast", 0)) blast_power = E.add(blast_power, part.products.get("blast", 0))
# A zero blast does not need density/mass/volume recalculated. For an actual
# 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:
if budget != null and not await budget.pause(): return false if budget != null and not await budget.pause(): return false
var amount: Variant = part.products[successor] var amount: Variant = part.products[successor]
if E.le(amount, 0): continue if E.le(amount, 0) or E.eq(successor, "blast") or not world.catalog.entities.has(successor): continue
if E.eq(successor, "blast"): if E.eq(successor, id) and (not world.catalog.entities[id].life_rule.is_empty() or Global.CivilizationRules.has(id)):
continue
if (not world.catalog.entities.has(successor)):
continue
if (E.eq(successor, id) and (((not world.catalog.entities[id].life_rule.is_empty()) or Global.CivilizationRules.has(id)))):
target.amount[id] = E.add(target.amount.get(id, 0), amount) target.amount[id] = E.add(target.amount.get(id, 0), amount)
else: else:
amount = world.ledger.credit(target, successor, amount) amount = world.ledger.credit(target, successor, amount)
var produces: bool = not world.automatic.yearly_products(world.catalog.entities[successor]).is_empty() var produces: bool = not world.automatic.yearly_products(world.catalog.entities[successor]).is_empty()
var newborn_adjustment: Dictionary = {} var newborn_adjustment: Dictionary = {}
var cutoff: Variant = (to_year if world.stellar.uses_short_lived_fast_path(successor) else E.neg(INF)) var cutoff: Variant = to_year if world.stellar.uses_short_lived_fast_path(successor) else E.neg(INF)
var born_from: Variant = part.from var born_from: Variant = part.from
var born_to: Variant = part.to var born_to: Variant = part.to
if E.gt(resolution, 0) and (Global.CivilizationRules.has(successor) or not world.catalog.entities[successor].life_rule.is_empty()): if E.gt(resolution, 0) and (Global.CivilizationRules.has(successor) or not world.catalog.entities[successor].life_rule.is_empty()):
# Biological stages (including C0.0/C0.1) and civilizations
# share the coarse clock. Historical retry intervals must
# not retrigger their early population within the same bucket.
born_to = E.multiply(E.ceil_value(E.divide(born_to, resolution)), resolution) born_to = E.multiply(E.ceil_value(E.divide(born_to, resolution)), resolution)
born_from = born_to born_from = born_to
var profile_newborn: int = world.profile_start() var profile_newborn: int = world.profile_start()
@@ -213,17 +240,18 @@ func evolve_batch_generation(target: Dictionary, to_year: Variant, completed: Di
world.automatic.produce_newborn_lifetime(target, successor, amount, born_from, born_to, to_year, newborn_adjustment.get("past_years", 0), integral) world.automatic.produce_newborn_lifetime(target, successor, amount, born_from, born_to, to_year, newborn_adjustment.get("past_years", 0), integral)
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)
if Global.CivilizationRules.has(successor): born_civilizations[successor] = E.add(born_civilizations.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:
for member in world.catalog.members(resource): affected[member] = true for member in world.catalog.members(resource): affected[member] = true
world.ledger.round_object_allocations(target, affected) world.ledger.round_object_allocations(target, affected)
if budget != null: for id in born_civilizations:
await CivilizationEvolution.finish(world, target, civilization_transaction, to_year, Global.CivilizationRules, budget) var current: Variant = world.ledger.physical_amount(target, id)
if budget.cancelled: return false var born: Variant = born_civilizations[id]
else: target.ethics[id] = CivilizationState.merge_profiles(target.ethics.get(id, {}), E.maximum(0, E.subtract(current, born)), CivilizationState.empty_profile(), born)
CivilizationEvolution.finish(world, target, civilization_transaction, to_year, Global.CivilizationRules)
if E.gt(blast_power, 0): world.stellar.apply_blast(target, blast_power, environment) if E.gt(blast_power, 0): world.stellar.apply_blast(target, blast_power, environment)
CivilizationState.enforce_capacity(world, target, Global.CivilizationRules, to_year) CivilizationState.enforce_capacity(world, target, Global.CivilizationRules, to_year)
return true return true
+14
View File
@@ -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
+78 -6
View File
@@ -3,7 +3,7 @@ const E = preload("res://world/owniverse/math/exact.gd")
const CivilizationState = preload("res://world/owniverse/civilization_state.gd") const CivilizationState = preload("res://world/owniverse/civilization_state.gd")
const QuantityMath = preload("res://world/owniverse/quantity_math.gd") const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
func try_production(target: Dictionary, entity: OwniverseEntity, amount: Variant, automatic: bool = false) -> bool: func try_production(target: Dictionary, entity: OwniverseEntity, amount: Variant, automatic: bool = false, physical: Dictionary = {}, helium_share_override: Variant = NAN) -> bool:
if (E.le(amount, 0) or E.ne(amount, E.floor_value(amount))): if (E.le(amount, 0) or E.ne(amount, E.floor_value(amount))):
return false return false
var trial: Dictionary = target.duplicate() var trial: Dictionary = target.duplicate()
@@ -13,10 +13,25 @@ func try_production(target: Dictionary, entity: OwniverseEntity, amount: Variant
trial.batches = target.batches.duplicate() trial.batches = target.batches.duplicate()
# Physical object totals do not change while resources are withdrawn from # Physical object totals do not change while resources are withdrawn from
# this transaction. Keep one snapshot instead of scanning every resource. # this transaction. Keep one snapshot instead of scanning every resource.
var physical: Dictionary = world.ledger.physical_amounts(trial) var shared_physical: bool = not physical.is_empty()
if not shared_physical: physical = world.ledger.physical_amounts(trial)
var next_physical: Dictionary = physical.duplicate() if shared_physical else {}
var copied_batches: Dictionary = {} var copied_batches: Dictionary = {}
var helium_share: Variant = stellar_helium_share(target) if not E.finite(helium_share_override) else helium_share_override
for resource in entity.cost: for resource in entity.cost:
var cost: Variant = E.round_value(E.multiply(E.maximum(0, entity.cost[resource]), amount)) var cost: Variant = E.round_value(E.multiply(E.maximum(0, entity.cost[resource]), amount))
if E.eq(resource, "H") and uses_stellar_helium(entity):
var helium_cost: Variant = E.minimum(E.floor_value(E.multiply(cost, helium_share)), world.ledger.available(trial, "He"))
cost = E.subtract(cost, helium_cost)
if E.gt(helium_cost, 0):
var helium_taken: Dictionary = world.ledger.take_available(trial, "He", helium_cost)
var total_helium_taken: Variant = 0
for id in helium_taken:
total_helium_taken = E.add(total_helium_taken, helium_taken[id])
if shared_physical and next_physical.has(id):
next_physical[id] = E.subtract(next_physical[id], helium_taken[id])
if E.ne(total_helium_taken, helium_cost):
return false
if (automatic and E.lt(trial.auto_resources.get(resource, 0), cost)): if (automatic and E.lt(trial.auto_resources.get(resource, 0), cost)):
return false return false
var taken: Dictionary = world.ledger.take_available(trial, resource, cost) var taken: Dictionary = world.ledger.take_available(trial, resource, cost)
@@ -28,6 +43,7 @@ func try_production(target: Dictionary, entity: OwniverseEntity, amount: Variant
if automatic: if automatic:
trial.auto_resources[resource] = E.maximum(0, E.subtract(trial.auto_resources.get(resource, 0), cost)) trial.auto_resources[resource] = E.maximum(0, E.subtract(trial.auto_resources.get(resource, 0), cost))
for id in taken: for id in taken:
if shared_physical and next_physical.has(id): next_physical[id] = E.subtract(next_physical[id], taken[id])
# Isolate only batches actually touched by this transaction. # Isolate only batches actually touched by this transaction.
if (trial.batches.has(id) and (not copied_batches.has(id))): if (trial.batches.has(id) and (not copied_batches.has(id))):
trial.batches[id] = trial.batches[id].duplicate(true) trial.batches[id] = trial.batches[id].duplicate(true)
@@ -41,6 +57,7 @@ func try_production(target: Dictionary, entity: OwniverseEntity, amount: Variant
for field in ["amount", "binds", "batches", "auto_resources"]: for field in ["amount", "binds", "batches", "auto_resources"]:
if trial.has(field): if trial.has(field):
target[field] = trial[field] target[field] = trial[field]
if shared_physical: physical.merge(next_physical, true)
return true return true
@@ -95,6 +112,7 @@ func plan_smbh(desired_amount: Variant) -> Dictionary:
func produce_smbh(desired_amount: Variant) -> Variant: func produce_smbh(desired_amount: Variant) -> Variant:
if E.gt(world.state.get("apocalypse", {}).get("phase", 0), 0): return 0
var plan: Dictionary = plan_smbh(desired_amount) var plan: Dictionary = plan_smbh(desired_amount)
var consumed_mass: Variant = plan.amount var consumed_mass: Variant = plan.amount
var taken: Dictionary = plan.taken var taken: Dictionary = plan.taken
@@ -120,10 +138,12 @@ func produce_smbh(desired_amount: Variant) -> Variant:
world.ledger.clamp_auto_resources(next) world.ledger.clamp_auto_resources(next)
world.state = next world.state = next
world.presentation.refresh(world.state, world.catalog) world.presentation.refresh(world.state, world.catalog)
world.apocalypse.check_density(world.state)
return consumed_mass return consumed_mass
func produce(item_id: String, desired_amount: Variant) -> Variant: func produce(item_id: String, desired_amount: Variant) -> Variant:
if E.gt(world.state.get("apocalypse", {}).get("phase", 0), 0): return 0
if E.eq(item_id, "SMBH"): if E.eq(item_id, "SMBH"):
return produce_smbh(desired_amount) return produce_smbh(desired_amount)
# Use special production for S and H # Use special production for S and H
@@ -143,19 +163,23 @@ func produce(item_id: String, desired_amount: Variant) -> Variant:
return 0 return 0
var metallicity: Variant = world.environment.stellar_environment(world.state).metallicity var metallicity: Variant = world.environment.stellar_environment(world.state).metallicity
if (not try_production(world.state, entity, max_producible)): var helium_share: Variant = stellar_helium_share(world.state)
if (not try_production(world.state, entity, max_producible, false, {}, helium_share)):
return 0 return 0
# Add produced entities # Add produced entities
world.ledger.process_entity(item_id, max_producible) world.ledger.process_entity(item_id, max_producible)
world.batches.add_entity_to_batch(item_id, max_producible, world.state.time.datetime) var lifespan_multiplier: Variant = stellar_lifespan_multiplier(entity, helium_share)
world.batches.append_batch(world.state, item_id, max_producible, world.state.time.datetime, world.state.time.datetime, {}, -INF, null, null, NAN, lifespan_multiplier)
world.stellar.create_planets(world.state, item_id, max_producible, world.state.time.datetime, world.state.time.datetime, metallicity) world.stellar.create_planets(world.state, item_id, max_producible, world.state.time.datetime, world.state.time.datetime, metallicity)
world.ledger.refresh_groups(world.state) world.ledger.refresh_groups(world.state)
world.presentation.refresh(world.state, world.catalog) world.presentation.refresh(world.state, world.catalog)
world.apocalypse.check_density(world.state)
return max_producible return max_producible
func produce_hs(entity_id: String, value: Variant) -> Variant: func produce_hs(entity_id: String, value: Variant) -> Variant:
if E.gt(world.state.get("apocalypse", {}).get("phase", 0), 0): return 0
if E.le(value, 0): if E.le(value, 0):
return 0 return 0
@@ -167,7 +191,8 @@ func produce_hs(entity_id: String, value: Variant) -> Variant:
world.state.energy = E.subtract(world.state.energy, to_produce) world.state.energy = E.subtract(world.state.energy, to_produce)
world.ledger.process_entity(entity_id, to_produce) world.ledger.process_entity(entity_id, to_produce)
world.presentation.refresh(world.state, world.catalog)
world.apocalypse.check_density(world.state)
return to_produce return to_produce
@@ -175,6 +200,39 @@ func production_step(entity: OwniverseEntity) -> Variant:
return QuantityMath.production_step(E.add(entity.cost.values(), entity.bind.values())) return QuantityMath.production_step(E.add(entity.cost.values(), entity.bind.values()))
func uses_stellar_helium(entity: OwniverseEntity) -> bool:
return E.eq(entity.group_as, "Star") and entity.cost.has("H")
## Unity's Hel state is the free helium share of the H+He mixture, capped at 50%.
func stellar_helium_share(source: Dictionary) -> Variant:
var hydrogen: Variant = world.ledger.available(source, "H")
var helium: Variant = world.ledger.available(source, "He")
var total: Variant = E.add(hydrogen, helium)
if E.le(total, 0): return 0
var parameters: Dictionary = Global.ExtraParams
var share: Variant = E.multiply(E.divide(helium, total), parameters.get("helium_multiplier", 1))
share = E.minimum(share, E.of("0.5"))
share = E.minimum(share, E.maximum(0, parameters.get("helium_bonus_limit", 1)))
return E.clamp_value(share, 0, 1)
func stellar_lifespan_multiplier(entity: OwniverseEntity, helium_share: Variant) -> Variant:
return E.maximum(0, E.subtract(1, E.multiply(E.of("1.5"), helium_share))) if uses_stellar_helium(entity) else 1
## Maximum star count when helium may replace this share of its hydrogen cost.
func stellar_hydrogen_capacity(hydrogen_cost: Variant, hydrogen_available: Variant, helium_available: Variant, helium_share: Variant) -> Variant:
if E.le(hydrogen_cost, 0): return INF
if E.le(helium_share, 0) or E.le(helium_available, 0):
return E.floor_value(E.divide(hydrogen_available, hydrogen_cost))
var hydrogen_only_capacity: Variant = E.divide(hydrogen_available, E.multiply(hydrogen_cost, E.subtract(1, helium_share)))
var helium_saturation_capacity: Variant = E.divide(helium_available, E.multiply(hydrogen_cost, helium_share))
if E.le(hydrogen_only_capacity, helium_saturation_capacity):
return E.floor_value(hydrogen_only_capacity)
return E.floor_value(E.divide(E.add(hydrogen_available, helium_available), hydrogen_cost))
func calculate_max_producible(entity: OwniverseEntity, desired_amount: Variant = null) -> Variant: func calculate_max_producible(entity: OwniverseEntity, desired_amount: Variant = null) -> Variant:
if E.eq(entity.id, "SMBH"): if E.eq(entity.id, "SMBH"):
return plan_smbh(smbh_available() if desired_amount == null else desired_amount).amount return plan_smbh(smbh_available() if desired_amount == null else desired_amount).amount
@@ -192,10 +250,24 @@ func calculate_max_producible(entity: OwniverseEntity, desired_amount: Variant =
if (not has_requirements): if (not has_requirements):
return 0 return 0
var available: Variant = INF if desired_amount == null else E.maximum(0, desired_amount) var available: Variant = INF if desired_amount == null else E.maximum(0, desired_amount)
var helium_share: Variant = stellar_helium_share(world.state)
for resource in requirements: for resource in requirements:
if E.eq(resource, "H") and uses_stellar_helium(entity):
var hydrogen_cost: Variant = E.maximum(0, entity.cost.get("H", 0))
available = E.minimum(available, stellar_hydrogen_capacity(hydrogen_cost, world.ledger.available(world.state, "H"), world.ledger.available(world.state, "He"), helium_share))
var hydrogen_binding: Variant = E.maximum(0, entity.bind.get("H", 0))
if E.gt(hydrogen_binding, 0):
available = E.minimum(available, E.floor_value(E.divide(world.ledger.available(world.state, "H"), hydrogen_binding)))
continue
if E.gt(requirements[resource], 0): if E.gt(requirements[resource], 0):
available = E.minimum(available, E.floor_value(E.divide(world.ledger.available(world.state, resource), requirements[resource]))) available = E.minimum(available, E.floor_value(E.divide(world.ledger.available(world.state, resource), requirements[resource])))
var concrete: Dictionary = world.ledger.expand_requirements(world.state, requirements) var concrete_requirements: Dictionary = requirements.duplicate()
if uses_stellar_helium(entity):
# The direct H cost was already handled by the H/He capacity above;
# preserve any separate H binding requirement.
concrete_requirements["H"] = E.maximum(0, E.subtract(concrete_requirements.get("H", 0), entity.cost.get("H", 0)))
if E.le(concrete_requirements["H"], 0): concrete_requirements.erase("H")
var concrete: Dictionary = world.ledger.expand_requirements(world.state, concrete_requirements)
for resource in concrete: for resource in concrete:
if E.gt(concrete[resource], 0): if E.gt(concrete[resource], 0):
available = E.minimum(available, E.floor_value(E.divide(world.ledger.available(world.state, resource), concrete[resource]))) available = E.minimum(available, E.floor_value(E.divide(world.ledger.available(world.state, resource), concrete[resource])))
+9 -14
View File
@@ -67,20 +67,15 @@ static func divmod(a: Array, b: Array) -> Array:
assert(b != [0], "Division by zero") assert(b != [0], "Division by zero")
if b == [1]: return [a.duplicate(), [0]] if b == [1]: return [a.duplicate(), [0]]
if compare(a, b) < 0: return [[0], a.duplicate()] if compare(a, b) < 0: return [[0], a.duplicate()]
# Decimal ratios often share entire base-1e9 factors. Cancel them before # A divisor with trailing base limbs can divide the high part directly.
# long division; restore the remainder's original scale afterwards. # The low numerator limbs belong entirely to the remainder.
var shared: int = 0 var shift: int = 0
while shared < mini(a.size(), b.size()) - 1 and a[shared] == 0 and b[shared] == 0: while shift < b.size() - 1 and b[shift] == 0: shift += 1
shared += 1 if shift > 0:
if shared: var divided: Array = divmod(a.slice(shift), b.slice(shift))
var reduced: Array = divmod(a.slice(shared), b.slice(shared)) var restored: Array = a.slice(0, shift)
if reduced[1] != [0]: if divided[1] != [0]: restored.append_array(divided[1])
var restored: Array = [] return [divided[0], trim(restored)]
restored.resize(shared)
restored.fill(0)
restored.append_array(reduced[1])
reduced[1] = restored
return reduced
if b.size() == 1: if b.size() == 1:
var quotient: Array = [] var quotient: Array = []
quotient.resize(a.size()) quotient.resize(a.size())
+33
View File
@@ -132,6 +132,23 @@ static func compare(a, b) -> int:
if magnitude == 0 and x[1] != y[1]: magnitude = 1 if x[1] > y[1] else -1 if magnitude == 0 and x[1] != y[1]: magnitude = 1 if x[1] > y[1] else -1
return magnitude * x[2] return magnitude * x[2]
## Ratios used only for float profiles, never for integer quota allocation.
static func ratio_approx(a, b) -> float:
var numerator: float = approx(a)
var denominator: float = approx(b)
if is_finite(numerator) and is_finite(denominator): return numerator / denominator
var x: Array = _blocks(a)
var y: Array = _blocks(b)
var nx: int = maxi(0, x[0].size() - 3)
var ny: int = maxi(0, y[0].size() - 3)
var leading_x: float = 0.0
var leading_y: float = 0.0
for i in range(x[0].size() - 1, nx - 1, -1): leading_x = leading_x * SMALL + x[0][i]
for i in range(y[0].size() - 1, ny - 1, -1): leading_y = leading_y * SMALL + y[0][i]
var power: int = 9 * (nx - ny)
if power < -323: return 0.0
return leading_x / leading_y * pow(10.0, power) * x[2] * y[2]
static func eq(a, b) -> bool: static func eq(a, b) -> bool:
if a == null: return b == null if a == null: return b == null
if b == null: return false if b == null: return false
@@ -225,6 +242,22 @@ static func mul_div(a, b, divisor):
return make(Big.multiply(Big.multiply(x[0], y[0]), z[1]), Big.multiply(Big.multiply(x[1], y[1]), z[0]), x[2] * y[2] * z[2]) return make(Big.multiply(Big.multiply(x[0], y[0]), z[1]), Big.multiply(Big.multiply(x[1], y[1]), z[0]), x[2] * y[2] * z[2])
## Reuse the count and divisor decomposition across all quotas. ## Reuse the count and divisor decomposition across all quotas.
static func whole_quotas(count, weights: Array, divisor: int) -> Array:
assert(divisor > 0 and ge(count, 0) and eq(count, floor_value(count)))
var population: Array = _blocks(count)[0]
var denominator: Array = _int_limbs(divisor)
var result: Array = []
for weight: int in weights:
if weight == 0:
result.append([0, 0.0])
elif count is int and count <= MAX_INT / weight:
var product: int = count * weight
result.append([product / divisor, float(product % divisor) / divisor])
else:
var qr: Array = Big.divmod(Big.multiply(population, _int_limbs(weight)), denominator)
result.append([_compose(qr[0]), _fraction_ratio(qr[1], denominator)])
return result
static func mul_div_many(a, values: Array, divisor) -> Array: static func mul_div_many(a, values: Array, divisor) -> Array:
var result: Array = [] var result: Array = []
if not finite(a) or not finite(divisor): if not finite(a) or not finite(divisor):
+138 -3
View File
@@ -4,9 +4,75 @@ const CivilizationState = preload("res://world/owniverse/civilization_state.gd")
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")
## Diagnostic switch for paired measurements of the old and new timing models.
var cycle_stars: bool = true
func star_cycle_width(target: Dictionary) -> Variant:
var speed: int = int(target.get("speed", 1))
var width: Variant = E.multiply(E.power(10, speed - 1), world.frame_step_seconds)
return E.multiply(width, E.power(2, world.adaptive_quality))
## The cycle grid may widen a short window, never subdivide a wide one.
func uses_cycle_star(target: Dictionary, id: String, lifespan: Variant) -> bool:
return cycle_stars and int(target.get("speed", 1)) >= 4 and world.catalog.entities[id].group_as == "Star" and E.le(window_duration(target, lifespan), star_cycle_width(target))
func coarsen_cycle_stars(target: Dictionary, id: String) -> void:
var width: Variant = star_cycle_width(target)
var anchor: Variant = target.time.datetime
var groups: Dictionary = {}
var result: Array = []
var changed: bool = false
for batch: Dictionary in target.batches[id]:
var start: Variant = E.maximum(batch.start_year, batch.get("processed_until", batch.start_year))
var end: Variant = batch.end_year
# A cohort spanning more than one cycle stays intact.
if E.gt(E.subtract(end, start), width):
result.append(batch)
continue
var first: Variant = E.ceil_value(E.divide(E.subtract(start, anchor), width)) if E.eq(start, end) else E.add(E.floor_value(E.divide(E.subtract(start, anchor), width)), 1)
var last: Variant = E.ceil_value(E.divide(E.subtract(end, anchor), width))
if not E.eq(first, last):
result.append(batch)
continue
var key: String = str(last)
if not groups.has(key):
groups[key] = {"batch": batch, "merged": false}
continue
var entry: Dictionary = groups[key]
if not entry.merged:
entry.batch = entry.batch.duplicate()
entry.merged = true
var combined: Dictionary = entry.batch
combined.amount = E.add(combined.amount, batch.amount)
combined.withdraw_remainder = E.add(combined.get("withdraw_remainder", 0), batch.get("withdraw_remainder", 0))
if combined.has("created_from") and batch.has("created_from"):
combined.created_from = E.minimum(combined.created_from, batch.created_from)
combined.created_to = E.maximum(combined.created_to, batch.created_to)
else:
var lifespan: Variant = effective_lifespan(target, id)
combined.created_from = E.minimum(combined.get("created_from", E.maximum(0, E.subtract(combined.start_year, lifespan))), batch.get("created_from", E.maximum(0, E.subtract(batch.start_year, lifespan))))
combined.created_to = E.maximum(combined.created_from, E.maximum(combined.get("created_to", E.maximum(0, E.subtract(combined.end_year, lifespan))), batch.get("created_to", E.maximum(0, E.subtract(batch.end_year, lifespan)))))
if E.eq(combined.created_to, combined.created_from): combined.created_to = E.add(combined.created_from, width)
combined.start_year = E.add(anchor, E.multiply(last, width))
combined.end_year = combined.start_year
combined.erase("processed_until")
changed = true
if not changed: return
for entry: Dictionary in groups.values(): result.append(entry.batch)
result.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return E.lt(a.get("created_from", a.start_year), b.get("created_from", b.start_year)))
target.batches[id] = result
_batch_indices.erase(id)
_cycle_indices.erase(id)
_ready_indices.erase(id)
_instant_appends.erase(id)
func temporal_resolution(target: Dictionary) -> Variant: func temporal_resolution(target: Dictionary) -> Variant:
var speed: int = int(target.get("speed", 1)) var speed: int = int(target.get("speed", 1))
return E.divide(E.power(10, speed - 1), 10) if speed >= 4 else 0 if speed < 4: return 0
# Keep the existing 0.1-second resolution at the default 5 Hz cadence,
# and scale temporal grouping with the adaptive cycle interval.
var width: Variant = E.multiply(E.power(10, speed - 1), world.frame_step_seconds / 2.0)
return E.multiply(width, E.power(2, world.adaptive_quality))
func window_duration(target: Dictionary, lifespan: Variant) -> Variant: func window_duration(target: Dictionary, lifespan: Variant) -> Variant:
if E.le(lifespan, 10): return lifespan if E.le(lifespan, 10): return lifespan
@@ -20,6 +86,9 @@ func coarsen(target: Dictionary) -> void:
for id in target.batches: for id in target.batches:
if target.batches[id].size() <= 1: continue if target.batches[id].size() <= 1: continue
if not world.catalog.entities.has(id): continue if not world.catalog.entities.has(id): continue
if cycle_stars and world.catalog.entities[id].group_as == "Star":
coarsen_cycle_stars(target, id)
continue
var lifespan: Variant = effective_lifespan(target, id) var lifespan: Variant = effective_lifespan(target, id)
if E.le(lifespan, 10): continue if E.le(lifespan, 10): continue
var width: Variant = window_duration(target, lifespan) var width: Variant = window_duration(target, lifespan)
@@ -58,6 +127,69 @@ var _instant_target: Dictionary = {}
var _instant_appends: Dictionary = {} var _instant_appends: Dictionary = {}
var _static_spans: Dictionary = {} var _static_spans: Dictionary = {}
var _star_spans: Dictionary = {} var _star_spans: Dictionary = {}
var _cycle_target: Dictionary = {}
var _cycle_indices: Dictionary = {}
## High-speed stars share a whole transition at a simulation-cycle boundary.
## Used only when the original window is no wider than a simulation cycle.
func append_cycle_star(target: Dictionary, id: String, amount: Variant, created_from: Variant, created_to: Variant, lifespan: Variant, completed: Dictionary, completed_before: Variant, budget: RefCounted, newborn_adjustment: Variant, newborn_until: Variant) -> Variant:
var width: Variant = star_cycle_width(target)
var anchor: Variant = target.time.datetime
var batches: Array = target.batches[id]
if not is_same(_cycle_target, target):
_cycle_target = target
_cycle_indices.clear()
var cached: Dictionary = _cycle_indices.get(id, {})
if cached.is_empty() or not is_same(cached.batches, batches) or cached.count != batches.size():
var deaths: Dictionary = {}
for batch: Dictionary in batches:
if E.eq(batch.start_year, batch.end_year): deaths[str(batch.end_year)] = batch
cached = {"batches": batches, "count": batches.size(), "deaths": deaths}
_cycle_indices[id] = cached
var duration: Variant = E.subtract(created_to, created_from)
var first_death: Variant = E.add(created_from, lifespan)
var index: Variant = E.ceil_value(E.divide(E.subtract(first_death, anchor), width)) if E.eq(duration, 0) else E.add(E.floor_value(E.divide(E.subtract(first_death, anchor), width)), 1)
var remaining: Variant = amount
var integral: Variant = E.multiply(amount, E.maximum(0, E.subtract(completed_before, E.divide(E.add(created_from, created_to), 2)))) if E.finite(completed_before) else NAN
var order_changed: bool = false
while true:
if budget != null and not await budget.pause(): return NAN
# Jump over windows with zero whole allocation for sparse populations.
if E.gt(duration, 0):
var threshold: Variant = E.add(created_from, E.mul_div(duration, E.add(E.subtract(amount, remaining), 1), amount))
index = E.maximum(index, E.ceil_value(E.divide(E.subtract(E.add(threshold, lifespan), anchor), width)))
var death: Variant = E.add(anchor, E.multiply(index, width))
var part_end: Variant = E.minimum(created_to, E.subtract(death, lifespan))
var count: Variant = remaining
if E.gt(duration, 0) and E.lt(part_end, created_to):
count = E.subtract(E.floor_value(E.mul_div(amount, E.subtract(part_end, created_from), duration)), E.subtract(amount, remaining))
if E.gt(count, 0):
if E.le(death, completed_before):
if not completed.has(id): completed[id] = []
completed[id].append({"amount": count, "from": death, "to": death})
integral = E.subtract(integral, E.multiply(count, expired_years(completed_before, death, death)))
else:
var key: String = str(death)
var batch: Dictionary = cached.deaths.get(key, {})
if not batch.is_empty() and not E.eq(batch.end_year, death): batch = {}
if batch.is_empty():
var birth_start: Variant = E.maximum(0, E.subtract(E.subtract(death, lifespan), width))
batch = {"created_from": birth_start, "created_to": E.add(birth_start, width), "start_year": death, "end_year": death, "amount": 0}
order_changed = order_changed or (not batches.is_empty() and E.lt(batch.created_from, batches.back().get("created_from", -INF)))
batches.append(batch)
cached.deaths[key] = batch
batch.amount = E.add(batch.amount, count)
if newborn_adjustment != null and E.finite(newborn_until):
newborn_adjustment.past_years = E.add(newborn_adjustment.get("past_years", 0), E.multiply(count, expired_years(newborn_until, death, death)))
remaining = E.subtract(remaining, count)
if E.eq(duration, 0) or E.ge(part_end, created_to): break
index = E.add(index, 1)
if order_changed:
batches.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return E.lt(a.get("created_from", -INF), b.get("created_from", -INF)))
cached.count = batches.size()
_batch_indices.erase(id)
_instant_appends.erase(id)
return E.maximum(0, integral) if E.finite(integral) else integral
func effective_lifespan(target: Dictionary, id: String) -> Variant: func effective_lifespan(target: Dictionary, id: String) -> Variant:
var entity: OwniverseEntity = world.catalog.entities[id] var entity: OwniverseEntity = world.catalog.entities[id]
@@ -126,8 +258,9 @@ func scale_batches(target: Dictionary, id: String, factor: Variant) -> void:
share.batch.withdraw_remainder = E.minimum(share.batch.get("withdraw_remainder", 0), share.batch.amount) share.batch.withdraw_remainder = E.minimum(share.batch.get("withdraw_remainder", 0), share.batch.amount)
func append_batch(target: Dictionary, id: String, amount: Variant, created_from: Variant, created_to: Variant, completed: Dictionary = { }, completed_before: Variant = - INF, budget: RefCounted = null, newborn_adjustment: Variant = null, newborn_until: Variant = NAN) -> Variant: func append_batch(target: Dictionary, id: String, amount: Variant, created_from: Variant, created_to: Variant, completed: Dictionary = { }, completed_before: Variant = - INF, budget: RefCounted = null, newborn_adjustment: Variant = null, newborn_until: Variant = NAN, lifespan_multiplier: Variant = 1) -> Variant:
var _keep_alive: RefCounted = self var _keep_alive: RefCounted = self
if Global.CivilizationRules.has(id): return NAN
if ((E.le(amount, 0) or (not world.catalog.entities.has(id))) or E.le(world.catalog.entities[id].lifespan, 0)): if ((E.le(amount, 0) or (not world.catalog.entities.has(id))) or E.le(world.catalog.entities[id].lifespan, 0)):
return NAN return NAN
if E.lt(created_to, created_from): if E.lt(created_to, created_from):
@@ -138,7 +271,9 @@ func append_batch(target: Dictionary, id: String, amount: Variant, created_from:
if (not target.has("batch_origins")): if (not target.has("batch_origins")):
target.batch_origins = {} target.batch_origins = {}
var batches: Array = target.batches[id] var batches: Array = target.batches[id]
var lifespan: Variant = effective_lifespan(target, id) var lifespan: Variant = E.multiply(effective_lifespan(target, id), E.maximum(0, lifespan_multiplier))
if uses_cycle_star(target, id, lifespan):
return await append_cycle_star(target, id, amount, created_from, created_to, lifespan, completed, completed_before, budget, newborn_adjustment, newborn_until)
var batch_duration: Variant = window_duration(target, lifespan) var batch_duration: Variant = window_duration(target, lifespan)
if E.le(batch_duration, 0): if E.le(batch_duration, 0):
return NAN return NAN
+41 -25
View File
@@ -63,29 +63,41 @@ func expand_requirements(source: Dictionary, requirements: Dictionary) -> Dictio
func take_available(target: Dictionary, id: String, amount: Variant) -> Dictionary: func take_available(target: Dictionary, id: String, amount: Variant) -> Dictionary:
var quantities: Dictionary = {} var quantities: Dictionary = {}
for member in world.catalog.members(id): for member in world.catalog.members(id):
quantities[member] = target.amount.get(member, 0) quantities[member] = E.maximum(0, target.amount.get(member, 0))
var distribution: Dictionary = QuantityMath.normalize_distribution(quantities)
var taken: Dictionary = {} var taken: Dictionary = {}
var total: Variant = E.minimum(amount, available(target, id)) var stock_total: Variant = 0
var discrete: bool = (not distribution.is_empty()) for quantity in quantities.values(): stock_total = E.add(stock_total, quantity)
for member in distribution: var total: Variant = E.minimum(amount, stock_total)
discrete = (discrete and is_discrete(member)) var discrete: bool = true
if discrete: for member in quantities:
total = E.floor_value(total) discrete = discrete and is_discrete(member)
var shares: Array = [] if discrete: total = E.floor_value(total)
var assigned: Variant = 0 if E.le(total, 0) or E.le(stock_total, 0): return taken
for member in distribution: if E.eq(total, stock_total):
var expected: Variant = E.multiply(total, distribution[member]) taken = quantities
taken[member] = (E.floor_value(expected) if discrete else expected) elif discrete:
assigned = E.add(assigned, taken[member]) var shares: Array = []
shares.append({"id": member, "rest": E.subtract(expected, taken[member])}) var assigned: Variant = 0
if discrete: for member in quantities:
shares.sort_custom(func ( a : Dictionary , b : Dictionary ) -> bool : return (E.gt(a.rest, b.rest) if E.ne(a.rest, b.rest) else E.lt(str(a.id), str(b.id)))) var expected: Variant = E.mul_div(total, quantities[member], stock_total)
for share : Dictionary in shares: var whole: Variant = E.floor_value(expected)
if E.ge(assigned, total): taken[member] = whole
break assigned = E.add(assigned, whole)
taken[share.id] = E.add(taken[share.id], 1) shares.append({"id": member, "rest": E.subtract(expected, whole)})
assigned = E.add(assigned, 1) shares.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return E.gt(a.rest, b.rest) if E.ne(a.rest, b.rest) else E.lt(str(a.id), str(b.id)))
var missing: Variant = E.subtract(total, assigned)
for share: Dictionary in shares:
if E.le(missing, 0): break
var member: String = share.id
if E.lt(taken[member], quantities[member]):
taken[member] = E.add(taken[member], 1)
missing = E.subtract(missing, 1)
assert(E.eq(missing, 0), "Discrete resource allocation must exhaust its requested amount")
else:
var distribution: Dictionary = QuantityMath.normalize_distribution(quantities)
for member in distribution:
taken[member] = E.multiply(total, distribution[member])
for member in taken: for member in taken:
target.amount[member] = E.maximum(0, E.subtract(target.amount.get(member, 0), taken[member])) target.amount[member] = E.maximum(0, E.subtract(target.amount.get(member, 0), taken[member]))
return taken return taken
@@ -101,9 +113,13 @@ func credit(target: Dictionary, id: String, amount: Variant) -> Variant:
if is_discrete(id): if is_discrete(id):
if (not target.has("production_remainders")): if (not target.has("production_remainders")):
target.production_remainders = {} target.production_remainders = {}
var expected: Variant = E.add(amount, target.production_remainders.get(id, 0)) var carry: Variant = target.production_remainders.get(id, 0)
amount = E.floor_value(expected) if E.eq(amount, E.floor_value(amount)) and E.ge(carry, 0) and E.lt(carry, 1):
target.production_remainders[id] = E.subtract(expected, amount) target.production_remainders[id] = carry
else:
var expected: Variant = E.add(amount, carry)
amount = E.floor_value(expected)
target.production_remainders[id] = E.subtract(expected, amount)
target.amount[id] = E.add(target.amount.get(id, 0), amount) target.amount[id] = E.add(target.amount.get(id, 0), amount)
target.stats.total_amount[id] = E.add(target.stats.total_amount.get(id, 0), amount) target.stats.total_amount[id] = E.add(target.stats.total_amount.get(id, 0), amount)
if ((world.catalog.entities.has(id) and E.ne(world.catalog.entities[id].group_as, "")) and E.ne(world.catalog.entities[id].group_as, id)): if ((world.catalog.entities.has(id) and E.ne(world.catalog.entities[id].group_as, "")) and E.ne(world.catalog.entities[id].group_as, id)):
+124 -47
View File
@@ -1,5 +1,6 @@
extends Node extends Node
class_name Owniverse class_name Owniverse
signal production_applied(id: String)
const E = preload("res://world/owniverse/math/exact.gd") const E = preload("res://world/owniverse/math/exact.gd")
## Owns the state and modules. Rules live in the modules below. ## Owns the state and modules. Rules live in the modules below.
const SimulationTime = preload("res://world/owniverse/simulation_time.gd") const SimulationTime = preload("res://world/owniverse/simulation_time.gd")
@@ -11,11 +12,66 @@ var ledger = preload("res://world/owniverse/object_ledger.gd").new(self)
var production = preload("res://world/owniverse/manual_production.gd").new(self) var production = preload("res://world/owniverse/manual_production.gd").new(self)
var automatic = preload("res://world/owniverse/automatic_production.gd").new(self) var automatic = preload("res://world/owniverse/automatic_production.gd").new(self)
var stellar = preload("res://world/owniverse/stellar_rules.gd").new(self) var stellar = preload("res://world/owniverse/stellar_rules.gd").new(self)
var apocalypse = preload("res://world/owniverse/apocalypse_controller.gd").new(self)
var life = preload("res://world/owniverse/life_evolution.gd").new(self) var life = preload("res://world/owniverse/life_evolution.gd").new(self)
var environment = preload("res://world/owniverse/universe_environment.gd").new(self) var environment = preload("res://world/owniverse/universe_environment.gd").new(self)
var evolution = preload("res://world/owniverse/evolution_coordinator.gd").new(self) var evolution = preload("res://world/owniverse/evolution_coordinator.gd").new(self)
var civilization_flow = preload("res://world/owniverse/civilization_flow.gd").new(self)
var profile_enabled: bool = false var profile_enabled: bool = false
var profile: Dictionary = {} var profile: Dictionary = {}
## Runtime-only quality controller. It is deliberately not serialized.
var adaptive_quality: int = 0
var adaptive_cycle_ema_ms: float = 0.0
var adaptive_recovery_cycles: int = 0
var adaptive_generation_phase: int = 0
## Runtime-only cadence: 5, 4, 3, or 2 full simulation cycles per second.
var adaptive_cadence: bool = true
var cadence_level: int = 0
var cadence_recovery_cycles: int = 0
const CADENCE_STEPS: Array[float] = [0.2, 0.25, 1.0 / 3.0, 0.5]
var frame_step_seconds: float = 0.2
func update_adaptive_quality(cycle_usec: int) -> void:
var cycle_ms: float = float(cycle_usec) / 1000.0
adaptive_cycle_ema_ms = cycle_ms if adaptive_cycle_ema_ms <= 0.0 else adaptive_cycle_ema_ms * 0.8 + cycle_ms * 0.2
if adaptive_cycle_ema_ms > 300.0:
adaptive_quality = 3
adaptive_recovery_cycles = 0
elif adaptive_quality >= 2 and adaptive_cycle_ema_ms > 200.0:
adaptive_quality = 3
adaptive_recovery_cycles = 0
elif adaptive_cycle_ema_ms > 220.0:
adaptive_quality = maxi(adaptive_quality, 2)
adaptive_recovery_cycles = 0
elif adaptive_cycle_ema_ms > 200.0:
adaptive_quality = maxi(adaptive_quality, 1)
adaptive_recovery_cycles = 0
elif adaptive_cycle_ema_ms < 140.0:
adaptive_recovery_cycles += 1
if adaptive_recovery_cycles >= 30:
adaptive_quality = maxi(0, adaptive_quality - 1)
adaptive_recovery_cycles = 0
else:
adaptive_recovery_cycles = 0
if adaptive_cadence:
var step: float = CADENCE_STEPS[cadence_level]
if adaptive_cycle_ema_ms > step * 900.0 and cadence_level < CADENCE_STEPS.size() - 1:
cadence_level += 1
frame_step_seconds = CADENCE_STEPS[cadence_level]
cadence_recovery_cycles = 0
elif adaptive_cycle_ema_ms < step * 650.0 and cadence_level > 0:
cadence_recovery_cycles += 1
if cadence_recovery_cycles >= 30:
cadence_level -= 1
frame_step_seconds = CADENCE_STEPS[cadence_level]
cadence_recovery_cycles = 0
else:
cadence_recovery_cycles = 0
if adaptive_quality >= 3:
adaptive_generation_phase = 1 - adaptive_generation_phase
func adaptive_timer_usec() -> int:
return Time.get_ticks_usec() - (frame_budget.waiting_usec if frame_busy and frame_budget != null else 0)
func profile_start() -> int: func profile_start() -> int:
if not profile_enabled: return 0 if not profile_enabled: return 0
@@ -54,6 +110,7 @@ var state: Dictionary = {
'civilization_stats': {}, 'civilization_stats': {},
'civilization_homes': {}, 'civilization_homes': {},
'ethics': {}, 'ethics': {},
'apocalypse': {'phase': 0, 'type': 0, 'warning': 0, 'stage': 1, 'tick': 0, 'end_ticks': 0},
'stats': {'total_amount': {}}, 'stats': {'total_amount': {}},
} }
@@ -63,65 +120,86 @@ var frame_clock: Dictionary = {}
var frame_budget: RefCounted var frame_budget: RefCounted
var _requested_speed: int = 0 var _requested_speed: int = 0
var _queued_productions: Array = [] var _queued_productions: Array = []
var production_queue_busy: bool = false
var _production_queue_cancelled: bool = false
var frame_active_clock: Dictionary = {} var frame_active_clock: Dictionary = {}
var _resume_clock: Dictionary = {} var _resume_clock: Dictionary = {}
var frame_step_seconds: float = 0.1 var frame_elapsed_seconds: float = 0.0
var _frame_step_speed: int = 0 const production_slice_usec: int = 4000
var _frame_started_usec: int = 0
var _frame_duration_seconds: float = 0.0
var _frame_cost_ratio: float = 0.0
## Hysteresis prevents alternating step sizes around one queue threshold. ## The HUD follows requested time; world state remains at the last committed cycle.
## Only scheduling uses float; both clocks and the accumulated debt stay exact. func display_clock() -> Dictionary:
func update_frame_step() -> float: return frame_clock if not frame_clock.is_empty() else state.time
if _frame_step_speed != speed:
_frame_step_speed = speed
frame_step_seconds = 0.1
_frame_cost_ratio = 0.0
if speed < 4: return 1.0 / 60.0
var debt: Variant = E.maximum(0, E.subtract(frame_clock.get("datetime", state.time.datetime), state.time.datetime))
var seconds: float = E.approx(E.divide(debt, state.speed_in_year))
if seconds > frame_step_seconds * 2.0 or (_frame_cost_ratio > 0.85 and seconds > frame_step_seconds):
frame_step_seconds = 0.2 if frame_step_seconds < 0.2 else 0.5
elif seconds < frame_step_seconds * 0.5:
frame_step_seconds = 0.2 if frame_step_seconds > 0.2 else 0.1
return frame_step_seconds
## Keep the requested clock advancing while a transaction spans render frames. ## Keep the requested clock advancing while a transaction spans render frames.
func evolute_frame(delta: float, tree: SceneTree) -> void: func evolute_frame(delta: float, tree: SceneTree) -> void:
if not is_finite(delta) or delta < 0.0 or not is_finite(delta * pow(10.0, speed - 1)): return if not is_finite(delta) or delta < 0.0 or not is_finite(delta * pow(10.0, speed - 1)): return
if E.ge(state.get("apocalypse", {}).get("phase", 0), 2):
frame_clock = state.time.duplicate()
frame_elapsed_seconds = 0.0
return
if frame_clock.is_empty(): frame_clock = state.time.duplicate() if frame_clock.is_empty(): frame_clock = state.time.duplicate()
frame_clock = SimulationTime.advance(frame_clock, delta, speed) frame_clock = SimulationTime.advance(frame_clock, delta, speed)
if frame_busy: return frame_elapsed_seconds += delta
if E.le(frame_clock.datetime, state.time.datetime) and _resume_clock.is_empty(): return if frame_busy or production_queue_busy: return
var next_clock: Dictionary = _resume_clock
if next_clock.is_empty() and not _queued_productions.is_empty():
_start_production_drain(tree)
return
if next_clock.is_empty():
# Cadence limits starts; each transaction consumes the whole pending interval.
if frame_elapsed_seconds + 1e-12 < frame_step_seconds: return
next_clock = frame_clock.duplicate()
frame_elapsed_seconds = 0.0
frame_busy = true frame_busy = true
frame_budget = preload("res://world/owniverse/frame_budget.gd").new(tree) frame_budget = preload("res://world/owniverse/frame_budget.gd").new(tree)
frame_active_clock = _resume_clock if not _resume_clock.is_empty() else frame_clock.duplicate() frame_active_clock = next_clock
# Bound catch-up work; never turn the entire accumulated debt into one step.
if _resume_clock.is_empty():
var normal_clock: Dictionary = SimulationTime.advance(state.time, update_frame_step(), speed)
if E.lt(normal_clock.datetime, frame_clock.datetime):
frame_active_clock = normal_clock
_resume_clock = {} _resume_clock = {}
_frame_duration_seconds = E.approx(E.divide(E.subtract(frame_active_clock.datetime, state.time.datetime), state.speed_in_year))
_frame_started_usec = Time.get_ticks_usec()
evolution.run_frame(frame_active_clock, frame_budget) evolution.run_frame(frame_active_clock, frame_budget)
func request_production(id: String, amount: Variant, resource: bool = false) -> void: func request_production(id: String, amount: Variant, resource: bool = false) -> bool:
if frame_busy: if E.gt(state.get("apocalypse", {}).get("phase", 0), 0): return false
if frame_busy or production_queue_busy:
_queued_productions.append({"id": id, "amount": amount, "resource": resource}) _queued_productions.append({"id": id, "amount": amount, "resource": resource})
elif resource: return true
production.produce_hs(id, amount) _apply_production({"id": id, "amount": amount, "resource": resource})
return false
func _apply_production(command: Dictionary) -> void:
if command.resource:
production.produce_hs(command.id, command.amount)
else: else:
production.produce(id, amount) production.produce(command.id, command.amount)
production_applied.emit(command.id)
func _start_production_drain(tree: SceneTree) -> void:
if production_queue_busy or _queued_productions.is_empty(): return
production_queue_busy = true
_production_queue_cancelled = false
_drain_productions(tree)
func _drain_productions(tree: SceneTree) -> void:
var keep_alive: Owniverse = self
while not _queued_productions.is_empty():
var slice_started: int = Time.get_ticks_usec()
while not _queued_productions.is_empty() and Time.get_ticks_usec() - slice_started < production_slice_usec:
var command: Dictionary = _queued_productions.pop_front()
_apply_production(command)
if _queued_productions.is_empty(): break
await tree.process_frame
if keep_alive._production_queue_cancelled: return
production_queue_busy = false
## Persist work requests, never the partially calculated transaction. ## Persist work requests, never the partially calculated transaction.
func frame_checkpoint() -> Dictionary: func frame_checkpoint() -> Dictionary:
return {"requested_clock": frame_clock, "active_clock": frame_active_clock if frame_busy else {}, "speed": speed, "productions": _queued_productions} return {"requested_clock": frame_clock, "active_clock": frame_active_clock if frame_busy else {}, "speed": speed, "productions": _queued_productions, "elapsed_seconds": frame_elapsed_seconds}
func restore_frame_checkpoint(checkpoint: Dictionary) -> void: func restore_frame_checkpoint(checkpoint: Dictionary) -> void:
frame_clock = checkpoint.get("requested_clock", {}) frame_clock = checkpoint.get("requested_clock", {})
_resume_clock = checkpoint.get("active_clock", {}) _resume_clock = checkpoint.get("active_clock", {})
var reserved_clock: Dictionary = _resume_clock if not _resume_clock.is_empty() else state.time
var pending_years: Variant = E.maximum(0, E.subtract(frame_clock.get("datetime", reserved_clock.datetime), reserved_clock.datetime))
frame_elapsed_seconds = float(checkpoint.get("elapsed_seconds", E.approx(pending_years) / pow(10.0, int(checkpoint.get("speed", state.speed)) - 1)))
_queued_productions = checkpoint.get("productions", []) _queued_productions = checkpoint.get("productions", [])
if _resume_clock.is_empty(): if _resume_clock.is_empty():
speed = checkpoint.get("speed", state.speed) speed = checkpoint.get("speed", state.speed)
@@ -129,31 +207,30 @@ func restore_frame_checkpoint(checkpoint: Dictionary) -> void:
_requested_speed = checkpoint.get("speed", state.speed) _requested_speed = checkpoint.get("speed", state.speed)
func finish_frame() -> void: func finish_frame() -> void:
if _frame_started_usec > 0 and _frame_duration_seconds > 0:
var ratio: float = (Time.get_ticks_usec() - _frame_started_usec) / 1000000.0 / _frame_duration_seconds
_frame_cost_ratio = ratio if _frame_cost_ratio == 0.0 else lerpf(_frame_cost_ratio, ratio, 0.25)
frame_busy = false frame_busy = false
frame_commits += 1 frame_commits += 1
if _requested_speed != 0: if _requested_speed != 0:
speed = _requested_speed speed = _requested_speed
_requested_speed = 0 _requested_speed = 0
var productions: Array = _queued_productions if not _queued_productions.is_empty():
_queued_productions = [] _start_production_drain(frame_budget.tree)
for command in productions:
request_production(command.id, command.amount, command.resource)
func _notification(what: int) -> void: func _notification(what: int) -> void:
if what == NOTIFICATION_PREDELETE and frame_budget != null: if what == NOTIFICATION_PREDELETE:
frame_budget.cancelled = true _production_queue_cancelled = true
if frame_budget != null:
frame_budget.cancelled = true
var speed: int : var speed: int :
get: get:
return _requested_speed if _requested_speed != 0 else state.speed return _requested_speed if _requested_speed != 0 else state.speed
set(value): set(value):
var requested_speed: int = clampi(value, 1, Global.SPEED_LIMIT)
if E.eq(state.get("apocalypse", {}).get("phase", 0), 1): requested_speed = 1
if frame_busy: if frame_busy:
_requested_speed = clampi(value, 1, Global.SPEED_LIMIT) _requested_speed = requested_speed
return return
state.speed = clampi(value, 1, Global.SPEED_LIMIT) state.speed = requested_speed
state.speed_in_year = E.power(10, E.subtract(state.speed, 1)) state.speed_in_year = E.power(10, E.subtract(state.speed, 1))
func evolute(delta: Variant) -> void: func evolute(delta: Variant) -> void:
+142 -3
View File
@@ -45,6 +45,41 @@ static func allocate_whole(requests: Dictionary, limit: Variant) -> Dictionary:
total = E.add(total, result[id]) total = E.add(total, result[id])
return _allocate_whole_with_total(requests, limit, result, total) return _allocate_whole_with_total(requests, limit, result, total)
## Whole stocks apportioned using float shares. Both the total and every stock
## ceiling are enforced exactly after quota rounding; no quantity becomes float.
static func allocate_profiled(stocks: Dictionary, limit: Variant) -> Dictionary:
var total: Variant = sum(stocks)
var available: Variant = E.floor_value(E.minimum(E.maximum(0, limit), total))
if E.eq(available, total): return stocks.duplicate()
var ids: Array = stocks.keys()
var shares: Array = []
var result: Dictionary = {}
var only: Variant = null
var positives: int = 0
for id in ids:
result[id] = 0
if E.gt(stocks[id], 0):
only = id
positives += 1
if E.le(available, 0): return result
if positives == 1:
result[only] = available
return result
for id in ids: shares.append(E.ratio_approx(stocks[id], total))
result = split_profile({}, "stock", available, ids, shares)
var assigned: Variant = 0
for id in ids:
result[id] = E.minimum(stocks[id], result[id])
assigned = E.add(assigned, result[id])
var missing: Variant = E.subtract(available, assigned)
for id in ids:
if E.le(missing, 0): break
var extra: Variant = E.minimum(missing, E.subtract(stocks[id], result[id]))
result[id] = E.add(result[id], extra)
missing = E.subtract(missing, extra)
assert(E.eq(missing, 0), "Whole stock allocation must exhaust the available quota")
return result
## Variant for callers that already maintain the sum of floored request values. ## Variant for callers that already maintain the sum of floored request values.
## The supplied total must equal sum(floor(max(0, requests[id]))). ## The supplied total must equal sum(floor(max(0, requests[id]))).
@@ -61,6 +96,15 @@ static func _allocate_whole_with_total(requests: Dictionary, limit: Variant, res
if E.le(available, 0): if E.le(available, 0):
for id in result: result[id] = 0 for id in result: result[id] = 0
return result return result
var only: Variant = null
var positive_count: int = 0
for id in result:
if E.gt(result[id], 0):
only = id
positive_count += 1
if positive_count == 1:
result[only] = available
return result
var ids: Array = requests.keys() var ids: Array = requests.keys()
var rests: Array = [] var rests: Array = []
var order: Array = [] var order: Array = []
@@ -123,11 +167,72 @@ 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 quotas: Array = E.whole_quotas(count, weights, SCALE)
var rests: Array = []
var order: Array = []
var total: Variant = 0
for i in ids.size():
var fraction: float = quotas[i][1] + E.approx(remainders.get(cached.keys[i], 0))
var adjustment: int = floori(fraction)
result[ids[i]] = E.maximum(0, E.add(quotas[i][0], adjustment))
total = E.add(total, result[ids[i]])
rests.append(fraction - adjustment if E.ge(quotas[i][0], -adjustment) else fraction)
order.append(i)
order.sort_custom(func(a: int, b: int) -> bool: return rests[a] > rests[b] if 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)
rests[i] -= 1.0
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)
rests[i] += 1.0
total = E.subtract(total, 1)
for i in ids.size():
remainders[cached.keys[i]] = clampf(rests[i], -1.0, 1.0)
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 +282,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 = []
@@ -198,7 +303,41 @@ static func split_arrays(remainders: Dictionary, key: String, count: Variant, id
static func accumulate_rate(remainders: Dictionary, key: String, count: Variant, probability: Variant) -> Variant: static func accumulate_rate(remainders: Dictionary, key: String, count: Variant, probability: Variant) -> Variant:
var expected: Variant = E.add(E.multiply(count, E.maximum(0, probability)), remainders.get(key, 0)) # Reward rates are floats while populations are exact whole values. Keep
# the product as quotient plus fractional carry instead of constructing an
# intermediate Exact Value and flooring it again.
if probability is float and is_finite(probability) and probability >= 0.0 and (count is int or (count is E.Value and count.fraction == 0.0)) and E.ge(count, 0):
var terms: Array = E.parts(probability)
if terms[2] > 0:
var population: Array = E._blocks(count)[0]
var whole_product: Array = E.Big.multiply(population, terms[0])
var qr: Array = E.Big.divmod(whole_product, terms[1])
var fractional: float = E._fraction_ratio(qr[1], terms[1])
var carry: float = E.approx(remainders.get(key, 0))
if carry >= 0.0 and carry < 1.0:
if qr[1] == [0]:
remainders[key] = carry
return E._compose(qr[0])
fractional += carry
if fractional > 0.5 and absf(fractional - roundf(fractional)) <= 1e-12:
fractional = roundf(fractional)
var extra: int = floori(fractional)
remainders[key] = fractional - extra
return E.add(E._compose(qr[0]), extra)
var product: Variant = E.multiply(count, E.maximum(0, probability))
var carry: float = E.approx(remainders.get(key, 0))
if carry >= 0.0 and carry < 1.0 and E.ge(product, 0):
if product is int:
remainders[key] = carry
return product
if product is E.Value:
var fraction: float = product.fraction + carry
if product.fraction > 0.0 and carry > 0.0 and fraction > 0.5 and absf(fraction - roundf(fraction)) <= 1e-12:
fraction = roundf(fraction)
var extra: int = floori(fraction)
remainders[key] = fraction - extra
return E.add(E.floor_value(product), extra)
var expected: Variant = E.add(product, remainders.get(key, 0))
var actual: Variant = E.floor_value(expected) var actual: Variant = E.floor_value(expected)
remainders[key] = E.maximum(0, E.subtract(expected, actual)) remainders[key] = E.maximum(0, E.subtract(expected, actual))
return actual return actual
+2 -2
View File
@@ -2,8 +2,8 @@ extends "res://world/owniverse/simulation_module.gd"
const E = preload("res://world/owniverse/math/exact.gd") const E = preload("res://world/owniverse/math/exact.gd")
const WorldParameters = preload("res://world/owniverse/world_parameters.gd") const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
func stellar_environment(source: Dictionary) -> Dictionary: func stellar_environment(source: Dictionary, quantities: Dictionary = {}) -> Dictionary:
var quantities: Dictionary = world.ledger.physical_amounts(source) if quantities.is_empty(): quantities = world.ledger.physical_amounts(source)
# SMBH is a self-group in GI; its quantity stores mass, not object count. # SMBH is a self-group in GI; its quantity stores mass, not object count.
var mass: Variant = source.amount.get("SMBH", 0) var mass: Variant = source.amount.get("SMBH", 0)
# Geometric volume includes a 1e22 scale per void and can exceed E27. # Geometric volume includes a 1e22 scale per void and can exceed E27.
+17 -7
View File
@@ -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]
+36 -8
View File
@@ -64,12 +64,15 @@
[ext_resource type="Texture2D" uid="uid://bce2bpw5642er" path="res://ui/achievements/achievements.en.png" id="61_d36xh"] [ext_resource type="Texture2D" uid="uid://bce2bpw5642er" path="res://ui/achievements/achievements.en.png" id="61_d36xh"]
[ext_resource type="Script" uid="uid://vy0dj2cqfmk0" path="res://ui/credits/credits.gd" id="62_o2i0n"] [ext_resource type="Script" uid="uid://vy0dj2cqfmk0" path="res://ui/credits/credits.gd" id="62_o2i0n"]
[ext_resource type="Texture2D" uid="uid://c46h78ewwlafk" path="res://ui/credits/credits.en.png" id="62_vu83j"] [ext_resource type="Texture2D" uid="uid://c46h78ewwlafk" path="res://ui/credits/credits.en.png" id="62_vu83j"]
[ext_resource type="PackedScene" uid="uid://cs8a5u3ecf51t" path="res://ui/achievements/achievement_item.tscn" id="64_0drd1"]
[ext_resource type="Texture2D" uid="uid://drvvkd724g8hy" path="res://ui/credits/kid_gp_light.png" id="64_rur7c"] [ext_resource type="Texture2D" uid="uid://drvvkd724g8hy" path="res://ui/credits/kid_gp_light.png" id="64_rur7c"]
[ext_resource type="Texture2D" uid="uid://b33ji4mpyw4ks" path="res://ui/credits/kid_gp.png" id="65_y1rwn"] [ext_resource type="Texture2D" uid="uid://b33ji4mpyw4ks" path="res://ui/credits/kid_gp.png" id="65_y1rwn"]
[ext_resource type="Texture2D" uid="uid://dgt52qiyuirso" path="res://ui/indicators/density/warning_border.png" id="66_rur7c"] [ext_resource type="Texture2D" uid="uid://dgt52qiyuirso" path="res://ui/indicators/density/warning_border.png" id="66_rur7c"]
[ext_resource type="Script" uid="uid://btsrt6rw3nr04" path="res://ui/audio/owniverse_music_player.gd" id="67_music"] [ext_resource type="Script" uid="uid://btsrt6rw3nr04" path="res://ui/audio/owniverse_music_player.gd" id="67_music"]
[ext_resource type="Script" uid="uid://botb0k4qr0hqo" path="res://ui/audio/owniverse_effects_player.gd" id="68_effects"] [ext_resource type="Script" uid="uid://botb0k4qr0hqo" path="res://ui/audio/owniverse_effects_player.gd" id="68_effects"]
[ext_resource type="Script" path="res://ui/report/report.gd" id="69_report"]
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_ifjjt"] [sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_ifjjt"]
[sub_resource type="ButtonGroup" id="ButtonGroup_58wjt"] [sub_resource type="ButtonGroup" id="ButtonGroup_58wjt"]
@@ -234,23 +237,23 @@ theme_override_constants/separation = 0
layout_mode = 2 layout_mode = 2
theme_override_constants/separation = 0 theme_override_constants/separation = 0
[node name="PageHeader" type="HBoxContainer" parent="Stage/World/PageScroller/HBoxContainer/ForcePage" unique_id=2102307933] [node name="ForceHeader" type="HBoxContainer" parent="Stage/World/PageScroller/HBoxContainer/ForcePage" unique_id=2102307933]
custom_minimum_size = Vector2(0, 128) custom_minimum_size = Vector2(0, 128)
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 6 size_flags_horizontal = 6
size_flags_vertical = 4 size_flags_vertical = 4
theme_override_constants/separation = 0 theme_override_constants/separation = 0
[node name="LeftIndicator" type="TextureButton" parent="Stage/World/PageScroller/HBoxContainer/ForcePage/PageHeader" unique_id=663302862] [node name="LeftIndicator" type="TextureButton" parent="Stage/World/PageScroller/HBoxContainer/ForcePage/ForceHeader" unique_id=663302862]
custom_minimum_size = Vector2(316, 64) custom_minimum_size = Vector2(316, 64)
layout_mode = 2 layout_mode = 2
[node name="Header" type="TextureRect" parent="Stage/World/PageScroller/HBoxContainer/ForcePage/PageHeader" unique_id=1295272502] [node name="Header" type="TextureRect" parent="Stage/World/PageScroller/HBoxContainer/ForcePage/ForceHeader" unique_id=1295272502]
custom_minimum_size = Vector2(440, 64) custom_minimum_size = Vector2(440, 64)
layout_mode = 2 layout_mode = 2
texture = ExtResource("13_dp8ul") texture = ExtResource("13_dp8ul")
[node name="RighIndicator" type="TextureButton" parent="Stage/World/PageScroller/HBoxContainer/ForcePage/PageHeader" unique_id=504419468] [node name="RighIndicator" type="TextureButton" parent="Stage/World/PageScroller/HBoxContainer/ForcePage/ForceHeader" unique_id=504419468]
custom_minimum_size = Vector2(316, 64) custom_minimum_size = Vector2(316, 64)
layout_mode = 2 layout_mode = 2
@@ -1011,7 +1014,8 @@ layout_mode = 2
texture_normal = ExtResource("52_uvuuk") texture_normal = ExtResource("52_uvuuk")
[node name="Report" type="VBoxContainer" parent="Stage" unique_id=1422399839] [node name="Report" type="VBoxContainer" parent="Stage" unique_id=1422399839]
visible = false script = ExtResource("69_report")
custom_minimum_size = Vector2(1080, 1920)
layout_mode = 0 layout_mode = 0
offset_right = 40.0 offset_right = 40.0
offset_bottom = 40.0 offset_bottom = 40.0
@@ -1032,17 +1036,26 @@ custom_minimum_size = Vector2(1080, 0)
layout_mode = 2 layout_mode = 2
[node name="Header" type="TextureRect" parent="Stage/Report/Header" unique_id=1321780840] [node name="Header" type="TextureRect" parent="Stage/Report/Header" unique_id=1321780840]
custom_minimum_size = Vector2(824, 256)
layout_mode = 2 layout_mode = 2
texture = ExtResource("60_an6vj") texture = ExtResource("60_an6vj")
[node name="ReportBody" type="RichTextLabel" parent="Stage/Report" unique_id=1952288631]
layout_mode = 2
size_flags_vertical = 3
bbcode_enabled = true
scroll_active = true
theme_override_font_sizes/normal_font_size = 30
theme_override_font_sizes/bold_font_size = 30
[node name="Achievements" type="VBoxContainer" parent="Stage" unique_id=1847732432] [node name="Achievements" type="VBoxContainer" parent="Stage" unique_id=1847732432]
visible = false visible = false
custom_minimum_size = Vector2(1080, 1920)
layout_mode = 0 layout_mode = 0
offset_right = 40.0 offset_right = 40.0
offset_bottom = 40.0 offset_bottom = 40.0
[node name="TopMenu" type="HBoxContainer" parent="Stage/Achievements" unique_id=40287683] [node name="TopMenu" type="HBoxContainer" parent="Stage/Achievements" unique_id=40287683]
custom_minimum_size = Vector2(0, 128)
layout_mode = 2 layout_mode = 2
[node name="BackButton" type="TextureButton" parent="Stage/Achievements/TopMenu" unique_id=1719885364] [node name="BackButton" type="TextureButton" parent="Stage/Achievements/TopMenu" unique_id=1719885364]
@@ -1054,18 +1067,31 @@ texture_normal = ExtResource("44_uvuuk")
texture_pressed = ExtResource("45_an6vj") texture_pressed = ExtResource("45_an6vj")
[node name="Header" type="CenterContainer" parent="Stage/Achievements" unique_id=51521239] [node name="Header" type="CenterContainer" parent="Stage/Achievements" unique_id=51521239]
custom_minimum_size = Vector2(1080, 0)
layout_mode = 2 layout_mode = 2
[node name="Title" type="TextureRect" parent="Stage/Achievements/Header" unique_id=1195638572] [node name="Title" type="TextureRect" parent="Stage/Achievements/Header" unique_id=1195638572]
custom_minimum_size = Vector2(824, 256) custom_minimum_size = Vector2(824, 128)
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 4 size_flags_horizontal = 4
size_flags_vertical = 4 size_flags_vertical = 4
texture = ExtResource("61_d36xh") texture = ExtResource("61_d36xh")
stretch_mode = 3 stretch_mode = 3
[node name="AchievementList" type="ScrollContainer" parent="Stage/Achievements" unique_id=1872434750]
layout_mode = 2
size_flags_vertical = 3
horizontal_scroll_mode = 0
[node name="AchievementItems" type="VBoxContainer" parent="Stage/Achievements/AchievementList" unique_id=1878761996]
layout_mode = 2
size_flags_horizontal = 3
[node name="AchievementItem" parent="Stage/Achievements/AchievementList/AchievementItems" unique_id=1952626354 instance=ExtResource("64_0drd1")]
visible = false
layout_mode = 2
[node name="Credits" type="VBoxContainer" parent="Stage" unique_id=785042592] [node name="Credits" type="VBoxContainer" parent="Stage" unique_id=785042592]
visible = false
layout_mode = 0 layout_mode = 0
offset_right = 1080.0 offset_right = 1080.0
offset_bottom = 1920.0 offset_bottom = 1920.0
@@ -1141,6 +1167,8 @@ script = ExtResource("67_music")
[connection signal="pressed" from="Stage/MainMenu/TopMenu/BackButton" to="Stage" method="_on_topmenu_button_pressed" binds= ["World"]] [connection signal="pressed" from="Stage/MainMenu/TopMenu/BackButton" to="Stage" method="_on_topmenu_button_pressed" binds= ["World"]]
[connection signal="pressed" from="Stage/MainMenu/CreateOwniverse" to="Stage" method="create_new_universe"] [connection signal="pressed" from="Stage/MainMenu/CreateOwniverse" to="Stage" method="create_new_universe"]
[connection signal="pressed" from="Stage/MainMenu/Achievements" to="Stage" method="_on_topmenu_button_pressed" binds= ["Achievements"]] [connection signal="pressed" from="Stage/MainMenu/Achievements" to="Stage" method="_on_topmenu_button_pressed" binds= ["Achievements"]]
[connection signal="pressed" from="Stage/MainMenu/Language/English" to="Stage/MainMenu" method="_on_change_locale_pressed" binds= ["en"]]
[connection signal="pressed" from="Stage/MainMenu/Language/Russian" to="Stage/MainMenu" method="_on_change_locale_pressed" binds= ["ru"]]
[connection signal="pressed" from="Stage/MainMenu/Media/Sound" to="Stage/MainMenu" method="_on_sound_pressed"] [connection signal="pressed" from="Stage/MainMenu/Media/Sound" to="Stage/MainMenu" method="_on_sound_pressed"]
[connection signal="pressed" from="Stage/MainMenu/Media/Music" to="Stage/MainMenu" method="_on_music_pressed"] [connection signal="pressed" from="Stage/MainMenu/Media/Music" to="Stage/MainMenu" method="_on_music_pressed"]
[connection signal="value_changed" from="Stage/MainMenu/Volume/SoundVolume" to="Stage/MainMenu" method="_on_sound_volume_value_changed"] [connection signal="value_changed" from="Stage/MainMenu/Volume/SoundVolume" to="Stage/MainMenu" method="_on_sound_volume_value_changed"]