Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a7c05e3796 | |||
| 2aced0c61a | |||
| e1782c7b10 | |||
| e41d652631 | |||
| fe68e788b3 | |||
| 3047ef5a85 | |||
| f53181b9ad | |||
| 273093bd9e | |||
| 00b6192d19 | |||
| 02c9843e1b | |||
| 459fbb2a4d | |||
| c42940369a | |||
| 9357f757a9 | |||
| 413f2be904 | |||
| 3fcc7211ce | |||
| 6ee66a627d |
+6
-1
@@ -30,11 +30,14 @@ var Templates: Dictionary = {}
|
||||
var LifeRules: Dictionary = {}
|
||||
var CivilizationRules: Dictionary = {}
|
||||
var ExtraParams: Dictionary = {}
|
||||
var ApocalypseQueue: Dictionary = {}
|
||||
var Achievements: Dictionary = {}
|
||||
|
||||
|
||||
var Music: 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:
|
||||
@@ -92,6 +95,8 @@ func _ready() -> void:
|
||||
OwniverseItems = OwniverseItemLoader.get_data()
|
||||
Templates = OwniverseTemplateLoader.get_data()
|
||||
CivilizationRules = OwniverseCivilizationRuleLoader.get_data()
|
||||
ApocalypseQueue = OwniverseApocalypseQueueLoader.get_data()
|
||||
Achievements = OwniverseAchievementLoader.get_data()
|
||||
|
||||
Music = OwniverseAudioLoader.get_music()
|
||||
Effects = OwniverseAudioLoader.get_effects()
|
||||
|
||||
@@ -6,7 +6,7 @@ const E = preload("res://world/owniverse/math/exact.gd")
|
||||
const SETTING_SECTION = "Settings"
|
||||
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 WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
||||
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
|
||||
# their quantities and receive neutral defaults from Owniverse on load.
|
||||
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"
|
||||
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 ((not _number_map(saved.world_parameters)) or E.ne(saved.world_parameters.size(), WorldParameters.defaults().size())):
|
||||
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))):
|
||||
return "invalid civilization state"
|
||||
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))
|
||||
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]:
|
||||
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))
|
||||
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:
|
||||
if (not _number_map(saved.civilization_homes[id], true)):
|
||||
return "invalid civilization homes"
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
1|LUB|1|3|50|Вселенский пузырь
|
||||
2|LGW|1|3|5|Великая стена
|
||||
@@ -130,3 +138,7 @@ Num|Shortcut|Stage|Screen|Tick|Desc
|
||||
129|H|11|1|5|Водород
|
||||
130|SMBH|12|2|5|СМЧД
|
||||
131|S|12|1|5|Пространство
|
||||
"""
|
||||
|
||||
|
||||
#endregion
|
||||
@@ -0,0 +1 @@
|
||||
uid://8bh387weuy7a
|
||||
@@ -1008,7 +1008,7 @@ Trends.Report.Header|[b]Влияние развитых цивилизаций
|
||||
Int.Fed.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||||
|
||||
Emerged.Report.Header|[b]Максимальное число возникших сущностей[/b]|||||
|
||||
Emerged.Report.Header|[b]Всего возникло сущностей[/b]|||||
|
||||
Ctm.Report.Max|Трансцендентная цивилизация мистиков: [color=#cccc88]%value0%[/color]|max||||
|
||||
Ctb.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||||
|
||||
AB.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||||
|
||||
LGW.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||||
|
||||
RD.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||||
|
||||
H.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.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||||
|
||||
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||||
|
||||
Ctb.Max|Transcendent builders 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||||
|
||||
AB.Max|Asteroids: [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||||
|
||||
LGW.Max|Great Wall: [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||||
|
||||
RD.Max|Red 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||||
|
||||
H.Max|Hydrogen: [color=#cccc88]%value0%[/color]|max||||
|
||||
He.Max|Helium: [color=#cccc88]%value0%[/color]|max||||
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
[runnable_presets]
|
||||
|
||||
"Windows Desktop"="Windows Desktop"
|
||||
Android="Android"
|
||||
|
||||
[preset.0]
|
||||
|
||||
@@ -11,7 +12,7 @@ custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_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()
|
||||
patch_delta_encoding=false
|
||||
patch_delta_compression_level_zstd=19
|
||||
|
||||
+82
-25
@@ -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
|
||||
@@ -56,7 +62,7 @@ converted = floor(expected)
|
||||
remainder = expected - converted
|
||||
```
|
||||
|
||||
Конец окна списывает остаток. Переход не уничтожает всю партию в начале окна. Начала и длительности сохраняют модель GI, включая годовую длительность событий SN/GRB.
|
||||
Конец постепенного окна списывает остаток. Новые звёздные партии высоких скоростей вместо такого окна имеют одну границу полного перехода. Длительность событий SN/GRB остаётся годовой.
|
||||
|
||||
## Планеты
|
||||
|
||||
@@ -111,7 +117,7 @@ GI не хранит накопленные счётчики первичной
|
||||
|
||||
Прогресс и деградация округляются вниз с сохранением остатков. Если оба накопленных исхода конкурируют за последнюю планету, распределяются только существующие объекты, а неиспользованный остаток переносится на следующую попытку. Деградация распределяется по весам `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. Для существующих этических цивилизаций пользователь выбрал равномерное распределение между шестью этиками с детерминированным целым остатком.
|
||||
|
||||
@@ -123,39 +129,37 @@ HP деградирует в RP. У эонов возможен откат к п
|
||||
|
||||
`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 требуется соответствующий запас структур, общий для использующих его стадий. При потере запаса избыток деградирует. Этот бюджет действует одновременно со звёздным.
|
||||
|
||||
Размещение выбирает структуры в порядке убывания `civ_bonus`, начиная с соответствующего стадии `claim`. Прямое размещение ограничено числом структур; остальные разрешённые структуры при наличии дают общий бонус, как в Unity. Период новых партий равен `span / средневзвешенный civ_bonus`, для неразмещённой части множитель равен 1. Уже назначенные сроки партий не переписываются при изменении бонуса.
|
||||
Размещение выбирает структуры в порядке убывания `civ_bonus`, начиная с соответствующего стадии `claim`. Прямое размещение ограничено числом структур; остальные разрешённые структуры при наличии дают общий бонус, как в Unity. Эффективный средний интервал попыток равен `span / средневзвешенный civ_bonus`, для неразмещённой части множитель равен 1. Бонус входит в оператор следующего расчётного интервала.
|
||||
|
||||
Ct не получает часы. Инициализация отсутствующих партий жизни выполняется при загрузке и перед продвижением симуляции. Параметры мира и тенденции описаны ниже; интерфейс применения сил в этот перенос не входит.
|
||||
|
||||
@@ -173,9 +177,11 @@ Ct не получает часы. Инициализация отсутству
|
||||
- `1e-5 < density < 0.9`: обычная шкала, `ln(density × 1e5) / ln(9e4)`;
|
||||
- `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
|
||||
и параметры ForceBox загружаются, но пока не используются в GI: соответствующие
|
||||
G, C, space_generation, ChatPerTick и параметры ForceBox загружаются, но пока не
|
||||
используются в GI: соответствующие
|
||||
формулы и механики старой Unity-версии здесь отсутствуют. Производство объектов
|
||||
в 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` проверяет загрузку точных чисел и изменение энергии,
|
||||
плотности, объёма пустот, порогов индикатора и бонуса горючести.
|
||||
|
||||
|
||||
## Достижения и уведомления
|
||||
|
||||
`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
@@ -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 WorldIndicators = preload("res://ui/indicators/world_indicators.gd")
|
||||
var achievements: Node
|
||||
var world_indicators: RefCounted
|
||||
var indicator_elapsed: float = 0.0
|
||||
var indicator_revision: int = -1
|
||||
@@ -98,7 +99,14 @@ var g_active_items: Dictionary = {}
|
||||
var pending_page: int = -1
|
||||
var active_actions_dirty: bool = true
|
||||
var active_actions_elapsed: float = 0.0
|
||||
var clock_display_elapsed: float = 0.0
|
||||
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_items: Dictionary[String, OwniverseItem] = {}
|
||||
@@ -273,6 +281,7 @@ func _swipe_to_nearest_page() -> void:
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if E.eq(owniverse.state.get("apocalypse", {}).get("phase", 0), 1): return
|
||||
if event is InputEventScreenDrag:
|
||||
#effects_player.play_effect("swipe")
|
||||
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(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)
|
||||
if item_id == "SMBH":
|
||||
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
|
||||
|
||||
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
|
||||
@@ -327,6 +340,10 @@ func _ready() -> void:
|
||||
create_new_universe()
|
||||
load_data()
|
||||
_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
|
||||
|
||||
@@ -345,6 +362,7 @@ func _enable_interface_sounds() -> void:
|
||||
func create_new_universe() -> void:
|
||||
var previous: Owniverse = owniverse
|
||||
owniverse = Owniverse.new()
|
||||
owniverse.production_applied.connect(_on_owniverse_production_applied)
|
||||
var inited_result: Dictionary = init_enitities()
|
||||
locked_items = inited_result.locked_items
|
||||
|
||||
@@ -384,7 +402,7 @@ func create_new_universe() -> void:
|
||||
func _refresh_universe_ui() -> void:
|
||||
owniverse.presentation.refresh(owniverse.state, owniverse.catalog)
|
||||
_change_speed(0)
|
||||
set_datetime(owniverse.state.time.year)
|
||||
set_datetime(owniverse.display_clock().year)
|
||||
set_owniverse_item_value()
|
||||
_unlock_items()
|
||||
if world_indicators != null: world_indicators.update(owniverse)
|
||||
@@ -392,6 +410,12 @@ func _refresh_universe_ui() -> void:
|
||||
func _process(delta: float) -> void:
|
||||
|
||||
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
|
||||
if indicator_elapsed >= 0.1 and indicator_revision != owniverse.presentation.revision:
|
||||
indicator_elapsed = 0.0
|
||||
@@ -399,9 +423,9 @@ func _process(delta: float) -> void:
|
||||
indicator_revision = owniverse.presentation.revision
|
||||
_update_descriptions(delta)
|
||||
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):
|
||||
set_datetime(owniverse.state.time.year)
|
||||
displayed_year = owniverse.state.time.year
|
||||
var committed_year: Variant = owniverse.state.time.year
|
||||
if displayed_year == null or E.ne(displayed_year, committed_year):
|
||||
displayed_year = committed_year
|
||||
|
||||
#owniverse.cycle(delta)
|
||||
|
||||
@@ -421,6 +445,7 @@ func _process(delta: float) -> 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
|
||||
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))
|
||||
|
||||
|
||||
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:
|
||||
var current_screen: String = ""
|
||||
for screen_id in screens:
|
||||
|
||||
+58
-19
@@ -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%). Парсер планет сохраняет дроби и поддерживает один элемент без `&`.
|
||||
|
||||
@@ -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`
|
||||
|
||||
Stage вызывает `evolute_frame`: транзакция получает бюджет 40 мс и уступает
|
||||
следующему кадру внутри создания партий и обработки стадий. Расчёт остаётся
|
||||
на главном потоке, состояние публикуется только после полного завершения.
|
||||
Длительность одной транзакции ограничена обычным шагом 1/60 секунды выбранной
|
||||
скорости. Запрошенное время хранится отдельно, поэтому отставание не превращается
|
||||
в один всё более тяжёлый шаг. При предельной нагрузке время симуляции может
|
||||
отставать от запрошенного; это не ограничение FPS интерфейса и не удаление событий.
|
||||
Запрошенные игровые часы и счётчик UI обновляются независимо от тяжёлого расчёта;
|
||||
Stage показывает запрошенное время раз в 0,1 секунды (10 Гц), а опубликованное
|
||||
состояние мира остаётся на последнем завершённом цикле. Расчёт запускается после
|
||||
накопления выбранного интервала и обрабатывает весь ожидающий интервал. Время,
|
||||
поступившее во время расчёта, обрабатывается следующим циклом. Контроллер нагрузки
|
||||
меняет целевой шаг между 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. Замер включает сокращение накопленного отставания сохранения;
|
||||
это результат конкретного сценария на Mac, а не гарантия для Android.
|
||||
|
||||
Адаптивный шаг на скоростях 4–6 выбирает 0.1, 0.2 или 0.5 реальной секунды.
|
||||
Долг больше двух шагов повышает ступень, меньше половины шага понижает её.
|
||||
Шаг ограничен доступным запрошенным временем. Скорости 1–3 сохраняют 1/60;
|
||||
смена скорости сбрасывает ступень. Возобновляемая транзакция сохраняет точную
|
||||
границу из checkpoint, адаптация применяется к следующей транзакции.
|
||||
Бюджет кадра остаётся 40 мс; ширина временных партий не меняется.
|
||||
`test_frame_evolution` проверяет ступени, гистерезис, сохранение долга,
|
||||
переключение скорости, возобновление и совпадение с прямым расчётом того же шага.
|
||||
На свежей диагностической копии сохранения с долгом 407.79 млн лет:
|
||||
600 кадров реального UI на M4 Pro дали 18.05 FPS (p95 92.91 мс,
|
||||
максимум 122.45 мс), поступило 3.335 млн лет, обработано 6.480 млн:
|
||||
пропускная способность 1.943 вместо 0.787 при фиксированном шаге 0.1.
|
||||
Разбиение времени влияет на дискретные производство и глобальные параметры;
|
||||
адаптивные шаги не обещают совпадения результата с последовательностью 0.1.
|
||||
Целевая частота запуска — пять циклов в секунду. Размер шага равен всему накопленному интервалу. Checkpoint сохраняет границу активного расчёта и ожидающее реальное время. Тест проверяет все шесть скоростей, смену скорости, задержку запуска и накопление времени во время расчёта.
|
||||
|
||||
Дипломатический drift использует шесть взвешенных вкладов вместо матрицы
|
||||
пар этик: вес = сумма вкладов + 195 × свой вклад − противоположный вклад.
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
extends Control
|
||||
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
const RESULT_PATH := "user://android_benchmark_result.txt"
|
||||
const FRAME_COUNT := 120
|
||||
|
||||
var status_label: Label
|
||||
|
||||
func _ready() -> void:
|
||||
status_label = Label.new()
|
||||
status_label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
status_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
status_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
status_label.add_theme_font_size_override("font_size", 32)
|
||||
add_child(status_label)
|
||||
call_deferred("_run")
|
||||
|
||||
func _set_status(message: String) -> void:
|
||||
status_label.text = message
|
||||
var file := FileAccess.open(RESULT_PATH, FileAccess.WRITE)
|
||||
if file != null:
|
||||
file.store_line(message)
|
||||
file.flush()
|
||||
|
||||
func _restore(value):
|
||||
if value is float and value == floorf(value) and absf(value) <= 9007199254740991:
|
||||
return int(value)
|
||||
if value is Dictionary:
|
||||
for key in value:
|
||||
value[key] = _restore(value[key])
|
||||
elif value is Array:
|
||||
for index in value.size():
|
||||
value[index] = _restore(value[index])
|
||||
return value
|
||||
|
||||
func _load_payload_threaded() -> Dictionary:
|
||||
var file := FileAccess.open("res://tests/artifacts/all_objects_speed6.adventure.json", FileAccess.READ)
|
||||
if file == null:
|
||||
return {"error": "Cannot open benchmark save: %s" % FileAccess.get_open_error()}
|
||||
var parser := JSON.new()
|
||||
var error := parser.parse(file.get_as_text())
|
||||
if error != OK:
|
||||
return {"error": "JSON parse error at line %d: %s" % [parser.get_error_line(), parser.get_error_message()]}
|
||||
var data = E.decode(_restore(parser.data))
|
||||
if not data is Dictionary or not data.has("state"):
|
||||
return {"error": "Benchmark save has no state"}
|
||||
return {"payload": data}
|
||||
|
||||
func _run() -> void:
|
||||
_set_status("Reading and decoding benchmark save on a worker thread…")
|
||||
var worker := Thread.new()
|
||||
var start_error := worker.start(Callable(self, "_load_payload_threaded"))
|
||||
if start_error != OK:
|
||||
_set_status("ERROR: could not start loader thread (%s)" % start_error)
|
||||
return
|
||||
while worker.is_alive():
|
||||
await get_tree().process_frame
|
||||
var loaded: Dictionary = worker.wait_to_finish()
|
||||
if loaded.has("error"):
|
||||
_set_status("ERROR: " + str(loaded.error))
|
||||
return
|
||||
_set_status("Save decoded. Initializing UI scene…")
|
||||
var payload: Dictionary = loaded.payload
|
||||
var scene: Node = load("res://world/world.tscn").instantiate()
|
||||
var stage = scene.get_node("Stage")
|
||||
stage.owniverse.free()
|
||||
stage.set_script(load("res://tests/fixtures/benchmark_ui_stage.gd"))
|
||||
get_tree().root.add_child(scene)
|
||||
await get_tree().process_frame
|
||||
stage.owniverse.state = payload.state.duplicate(true)
|
||||
stage.owniverse.restore_frame_checkpoint(payload.get("simulation", {}))
|
||||
stage.owniverse.presentation.refresh(stage.owniverse.state, stage.owniverse.catalog)
|
||||
stage.set_owniverse_item_value()
|
||||
stage.stage_page_scroller.scroll_horizontal = 4 * stage.PAGE_WIDTH
|
||||
var initial_requested = stage.owniverse.frame_clock.get("datetime", stage.owniverse.state.time.datetime)
|
||||
var initial_published = stage.owniverse.state.time.datetime
|
||||
stage.owniverse.profile_enabled = true
|
||||
stage.measuring = true
|
||||
_set_status("Running speed 6: 0/%d frames" % FRAME_COUNT)
|
||||
for frame in FRAME_COUNT:
|
||||
await get_tree().process_frame
|
||||
if frame % 5 == 4:
|
||||
_set_status("Running speed 6: %d/%d frames; commits=%d" % [frame + 1, FRAME_COUNT, stage.owniverse.frame_commits])
|
||||
stage.measuring = false
|
||||
var samples: Array = stage.frame_samples
|
||||
if samples.is_empty():
|
||||
_set_status("ERROR: no rendered frame samples; frames=%d" % stage.frame_samples.size())
|
||||
return
|
||||
var total: float = 0.0
|
||||
for sample in samples:
|
||||
total += sample
|
||||
samples.sort()
|
||||
var requested_advance = E.subtract(stage.owniverse.frame_clock.datetime, initial_requested)
|
||||
var published_advance = E.subtract(stage.owniverse.state.time.datetime, initial_published)
|
||||
var process_samples: Array = stage.process_samples
|
||||
process_samples.sort()
|
||||
var process_total: float = 0.0
|
||||
for sample in process_samples:
|
||||
process_total += sample
|
||||
var process_summary := "no _process samples"
|
||||
if not process_samples.is_empty():
|
||||
process_summary = "mean=%.2fms p95=%.2fms max=%.2fms samples=%d" % [process_total / process_samples.size(), process_samples[int(ceil(process_samples.size() * 0.95)) - 1], process_samples.back(), process_samples.size()]
|
||||
var budget: Variant = stage.owniverse.frame_budget
|
||||
var max_slice_summary: String = "none"
|
||||
if budget != null:
|
||||
max_slice_summary = "%.2fms (%s)" % [budget.max_slice_usec / 1000.0, budget.max_slice_phase]
|
||||
var result := "DONE\nFPS=%.2f mean=%.2fms p95=%.2fms max=%.2fms frames=%d commits=%d\nStage _process CPU: %s\nMax uninterrupted simulation slice: %s\nRequested=%s published=%s throughput=%s\nFinal adaptive step=%s\nProfile=%s" % [1000.0 / (total / samples.size()), total / samples.size(), samples[int(ceil(samples.size() * 0.95)) - 1], samples.back(), samples.size(), stage.owniverse.frame_commits, process_summary, max_slice_summary, str(requested_advance), str(published_advance), str(E.divide(published_advance, requested_advance)), str(stage.owniverse.frame_step_seconds), str(stage.owniverse.profile)]
|
||||
_set_status(result)
|
||||
get_tree().quit()
|
||||
@@ -0,0 +1 @@
|
||||
uid://dkbyhj8ib0eyf
|
||||
@@ -0,0 +1,12 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://tests/android_benchmark.gd" id="1"]
|
||||
|
||||
[node name="AndroidBenchmark" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1")
|
||||
@@ -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 --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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://3fogff2lfmy5
|
||||
@@ -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
|
||||
@@ -40,7 +40,7 @@ func run():
|
||||
for id in stage.owniverse.catalog.entities:
|
||||
if E.gt(stage.owniverse.ledger.physical_amount(stage.owniverse.state, id), 0): remaining_objects += 1
|
||||
print("Remaining objects: ", remaining_objects)
|
||||
print("Final adaptive step: ", stage.owniverse.frame_step_seconds)
|
||||
print("Simulation step: ", stage.owniverse.frame_step_seconds)
|
||||
for entity in stage.owniverse.catalog.entities.values(): entity.free()
|
||||
stage.owniverse.free()
|
||||
scene.free()
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://d0plagt7blmcf
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://uslcyxm8vpcn
|
||||
@@ -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))
|
||||
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")
|
||||
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 = []
|
||||
actions.action_pressed.connect(func ( _page , amount ) : emitted.append(amount))
|
||||
actions._on_action_button_pressed(2)
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://ct356urggcgal
|
||||
@@ -109,5 +109,103 @@ func run() -> void:
|
||||
world.batches.withdraw_batches(world.state, "N", 1)
|
||||
check(world.state.batches.N.is_empty(), "Scaling invalidates readiness and removes zero cohorts")
|
||||
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]))
|
||||
quit((0 if E.eq(failures, 0) else 1))
|
||||
|
||||
@@ -1,165 +1,136 @@
|
||||
extends SceneTree
|
||||
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 failures: int = 0
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func check(condition: bool, message: String) -> void:
|
||||
checks = E.add(checks, 1)
|
||||
if (not condition):
|
||||
failures = E.add(failures, 1)
|
||||
func _initialize(): call_deferred("run")
|
||||
func check(value: bool, message: String) -> void:
|
||||
checks += 1
|
||||
if not value:
|
||||
failures += 1
|
||||
push_error(message)
|
||||
|
||||
func fixture():
|
||||
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||
var global: Node = root.get_node("Global")
|
||||
for id in global.OwniverseEntities:
|
||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||
world.catalog.entities[id] = entity
|
||||
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
|
||||
for id in world.catalog.entities:
|
||||
world.state.amount[id] = 0
|
||||
world.state.stats.total_amount[id] = 0
|
||||
if E.ne(entity.group_as, ""):
|
||||
world.state.amount[entity.group_as] = 0
|
||||
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
|
||||
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"]:
|
||||
if world.state.amount.has(id): world.state.amount[id] = E.of("1e60")
|
||||
return world
|
||||
|
||||
func dispose(world) -> void:
|
||||
for entity in world.catalog.entities.values():
|
||||
entity.free()
|
||||
func dispose(world):
|
||||
for entity in world.catalog.entities.values(): entity.free()
|
||||
world.free()
|
||||
|
||||
func seed_population(world, id: String, count: Variant, time: Variant = 1.0) -> void:
|
||||
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:
|
||||
func population(world) -> Variant:
|
||||
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
|
||||
|
||||
func invariants(world) -> void:
|
||||
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:
|
||||
check(E.eq(sum_ethics(world, id), physical.get(id, 0)), E.add("Ethics equal physical population for ", id))
|
||||
for n in world.state.ethics[id].values():
|
||||
if (E.lt(n, 0) or E.ne(n, E.floor_value(n))): check(false, "Invalid ethic amount")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Civilization state validates")
|
||||
var total: float = 0.0
|
||||
for value in world.state.ethics[id].values():
|
||||
check(is_finite(value) and value >= 0.0 and value <= 1.0, "Finite normalized ethic shares")
|
||||
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 baseline: Dictionary = global.CivilizationRules.duplicate(true)
|
||||
configure(global.CivilizationRules, 1.0, 0.0)
|
||||
var world = fixture()
|
||||
seed_population(world, "C0.2", 60000)
|
||||
world.evolution.evolve_batches(world.state, 1)
|
||||
check(E.gt(world.state.amount["C0.3"], 0), "C0.2 actually progresses")
|
||||
check(E.gt(world.state.amount["C0.1"], 0), "C0.2 actually degrades")
|
||||
check(E.eq(world.state.civilization_stats.get("wars", 0), 0), "Table disables early wars")
|
||||
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(E.eq(world.state.stats.total_amount["C0.2"], 0), "Survivors do not count as new civilizations")
|
||||
check(E.gt(world.state.civilization_stats.worship, 0), "Worship derived from surviving ethics")
|
||||
var count: Variant = E.of("10000000000000000000000000000000000000007")
|
||||
world.state.amount["C0.2"] = count
|
||||
world.state.ethics["C0.2"] = State.empty_profile()
|
||||
world.civilization_flow.evolve(world.state, 0, 100)
|
||||
check(E.eq(population(world), count), "Continuous chain conserves arbitrary-size whole 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.gt(world.state.amount["C0.4"], 0), "One interval includes multiple successive stages")
|
||||
check(not world.state.batches.has("C0.2") and not world.state.batches.has("C0.3"), "Civilizations do not create age batches")
|
||||
invariants(world)
|
||||
var restored = fixture()
|
||||
restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
|
||||
world.evolution.evolve_batches(world.state, 2100)
|
||||
restored.evolution.evolve_batches(restored.state, 2100)
|
||||
check(E.eq(world.state, restored.state), "Save resumes deterministic ethical evolution")
|
||||
invariants(world)
|
||||
dispose(restored)
|
||||
var half = fixture()
|
||||
half.state.amount["C0.2"] = count
|
||||
half.state.ethics["C0.2"] = State.empty_profile()
|
||||
half.civilization_flow.evolve(half.state, 0, 50)
|
||||
half.civilization_flow.evolve(half.state, 50, 100)
|
||||
for id in global.CivilizationRules:
|
||||
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)
|
||||
|
||||
configure(global.CivilizationRules, 0.0, 0.0)
|
||||
world = fixture()
|
||||
world.state.amount.RD = 100000000
|
||||
seed_population(world, "C2.0", 60000)
|
||||
world.state.amount["C1.9"] = 10000
|
||||
world.evolution.initialize_life_batches()
|
||||
world.evolution.evolve_batches(world.state, 1)
|
||||
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")
|
||||
world.state.amount["C0.2"] = 10001
|
||||
world.state.ethics["C0.2"] = {"m": 1.0, "l": 0.0, "w": 0.0, "d": 0.0, "c": 0.0, "b": 0.0}
|
||||
world.civilization_flow.evolve(world.state, 0, 100)
|
||||
check(E.eq(world.state.amount["C0.2"], 10001), "Disabled stage movement retains every individual")
|
||||
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")
|
||||
invariants(world)
|
||||
dispose(world)
|
||||
|
||||
configure(global.CivilizationRules, 1.0, 0.0)
|
||||
world = fixture()
|
||||
world.state.amount.RD = 1
|
||||
world.state.binds.RD = {"HP": 5, "GG": 5, "AB": 5, "RP": 5}
|
||||
seed_population(world, "C1.6", 60000)
|
||||
for row : Dictionary in global.CivilizationRules["C1.6"].ethics.values(): row.degradation = "0"
|
||||
world.evolution.evolve_batches(world.state, 1)
|
||||
var after: Dictionary = world.ledger.physical_amounts(world.state)
|
||||
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")
|
||||
check(E.eq(world.state.binds.RD.get("C1.4", 0), 20), "Colonies retain target host and use two-stages-earlier level")
|
||||
var totals: Variant = 0
|
||||
for id in after:
|
||||
if E.eq(world.catalog.entities[id].group_as, "Planet"): totals = E.add(totals, after[id])
|
||||
check(E.eq(totals, 60020), "Colonization replaces planets, does not create extra worlds")
|
||||
for id in ["RD", "OD", "YD", "WYD"]: world.state.amount[id] = 0
|
||||
world.state.amount["C1.9"] = 101
|
||||
world.civilization_flow.evolve(world.state, 0, 100)
|
||||
check(E.eq(world.state.amount["C1.9"], 101), "Unfunded interstellar flows return to their source")
|
||||
check(E.eq(population(world), 101), "Resource admission conserves exact population")
|
||||
dispose(world)
|
||||
|
||||
configure(global.CivilizationRules, 0.0, 0.0, 1)
|
||||
world = fixture()
|
||||
world.state.amount["C1.6"] = 60000
|
||||
world.state.binds.RD = {"HP": 5, "RP": 5, "GG": 5, "AB": 5}
|
||||
global.CivilizationRules["C1.6"].colonizes = true
|
||||
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)
|
||||
dispose(world)
|
||||
|
||||
global.CivilizationRules = baseline.duplicate(true)
|
||||
world = fixture()
|
||||
world.state.amount.RD = E.of("1e25")
|
||||
world.state.amount.BG = E.of("1e25")
|
||||
world.state.amount.TWD = E.of("1e25")
|
||||
world.state.amount.SNS = E.of("1e25")
|
||||
world.state.amount.MGT = E.of("1e25")
|
||||
world.state.amount.BH25 = E.of("1e25")
|
||||
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")
|
||||
world.state.amount["C2.0"] = 60000
|
||||
world.state.amount["C1.9"] = 10000
|
||||
world.civilization_flow.evolve(world.state, 0, 20000)
|
||||
check(E.gt(world.state.civilization_stats.wars, 0), "War rewards integrate encounters during the interval")
|
||||
check(E.gt(world.state.civilization_stats.collateral, 0), "Collateral damage reaches previous stages")
|
||||
check(E.eq(population(world), 70000), "War outcomes and collateral conserve occupied-world counts")
|
||||
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)
|
||||
global.CivilizationRules = baseline
|
||||
|
||||
world = fixture()
|
||||
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))
|
||||
print("Civilization flows: %d checks, %d failures" % [checks, failures])
|
||||
quit(1 if failures else 0)
|
||||
|
||||
@@ -48,18 +48,23 @@ func run() -> void:
|
||||
world.evolution.initialize_life_batches()
|
||||
check((E.eq(world.state.amount["C2.0"], 12) and E.eq(world.state.binds.RD["C2.0"], 0)), "Old space civilizations detach without loss")
|
||||
check(E.eq(world.state.binds.RD["C1.9"], 3), "Planetary civilizations retain their host")
|
||||
check(E.eq(world.state.ethics["C0.2"], {"m": 2, "l": 2, "w": 1, "d": 1, "c": 1, "b": 1}), "Migration uses equal ethics and deterministic remainder")
|
||||
check(world.state.ethics["C2.0"].values().all(func ( n ) : return E.eq(n, 2)), "Bound civilization ethics count physical population")
|
||||
check(world.state.ethics["C0.2"].values().all(func ( n ) : return E.near(n, 1.0 / 6.0)), "Migration initializes equal ethic shares")
|
||||
check(world.state.ethics["C2.0"].values().all(func ( n ) : return E.near(n, 1.0 / 6.0)), "Bound civilization ethics are population-independent shares")
|
||||
var before: Dictionary = world.state.duplicate(true)
|
||||
world.evolution.initialize_life_batches()
|
||||
check(E.eq(world.state, before), "Repeated initialization preserves ethics")
|
||||
var profiles_stable: bool = true
|
||||
for ethic_id in before.ethics:
|
||||
for ethic in before.ethics[ethic_id]:
|
||||
profiles_stable = profiles_stable and E.near(world.state.ethics[ethic_id][ethic], before.ethics[ethic_id][ethic])
|
||||
check(profiles_stable, "Repeated initialization preserves ethics")
|
||||
world.state.ethics["C0.2"] = {"m": 8, "l": 0, "w": 0, "d": 0, "c": 0, "b": 0}
|
||||
world.evolution.initialize_life_batches()
|
||||
check(E.eq(world.state.ethics["C0.2"].m, 8), "Existing unequal distribution is retained")
|
||||
check(E.eq(world.state.ethics["C0.2"].m, 1.0), "Existing unequal distribution is retained")
|
||||
world.state.amount["C0.2"] = 3
|
||||
world.evolution.initialize_life_batches()
|
||||
check(E.eq(world.state.ethics["C0.2"].m, 3), "Losses reduce ethics without inventing population")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Ethics state validates")
|
||||
check(E.eq(world.state.ethics["C0.2"].m, 1.0), "Losses retain normalized ethical composition")
|
||||
check(E.eq(world.ledger.physical_amounts(world.state)["C0.2"], 3), "Exact population remains in the object ledger")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 6, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Ethics state validates")
|
||||
var restored = fixture()
|
||||
restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
|
||||
restored.evolution.initialize_life_batches()
|
||||
@@ -74,7 +79,8 @@ func run() -> void:
|
||||
row.colonization = "0"
|
||||
world.state.binds.RD = {"C1.9": 10}
|
||||
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")
|
||||
dispose(world)
|
||||
var capacity = load("res://world/owniverse/civilization_state.gd")
|
||||
@@ -117,7 +123,7 @@ func run() -> void:
|
||||
world.evolution.initialize_life_batches()
|
||||
check(E.eq(world.state.amount["C4.2"], 2), "SMBH capacity uses stellar mass equivalent")
|
||||
check(E.eq(world.ledger.physical_amounts(world.state)["C1.9"], 1), "Unsupported civilization degrades through missing habitat families")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Capacity-limited state validates")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 6, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Capacity-limited state validates")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
@@ -128,7 +134,7 @@ func run() -> void:
|
||||
row.colonization = "0"
|
||||
world.state.binds.RD = {"C1.9": 5}
|
||||
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")
|
||||
dispose(world)
|
||||
print(("Civilization state: %d checks, %d failures" % [checks, failures]))
|
||||
|
||||
@@ -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.2", 0), 700), "C0.1 successor does not immediately attempt again")
|
||||
else:
|
||||
check(world.profile.civilization_transitions[1] == 1, "Ethical civilization is processed only once per birth bucket")
|
||||
check(world.profile.civilization_flow_plan[1] == 1, "Ethical civilization uses one continuous interval plan")
|
||||
var before: Dictionary = world.state.duplicate(true)
|
||||
world.evolution.evolute_to_datetime(10000)
|
||||
check(E.eq(world.state, before), "Repeating completed clock does not repeat transitions")
|
||||
var validation: String = root.get_node("SaveManager").validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state})
|
||||
var validation: String = root.get_node("SaveManager").validate_data({"format_version": 6, "app_version": "", "ui": {"current_page": 0}, "state": world.state})
|
||||
check(validation.is_empty(), "Coarse state validates: " + validation)
|
||||
world.evolution.evolute_to_datetime(20000)
|
||||
if id in ["C0.0", "C0.1"]:
|
||||
|
||||
@@ -94,7 +94,7 @@ func run():
|
||||
check(str(world.state.auto_star_deficits).length() < 500, "true", "fraction storage stays bounded")
|
||||
print("100 changing-composition cycles: %d ms" % (Time.get_ticks_msec() - started))
|
||||
var saves = root.get_node("SaveManager")
|
||||
check(saves.validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), "", "long-running state saves")
|
||||
check(saves.validate_data({"format_version": 6, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), "", "long-running state saves")
|
||||
for entity in world.catalog.entities.values(): entity.free()
|
||||
world.free()
|
||||
print("Exact math: %d checks, %d failures" % [checks, failures])
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bfgdsxoycr20e
|
||||
@@ -35,24 +35,28 @@ func run():
|
||||
direct = fixture()
|
||||
sliced = fixture()
|
||||
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(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")
|
||||
sliced.evolute_frame(0.01, self)
|
||||
var requested: Dictionary = sliced.frame_clock.duplicate()
|
||||
sliced.speed = 3
|
||||
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
|
||||
direct.evolute(1.0 / 60.0)
|
||||
while sliced.frame_busy or sliced.production_queue_busy: await process_frame
|
||||
direct.evolute(0.2)
|
||||
direct.production.produce_hs("H", 1)
|
||||
direct.speed = 3
|
||||
check(E.eq(direct.state, sliced.state), "Queued action and speed change survive publication")
|
||||
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()
|
||||
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)
|
||||
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")
|
||||
@@ -60,7 +64,7 @@ func run():
|
||||
dispose(direct)
|
||||
dispose(sliced)
|
||||
var original = fixture()
|
||||
original.evolute_frame(1.0 / 60.0, self)
|
||||
original.evolute_frame(0.2, self)
|
||||
original.request_production("H", 123, true)
|
||||
original.evolute_frame(0.01, self)
|
||||
original.speed = 3
|
||||
@@ -75,7 +79,7 @@ func run():
|
||||
expected.state = published.duplicate(true)
|
||||
resumed.restore_frame_checkpoint(checkpoint)
|
||||
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.production.produce_hs("H", 123)
|
||||
expected.speed = 3
|
||||
@@ -84,7 +88,7 @@ func run():
|
||||
dispose(resumed)
|
||||
dispose(expected)
|
||||
var cancelled = fixture()
|
||||
cancelled.evolute_frame(0.133333333333, self)
|
||||
cancelled.evolute_frame(0.2, self)
|
||||
check(cancelled.frame_busy, "Cancellation fixture has suspended work")
|
||||
var budget_ref: RefCounted = cancelled.frame_budget
|
||||
dispose(cancelled)
|
||||
@@ -96,25 +100,40 @@ func run():
|
||||
invalid.evolute(1e308)
|
||||
check(not invalid.frame_busy and invalid.frame_clock.is_empty() and E.eq(before, invalid.state), "Overflowing elapsed float cannot enter either clock API")
|
||||
dispose(invalid)
|
||||
var adaptive = fixture()
|
||||
adaptive.frame_clock = Clock.advance(adaptive.state.time, 10.0, 6)
|
||||
var debt_clock: Dictionary = adaptive.frame_clock.duplicate(true)
|
||||
check(adaptive.update_frame_step() == 0.2, "Growing debt increases step to 0.2 seconds")
|
||||
check(adaptive.update_frame_step() == 0.5, "Large debt increases step to bounded 0.5 seconds")
|
||||
check(adaptive.update_frame_step() == 0.5 and E.eq(adaptive.frame_clock, debt_clock), "Maximum step preserves requested time")
|
||||
adaptive.frame_clock = Clock.advance(adaptive.state.time, 0.3, 6)
|
||||
check(adaptive.update_frame_step() == 0.5, "Hysteresis keeps step between thresholds")
|
||||
adaptive.frame_clock = Clock.advance(adaptive.state.time, 0.05, 6)
|
||||
check(adaptive.update_frame_step() == 0.2, "Small debt reduces step")
|
||||
check(adaptive.update_frame_step() == 0.1, "Caught-up simulation returns to base step")
|
||||
adaptive.frame_clock = Clock.advance(adaptive.state.time, 0.15, 6)
|
||||
adaptive._frame_cost_ratio = 1.1
|
||||
check(adaptive.update_frame_step() == 0.2, "Measured slow transactions increase step before two-step debt")
|
||||
adaptive.frame_clock = Clock.advance(adaptive.state.time, 0.05, 6)
|
||||
adaptive.speed = 3
|
||||
check(adaptive.update_frame_step() == 1.0 / 60.0, "Low speeds retain 60 Hz scheduling")
|
||||
adaptive.speed = 6
|
||||
check(adaptive.update_frame_step() == 0.1, "Speed switch resets adaptive step")
|
||||
dispose(adaptive)
|
||||
for selected_speed in range(1, 7):
|
||||
var scheduled = fixture()
|
||||
scheduled.speed = selected_speed
|
||||
scheduled.evolute_frame(0.1, self)
|
||||
check(not scheduled.frame_busy and scheduled.frame_commits == 0, "Wait for a full step at speed %d" % selected_speed)
|
||||
scheduled.evolute_frame(0.1, self)
|
||||
while scheduled.frame_busy: await process_frame
|
||||
var cadence_clock: Dictionary = Clock.advance({"datetime": 0, "year": 0}, 0.2, selected_speed)
|
||||
check(scheduled.frame_commits == 1 and E.eq(scheduled.state.time, cadence_clock), "One complete 0.2 second cycle at speed %d" % selected_speed)
|
||||
scheduled.evolute_frame(0.0, self)
|
||||
check(not scheduled.frame_busy and scheduled.frame_commits == 1, "No extra cycle without elapsed time")
|
||||
dispose(scheduled)
|
||||
var behind = fixture()
|
||||
behind.evolute_frame(1.0, self)
|
||||
while behind.frame_busy: await process_frame
|
||||
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")
|
||||
behind.evolute_frame(0.0, self)
|
||||
check(behind.frame_commits == 1 and not behind.frame_busy, "Consumed interval cannot be processed twice")
|
||||
var next_interval: float = behind.frame_step_seconds + 0.03
|
||||
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])
|
||||
quit(1 if failures else 0)
|
||||
|
||||
+4
-4
@@ -46,7 +46,7 @@ func run() -> void:
|
||||
var before: Dictionary = world.state.duplicate(true)
|
||||
world.evolution.initialize_life_batches()
|
||||
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.RP, 100), "HP degradation produces RP")
|
||||
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")
|
||||
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
||||
check(E.eq(E.add(E.add(physical.HP, physical.EON1), physical.RP), 40000), "Life conserves planets")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Life save validates")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 6, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Life save validates")
|
||||
var restored = fixture()
|
||||
restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
|
||||
world.evolution.evolve_batches(world.state, 11000000)
|
||||
@@ -69,7 +69,7 @@ func run() -> void:
|
||||
# Test host preservation with a fixed rate, independent of game balance.
|
||||
world.catalog.entities.EON5.life_rule.progress = "0.00025"
|
||||
world.evolution.initialize_life_batches()
|
||||
world.evolution.evolve_batches(world.state, E.add(500000, E.divide(500000, 40)))
|
||||
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.amount["C0.0"], 0), "Life transition keeps planet bound")
|
||||
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)
|
||||
check(E.eq(world.state.binds.RD["C0.2"], 700), "C0.1 reaches ethical civilization without losing host")
|
||||
check(E.eq(world.state.binds.RD["C0.0"], 1000), "C0.1 degradation follows Unity")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Extended life save validates")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 6, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Extended life save validates")
|
||||
dispose(world)
|
||||
print(("Life: %d checks, %d failures" % [checks, failures]))
|
||||
quit((0 if E.eq(failures, 0) else 1))
|
||||
|
||||
@@ -17,7 +17,7 @@ func run() -> void:
|
||||
var World = load("res://world/owniverse/owniverse.gd")
|
||||
var first = 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 = []
|
||||
for name in names:
|
||||
check(E.ne(first.get(name), second.get(name)), E.add("World has its own ", name))
|
||||
|
||||
@@ -59,14 +59,27 @@ func run() -> void:
|
||||
world.state.auto_resources = {"H": 10}
|
||||
world.state.batches.H = [{"amount": 10, "start_year": 1, "end_year": 2}]
|
||||
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")
|
||||
world.state.amount.S = 10
|
||||
physical = world.ledger.physical_amounts(world.state)
|
||||
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.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")
|
||||
|
||||
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]))
|
||||
dispose(world)
|
||||
quit((1 if failures else 0))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
extends SceneTree
|
||||
const E = preload("res://world/owniverse/math/exact.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")
|
||||
var checks: int = 0
|
||||
var failures: int = 0
|
||||
@@ -11,52 +12,36 @@ func check(value: bool, message: String):
|
||||
failures += 1
|
||||
push_error(message)
|
||||
|
||||
func reference_drift(target: Dictionary, pop: Dictionary, rows: Dictionary, diplomacy: bool, colony: bool) -> Dictionary:
|
||||
var weights: Dictionary = {}
|
||||
for other in Civ.ETHICS: weights[other] = 0
|
||||
for ethic in pop:
|
||||
var contribution: Variant = E.multiply(pop[ethic], E.of(rows[ethic].diplomacy_rate) if diplomacy else 1)
|
||||
if E.eq(contribution, 0): continue
|
||||
for other in Civ.ETHICS:
|
||||
var weight: int = 9 if colony else 1
|
||||
if other == ethic: weight = 960 if colony else 196
|
||||
elif other == Civ.OPPOSITE[ethic]: weight = 4 if colony else 0
|
||||
weights[other] = E.add(weights[other], E.multiply(contribution, weight))
|
||||
if E.le(Civ.total(weights), 0): return pop.duplicate()
|
||||
return Civ.split(target, "test", Civ.total(pop), weights)
|
||||
|
||||
func run():
|
||||
var rows: Dictionary = {}
|
||||
for ethic in Civ.ETHICS: rows[ethic] = {"diplomacy_rate": "0.125" if ethic == "m" else "1.75"}
|
||||
for count in [0, 1, 7, 999999999, E.of("1e40")]:
|
||||
for diplomacy in [false, true]:
|
||||
for colony in [false, true]:
|
||||
var actual: Dictionary = {}
|
||||
var reference: Dictionary = {}
|
||||
for round_index in 5:
|
||||
var pop: Dictionary = {"m": count, "l": round_index, "w": 9, "d": 0, "c": 137, "b": 2}
|
||||
var result: Dictionary = Civ.drift(actual, "test", pop, rows, diplomacy, colony)
|
||||
if diplomacy:
|
||||
check(E.eq(result, reference_drift(reference, pop, rows, diplomacy, colony)), "Diplomatic drift preserves allocation")
|
||||
var carries_valid: bool = true
|
||||
for value in actual.get("civilization_remainders", {}).values():
|
||||
carries_valid = carries_valid and E.ge(value, -1) and E.le(value, 1)
|
||||
check(carries_valid, "Diplomatic drift carries stay bounded")
|
||||
else:
|
||||
check(E.eq(Civ.total(result), Civ.total(pop)), "Integer flow drift conserves population")
|
||||
var valid_whole: bool = true
|
||||
for ethic in Civ.ETHICS:
|
||||
valid_whole = valid_whole and E.ge(result[ethic], 0) and E.eq(result[ethic], E.floor_value(result[ethic]))
|
||||
check(valid_whole, "Integer flow drift has nonnegative whole counts")
|
||||
var decimal_rows: Dictionary = {}
|
||||
for ethic in Civ.ETHICS: decimal_rows[ethic] = {"diplomacy_rate": "0.333333333333" if ethic == "m" else "0.017"}
|
||||
var decimal_target: Dictionary = {}
|
||||
for count in [0, 1, 7, 999999999, E.of("1e40"), E.of("1e400")]:
|
||||
var carry: Dictionary = {}
|
||||
for round_index in 20:
|
||||
var pop: Dictionary = {"m": E.of("1e40"), "l": round_index, "w": 17, "d": 4, "c": 91, "b": 2}
|
||||
var result: Dictionary = Civ.drift(decimal_target, "decimal", pop, decimal_rows, true)
|
||||
check(E.eq(Civ.total(result), Civ.total(pop)), "Decimal diplomatic drift preserves whole population")
|
||||
for ethic in result:
|
||||
check(E.ge(result[ethic], 0) and E.eq(result[ethic], E.floor_value(result[ethic])), "Decimal drift returns nonnegative whole counts")
|
||||
var weights: Dictionary = {"m": round_index + 1.0, "l": 0.0, "w": 9.0, "d": 4.0, "c": 137.0, "b": 2.0}
|
||||
var profile: Dictionary = Civ.State.normalize_profile(weights)
|
||||
var shares: Array = []
|
||||
for ethic in Civ.ETHICS: shares.append(profile[ethic])
|
||||
var result: Dictionary = Q.split_profile(carry, "profile", count, Civ.ETHICS, shares)
|
||||
check(E.eq(Q.sum(result), count), "Profile quotas conserve arbitrary-size whole totals")
|
||||
for value in result.values(): check(E.ge(value, 0) and E.eq(value, E.floor_value(value)), "Profile quotas stay nonnegative whole")
|
||||
for value in carry.values(): check(is_finite(value) and value >= -1.0 and value <= 1.0, "Signed profile carries stay bounded")
|
||||
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")
|
||||
for count in [1, 101, E.of("1e40"), E.of("1e400")]:
|
||||
var stocks: Dictionary = {"a": count, "b": E.add(count, 1), "c": 0}
|
||||
var total: Variant = Q.sum(stocks)
|
||||
for limit in [0, count, E.subtract(total, 1), total]:
|
||||
var allocated: Dictionary = Q.allocate_profiled(stocks, limit)
|
||||
check(E.eq(Q.sum(allocated), limit), "Profiled stock allocation retains the exact granted total")
|
||||
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 probability in [0.1, 1.0 / 3.0, 1.23456789]:
|
||||
var carries: Dictionary = {}
|
||||
var old_carry: Variant = 0
|
||||
for iteration in 20:
|
||||
var expected: Variant = E.add(E.multiply(count, probability), old_carry)
|
||||
var whole: Variant = E.floor_value(expected)
|
||||
old_carry = E.maximum(0, E.subtract(expected, whole))
|
||||
var actual: Variant = Q.accumulate_rate(carries, "rate", count, probability)
|
||||
check(E.eq(actual, whole), "Fast float carries preserve the whole result of rate accumulation")
|
||||
check(E.near(carries.rate, old_carry), "Fast float carries preserve the fractional remainder")
|
||||
for speed in [1, 4, 6]:
|
||||
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
|
||||
@@ -70,39 +55,53 @@ func run():
|
||||
var row: Dictionary = rules[id].ethics.m
|
||||
var p: Variant = E.maximum(0, E.multiply(E.of(row.progress), 2))
|
||||
var d: Variant = E.maximum(0, E.of(row.degradation))
|
||||
check(E.eq(changed.m.pro, E.divide(p, E.maximum(1, E.add(p, d)))), "Coefficient cache follows modifiers")
|
||||
check(E.near(changed.m.pro, E.divide(p, E.maximum(1, E.add(p, d)))), "Coefficient cache follows modifiers")
|
||||
modifiers.progress = 1
|
||||
check(E.eq(Civ.coefficients(world, rules, id, modifiers), original), "Coefficient cache restores original rates")
|
||||
var local_rules: Dictionary = rules.duplicate(true)
|
||||
local_rules[id].ethics.m.progress = "0.375"
|
||||
changed = Civ.coefficients(world, local_rules, id, modifiers)
|
||||
check(E.eq(changed.m.pro, E.divide(E.of("0.375"), E.maximum(1, E.add(E.of("0.375"), d)))), "Coefficient cache follows rule edits")
|
||||
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": {}}
|
||||
target.amount = {}
|
||||
target.binds = {}
|
||||
target.stats = {"total_amount": {}}
|
||||
target.amount[id] = 60
|
||||
target.stats.total_amount[id] = 60
|
||||
Civ.State.reconcile(world, target, rules, {id: 60})
|
||||
target.ethics[id].m = 100
|
||||
target.ethics[id] = {"m": 1.0, "l": 0.0, "w": 0.0, "d": 0.0, "c": 0.0, "b": 0.0}
|
||||
Civ.State.reconcile(world, target, rules, {id: 60})
|
||||
check(E.eq(Civ.total(target.ethics[id]), 60), "Reconcile cache detects changed ethics")
|
||||
check(E.eq(target.ethics[id].m, 1.0), "Reconcile cache detects changed ethics")
|
||||
Civ.State.reconcile(world, target, rules, {id: 120})
|
||||
check(E.eq(Civ.total(target.ethics[id]), 120), "Reconcile cache detects changed population")
|
||||
check(E.eq(target.ethics[id].m, 1.0), "Reconcile cache detects changed population")
|
||||
target.amount[id] = 120
|
||||
Civ.worship(world, target, rules)
|
||||
target.ethics[id].m = E.add(target.ethics[id].m, 5)
|
||||
var expected: Variant = 0
|
||||
target.ethics[id].m = 0.5
|
||||
target.ethics[id].l = 0.5
|
||||
var expected: float = 0.0
|
||||
for stage in rules:
|
||||
if rules[stage].worships:
|
||||
for ethic in target.ethics[stage]:
|
||||
expected = E.add(expected, E.multiply(target.ethics[stage][ethic], E.of(rules[stage].ethics[ethic].worship)))
|
||||
check(E.eq(Civ.worship(world, target, rules), expected), "Worship cache detects changed ethics")
|
||||
var n: float = (0.0 + target.ethics[stage][ethic]) * E.approx(E.of(rules[stage].ethics[ethic].worship))
|
||||
n *= E.approx(target.amount.get(stage, 0))
|
||||
expected += n
|
||||
check(E.near(Civ.worship(world, target, rules), expected), "Worship cache detects changed ethics")
|
||||
var worship_rules: Dictionary = rules.duplicate(true)
|
||||
for stage in worship_rules:
|
||||
if worship_rules[stage].worships:
|
||||
worship_rules[stage].ethics.m.worship = "0.375"
|
||||
expected = 0
|
||||
expected = 0.0
|
||||
for stage in worship_rules:
|
||||
if worship_rules[stage].worships:
|
||||
for ethic in target.ethics[stage]:
|
||||
expected = E.add(expected, E.multiply(target.ethics[stage][ethic], E.of(worship_rules[stage].ethics[ethic].worship)))
|
||||
check(E.eq(Civ.worship(world, target, worship_rules), expected), "Worship cache detects changed rules")
|
||||
var n: float = (0.0 + target.ethics[stage][ethic]) * E.approx(E.of(worship_rules[stage].ethics[ethic].worship))
|
||||
n *= E.approx(target.amount.get(stage, 0))
|
||||
expected += n
|
||||
check(E.near(Civ.worship(world, target, worship_rules), expected), "Worship cache detects changed rules")
|
||||
world.ledger = load("res://tests/fixtures/audit_rounding_ledger.gd").new(world)
|
||||
# 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
|
||||
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)
|
||||
|
||||
@@ -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")
|
||||
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.RD = 4
|
||||
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.amount.HP, 0) and E.eq(world.state.stats.total_amount.HP, 1)), "Created planets are bound, counted once")
|
||||
var saves: Node = root.get_node("SaveManager")
|
||||
check(E.eq(saves.validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Planet carry save validates")
|
||||
check(E.eq(saves.validate_data({"format_version": 6, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Planet carry save validates")
|
||||
restored = fixture()
|
||||
restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
|
||||
world.stellar.create_planets(world.state, "RD", 37, 0, 0, E.of("0.7"))
|
||||
@@ -118,7 +118,7 @@ func run() -> void:
|
||||
check((E.eq(world.state.amount.RP, 0) and E.eq(world.state.amount["C1.3"], 0)), "Inherited planets never become free")
|
||||
world.evolution.evolve_batches(world.state, E.add(lifespan, width))
|
||||
check((E.eq(world.state.binds.RD["C1.3"], 0) and E.eq(world.state.binds.TWD["C1.3"], 4)), "No planets left at extinct parent type")
|
||||
check(E.eq(saves.validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Inherited state validates")
|
||||
check(E.eq(saves.validate_data({"format_version": 6, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Inherited state validates")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
@@ -158,7 +158,7 @@ func run() -> void:
|
||||
check((E.eq(world.state.amount.SN, 0) and E.eq(world.state.amount.LNS, 1)), "Supernova produces a remnant")
|
||||
check(((E.eq(world.state.amount.H, 8) and E.eq(world.state.amount.He, 50)) and E.eq(world.state.amount.Met, 11)), "Supernova returns table resources")
|
||||
check((not world.state.amount.has("blast")), "Supernova dispatches blast output")
|
||||
check(E.eq(saves.validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Blast state validates")
|
||||
check(E.eq(saves.validate_data({"format_version": 6, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Blast state validates")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
|
||||
+2
-2
@@ -51,10 +51,10 @@ func run() -> void:
|
||||
for invalid in [0, E.neg(1), NAN, INF, 1e308]:
|
||||
world.evolute(invalid)
|
||||
check(E.eq(world.state, before), "Invalid/overflow interval rejected")
|
||||
check(E.eq(saves.validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Fractional save is valid")
|
||||
check(E.eq(saves.validate_data({"format_version": 6, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Fractional save is valid")
|
||||
var invalid_save: Dictionary = world.state.duplicate(true)
|
||||
invalid_save.time.year = E.of("0.25")
|
||||
check(E.ne(saves.validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": invalid_save}), ""), "Reject inconsistent fractional clock")
|
||||
check(E.ne(saves.validate_data({"format_version": 6, "app_version": "", "ui": {"current_page": 0}, "state": invalid_save}), ""), "Reject inconsistent fractional clock")
|
||||
dispose(world)
|
||||
|
||||
for fps in [30, 60, 144]:
|
||||
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://brb04x12elvqt
|
||||
@@ -1,7 +1,6 @@
|
||||
extends SceneTree
|
||||
const E = preload("res://world/owniverse/math/exact.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 Descriptions = preload("res://world/owniverse/item_description.gd")
|
||||
@@ -91,24 +90,24 @@ func run() -> void:
|
||||
|
||||
world = fixture()
|
||||
world.state.amount["C0.2"] = 1000000
|
||||
world.state.ethics["C0.2"] = {"m": 1000000}
|
||||
world.state.ethics["C0.2"] = {"m": 1.0, "l": 0.0, "w": 0.0, "d": 0.0, "c": 0.0, "b": 0.0}
|
||||
var baseline: Dictionary = world.state.duplicate(true)
|
||||
var advanced: Dictionary = baseline.duplicate(true)
|
||||
advanced.world_parameters.Agg = 19
|
||||
var next_stage: String = global.CivilizationRules["C0.2"].next
|
||||
var outcomes: Array = []
|
||||
for target in [baseline, advanced]:
|
||||
var expired: Dictionary = {"C0.2": [{"amount": 1000000, "from": 0, "to": 1}]}
|
||||
var budget: Dictionary = CivilizationState.stellar_budget(world, target, global.CivilizationRules)
|
||||
CivilizationEvolution.prepare(world, target, expired, 1, budget, global.CivilizationRules)
|
||||
outcomes.append(expired["C0.2"][0].products.get(next_stage, 0))
|
||||
world.civilization_flow.evolve(target, 0, 2000)
|
||||
outcomes.append(target.amount.get(next_stage, 0))
|
||||
check(E.gt(outcomes[1], outcomes[0]), "Aggression changes actual ethical civilization transitions")
|
||||
dispose(world)
|
||||
|
||||
# Trends use ethical proportions and stage weights, not population magnitude.
|
||||
world = fixture()
|
||||
var rules: Dictionary = {"C2.0": {"home": "DwStar"}, "C2.1": {"home": "DwStar"}}
|
||||
world.state.ethics = {"C2.0": {"d": 100}, "C2.1": {"d": 100}}
|
||||
world.state.ethics = {"C2.0": {"m": 0.0, "l": 0.0, "w": 0.0, "d": 1.0, "c": 0.0, "b": 0.0}, "C2.1": {"m": 0.0, "l": 0.0, "w": 0.0, "d": 1.0, "c": 0.0, "b": 0.0}}
|
||||
world.state.amount["C2.1"] = 10
|
||||
world.state.binds["RD"] = {"C2.0": 10}
|
||||
var source: Dictionary = world.state.duplicate(true)
|
||||
var single: Dictionary = source.duplicate(true)
|
||||
var split: Dictionary = source.duplicate(true)
|
||||
@@ -117,10 +116,17 @@ func run() -> void:
|
||||
check(E.near(single.trends.Integration, E.of("0.018")), "Trend integrates elapsed years and doubled stage weights")
|
||||
for key in Parameters.TRENDS:
|
||||
check(E.near(single.trends[key], split.trends[key]), E.add("Trend is independent of frame partition: ", key))
|
||||
source.ethics["C2.0"].d = 1000000
|
||||
source.ethics["C2.0"].d = 1.0
|
||||
var larger: Dictionary = world.state.duplicate(true)
|
||||
larger.amount["C2.1"] = 1000000
|
||||
larger.binds["RD"]["C2.0"] = 1000000
|
||||
Parameters.advance(source, larger, rules, 1000000)
|
||||
check(E.near(larger.trends.Integration, single.trends.Integration), "Population proportions determine trend influence")
|
||||
var vacant: Dictionary = world.state.duplicate(true)
|
||||
vacant.amount["C2.1"] = 0
|
||||
vacant.binds["RD"]["C2.0"] = 0
|
||||
Parameters.advance(vacant, vacant, rules, 1000000)
|
||||
check(vacant.trends.values().all(func (value): return E.eq(value, 0)), "Empty saved ethic profiles do not generate trends")
|
||||
world.state.trends.Integration = E.of("0.99")
|
||||
Parameters.advance(source, world.state, rules, 100000000)
|
||||
check(E.eq(world.state.civilization_stats["trend:Integration"], 2), "Long steps process every crossed threshold")
|
||||
@@ -133,7 +139,7 @@ func run() -> void:
|
||||
Parameters.advance(source, world.state, rules, E.of("1.0e12"))
|
||||
check((E.ge(world.state.trends.Integration, 0) and E.lt(world.state.trends.Integration, 1)), "Very large intervals finish with a valid trend fraction")
|
||||
check(E.ge(world.state.world_parameters.Agg, 1), "Repeated integration events cannot reduce aggression below the scale")
|
||||
var payload: Dictionary = {"format_version": 5, "app_version": "test", "ui": {"current_page": 1}, "state": world.state}
|
||||
var payload: Dictionary = {"format_version": 6, "app_version": "test", "ui": {"current_page": 1}, "state": world.state}
|
||||
var manager: Node = root.get_node("SaveManager")
|
||||
check(E.eq(manager.validate_data(payload), ""), "New parameters and trends pass save validation")
|
||||
var old: Dictionary = payload.duplicate(true)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://b6sqyd1tpljr3
|
||||
@@ -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.
@@ -58,6 +58,13 @@ func _set_action_values(values: Array, displayed: Array) -> void:
|
||||
action_labels[ind].text = text
|
||||
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:
|
||||
if ((E.lt(value, 1) or (not E.finite(value))) or E.ne(value, E.floor_value(value))):
|
||||
|
||||
BIN
Binary file not shown.
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://v8h4ov33opyf
|
||||
BIN
Binary file not shown.
@@ -35,7 +35,7 @@ Stage обращается непосредственно к `world.presentation
|
||||
| `automatic_production.gd` / `automatic` | Выпуск за период и жизнь новорождённых объектов, разделение ресурсов на обычные и автоматические, энергия, квоты звёзд и бурых карликов |
|
||||
| `stellar_rules.gd` / `stellar` | Продукты звёздной эволюции, создание планет, выбор звёздных преемников, два уровня разрушения и глобальные бласты |
|
||||
| `life_evolution.gd` / `life` | Прогресс, деградация и повторная попытка планетарной жизни |
|
||||
| `civilization_evolution.gd`, `civilization_state.gd` | Существующие правила цивилизаций: этики, войны, колонизация, лимиты и места обитания |
|
||||
| `civilization_flow.gd`, `civilization_state.gd` | Непрерывные потоки цивилизаций, доли этик, целые квоты, лимиты и места обитания |
|
||||
| `universe_environment.gd` / `environment` | Плотность, металличность, доля попаданий бластов, изменение параметров мира с сохранением возраста звёзд |
|
||||
| `evolution_coordinator.gd` / `evolution` | Порядок фаз шага, последовательные поколения и публикация нового состояния |
|
||||
|
||||
@@ -50,7 +50,7 @@ Stage обращается непосредственно к `world.presentation
|
||||
|
||||
1. Определить обработанную десятую года. При продвижении времени восстановить недостающие партии жизни и проверить вместимость.
|
||||
2. Скопировать состояние, посчитать физические количества и производство за период.
|
||||
3. Обработать поколения до конца интервала. Для каждого поколения: локальные разрушения → выбытие партий всех типов → план переходов → резервирование передачи привязок → зачисление преемников и их партий → производство новорождённых → применение привязок и округление → завершение переходов цивилизаций → глобальные бласты → проверка вместимости.
|
||||
3. Интегрировать непрерывные потоки цивилизаций за весь интервал, затем обработать поколения звёзд и биологической жизни до конца интервала. Для каждого поколения: локальные разрушения → выбытие партий всех типов → план переходов → резервирование передачи привязок → зачисление преемников и их партий → производство новорождённых → применение привязок и округление → глобальные бласты → проверка вместимости.
|
||||
4. Обновить групповые количества, ограничить автоматические ресурсы и продвинуть параметры мира.
|
||||
5. Опубликовать новое состояние и обновить представление количеств.
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
if E.gt(target.get("apocalypse", {}).get("phase", 0), 0): return
|
||||
var basic: Dictionary = {}
|
||||
var automatic: Dictionary = {}
|
||||
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.
|
||||
var candidates: Dictionary = {}
|
||||
var helium_share: Variant = world.production.stellar_helium_share(previous)
|
||||
for id in distribution:
|
||||
var entity: OwniverseEntity = world.catalog.entities[id]
|
||||
if E.ne(entity.group_as, "Star"):
|
||||
@@ -87,11 +89,16 @@ func produce_for_period(previous: Dictionary, target: Dictionary, quantities: Di
|
||||
return
|
||||
var costs: Dictionary = {}
|
||||
var needs: Dictionary = {}
|
||||
var stellar_hydrogen_cost: Variant = 0
|
||||
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
|
||||
for resource in entity.cost:
|
||||
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
|
||||
for resource in world.catalog.entities[id].cost:
|
||||
var cost: Variant = E.multiply(E.maximum(0, world.catalog.entities[id].cost[resource]), candidates[id])
|
||||
costs[resource] = E.add(costs.get(resource, 0), cost)
|
||||
needs[resource] = E.add(needs.get(resource, 0), cost)
|
||||
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:
|
||||
if E.gt(costs[resource], 0):
|
||||
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:
|
||||
if E.gt(needs[resource], 0):
|
||||
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()
|
||||
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 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
|
||||
for id in order:
|
||||
var entity: OwniverseEntity = world.catalog.entities[id]
|
||||
@@ -125,10 +136,10 @@ func produce_for_period(previous: Dictionary, target: Dictionary, quantities: Di
|
||||
continue
|
||||
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 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):
|
||||
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
|
||||
budget = ordinary_star_budget(target)
|
||||
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))
|
||||
for id in created:
|
||||
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)
|
||||
|
||||
|
||||
@@ -182,11 +194,16 @@ func ordinary_star_budget(target: Dictionary) -> Dictionary:
|
||||
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 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:
|
||||
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
|
||||
if E.gt(cost, 0):
|
||||
capacity = E.minimum(capacity, E.floor_value(E.divide(budget.get(resource, 0), cost)))
|
||||
|
||||
@@ -1,205 +1,27 @@
|
||||
extends RefCounted
|
||||
## Shared civilization coefficients and worship derived from float profiles.
|
||||
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 WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
||||
const ETHICS = State.ETHICS
|
||||
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:
|
||||
return QuantityMath.sum(pop)
|
||||
|
||||
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:
|
||||
# Cache the weighted float worship rate multiplied by the exact population.
|
||||
static func worship(world, target: Dictionary, rules: Dictionary, physical: Dictionary = {}) -> Variant:
|
||||
var result: Variant = 0
|
||||
if physical.is_empty(): physical = world.ledger.physical_amounts(target)
|
||||
for id in rules:
|
||||
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 = {}
|
||||
for ethic in pop: coefficients[ethic] = rules[id].ethics[ethic].worship
|
||||
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 products: Dictionary = {}
|
||||
for ethic in pop: products[ethic] = E.multiply(pop[ethic], E.of(coefficients[ethic]))
|
||||
cached = {"population": pop.duplicate(), "coefficients": coefficients, "products": products}
|
||||
var count: Variant = physical.get(id, 0)
|
||||
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):
|
||||
var worship_rate: float = 0.0
|
||||
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
|
||||
for ethic in pop: result = E.add(result, cached.products[ethic])
|
||||
result = E.add(result, cached.product)
|
||||
return result
|
||||
|
||||
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 = {}
|
||||
for ethic in rules[id].ethics:
|
||||
var row: Dictionary = rules[id].ethics[ethic]
|
||||
var p: Variant = E.maximum(0, E.multiply(E.of(row.progress), modifiers.progress))
|
||||
var d: Variant = E.maximum(0, E.multiply(E.of(row.degradation), modifiers.degradation))
|
||||
var scale: Variant = E.maximum(1, E.add(p, d))
|
||||
rates[ethic] = {"pro": E.divide(p, scale), "deg": E.divide(d, scale),
|
||||
"war": E.minimum(1, E.multiply(E.of(row.war), modifiers.war)),
|
||||
"col": E.minimum(1, E.multiply(E.of(row.colonization), modifiers.colonization))}
|
||||
var p: float = maxf(0.0, E.approx(E.of(row.progress)) * E.approx(modifiers.progress))
|
||||
var d: float = maxf(0.0, E.approx(E.of(row.degradation)) * E.approx(modifiers.degradation))
|
||||
var scale: float = maxf(1.0, p + d)
|
||||
rates[ethic] = {"pro": p / scale, "deg": d / scale,
|
||||
"war": clampf(E.approx(E.of(row.war)) * E.approx(modifiers.war), 0.0, 1.0),
|
||||
"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}
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://clon18ixi6yqx
|
||||
@@ -5,11 +5,46 @@ const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
|
||||
|
||||
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:
|
||||
return (rules.has(id) and E.ne(str(rules[id].home), ""))
|
||||
|
||||
# Preserve any existing ethics. New populations start equally distributed;
|
||||
# reductions (including blasts) are proportional and never leave negative counts.
|
||||
# Keep one six-value float profile per stage. Absolute population remains in
|
||||
# the exact physical object ledger; flow calculations use shares directly.
|
||||
static func reconcile(world, target: Dictionary, rules: Dictionary, physical: Dictionary = {}, only_ids: Array = []) -> void:
|
||||
if (not target.has("ethics")):
|
||||
target.ethics = {}
|
||||
@@ -19,22 +54,10 @@ static func reconcile(world, target: Dictionary, rules: Dictionary, physical: Di
|
||||
for id in ids:
|
||||
if not rules.has(id): continue
|
||||
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, {})
|
||||
if not cached.is_empty() and E.eq(cached.count, count) and E.eq(cached.population, current):
|
||||
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
|
||||
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
|
||||
# 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()
|
||||
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)
|
||||
var ethical_totals: Dictionary = physical.duplicate()
|
||||
var budget: Dictionary = stellar_budget(world, target, rules, physical)
|
||||
var remaining: Dictionary = budget.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")
|
||||
world.batches.scale_batches(target, id, E.divide(supported, count))
|
||||
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)
|
||||
var removed: Dictionary = QuantityMath.allocate_whole(target.ethics[id], excess)
|
||||
if (not target.ethics.has(previous)):
|
||||
target.ethics[previous] = {}
|
||||
for ethic in ETHICS:
|
||||
target.ethics[id][ethic] = E.subtract(target.ethics[id][ethic], removed[ethic])
|
||||
target.ethics[previous][ethic] = E.add(target.ethics[previous].get(ethic, 0), removed[ethic])
|
||||
var previous_count: Variant = ethical_totals.get(previous, physical.get(previous, target.amount.get(previous, 0)))
|
||||
var previous_profile: Dictionary = target.ethics.get(previous, empty_profile())
|
||||
var source_profile: Dictionary = target.ethics.get(id, empty_profile())
|
||||
target.ethics[previous] = merge_profiles(previous_profile, previous_count, source_profile, excess)
|
||||
ethical_totals[id] = supported
|
||||
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"):
|
||||
var stars: Dictionary = {}
|
||||
var current_physical: Dictionary = world.ledger.physical_amounts(target)
|
||||
var current_physical: Dictionary = physical
|
||||
for star in current_physical:
|
||||
if (E.eq(world.catalog.entities[star].event_group, "DwStar") and E.gt(current_physical[star], 0)):
|
||||
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.amount[previous] = E.subtract(target.amount[previous], amount)
|
||||
if changed:
|
||||
physical = world.ledger.physical_amounts(target)
|
||||
reconcile(world, target, rules, physical)
|
||||
assign_homes(world, target, rules, physical)
|
||||
world.profile_end("enforce_capacity", profile_started)
|
||||
return physical
|
||||
|
||||
# Unity's structure chain: direct Claim resets the required structure count;
|
||||
# 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)
|
||||
|
||||
|
||||
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
|
||||
var base: Variant = rules[id].span
|
||||
if (not is_space_civilization(id, rules)): return base
|
||||
var population: Variant = target.amount.get(id, 0)
|
||||
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
|
||||
var weighted: Variant = population
|
||||
var multiplier: float = 1.0
|
||||
for home in homes:
|
||||
weighted = E.add(weighted, E.multiply(homes[home], (E.subtract(E.maximum(0, world.catalog.entities[home].civ_bonus), 1))))
|
||||
return (E.mul_div(base, population, weighted) if E.gt(weighted, 0) else base)
|
||||
multiplier += E.ratio_approx(homes[home], population) * (maxf(0.0, E.approx(world.catalog.entities[home].civ_bonus)) - 1.0)
|
||||
return E.approx(base) / multiplier if multiplier > 0.0 else base
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
extends "res://world/owniverse/simulation_module.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 SimulationTime = preload("res://world/owniverse/simulation_time.gd")
|
||||
const WorldParameters = preload("res://world/owniverse/world_parameters.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.enforce_capacity(world, world.state, Global.CivilizationRules, world.state.time.datetime)
|
||||
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
||||
var physical: Dictionary = CivilizationState.enforce_capacity(world, world.state, Global.CivilizationRules, world.state.time.datetime)
|
||||
for id in world.catalog.entities:
|
||||
if world.catalog.entities[id].life_rule.is_empty():
|
||||
continue
|
||||
@@ -17,6 +18,7 @@ func initialize_life_batches() -> void:
|
||||
for batch : Dictionary in world.state.batches.get(id, []):
|
||||
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)
|
||||
return physical
|
||||
|
||||
|
||||
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:
|
||||
var _keep_alive: RefCounted = self
|
||||
var adaptive_started: int = world.adaptive_timer_usec()
|
||||
# Reconcile only when simulation advances.
|
||||
# Fractional frame updates cannot change capacity or populations.
|
||||
var processed_year: Variant = SimulationTime.processed_year(datetime)
|
||||
var quantities: Dictionary = {}
|
||||
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()
|
||||
initialize_life_batches()
|
||||
quantities = initialize_life_batches()
|
||||
world.profile_end("initialize", profile_initialize)
|
||||
var previous: Dictionary = world.state
|
||||
if budget != null: budget.call("set_phase", "deep_copy_and_coarsen")
|
||||
var next: Dictionary = previous.duplicate_deep()
|
||||
next.version = SimulationTime.STATE_VERSION
|
||||
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 to_year: Variant = processed_year
|
||||
if E.gt(to_year, from_year):
|
||||
var quantities: Dictionary = world.ledger.physical_amounts(previous)
|
||||
var distribution: Dictionary = QuantityMath.normalize_distribution(quantities)
|
||||
next.distribution = distribution
|
||||
|
||||
var profile_production: int = world.profile_start()
|
||||
world.automatic.produce_for_period(previous, next, quantities, distribution, from_year, to_year)
|
||||
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()
|
||||
if budget != null:
|
||||
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()
|
||||
WorldParameters.advance(previous, next, Global.CivilizationRules, E.subtract(to_year, from_year))
|
||||
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}
|
||||
|
||||
# Publish only after every phase has finished; previous stays untouched.
|
||||
world.state = E.bound_state(next)
|
||||
if E.gt(to_year, from_year):
|
||||
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:
|
||||
@@ -78,15 +91,24 @@ func evolve_batches(target: Dictionary, to_year: Variant, budget: RefCounted = n
|
||||
"enlightenment": WorldParameters.value(target, "Enlightenment"),
|
||||
"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):
|
||||
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
|
||||
|
||||
|
||||
func evolve_batch_generation(target: Dictionary, to_year: Variant, completed: Dictionary = { }, budget: RefCounted = null, context: Dictionary = {}) -> bool:
|
||||
var _keep_alive: RefCounted = self
|
||||
# Apply direct completions at the original next-generation boundary, so
|
||||
# binding transfers, the blast environment and capacity checks keep order.
|
||||
# Apply direct completions at their original generation boundaries, so
|
||||
# binding transfers, blast environment and capacity checks keep order.
|
||||
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()
|
||||
completed.clear()
|
||||
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 expired: 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 profile_withdraw: int = world.profile_start()
|
||||
for id in batch_ids:
|
||||
@@ -122,8 +156,8 @@ func evolve_batch_generation(target: Dictionary, to_year: Variant, completed: Di
|
||||
break
|
||||
if (not has_expired):
|
||||
return false
|
||||
# This per-generation summary is valid until the next transition mutates
|
||||
# the cohort set. Reuse it for civilization planning and transfer ratios.
|
||||
# Aggregate quantities are used to calculate the type-level transfer ratio;
|
||||
# transition products remain calculated for each expiring cohort.
|
||||
var expired_totals: Dictionary = {}
|
||||
for id in expired:
|
||||
var total: Variant = 0
|
||||
@@ -132,29 +166,28 @@ func evolve_batch_generation(target: Dictionary, to_year: Variant, completed: Di
|
||||
var fractions: Dictionary = {}
|
||||
var outputs: Dictionary = {}
|
||||
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 civilization_transaction: Dictionary = await CivilizationEvolution.prepare(world, target, expired, to_year, star_budget, Global.CivilizationRules, current_quantities, budget, expired_totals)
|
||||
if budget != null and budget.cancelled: return false
|
||||
if budget != null: budget.call("set_phase", "civilization_prepare")
|
||||
var profile_products: int = world.profile_start()
|
||||
for id in expired:
|
||||
if budget != null and not await budget.pause(): return false
|
||||
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)
|
||||
outputs[id] = {}
|
||||
var combined: Dictionary = {}
|
||||
for part : Dictionary in expired[id]:
|
||||
if (not part.has("products")):
|
||||
for part: Dictionary in expired[id]:
|
||||
if not part.has("products"):
|
||||
part.products = world.stellar.evolution_products(target, world.catalog.entities[id], part.amount, context)
|
||||
for successor in part.products.keys():
|
||||
if Global.CivilizationRules.has(id):
|
||||
continue
|
||||
if Global.CivilizationRules.has(id): continue
|
||||
var requested: Variant = part.products[successor]
|
||||
var admitted: Variant = CivilizationState.admit_transition(star_budget, Global.CivilizationRules, id, successor, requested)
|
||||
part.products[successor] = admitted
|
||||
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:
|
||||
combined[successor] = E.add(combined.get(successor, 0), part.products[successor])
|
||||
outputs[id] = {}
|
||||
if E.gt(total, 0):
|
||||
for successor in combined:
|
||||
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
|
||||
for held in target.binds.get(id, {}): affected[held] = 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 new_owners: Dictionary = {}
|
||||
var born_civilizations: Dictionary = {}
|
||||
var blast_power: Variant = 0
|
||||
for parts: Array in expired.values():
|
||||
for part: Dictionary in parts:
|
||||
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 resolution: Variant = world.batches.temporal_resolution(target)
|
||||
if budget != null: budget.call("set_phase", "transfer_and_newborns")
|
||||
for id in expired:
|
||||
for part : Dictionary in expired[id]:
|
||||
for part: Dictionary in expired[id]:
|
||||
for successor in part.products:
|
||||
if budget != null and not await budget.pause(): return false
|
||||
var amount: Variant = part.products[successor]
|
||||
if E.le(amount, 0): continue
|
||||
if E.eq(successor, "blast"):
|
||||
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)))):
|
||||
if E.le(amount, 0) or E.eq(successor, "blast") or 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)
|
||||
else:
|
||||
amount = world.ledger.credit(target, successor, amount)
|
||||
var produces: bool = not world.automatic.yearly_products(world.catalog.entities[successor]).is_empty()
|
||||
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_to: Variant = part.to
|
||||
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_from = born_to
|
||||
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.profile_end("newborn_batches", profile_newborn)
|
||||
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)
|
||||
if budget != null: budget.call("set_phase", "finish_and_capacity")
|
||||
world.ledger.bind_new_owners(target, new_owners)
|
||||
for owner in new_owners:
|
||||
for resource in world.catalog.entities[owner].bind:
|
||||
for member in world.catalog.members(resource): affected[member] = true
|
||||
world.ledger.round_object_allocations(target, affected)
|
||||
if budget != null:
|
||||
await CivilizationEvolution.finish(world, target, civilization_transaction, to_year, Global.CivilizationRules, budget)
|
||||
if budget.cancelled: return false
|
||||
else:
|
||||
CivilizationEvolution.finish(world, target, civilization_transaction, to_year, Global.CivilizationRules)
|
||||
for id in born_civilizations:
|
||||
var current: Variant = world.ledger.physical_amount(target, id)
|
||||
var born: Variant = born_civilizations[id]
|
||||
target.ethics[id] = CivilizationState.merge_profiles(target.ethics.get(id, {}), E.maximum(0, E.subtract(current, born)), CivilizationState.empty_profile(), born)
|
||||
if E.gt(blast_power, 0): world.stellar.apply_blast(target, blast_power, environment)
|
||||
CivilizationState.enforce_capacity(world, target, Global.CivilizationRules, to_year)
|
||||
return true
|
||||
|
||||
@@ -5,18 +5,32 @@ var slice_usec: int = 40000
|
||||
var deadline: int
|
||||
var cancelled: bool = false
|
||||
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):
|
||||
tree = scene_tree
|
||||
slice_usec = milliseconds * 1000
|
||||
deadline = Time.get_ticks_usec() + slice_usec
|
||||
checkpoint_usec = deadline - slice_usec
|
||||
|
||||
func set_phase(value: String) -> void:
|
||||
phase = value
|
||||
|
||||
func pause() -> bool:
|
||||
var keep_alive: RefCounted = self
|
||||
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:
|
||||
var waiting_from: int = Time.get_ticks_usec()
|
||||
await tree.process_frame
|
||||
keep_alive.waiting_usec += Time.get_ticks_usec() - waiting_from
|
||||
deadline = Time.get_ticks_usec() + slice_usec
|
||||
checkpoint_usec = Time.get_ticks_usec()
|
||||
return not keep_alive.cancelled
|
||||
|
||||
@@ -3,7 +3,7 @@ const E = preload("res://world/owniverse/math/exact.gd")
|
||||
const CivilizationState = preload("res://world/owniverse/civilization_state.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))):
|
||||
return false
|
||||
var trial: Dictionary = target.duplicate()
|
||||
@@ -13,10 +13,25 @@ func try_production(target: Dictionary, entity: OwniverseEntity, amount: Variant
|
||||
trial.batches = target.batches.duplicate()
|
||||
# Physical object totals do not change while resources are withdrawn from
|
||||
# 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 helium_share: Variant = stellar_helium_share(target) if not E.finite(helium_share_override) else helium_share_override
|
||||
for resource in entity.cost:
|
||||
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)):
|
||||
return false
|
||||
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:
|
||||
trial.auto_resources[resource] = E.maximum(0, E.subtract(trial.auto_resources.get(resource, 0), cost))
|
||||
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.
|
||||
if (trial.batches.has(id) and (not copied_batches.has(id))):
|
||||
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"]:
|
||||
if trial.has(field):
|
||||
target[field] = trial[field]
|
||||
if shared_physical: physical.merge(next_physical, true)
|
||||
return true
|
||||
|
||||
|
||||
@@ -95,6 +112,7 @@ func plan_smbh(desired_amount: Variant) -> Dictionary:
|
||||
|
||||
|
||||
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 consumed_mass: Variant = plan.amount
|
||||
var taken: Dictionary = plan.taken
|
||||
@@ -120,10 +138,12 @@ func produce_smbh(desired_amount: Variant) -> Variant:
|
||||
world.ledger.clamp_auto_resources(next)
|
||||
world.state = next
|
||||
world.presentation.refresh(world.state, world.catalog)
|
||||
world.apocalypse.check_density(world.state)
|
||||
return consumed_mass
|
||||
|
||||
|
||||
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"):
|
||||
return produce_smbh(desired_amount)
|
||||
# Use special production for S and H
|
||||
@@ -143,19 +163,23 @@ func produce(item_id: String, desired_amount: Variant) -> Variant:
|
||||
return 0
|
||||
|
||||
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
|
||||
|
||||
# Add produced entities
|
||||
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.ledger.refresh_groups(world.state)
|
||||
world.presentation.refresh(world.state, world.catalog)
|
||||
world.apocalypse.check_density(world.state)
|
||||
return max_producible
|
||||
|
||||
|
||||
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):
|
||||
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.ledger.process_entity(entity_id, to_produce)
|
||||
|
||||
world.presentation.refresh(world.state, world.catalog)
|
||||
world.apocalypse.check_density(world.state)
|
||||
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()))
|
||||
|
||||
|
||||
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:
|
||||
if E.eq(entity.id, "SMBH"):
|
||||
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):
|
||||
return 0
|
||||
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:
|
||||
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):
|
||||
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:
|
||||
if E.gt(concrete[resource], 0):
|
||||
available = E.minimum(available, E.floor_value(E.divide(world.ledger.available(world.state, resource), concrete[resource])))
|
||||
|
||||
@@ -67,20 +67,15 @@ static func divmod(a: Array, b: Array) -> Array:
|
||||
assert(b != [0], "Division by zero")
|
||||
if b == [1]: return [a.duplicate(), [0]]
|
||||
if compare(a, b) < 0: return [[0], a.duplicate()]
|
||||
# Decimal ratios often share entire base-1e9 factors. Cancel them before
|
||||
# long division; restore the remainder's original scale afterwards.
|
||||
var shared: int = 0
|
||||
while shared < mini(a.size(), b.size()) - 1 and a[shared] == 0 and b[shared] == 0:
|
||||
shared += 1
|
||||
if shared:
|
||||
var reduced: Array = divmod(a.slice(shared), b.slice(shared))
|
||||
if reduced[1] != [0]:
|
||||
var restored: Array = []
|
||||
restored.resize(shared)
|
||||
restored.fill(0)
|
||||
restored.append_array(reduced[1])
|
||||
reduced[1] = restored
|
||||
return reduced
|
||||
# A divisor with trailing base limbs can divide the high part directly.
|
||||
# The low numerator limbs belong entirely to the remainder.
|
||||
var shift: int = 0
|
||||
while shift < b.size() - 1 and b[shift] == 0: shift += 1
|
||||
if shift > 0:
|
||||
var divided: Array = divmod(a.slice(shift), b.slice(shift))
|
||||
var restored: Array = a.slice(0, shift)
|
||||
if divided[1] != [0]: restored.append_array(divided[1])
|
||||
return [divided[0], trim(restored)]
|
||||
if b.size() == 1:
|
||||
var quotient: Array = []
|
||||
quotient.resize(a.size())
|
||||
|
||||
@@ -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
|
||||
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:
|
||||
if a == null: return b == null
|
||||
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])
|
||||
|
||||
## 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:
|
||||
var result: Array = []
|
||||
if not finite(a) or not finite(divisor):
|
||||
|
||||
@@ -4,9 +4,75 @@ const CivilizationState = preload("res://world/owniverse/civilization_state.gd")
|
||||
const SimulationTime = preload("res://world/owniverse/simulation_time.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:
|
||||
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:
|
||||
if E.le(lifespan, 10): return lifespan
|
||||
@@ -20,6 +86,9 @@ func coarsen(target: Dictionary) -> void:
|
||||
for id in target.batches:
|
||||
if target.batches[id].size() <= 1: 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)
|
||||
if E.le(lifespan, 10): continue
|
||||
var width: Variant = window_duration(target, lifespan)
|
||||
@@ -58,6 +127,69 @@ var _instant_target: Dictionary = {}
|
||||
var _instant_appends: Dictionary = {}
|
||||
var _static_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:
|
||||
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)
|
||||
|
||||
|
||||
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
|
||||
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)):
|
||||
return NAN
|
||||
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")):
|
||||
target.batch_origins = {}
|
||||
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)
|
||||
if E.le(batch_duration, 0):
|
||||
return NAN
|
||||
|
||||
@@ -63,29 +63,41 @@ func expand_requirements(source: Dictionary, requirements: Dictionary) -> Dictio
|
||||
func take_available(target: Dictionary, id: String, amount: Variant) -> Dictionary:
|
||||
var quantities: Dictionary = {}
|
||||
for member in world.catalog.members(id):
|
||||
quantities[member] = target.amount.get(member, 0)
|
||||
var distribution: Dictionary = QuantityMath.normalize_distribution(quantities)
|
||||
quantities[member] = E.maximum(0, target.amount.get(member, 0))
|
||||
var taken: Dictionary = {}
|
||||
var total: Variant = E.minimum(amount, available(target, id))
|
||||
var discrete: bool = (not distribution.is_empty())
|
||||
for member in distribution:
|
||||
discrete = (discrete and is_discrete(member))
|
||||
if discrete:
|
||||
total = E.floor_value(total)
|
||||
var stock_total: Variant = 0
|
||||
for quantity in quantities.values(): stock_total = E.add(stock_total, quantity)
|
||||
var total: Variant = E.minimum(amount, stock_total)
|
||||
var discrete: bool = true
|
||||
for member in quantities:
|
||||
discrete = discrete and is_discrete(member)
|
||||
if discrete: total = E.floor_value(total)
|
||||
if E.le(total, 0) or E.le(stock_total, 0): return taken
|
||||
if E.eq(total, stock_total):
|
||||
taken = quantities
|
||||
elif discrete:
|
||||
var shares: Array = []
|
||||
var assigned: Variant = 0
|
||||
for member in quantities:
|
||||
var expected: Variant = E.mul_div(total, quantities[member], stock_total)
|
||||
var whole: Variant = E.floor_value(expected)
|
||||
taken[member] = whole
|
||||
assigned = E.add(assigned, whole)
|
||||
shares.append({"id": member, "rest": E.subtract(expected, whole)})
|
||||
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:
|
||||
var expected: Variant = E.multiply(total, distribution[member])
|
||||
taken[member] = (E.floor_value(expected) if discrete else expected)
|
||||
assigned = E.add(assigned, taken[member])
|
||||
shares.append({"id": member, "rest": E.subtract(expected, taken[member])})
|
||||
if discrete:
|
||||
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))))
|
||||
for share : Dictionary in shares:
|
||||
if E.ge(assigned, total):
|
||||
break
|
||||
taken[share.id] = E.add(taken[share.id], 1)
|
||||
assigned = E.add(assigned, 1)
|
||||
taken[member] = E.multiply(total, distribution[member])
|
||||
|
||||
for member in taken:
|
||||
target.amount[member] = E.maximum(0, E.subtract(target.amount.get(member, 0), taken[member]))
|
||||
return taken
|
||||
@@ -101,7 +113,11 @@ func credit(target: Dictionary, id: String, amount: Variant) -> Variant:
|
||||
if is_discrete(id):
|
||||
if (not target.has("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)
|
||||
if E.eq(amount, E.floor_value(amount)) and E.ge(carry, 0) and E.lt(carry, 1):
|
||||
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)
|
||||
|
||||
+123
-46
@@ -1,5 +1,6 @@
|
||||
extends Node
|
||||
class_name Owniverse
|
||||
signal production_applied(id: String)
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
## Owns the state and modules. Rules live in the modules below.
|
||||
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 automatic = preload("res://world/owniverse/automatic_production.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 environment = preload("res://world/owniverse/universe_environment.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: 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:
|
||||
if not profile_enabled: return 0
|
||||
@@ -54,6 +110,7 @@ var state: Dictionary = {
|
||||
'civilization_stats': {},
|
||||
'civilization_homes': {},
|
||||
'ethics': {},
|
||||
'apocalypse': {'phase': 0, 'type': 0, 'warning': 0, 'stage': 1, 'tick': 0, 'end_ticks': 0},
|
||||
'stats': {'total_amount': {}},
|
||||
}
|
||||
|
||||
@@ -63,65 +120,86 @@ var frame_clock: Dictionary = {}
|
||||
var frame_budget: RefCounted
|
||||
var _requested_speed: int = 0
|
||||
var _queued_productions: Array = []
|
||||
var production_queue_busy: bool = false
|
||||
var _production_queue_cancelled: bool = false
|
||||
var frame_active_clock: Dictionary = {}
|
||||
var _resume_clock: Dictionary = {}
|
||||
var frame_step_seconds: float = 0.1
|
||||
var _frame_step_speed: int = 0
|
||||
var _frame_started_usec: int = 0
|
||||
var _frame_duration_seconds: float = 0.0
|
||||
var _frame_cost_ratio: float = 0.0
|
||||
var frame_elapsed_seconds: float = 0.0
|
||||
const production_slice_usec: int = 4000
|
||||
|
||||
## Hysteresis prevents alternating step sizes around one queue threshold.
|
||||
## Only scheduling uses float; both clocks and the accumulated debt stay exact.
|
||||
func update_frame_step() -> float:
|
||||
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
|
||||
## The HUD follows requested time; world state remains at the last committed cycle.
|
||||
func display_clock() -> Dictionary:
|
||||
return frame_clock if not frame_clock.is_empty() else state.time
|
||||
|
||||
## Keep the requested clock advancing while a transaction spans render frames.
|
||||
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 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()
|
||||
frame_clock = SimulationTime.advance(frame_clock, delta, speed)
|
||||
if frame_busy: return
|
||||
if E.le(frame_clock.datetime, state.time.datetime) and _resume_clock.is_empty(): return
|
||||
frame_elapsed_seconds += delta
|
||||
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_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()
|
||||
# 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
|
||||
frame_active_clock = next_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)
|
||||
|
||||
func request_production(id: String, amount: Variant, resource: bool = false) -> void:
|
||||
if frame_busy:
|
||||
func request_production(id: String, amount: Variant, resource: bool = false) -> bool:
|
||||
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})
|
||||
elif resource:
|
||||
production.produce_hs(id, amount)
|
||||
return true
|
||||
_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:
|
||||
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.
|
||||
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:
|
||||
frame_clock = checkpoint.get("requested_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", [])
|
||||
if _resume_clock.is_empty():
|
||||
speed = checkpoint.get("speed", state.speed)
|
||||
@@ -129,31 +207,30 @@ func restore_frame_checkpoint(checkpoint: Dictionary) -> void:
|
||||
_requested_speed = checkpoint.get("speed", state.speed)
|
||||
|
||||
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_commits += 1
|
||||
if _requested_speed != 0:
|
||||
speed = _requested_speed
|
||||
_requested_speed = 0
|
||||
var productions: Array = _queued_productions
|
||||
_queued_productions = []
|
||||
for command in productions:
|
||||
request_production(command.id, command.amount, command.resource)
|
||||
if not _queued_productions.is_empty():
|
||||
_start_production_drain(frame_budget.tree)
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
if what == NOTIFICATION_PREDELETE and frame_budget != null:
|
||||
if what == NOTIFICATION_PREDELETE:
|
||||
_production_queue_cancelled = true
|
||||
if frame_budget != null:
|
||||
frame_budget.cancelled = true
|
||||
|
||||
var speed: int :
|
||||
get:
|
||||
return _requested_speed if _requested_speed != 0 else state.speed
|
||||
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:
|
||||
_requested_speed = clampi(value, 1, Global.SPEED_LIMIT)
|
||||
_requested_speed = requested_speed
|
||||
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))
|
||||
|
||||
func evolute(delta: Variant) -> void:
|
||||
|
||||
@@ -45,6 +45,41 @@ static func allocate_whole(requests: Dictionary, limit: Variant) -> Dictionary:
|
||||
total = E.add(total, result[id])
|
||||
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.
|
||||
## 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):
|
||||
for id in result: result[id] = 0
|
||||
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 rests: Array = []
|
||||
var order: Array = []
|
||||
@@ -123,11 +167,72 @@ static func sum(pop: Dictionary) -> Variant:
|
||||
|
||||
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:
|
||||
if E.le(count, 0): return {}
|
||||
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 = {}
|
||||
if E.le(count, 0): return result
|
||||
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
|
||||
# original algorithm. Retain them to preserve ties and saved errors exactly.
|
||||
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)
|
||||
var order: 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:
|
||||
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)
|
||||
remainders[key] = E.maximum(0, E.subtract(expected, actual))
|
||||
return actual
|
||||
|
||||
@@ -2,8 +2,8 @@ extends "res://world/owniverse/simulation_module.gd"
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
||||
|
||||
func stellar_environment(source: Dictionary) -> Dictionary:
|
||||
var quantities: Dictionary = world.ledger.physical_amounts(source)
|
||||
func stellar_environment(source: Dictionary, quantities: Dictionary = {}) -> Dictionary:
|
||||
if quantities.is_empty(): quantities = world.ledger.physical_amounts(source)
|
||||
# SMBH is a self-group in GI; its quantity stores mass, not object count.
|
||||
var mass: Variant = source.amount.get("SMBH", 0)
|
||||
# Geometric volume includes a 1e22 scale per void and can exceed E27.
|
||||
|
||||
@@ -62,14 +62,24 @@ static func advance(previous: Dictionary, target: Dictionary, rules: Dictionary,
|
||||
for id in rules:
|
||||
if E.ne(str(rules[id].home), ""): stages.append(id)
|
||||
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
|
||||
for id in stages:
|
||||
if not populated.has(id):
|
||||
weight = E.multiply(weight, 2)
|
||||
continue
|
||||
var population: Dictionary = previous.get("ethics", {}).get(id, {})
|
||||
var count: Variant = 0
|
||||
for n in population.values(): count = E.add(count, n)
|
||||
if E.gt(count, 0):
|
||||
for ethic in population:
|
||||
var proportion: Variant = E.divide(population[ethic], count)
|
||||
var proportion: float = maxf(0.0, float(population[ethic]))
|
||||
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)
|
||||
|
||||
+36
-8
@@ -64,12 +64,15 @@
|
||||
[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="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://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="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" path="res://ui/report/report.gd" id="69_report"]
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_ifjjt"]
|
||||
|
||||
[sub_resource type="ButtonGroup" id="ButtonGroup_58wjt"]
|
||||
@@ -234,23 +237,23 @@ theme_override_constants/separation = 0
|
||||
layout_mode = 2
|
||||
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)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 6
|
||||
size_flags_vertical = 4
|
||||
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)
|
||||
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)
|
||||
layout_mode = 2
|
||||
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)
|
||||
layout_mode = 2
|
||||
|
||||
@@ -1011,7 +1014,8 @@ layout_mode = 2
|
||||
texture_normal = ExtResource("52_uvuuk")
|
||||
|
||||
[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
|
||||
offset_right = 40.0
|
||||
offset_bottom = 40.0
|
||||
@@ -1032,17 +1036,26 @@ custom_minimum_size = Vector2(1080, 0)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Header" type="TextureRect" parent="Stage/Report/Header" unique_id=1321780840]
|
||||
custom_minimum_size = Vector2(824, 256)
|
||||
layout_mode = 2
|
||||
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]
|
||||
visible = false
|
||||
custom_minimum_size = Vector2(1080, 1920)
|
||||
layout_mode = 0
|
||||
offset_right = 40.0
|
||||
offset_bottom = 40.0
|
||||
|
||||
[node name="TopMenu" type="HBoxContainer" parent="Stage/Achievements" unique_id=40287683]
|
||||
custom_minimum_size = Vector2(0, 128)
|
||||
layout_mode = 2
|
||||
|
||||
[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")
|
||||
|
||||
[node name="Header" type="CenterContainer" parent="Stage/Achievements" unique_id=51521239]
|
||||
custom_minimum_size = Vector2(1080, 0)
|
||||
layout_mode = 2
|
||||
|
||||
[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
|
||||
size_flags_horizontal = 4
|
||||
size_flags_vertical = 4
|
||||
texture = ExtResource("61_d36xh")
|
||||
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]
|
||||
visible = false
|
||||
layout_mode = 0
|
||||
offset_right = 1080.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/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/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/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"]
|
||||
|
||||
Reference in New Issue
Block a user