From 459fbb2a4d3c2a932e72ef38bdd0dd0ab1c6146d Mon Sep 17 00:00:00 2001 From: Kirill Date: Thu, 8 Oct 2026 21:25:39 +0300 Subject: [PATCH] Refactor civilization flow reward calculations for improved accuracy and performance --- tests/artifacts/all_objects_speed6_results.md | 17 + ...vilization_flow_prepare_step3_results.json | 645 ++++++++++++++++++ world/owniverse/quantity_math.gd | 21 + 3 files changed, 683 insertions(+) create mode 100644 tests/artifacts/civilization_flow_prepare_step3_results.json diff --git a/tests/artifacts/all_objects_speed6_results.md b/tests/artifacts/all_objects_speed6_results.md index 25f490a..5ebfa0c 100644 --- a/tests/artifacts/all_objects_speed6_results.md +++ b/tests/artifacts/all_objects_speed6_results.md @@ -441,3 +441,20 @@ godot --headless --path . --script res://tests/benchmark_civilization_flows.gd - Пять полных циклов в секунду пока не достигнуты: тяжёлый цикл составляет 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`. diff --git a/tests/artifacts/civilization_flow_prepare_step3_results.json b/tests/artifacts/civilization_flow_prepare_step3_results.json new file mode 100644 index 0000000..b3ca12e --- /dev/null +++ b/tests/artifacts/civilization_flow_prepare_step3_results.json @@ -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 +} diff --git a/world/owniverse/quantity_math.gd b/world/owniverse/quantity_math.gd index 6f452d5..c0cdf34 100644 --- a/world/owniverse/quantity_math.gd +++ b/world/owniverse/quantity_math.gd @@ -303,6 +303,27 @@ static func split_arrays(remainders: Dictionary, key: String, count: Variant, id static func accumulate_rate(remainders: Dictionary, key: String, count: Variant, probability: Variant) -> Variant: + # 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):