diff --git a/simulation_rules.md b/simulation_rules.md index cf5f6d6..7da10c8 100644 --- a/simulation_rules.md +++ b/simulation_rules.md @@ -143,6 +143,10 @@ HP деградирует в RP. У эонов возможен откат к п При колонизации планеты сначала списываются по типам и владельцам, затем суммарное списание каждого владельца распределяется между стадиями колоний. Каждая стадия получает одну суммарную выдачу. Общие целые квоты, число списанных планет и количество колоний сохраняются точно. Из-за объединения распределений единичные остатки между владельцами могут отличаться от прежнего последовательного порядка; вероятности переходов и формулы этик не меняются. +В пределах цивилизационной транзакции точные общие количества передаются между фазами. Применение переходов, колонизация и коррекция ёмкости возвращают обновлённый снимок; перемещение между владельцами само по себе общую численность не меняет. Этот снимок используется для периодов, ограничений, нормализации этик и поклонения. После изменения запасов другими подсистемами снимок строится заново. Снимки не сохраняются в файл и не заменяют проверку состояния при внешних изменениях словарей. + +Автоматическое создание нескольких типов звёзд также использует один текущий снимок. Успешная пробная операция вычитает фактически израсходованные количества; перенос ресурса в привязку сохраняет общий запас. При отказе пробной операции снимок остаётся неизменным. Новые звёзды выдаются после завершения отбора типов, как и прежде. + Космические стадии C2.0–C4.4 свободны от планетных привязок. Общие бюджеты звёздных семейств и структур учитывают уже занятые резервы. Неподдерживаемый промежуточный переход блокируется в операторе; конечные положительные запросы делят свободную ёмкость одновременно. Отклонённые количества возвращаются к источнику. Освободившаяся при переходах ёмкость доступна следующему шагу. Если внешние изменения уменьшили ёмкость ниже существующего населения, избыток последовательно деградирует с приоритетом старших стадий. При возвращении в планетные стадии используются реальные карликовые звёзды, при отсутствии хозяина население остаётся свободным. Порядок полного шага: производство за интервал → непрерывные потоки существующих цивилизаций → поколения звёзд и биологической жизни и бласты → итоговые ограничения среды → тренды. Возникшие из C0.1 цивилизации C0.2 получают равномерный профиль, смешанный с существующим населением, и начинают непрерывную эволюцию со следующего шага. Разные размеры полного шага могут отличаться из-за этого порядка, ограничений среды и нелинейных эффектов; чистая непрерывная цепочка без ограничений согласована при разделении интервала. diff --git a/tests/README.md b/tests/README.md index f7e6b46..25557f4 100644 --- a/tests/README.md +++ b/tests/README.md @@ -240,6 +240,10 @@ 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 ``` diff --git a/tests/artifacts/all_objects_speed6_results.md b/tests/artifacts/all_objects_speed6_results.md index 7fe6700..25f490a 100644 --- a/tests/artifacts/all_objects_speed6_results.md +++ b/tests/artifacts/all_objects_speed6_results.md @@ -412,3 +412,32 @@ godot --headless --path . --script res://tests/benchmark_many_objects.gd -- cycl 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`. Сохранение игрока не использовалось. diff --git a/tests/artifacts/physical_totals_step2_results.json b/tests/artifacts/physical_totals_step2_results.json new file mode 100644 index 0000000..83e4067 --- /dev/null +++ b/tests/artifacts/physical_totals_step2_results.json @@ -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" + } +} diff --git a/tests/test_flow_commit.gd b/tests/test_flow_commit.gd index a85b3c6..2e949d7 100644 --- a/tests/test_flow_commit.gd +++ b/tests/test_flow_commit.gd @@ -14,7 +14,10 @@ func run(): 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) + 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"]: @@ -33,6 +36,14 @@ func run(): 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] = {} @@ -43,7 +54,13 @@ func run(): "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) + 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") diff --git a/tests/test_production_transaction.gd b/tests/test_production_transaction.gd index b43c577..96b389f 100644 --- a/tests/test_production_transaction.gd +++ b/tests/test_production_transaction.gd @@ -59,11 +59,16 @@ 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") diff --git a/world/owniverse/automatic_production.gd b/world/owniverse/automatic_production.gd index 251fbc7..67543a6 100644 --- a/world/owniverse/automatic_production.gd +++ b/world/owniverse/automatic_production.gd @@ -116,7 +116,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] @@ -128,7 +129,7 @@ func produce_for_period(previous: Dictionary, target: Dictionary, quantities: Di var actual: Variant = E.minimum(desired, auto_capacity(target, entity, budget)) 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)): continue budget = ordinary_star_budget(target) created[id] = actual diff --git a/world/owniverse/civilization_evolution.gd b/world/owniverse/civilization_evolution.gd index cce2dde..3073b04 100644 --- a/world/owniverse/civilization_evolution.gd +++ b/world/owniverse/civilization_evolution.gd @@ -6,9 +6,9 @@ const ETHICS = State.ETHICS const OPPOSITE = {"m": "l", "l": "m", "w": "d", "d": "w", "c": "b", "b": "c"} # Cache the weighted float worship rate multiplied by the exact population. -static func worship(world, target: Dictionary, rules: Dictionary) -> Variant: +static func worship(world, target: Dictionary, rules: Dictionary, physical: Dictionary = {}) -> Variant: var result: Variant = 0 - var physical: Dictionary = world.ledger.physical_amounts(target) + if physical.is_empty(): physical = world.ledger.physical_amounts(target) for id in rules: if not rules[id].worships: continue var pop: Dictionary = State.normalize_profile(target.ethics.get(id, {})) diff --git a/world/owniverse/civilization_flow.gd b/world/owniverse/civilization_flow.gd index 5de2cde..c289233 100644 --- a/world/owniverse/civilization_flow.gd +++ b/world/owniverse/civilization_flow.gd @@ -10,7 +10,7 @@ var _generator_key: String = "" var _generator: Dictionary = {} var _solutions: Dictionary = {} -func _edges(target: Dictionary, rules: Dictionary) -> 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() @@ -18,13 +18,13 @@ func _edges(target: Dictionary, rules: Dictionary) -> Dictionary: var rates: Dictionary = {} var spans: Dictionary = {} var progression: Dictionary = {} - var budget: Dictionary = State.stellar_budget(world, target, rules) + 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))) + 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)): @@ -171,11 +171,11 @@ func _solve_basis(generator: Dictionary, source: String, years: float) -> Dictio 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) -> void: +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))) + 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) @@ -307,11 +307,16 @@ func _limit(plans: Dictionary, budget: Dictionary, rules: Dictionary) -> void: _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: +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: @@ -368,7 +373,9 @@ func _commit(target: Dictionary, physical: Dictionary, plans: Dictionary, rules: 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 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] @@ -385,7 +392,6 @@ func _commit(target: Dictionary, physical: Dictionary, plans: Dictionary, rules: 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: if not rules.has(destination) or E.le(final.get(destination, 0), 0): continue var profile: Dictionary = {} @@ -400,6 +406,7 @@ func _commit(target: Dictionary, physical: Dictionary, plans: Dictionary, rules: 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) @@ -407,16 +414,16 @@ func _host_shares(hosts: Dictionary) -> 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) -> void: - if colonies.is_empty(): return - var physical: Dictionary = world.ledger.physical_amounts(target) +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 + 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: @@ -435,6 +442,7 @@ func _colonize(target: Dictionary, colonies: Dictionary, year: Variant, rules: D 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] @@ -449,7 +457,8 @@ func _colonize(target: Dictionary, colonies: Dictionary, year: Variant, rules: D for destination in granted: var count: Variant = granted[destination] if E.le(count, 0): continue - world.ledger.credit(target, destination, count) + 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] @@ -466,6 +475,9 @@ func _colonize(target: Dictionary, colonies: Dictionary, year: Variant, rules: D 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 @@ -476,12 +488,12 @@ func evolve(target: Dictionary, from_year: Variant, to_year: Variant, timer: Ref # 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) + 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 physical: Dictionary = world.ledger.physical_amounts(target) 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) @@ -509,18 +521,18 @@ func evolve(target: Dictionary, from_year: Variant, to_year: Variant, timer: Ref _limit(plans, State.stellar_budget(world, target, rules, physical), rules) world.profile_end("civilization_flow_capacity", started) started = world.profile_start() - _commit(target, physical, plans, rules, end) + 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() - _colonize(target, transaction.colonies, end, rules) + 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))) + _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) - State.enforce_capacity(world, target, rules, end) - State.reconcile(world, target, rules) - target.civilization_stats.worship = Civ.worship(world, target, rules) + 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 diff --git a/world/owniverse/civilization_state.gd b/world/owniverse/civilization_state.gd index 67f3433..4f11904 100644 --- a/world/owniverse/civilization_state.gd +++ b/world/owniverse/civilization_state.gd @@ -133,10 +133,11 @@ 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) @@ -163,7 +164,9 @@ 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 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()) @@ -174,7 +177,7 @@ static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year: # 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] @@ -192,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. @@ -247,13 +250,13 @@ 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 multiplier: float = 1.0 for home in homes: diff --git a/world/owniverse/evolution_coordinator.gd b/world/owniverse/evolution_coordinator.gd index 23ba758..b605ec4 100644 --- a/world/owniverse/evolution_coordinator.gd +++ b/world/owniverse/evolution_coordinator.gd @@ -5,13 +5,12 @@ 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 @@ -19,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: @@ -31,10 +31,11 @@ func evolute_to_datetime(datetime: Variant, clock: Dictionary = {}, budget: RefC # 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") @@ -46,7 +47,6 @@ 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 diff --git a/world/owniverse/manual_production.gd b/world/owniverse/manual_production.gd index 7a61b93..7df6e9f 100644 --- a/world/owniverse/manual_production.gd +++ b/world/owniverse/manual_production.gd @@ -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 = {}) -> bool: if (E.le(amount, 0) or E.ne(amount, E.floor_value(amount))): return false var trial: Dictionary = target.duplicate() @@ -13,7 +13,9 @@ 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 = {} for resource in entity.cost: var cost: Variant = E.round_value(E.multiply(E.maximum(0, entity.cost[resource]), amount)) @@ -28,6 +30,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 +44,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 diff --git a/world/owniverse/universe_environment.gd b/world/owniverse/universe_environment.gd index ba6b131..d50e27a 100644 --- a/world/owniverse/universe_environment.gd +++ b/world/owniverse/universe_environment.gd @@ -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.