Refactor capacity enforcement and production logic

- Updated `enforce_capacity` function to accept an optional `physical` parameter, allowing for more flexible handling of physical amounts.
- Modified `initialize_life_batches` to return physical amounts after enforcing capacity, improving data flow.
- Enhanced `try_production` to utilize shared physical amounts, reducing redundant calculations.
- Adjusted `stellar_environment` to accept an optional `quantities` parameter, streamlining its usage.
- Added new test results for physical totals to validate performance improvements across various scenarios.
This commit is contained in:
Kirill
2026-10-08 21:10:36 +03:00
parent 9357f757a9
commit c42940369a
13 changed files with 994 additions and 45 deletions
+4
View File
@@ -143,6 +143,10 @@ HP деградирует в RP. У эонов возможен откат к п
При колонизации планеты сначала списываются по типам и владельцам, затем суммарное списание каждого владельца распределяется между стадиями колоний. Каждая стадия получает одну суммарную выдачу. Общие целые квоты, число списанных планет и количество колоний сохраняются точно. Из-за объединения распределений единичные остатки между владельцами могут отличаться от прежнего последовательного порядка; вероятности переходов и формулы этик не меняются. При колонизации планеты сначала списываются по типам и владельцам, затем суммарное списание каждого владельца распределяется между стадиями колоний. Каждая стадия получает одну суммарную выдачу. Общие целые квоты, число списанных планет и количество колоний сохраняются точно. Из-за объединения распределений единичные остатки между владельцами могут отличаться от прежнего последовательного порядка; вероятности переходов и формулы этик не меняются.
В пределах цивилизационной транзакции точные общие количества передаются между фазами. Применение переходов, колонизация и коррекция ёмкости возвращают обновлённый снимок; перемещение между владельцами само по себе общую численность не меняет. Этот снимок используется для периодов, ограничений, нормализации этик и поклонения. После изменения запасов другими подсистемами снимок строится заново. Снимки не сохраняются в файл и не заменяют проверку состояния при внешних изменениях словарей.
Автоматическое создание нескольких типов звёзд также использует один текущий снимок. Успешная пробная операция вычитает фактически израсходованные количества; перенос ресурса в привязку сохраняет общий запас. При отказе пробной операции снимок остаётся неизменным. Новые звёзды выдаются после завершения отбора типов, как и прежде.
Космические стадии C2.0–C4.4 свободны от планетных привязок. Общие бюджеты звёздных семейств и структур учитывают уже занятые резервы. Неподдерживаемый промежуточный переход блокируется в операторе; конечные положительные запросы делят свободную ёмкость одновременно. Отклонённые количества возвращаются к источнику. Освободившаяся при переходах ёмкость доступна следующему шагу. Если внешние изменения уменьшили ёмкость ниже существующего населения, избыток последовательно деградирует с приоритетом старших стадий. При возвращении в планетные стадии используются реальные карликовые звёзды, при отсутствии хозяина население остаётся свободным. Космические стадии C2.0–C4.4 свободны от планетных привязок. Общие бюджеты звёздных семейств и структур учитывают уже занятые резервы. Неподдерживаемый промежуточный переход блокируется в операторе; конечные положительные запросы делят свободную ёмкость одновременно. Отклонённые количества возвращаются к источнику. Освободившаяся при переходах ёмкость доступна следующему шагу. Если внешние изменения уменьшили ёмкость ниже существующего населения, избыток последовательно деградирует с приоритетом старших стадий. При возвращении в планетные стадии используются реальные карликовые звёзды, при отсутствии хозяина население остаётся свободным.
Порядок полного шага: производство за интервал → непрерывные потоки существующих цивилизаций → поколения звёзд и биологической жизни и бласты → итоговые ограничения среды → тренды. Возникшие из C0.1 цивилизации C0.2 получают равномерный профиль, смешанный с существующим населением, и начинают непрерывную эволюцию со следующего шага. Разные размеры полного шага могут отличаться из-за этого порядка, ограничений среды и нелинейных эффектов; чистая непрерывная цепочка без ограничений согласована при разделении интервала. Порядок полного шага: производство за интервал → непрерывные потоки существующих цивилизаций → поколения звёзд и биологической жизни и бласты → итоговые ограничения среды → тренды. Возникшие из C0.1 цивилизации C0.2 получают равномерный профиль, смешанный с существующим населением, и начинают непрерывную эволюцию со следующего шага. Разные размеры полного шага могут отличаться из-за этого порядка, ограничений среды и нелинейных эффектов; чистая непрерывная цепочка без ограничений согласована при разделении интервала.
+4
View File
@@ -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_flow_commit.gd` проверяет консолидированное применение переходов и колонизации: точные квоты получателей, сохранение количества у каждого владельца, профили этик, статистику создания и отсутствие привязок у Ct. Проверки используют несколько владельцев и численность 103, 10^40 + 103 и 10^400 + 103.
Дополнительно проверяется точное совпадение обновляемых общих количеств с полным пересчётом после переходов, колонизации и каскадной деградации при нехватке ёмкости. Входные снимки остаются неизменными.
`test_production_transaction.gd` проверяет, что успешное создание обновляет общий снимок, привязка сохраняет общий ресурс, а отклонённая операция сохраняет и состояние, и снимок.
```sh ```sh
godot --headless --path Owniverse.GI --script res://tests/test_flow_commit.gd godot --headless --path Owniverse.GI --script res://tests/test_flow_commit.gd
``` ```
@@ -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/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 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`. Сохранение игрока не использовалось.
@@ -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"
}
}
+19 -2
View File
@@ -14,7 +14,10 @@ func run():
var profiles: Dictionary = {} var profiles: Dictionary = {}
for id in counts: profiles[id] = profile for id in counts: profiles[id] = profile
var physical: Dictionary = world.ledger.physical_amounts(world.state) 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: for id in counts:
check(E.eq(world.ledger.physical_amount(world.state, id), counts[id]), "Commit preserves exact destination quota: " + id) check(E.eq(world.ledger.physical_amount(world.state, id), counts[id]), "Commit preserves exact destination quota: " + id)
for owner in ["RD", "OD"]: 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") check(E.eq(world.state.binds.RD.get("Ct", 0), 0), "Transcendent population cannot retain a stellar owner")
dispose(world) dispose(world)
world = fixture() 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 = {} var original_owners: Dictionary = {}
for owner in ["RD", "OD", "YD", "WYD"]: for owner in ["RD", "OD", "YD", "WYD"]:
world.state.binds[owner] = {} world.state.binds[owner] = {}
@@ -43,7 +54,13 @@ func run():
"C1.4": {"counts": {"C1.4": E.multiply(count, 3)}, "profiles": {"C1.4": profile}}, "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}} "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.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.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.stats.total_amount["C1.4"], E.multiply(count, 3)), "Consolidated credit preserves creation statistics")
+7 -2
View File
@@ -59,11 +59,16 @@ func run() -> void:
world.state.auto_resources = {"H": 10} world.state.auto_resources = {"H": 10}
world.state.batches.H = [{"amount": 10, "start_year": 1, "end_year": 2}] world.state.batches.H = [{"amount": 10, "start_year": 1, "end_year": 2}]
var before: Dictionary = world.state.duplicate(true) var before: Dictionary = world.state.duplicate(true)
check((not world.production.try_production(world.state, world.catalog.entities.N, 1, true)), "Binding failure rejects production") var physical: Dictionary = world.ledger.physical_amounts(world.state)
var frozen_physical: Dictionary = physical.duplicate()
check((not world.production.try_production(world.state, world.catalog.entities.N, 1, true, physical)), "Binding failure rejects production")
check(E.eq(physical, frozen_physical), "Failed production preserves the shared quantity snapshot")
check(E.eq(world.state, before), "Failure preserves costs, batches and budget") check(E.eq(world.state, before), "Failure preserves costs, batches and budget")
world.state.amount.S = 10 world.state.amount.S = 10
physical = world.ledger.physical_amounts(world.state)
var old_batches: Array = world.state.batches.H var old_batches: Array = world.state.batches.H
check(world.production.try_production(world.state, world.catalog.entities.N, 2, true), "Funded production succeeds") check(world.production.try_production(world.state, world.catalog.entities.N, 2, true, physical), "Funded production succeeds")
check(E.eq(physical, world.ledger.physical_amounts(world.state)), "Successful production updates exact totals without changing bound-resource totals")
check((E.eq(world.state.amount.H, 6) and E.eq(world.state.amount.S, 4)), "Exact cost and binding") check((E.eq(world.state.amount.H, 6) and E.eq(world.state.amount.S, 4)), "Exact cost and binding")
check(E.eq(world.state.auto_resources.H, 6), "Exact automatic budget") check(E.eq(world.state.auto_resources.H, 6), "Exact automatic budget")
check((E.eq(old_batches[0].amount, 10) and E.eq(world.state.batches.H[0].amount, 6)), "Consumed batches isolated") check((E.eq(old_batches[0].amount, 10) and E.eq(world.state.batches.H[0].amount, 6)), "Consumed batches isolated")
+3 -2
View File
@@ -116,7 +116,8 @@ func produce_for_period(previous: Dictionary, target: Dictionary, quantities: Di
var order: Array = candidates.keys() var order: Array = candidates.keys()
order.sort_custom(func ( a : String , b : String ) -> bool : return (E.lt(candidates[a], candidates[b]) if E.ne(candidates[a], candidates[b]) else E.lt(a, b))) order.sort_custom(func ( a : String , b : String ) -> bool : return (E.lt(candidates[a], candidates[b]) if E.ne(candidates[a], candidates[b]) else E.lt(a, b)))
var created: Dictionary = {} var created: Dictionary = {}
var metallicity: Variant = world.environment.stellar_environment(target).metallicity var current_quantities: Dictionary = world.ledger.physical_amounts(target)
var metallicity: Variant = world.environment.stellar_environment(target, current_quantities).metallicity
var total_created: Variant = 0 var total_created: Variant = 0
for id in order: for id in order:
var entity: OwniverseEntity = world.catalog.entities[id] var entity: OwniverseEntity = world.catalog.entities[id]
@@ -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)) var actual: Variant = E.minimum(desired, auto_capacity(target, entity, budget))
if E.le(actual, 0): if E.le(actual, 0):
continue continue
if (not world.production.try_production(target, entity, actual, true)): if (not world.production.try_production(target, entity, actual, true, current_quantities)):
continue continue
budget = ordinary_star_budget(target) budget = ordinary_star_budget(target)
created[id] = actual created[id] = actual
+2 -2
View File
@@ -6,9 +6,9 @@ const ETHICS = State.ETHICS
const OPPOSITE = {"m": "l", "l": "m", "w": "d", "d": "w", "c": "b", "b": "c"} const OPPOSITE = {"m": "l", "l": "m", "w": "d", "d": "w", "c": "b", "b": "c"}
# Cache the weighted float worship rate multiplied by the exact population. # 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 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: for id in rules:
if not rules[id].worships: continue if not rules[id].worships: continue
var pop: Dictionary = State.normalize_profile(target.ethics.get(id, {})) var pop: Dictionary = State.normalize_profile(target.ethics.get(id, {}))
+33 -21
View File
@@ -10,7 +10,7 @@ var _generator_key: String = ""
var _generator: Dictionary = {} var _generator: Dictionary = {}
var _solutions: 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 modifiers: Dictionary = Parameters.life_modifiers(target)
var order: float = maxf(1e-12, E.approx(Parameters.value(target, "Order"))) var order: float = maxf(1e-12, E.approx(Parameters.value(target, "Order")))
var stages: Array = rules.keys() var stages: Array = rules.keys()
@@ -18,13 +18,13 @@ func _edges(target: Dictionary, rules: Dictionary) -> Dictionary:
var rates: Dictionary = {} var rates: Dictionary = {}
var spans: Dictionary = {} var spans: Dictionary = {}
var progression: 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) var signature: String = str(modifiers) + ":" + str(order)
for id in stages.duplicate(): for id in stages.duplicate():
if rules[id].ethics.is_empty(): continue if rules[id].ethics.is_empty(): continue
rates[id] = Civ.coefficients(world, rules, id, modifiers) rates[id] = Civ.coefficients(world, rules, id, modifiers)
progression[id] = _can_progress(id, rules[id].next, budget, rules) 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]) signature += id + str(spans[id]) + str(rates[id]) + str(rules[id]) + str(progression[id])
for row: Dictionary in rules[id].ethics.values(): for row: Dictionary in rules[id].ethics.values():
for destination in Q.rule_weights(str(row.fall)): 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 for node in range(e, size, 6): occupation[node] *= factor
return {"mass": mass, "occupation": occupation} 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: for id in rules:
if rules[id].ethics.is_empty() or not target.ethics.has(id): continue if rules[id].ethics.is_empty() or not target.ethics.has(id): continue
var profile: Dictionary = State.normalize_profile(target.ethics[id]) 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 duration: float = years / span
var pair_decay: float = exp(-0.03 * duration) var pair_decay: float = exp(-0.03 * duration)
var opposite_decay: float = exp(-0.02 * 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]) _merge(plans[source], source, rejected, plans[source].profiles[destination])
## Apply one conserved source-by-owner table, then credit each destination once. ## 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 credits: Dictionary = {}
var contributions: Dictionary = {} var contributions: Dictionary = {}
var births: Dictionary = {} var births: Dictionary = {}
var ownership: 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} var space_stages: Dictionary = {"Ct": true}
for id in rules: space_stages[id] = id == "Ct" or str(rules[id].home) != "" for id in rules: space_stages[id] = id == "Ct" or str(rules[id].home) != ""
for source in plans: 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] 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 [] var host_shares: Array = _host_shares(hosts) if not hosts.is_empty() else []
for destination in credits: 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 var bound: Variant = 0
for owner in credits[destination]: for owner in credits[destination]:
var count: Variant = credits[destination][owner] var count: Variant = credits[destination][owner]
@@ -385,7 +392,6 @@ func _commit(target: Dictionary, physical: Dictionary, plans: Dictionary, rules:
bound = E.add(bound, count) bound = E.add(bound, count)
target.amount[destination] = E.subtract(target.amount[destination], bound) target.amount[destination] = E.subtract(target.amount[destination], bound)
if not rules.has(destination): births[destination] = created if not rules.has(destination): births[destination] = created
var final: Dictionary = world.ledger.physical_amounts(target)
for destination in contributions: for destination in contributions:
if not rules.has(destination) or E.le(final.get(destination, 0), 0): continue if not rules.has(destination) or E.le(final.get(destination, 0), 0): continue
var profile: Dictionary = {} 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 for ethic in ETHICS: profile[ethic] += entry.profile[ethic] * ratio
target.ethics[destination] = State.normalize_profile(profile) target.ethics[destination] = State.normalize_profile(profile)
for destination in births: world.batches.append_batch(target, destination, births[destination], year, year) for destination in births: world.batches.append_batch(target, destination, births[destination], year, year)
return final
func _host_shares(hosts: Dictionary) -> Array: func _host_shares(hosts: Dictionary) -> Array:
var total: Variant = Q.sum(hosts) 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)) for value in hosts.values(): result.append(E.ratio_approx(value, total))
return result return result
func _colonize(target: Dictionary, colonies: Dictionary, year: Variant, rules: Dictionary) -> void: func _colonize(target: Dictionary, colonies: Dictionary, year: Variant, rules: Dictionary, physical: Dictionary = {}) -> Dictionary:
if colonies.is_empty(): return if physical.is_empty(): physical = world.ledger.physical_amounts(target)
var physical: Dictionary = world.ledger.physical_amounts(target) if colonies.is_empty(): return physical
var stocks: Dictionary = {} var stocks: Dictionary = {}
for id in ["GG", "AB", "RP", "HP"]: stocks[id] = physical.get(id, 0) for id in ["GG", "AB", "RP", "HP"]: stocks[id] = physical.get(id, 0)
var requests: Dictionary = {} var requests: Dictionary = {}
for id in colonies: requests[id] = colonies[id].counts.get(id, 0) for id in colonies: requests[id] = colonies[id].counts.get(id, 0)
var granted: Dictionary = Q.allocate_profiled(requests, Q.sum(stocks)) var granted: Dictionary = Q.allocate_profiled(requests, Q.sum(stocks))
var total: Variant = Q.sum(granted) 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 taken_resources: Dictionary = Q.allocate_profiled(stocks, total)
var removed_by_owner: Dictionary = {} var removed_by_owner: Dictionary = {}
for resource in taken_resources: 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) removed_by_owner[owner] = E.add(removed_by_owner.get(owner, 0), amount)
var remaining: Dictionary = granted.duplicate() var remaining: Dictionary = granted.duplicate()
var placements: Dictionary = {} var placements: Dictionary = {}
var final: Dictionary = physical.duplicate()
var owners: Array = removed_by_owner.keys() var owners: Array = removed_by_owner.keys()
for i in owners.size(): for i in owners.size():
var owner: String = owners[i] var owner: String = owners[i]
@@ -449,7 +457,8 @@ func _colonize(target: Dictionary, colonies: Dictionary, year: Variant, rules: D
for destination in granted: for destination in granted:
var count: Variant = granted[destination] var count: Variant = granted[destination]
if E.le(count, 0): continue 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 var bound: Variant = 0
for owner in placements.get(destination, {}): for owner in placements.get(destination, {}):
var held: Variant = placements[destination][owner] 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)) 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.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) 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: func evolve(target: Dictionary, from_year: Variant, to_year: Variant, timer: RefCounted = null) -> void:
var keep_alive: RefCounted = self 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. # Bound the Poisson mean, processing the entire requested interval.
var maximum_years: float = years var maximum_years: float = years
var cursor: Variant = from_year var cursor: Variant = from_year
var physical: Dictionary = world.ledger.physical_amounts(target)
while E.lt(cursor, to_year): while E.lt(cursor, to_year):
if timer != null and not await timer.pause(): return 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 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 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 started: int = world.profile_start()
var transaction: Dictionary = _plans(target, physical, rules, generator, E.approx(E.subtract(end, cursor))) var transaction: Dictionary = _plans(target, physical, rules, generator, E.approx(E.subtract(end, cursor)))
world.profile_end("civilization_flow_plan", started) 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) _limit(plans, State.stellar_budget(world, target, rules, physical), rules)
world.profile_end("civilization_flow_capacity", started) world.profile_end("civilization_flow_capacity", started)
started = world.profile_start() started = world.profile_start()
_commit(target, physical, plans, rules, end) physical = _commit(target, physical, plans, rules, end)
world.profile_end("civilization_flow_ledger", started) world.profile_end("civilization_flow_ledger", started)
if timer != null and not await timer.pause(): return if timer != null and not await timer.pause(): return
started = world.profile_start() 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) world.profile_end("civilization_flow_colonies", started)
started = world.profile_start() 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.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) target.civilization_stats.collateral = E.add(target.civilization_stats.get("collateral", 0), damage_total)
State.enforce_capacity(world, target, rules, end) physical = State.enforce_capacity(world, target, rules, end, physical)
State.reconcile(world, target, rules) State.reconcile(world, target, rules, physical)
target.civilization_stats.worship = Civ.worship(world, target, rules) target.civilization_stats.worship = Civ.worship(world, target, rules, physical)
world.profile_end("civilization_flow_finish", started) world.profile_end("civilization_flow_finish", started)
cursor = end cursor = end
+10 -7
View File
@@ -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 # Resolve shortages from the most advanced stages downwards. This gives existing
# advanced populations priority and lets a demotion cascade in the same cycle. # advanced populations priority and lets a demotion cascade in the same cycle.
static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year: Variant) -> void: static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year: Variant, physical: Dictionary = {}) -> Dictionary:
var profile_started: int = world.profile_start() var profile_started: int = world.profile_start()
detach_space_civilizations(target, rules) detach_space_civilizations(target, rules)
var physical: Dictionary = world.ledger.physical_amounts(target) if physical.is_empty(): physical = world.ledger.physical_amounts(target)
else: physical = physical.duplicate()
reconcile(world, target, rules, physical) reconcile(world, target, rules, physical)
var ethical_totals: Dictionary = physical.duplicate() var ethical_totals: Dictionary = physical.duplicate()
var budget: Dictionary = stellar_budget(world, target, rules, physical) 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") var previous: String = (stages[E.subtract(index, 1)] if E.gt(index, 0) else "C1.9")
world.batches.scale_batches(target, id, E.divide(supported, count)) world.batches.scale_batches(target, id, E.divide(supported, count))
target.amount[id] = supported target.amount[id] = supported
world.ledger.credit(target, previous, excess) var credited: Variant = world.ledger.credit(target, previous, excess)
physical[id] = E.maximum(0, supported)
physical[previous] = E.add(physical.get(previous, 0), credited)
world.batches.append_batch(target, previous, excess, year, year) world.batches.append_batch(target, previous, excess, year, year)
var previous_count: Variant = ethical_totals.get(previous, physical.get(previous, target.amount.get(previous, 0))) var previous_count: Variant = ethical_totals.get(previous, physical.get(previous, target.amount.get(previous, 0)))
var previous_profile: Dictionary = target.ethics.get(previous, empty_profile()) var 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. # Capacity losses are proportional, so the surviving source keeps its profile.
if E.eq(previous, "C1.9"): if E.eq(previous, "C1.9"):
var stars: Dictionary = {} var stars: Dictionary = {}
var current_physical: Dictionary = world.ledger.physical_amounts(target) var current_physical: Dictionary = physical
for star in current_physical: for star in current_physical:
if (E.eq(world.catalog.entities[star].event_group, "DwStar") and E.gt(current_physical[star], 0)): if (E.eq(world.catalog.entities[star].event_group, "DwStar") and E.gt(current_physical[star], 0)):
stars[star] = current_physical[star] stars[star] = current_physical[star]
@@ -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.binds[owner][previous] = E.add(target.binds[owner].get(previous, 0), amount)
target.amount[previous] = E.subtract(target.amount[previous], amount) target.amount[previous] = E.subtract(target.amount[previous], amount)
if changed: if changed:
physical = world.ledger.physical_amounts(target)
reconcile(world, target, rules, physical) reconcile(world, target, rules, physical)
assign_homes(world, target, rules, physical) assign_homes(world, target, rules, physical)
world.profile_end("enforce_capacity", profile_started) world.profile_end("enforce_capacity", profile_started)
return physical
# Unity's structure chain: direct Claim resets the required structure count; # Unity's structure chain: direct Claim resets the required structure count;
# intervening stages require ten times more units of the preceding structure. # intervening stages require ten times more units of the preceding structure.
@@ -247,13 +250,13 @@ static func assign_homes(world, target: Dictionary, rules: Dictionary, physical:
world.profile_end("assign_homes", profile_started) world.profile_end("assign_homes", profile_started)
static func effective_span(world, target: Dictionary, id: String, rules: Dictionary) -> Variant: static func effective_span(world, target: Dictionary, id: String, rules: Dictionary, physical: Dictionary = {}) -> Variant:
if (not rules.has(id)): return world.catalog.entities[id].lifespan if (not rules.has(id)): return world.catalog.entities[id].lifespan
var base: Variant = rules[id].span var base: Variant = rules[id].span
if (not is_space_civilization(id, rules)): return base if (not is_space_civilization(id, rules)): return base
var population: Variant = target.amount.get(id, 0) var population: Variant = target.amount.get(id, 0)
if E.le(population, 0): return base if E.le(population, 0): return base
var homes: Dictionary = stage_homes(world, target, id, world.catalog.civilization_layout(rules)) var homes: Dictionary = stage_homes(world, target, id, world.catalog.civilization_layout(rules), physical)
target.civilization_homes[id] = homes target.civilization_homes[id] = homes
var multiplier: float = 1.0 var multiplier: float = 1.0
for home in homes: for home in homes:
+5 -5
View File
@@ -5,13 +5,12 @@ const SimulationTime = preload("res://world/owniverse/simulation_time.gd")
const WorldParameters = preload("res://world/owniverse/world_parameters.gd") const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
const QuantityMath = preload("res://world/owniverse/quantity_math.gd") const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
func initialize_life_batches() -> void: func initialize_life_batches() -> Dictionary:
for id in Global.CivilizationRules: for id in Global.CivilizationRules:
world.state.batches.erase(id) world.state.batches.erase(id)
world.state.batch_origins.erase(id) world.state.batch_origins.erase(id)
CivilizationState.detach_space_civilizations(world.state, Global.CivilizationRules) CivilizationState.detach_space_civilizations(world.state, Global.CivilizationRules)
CivilizationState.enforce_capacity(world, world.state, Global.CivilizationRules, world.state.time.datetime) var physical: Dictionary = CivilizationState.enforce_capacity(world, world.state, Global.CivilizationRules, world.state.time.datetime)
var physical: Dictionary = world.ledger.physical_amounts(world.state)
for id in world.catalog.entities: for id in world.catalog.entities:
if world.catalog.entities[id].life_rule.is_empty(): if world.catalog.entities[id].life_rule.is_empty():
continue continue
@@ -19,6 +18,7 @@ func initialize_life_batches() -> void:
for batch : Dictionary in world.state.batches.get(id, []): for batch : Dictionary in world.state.batches.get(id, []):
tracked = E.add(tracked, batch.amount) tracked = E.add(tracked, batch.amount)
world.batches.append_batch(world.state, id, E.maximum(0, E.subtract(physical.get(id, 0), tracked)), world.state.time.datetime, world.state.time.datetime) world.batches.append_batch(world.state, id, E.maximum(0, E.subtract(physical.get(id, 0), tracked)), world.state.time.datetime, world.state.time.datetime)
return physical
func run_frame(clock: Dictionary, budget: RefCounted) -> void: func run_frame(clock: Dictionary, budget: RefCounted) -> void:
@@ -31,10 +31,11 @@ func evolute_to_datetime(datetime: Variant, clock: Dictionary = {}, budget: RefC
# Reconcile only when simulation advances. # Reconcile only when simulation advances.
# Fractional frame updates cannot change capacity or populations. # Fractional frame updates cannot change capacity or populations.
var processed_year: Variant = SimulationTime.processed_year(datetime) var processed_year: Variant = SimulationTime.processed_year(datetime)
var quantities: Dictionary = {}
if E.gt(processed_year, world.state.time.year): if E.gt(processed_year, world.state.time.year):
if budget != null: budget.call("set_phase", "initialize_life_batches") if budget != null: budget.call("set_phase", "initialize_life_batches")
var profile_initialize: int = world.profile_start() var profile_initialize: int = world.profile_start()
initialize_life_batches() quantities = initialize_life_batches()
world.profile_end("initialize", profile_initialize) world.profile_end("initialize", profile_initialize)
var previous: Dictionary = world.state var previous: Dictionary = world.state
if budget != null: budget.call("set_phase", "deep_copy_and_coarsen") 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 from_year: Variant = previous.time.year
var to_year: Variant = processed_year var to_year: Variant = processed_year
if E.gt(to_year, from_year): if E.gt(to_year, from_year):
var quantities: Dictionary = world.ledger.physical_amounts(previous)
var distribution: Dictionary = QuantityMath.normalize_distribution(quantities) var distribution: Dictionary = QuantityMath.normalize_distribution(quantities)
next.distribution = distribution next.distribution = distribution
+6 -2
View File
@@ -3,7 +3,7 @@ const E = preload("res://world/owniverse/math/exact.gd")
const CivilizationState = preload("res://world/owniverse/civilization_state.gd") const CivilizationState = preload("res://world/owniverse/civilization_state.gd")
const QuantityMath = preload("res://world/owniverse/quantity_math.gd") const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
func try_production(target: Dictionary, entity: OwniverseEntity, amount: Variant, automatic: bool = false) -> bool: func try_production(target: Dictionary, entity: OwniverseEntity, amount: Variant, automatic: bool = false, physical: Dictionary = {}) -> bool:
if (E.le(amount, 0) or E.ne(amount, E.floor_value(amount))): if (E.le(amount, 0) or E.ne(amount, E.floor_value(amount))):
return false return false
var trial: Dictionary = target.duplicate() var trial: Dictionary = target.duplicate()
@@ -13,7 +13,9 @@ func try_production(target: Dictionary, entity: OwniverseEntity, amount: Variant
trial.batches = target.batches.duplicate() trial.batches = target.batches.duplicate()
# Physical object totals do not change while resources are withdrawn from # Physical object totals do not change while resources are withdrawn from
# this transaction. Keep one snapshot instead of scanning every resource. # this transaction. Keep one snapshot instead of scanning every resource.
var physical: Dictionary = world.ledger.physical_amounts(trial) var shared_physical: bool = not physical.is_empty()
if not shared_physical: physical = world.ledger.physical_amounts(trial)
var next_physical: Dictionary = physical.duplicate() if shared_physical else {}
var copied_batches: Dictionary = {} var copied_batches: Dictionary = {}
for resource in entity.cost: for resource in entity.cost:
var cost: Variant = E.round_value(E.multiply(E.maximum(0, entity.cost[resource]), amount)) var cost: Variant = E.round_value(E.multiply(E.maximum(0, entity.cost[resource]), amount))
@@ -28,6 +30,7 @@ func try_production(target: Dictionary, entity: OwniverseEntity, amount: Variant
if automatic: if automatic:
trial.auto_resources[resource] = E.maximum(0, E.subtract(trial.auto_resources.get(resource, 0), cost)) trial.auto_resources[resource] = E.maximum(0, E.subtract(trial.auto_resources.get(resource, 0), cost))
for id in taken: for id in taken:
if shared_physical and next_physical.has(id): next_physical[id] = E.subtract(next_physical[id], taken[id])
# Isolate only batches actually touched by this transaction. # Isolate only batches actually touched by this transaction.
if (trial.batches.has(id) and (not copied_batches.has(id))): if (trial.batches.has(id) and (not copied_batches.has(id))):
trial.batches[id] = trial.batches[id].duplicate(true) trial.batches[id] = trial.batches[id].duplicate(true)
@@ -41,6 +44,7 @@ func try_production(target: Dictionary, entity: OwniverseEntity, amount: Variant
for field in ["amount", "binds", "batches", "auto_resources"]: for field in ["amount", "binds", "batches", "auto_resources"]:
if trial.has(field): if trial.has(field):
target[field] = trial[field] target[field] = trial[field]
if shared_physical: physical.merge(next_physical, true)
return true return true
+2 -2
View File
@@ -2,8 +2,8 @@ extends "res://world/owniverse/simulation_module.gd"
const E = preload("res://world/owniverse/math/exact.gd") const E = preload("res://world/owniverse/math/exact.gd")
const WorldParameters = preload("res://world/owniverse/world_parameters.gd") const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
func stellar_environment(source: Dictionary) -> Dictionary: func stellar_environment(source: Dictionary, quantities: Dictionary = {}) -> Dictionary:
var quantities: Dictionary = world.ledger.physical_amounts(source) if quantities.is_empty(): quantities = world.ledger.physical_amounts(source)
# SMBH is a self-group in GI; its quantity stores mass, not object count. # SMBH is a self-group in GI; its quantity stores mass, not object count.
var mass: Variant = source.amount.get("SMBH", 0) var mass: Variant = source.amount.get("SMBH", 0)
# Geometric volume includes a 1e22 scale per void and can exceed E27. # Geometric volume includes a 1e22 scale per void and can exceed E27.