From 9357f757a95db96274d4e115c7e498b58eca6902 Mon Sep 17 00:00:00 2001 From: Kirill Date: Thu, 8 Oct 2026 20:43:51 +0300 Subject: [PATCH] Refactor civilization flow mechanics: enhance commit and colonization logic, add tests for flow application and resource allocation, and optimize performance metrics. --- simulation_rules.md | 4 + tests/README.md | 6 + tests/artifacts/all_objects_speed6_results.md | 33 + .../artifacts/flow_commit_step1_results.json | 763 ++++++++++++++++++ tests/test_flow_commit.gd | 61 ++ tests/test_flow_commit.gd.uid | 1 + world/owniverse/civilization_flow.gd | 110 ++- 7 files changed, 946 insertions(+), 32 deletions(-) create mode 100644 tests/artifacts/flow_commit_step1_results.json create mode 100644 tests/test_flow_commit.gd create mode 100644 tests/test_flow_commit.gd.uid diff --git a/simulation_rules.md b/simulation_rules.md index 68da07c..cf5f6d6 100644 --- a/simulation_rules.md +++ b/simulation_rules.md @@ -139,6 +139,10 @@ HP деградирует в RP. У эонов возможен откат к п Колонизация агрегирует запросы по стадии назначения (на две стадии ниже отправителя), ограничивает их общим запасом GG, AB, RP и HP и заменяет ровно выделенное число планет. Целые колонии сохраняют владельца целевой планеты; состав получает дрейф 960/4/9. Исходное население не расходуется. +Применение потоков сначала строит таблицу исходных владельцев. Для каждой исходной стадии готовые целые квоты распределяются по строкам владельцев; последняя строка получает точный остаток. Стадии с единственным свободным запасом применяют готовые квоты напрямую. Поступления объединяются по стадии и владельцу; физическая выдача и статистика создания выполняются один раз на получателя. Ct всегда остаётся свободной от привязок. + +При колонизации планеты сначала списываются по типам и владельцам, затем суммарное списание каждого владельца распределяется между стадиями колоний. Каждая стадия получает одну суммарную выдачу. Общие целые квоты, число списанных планет и количество колоний сохраняются точно. Из-за объединения распределений единичные остатки между владельцами могут отличаться от прежнего последовательного порядка; вероятности переходов и формулы этик не меняются. + Космические стадии C2.0–C4.4 свободны от планетных привязок. Общие бюджеты звёздных семейств и структур учитывают уже занятые резервы. Неподдерживаемый промежуточный переход блокируется в операторе; конечные положительные запросы делят свободную ёмкость одновременно. Отклонённые количества возвращаются к источнику. Освободившаяся при переходах ёмкость доступна следующему шагу. Если внешние изменения уменьшили ёмкость ниже существующего населения, избыток последовательно деградирует с приоритетом старших стадий. При возвращении в планетные стадии используются реальные карликовые звёзды, при отсутствии хозяина население остаётся свободным. Порядок полного шага: производство за интервал → непрерывные потоки существующих цивилизаций → поколения звёзд и биологической жизни и бласты → итоговые ограничения среды → тренды. Возникшие из C0.1 цивилизации C0.2 получают равномерный профиль, смешанный с существующим населением, и начинают непрерывную эволюцию со следующего шага. Разные размеры полного шага могут отличаться из-за этого порядка, ограничений среды и нелинейных эффектов; чистая непрерывная цепочка без ограничений согласована при разделении интервала. diff --git a/tests/README.md b/tests/README.md index 69dd95b..f7e6b46 100644 --- a/tests/README.md +++ b/tests/README.md @@ -238,6 +238,12 @@ godot --path Owniverse.GI --script res://tests/benchmark_civilizations.gd -- ui ## Расчёт между кадрами +`test_flow_commit.gd` проверяет консолидированное применение переходов и колонизации: точные квоты получателей, сохранение количества у каждого владельца, профили этик, статистику создания и отсутствие привязок у Ct. Проверки используют несколько владельцев и численность 103, 10^40 + 103 и 10^400 + 103. + +```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 мс и уступает diff --git a/tests/artifacts/all_objects_speed6_results.md b/tests/artifacts/all_objects_speed6_results.md index 47cae06..7fe6700 100644 --- a/tests/artifacts/all_objects_speed6_results.md +++ b/tests/artifacts/all_objects_speed6_results.md @@ -379,3 +379,36 @@ godot --headless --path . --script res://tests/benchmark_large_star_cohorts.gd - ```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 +``` diff --git a/tests/artifacts/flow_commit_step1_results.json b/tests/artifacts/flow_commit_step1_results.json new file mode 100644 index 0000000..0548e8c --- /dev/null +++ b/tests/artifacts/flow_commit_step1_results.json @@ -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 + } + } +} diff --git a/tests/test_flow_commit.gd b/tests/test_flow_commit.gd new file mode 100644 index 0000000..a85b3c6 --- /dev/null +++ b/tests/test_flow_commit.gd @@ -0,0 +1,61 @@ +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) + world.civilization_flow._commit(world.state, physical, {"C0.2": {"counts": counts, "profiles": profiles}}, rules, 100) + 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() + 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}} + } + world.civilization_flow._colonize(world.state, colonies, 100, rules) + 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) diff --git a/tests/test_flow_commit.gd.uid b/tests/test_flow_commit.gd.uid new file mode 100644 index 0000000..5608be9 --- /dev/null +++ b/tests/test_flow_commit.gd.uid @@ -0,0 +1 @@ +uid://bfgdsxoycr20e diff --git a/world/owniverse/civilization_flow.gd b/world/owniverse/civilization_flow.gd index 2eb29cc..5de2cde 100644 --- a/world/owniverse/civilization_flow.gd +++ b/world/owniverse/civilization_flow.gd @@ -306,53 +306,84 @@ func _limit(plans: Dictionary, budget: Dictionary, rules: Dictionary) -> void: 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) -> void: var credits: Dictionary = {} var contributions: Dictionary = {} var births: Dictionary = {} + var ownership: Dictionary = {} + 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 slots: Dictionary = {"": target.amount.get(source, 0)} - for owner in target.binds: - var held: Variant = target.binds[owner].get(source, 0) - if E.gt(held, 0): slots[owner] = held + 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]}) - if destination == source: continue - var taken: Dictionary = Q.allocate_profiled(slots, amount) - for owner in taken: - if E.le(taken[owner], 0): continue - slots[owner] = E.subtract(slots[owner], taken[owner]) - var host: String = owner - if State.is_space_civilization(destination, rules) or destination == "Ct": host = "" - elif owner == "" and State.is_space_civilization(source, rules): host = "@host" + 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), taken[owner]) - target.amount[source] = slots[""] - for owner in slots: - if owner != "": target.binds[owner][source] = slots[owner] + 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 created: Variant = world.ledger.credit(target, destination, Q.sum(credits[destination])) + 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 ownership: Dictionary = Q.split_profile(target.civilization_remainders, "flow:hosts:" + destination, count, hosts.keys(), _host_shares(hosts)) - for host in ownership: - if E.le(ownership[host], 0): 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), ownership[host]) - target.amount[destination] = E.subtract(target.amount[destination], count) + 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) - target.amount[destination] = E.subtract(target.amount[destination], count) + bound = E.add(bound, count) + target.amount[destination] = E.subtract(target.amount[destination], bound) if not rules.has(destination): births[destination] = created var final: Dictionary = world.ledger.physical_amounts(target) for destination in contributions: @@ -387,7 +418,7 @@ func _colonize(target: Dictionary, colonies: Dictionary, year: Variant, rules: D var total: Variant = Q.sum(granted) if E.le(total, 0): return var taken_resources: Dictionary = Q.allocate_profiled(stocks, total) - var remaining: Dictionary = granted.duplicate() + var removed_by_owner: Dictionary = {} for resource in taken_resources: var removed: Variant = taken_resources[resource] if E.le(removed, 0): continue @@ -401,15 +432,30 @@ func _colonize(target: Dictionary, colonies: Dictionary, year: Variant, rules: D 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) - var allocation: Dictionary = Q.allocate_profiled(remaining, amount) - for destination in allocation: - var count: Variant = allocation[destination] - if E.le(count, 0): continue - remaining[destination] = E.subtract(remaining[destination], count) - var created: Variant = world.ledger.credit(target, destination, count) - if owner != "" and not State.is_space_civilization(destination, rules): - target.amount[destination] = E.subtract(target.amount[destination], created) - target.binds[owner][destination] = E.add(target.binds[owner].get(destination, 0), created) + removed_by_owner[owner] = E.add(removed_by_owner.get(owner, 0), amount) + var remaining: Dictionary = granted.duplicate() + var placements: Dictionary = {} + 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 + world.ledger.credit(target, destination, count) + 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]