Refactor civilization flow mechanics: enhance commit and colonization logic, add tests for flow application and resource allocation, and optimize performance metrics.

This commit is contained in:
Kirill
2026-10-08 20:43:51 +03:00
parent 413f2be904
commit 9357f757a9
7 changed files with 946 additions and 32 deletions
+4
View File
@@ -139,6 +139,10 @@ HP деградирует в RP. У эонов возможен откат к п
Колонизация агрегирует запросы по стадии назначения (на две стадии ниже отправителя), ограничивает их общим запасом GG, AB, RP и HP и заменяет ровно выделенное число планет. Целые колонии сохраняют владельца целевой планеты; состав получает дрейф 960/4/9. Исходное население не расходуется. Колонизация агрегирует запросы по стадии назначения (на две стадии ниже отправителя), ограничивает их общим запасом GG, AB, RP и HP и заменяет ровно выделенное число планет. Целые колонии сохраняют владельца целевой планеты; состав получает дрейф 960/4/9. Исходное население не расходуется.
Применение потоков сначала строит таблицу исходных владельцев. Для каждой исходной стадии готовые целые квоты распределяются по строкам владельцев; последняя строка получает точный остаток. Стадии с единственным свободным запасом применяют готовые квоты напрямую. Поступления объединяются по стадии и владельцу; физическая выдача и статистика создания выполняются один раз на получателя. Ct всегда остаётся свободной от привязок.
При колонизации планеты сначала списываются по типам и владельцам, затем суммарное списание каждого владельца распределяется между стадиями колоний. Каждая стадия получает одну суммарную выдачу. Общие целые квоты, число списанных планет и количество колоний сохраняются точно. Из-за объединения распределений единичные остатки между владельцами могут отличаться от прежнего последовательного порядка; вероятности переходов и формулы этик не меняются.
Космические стадии C2.0–C4.4 свободны от планетных привязок. Общие бюджеты звёздных семейств и структур учитывают уже занятые резервы. Неподдерживаемый промежуточный переход блокируется в операторе; конечные положительные запросы делят свободную ёмкость одновременно. Отклонённые количества возвращаются к источнику. Освободившаяся при переходах ёмкость доступна следующему шагу. Если внешние изменения уменьшили ёмкость ниже существующего населения, избыток последовательно деградирует с приоритетом старших стадий. При возвращении в планетные стадии используются реальные карликовые звёзды, при отсутствии хозяина население остаётся свободным. Космические стадии C2.0–C4.4 свободны от планетных привязок. Общие бюджеты звёздных семейств и структур учитывают уже занятые резервы. Неподдерживаемый промежуточный переход блокируется в операторе; конечные положительные запросы делят свободную ёмкость одновременно. Отклонённые количества возвращаются к источнику. Освободившаяся при переходах ёмкость доступна следующему шагу. Если внешние изменения уменьшили ёмкость ниже существующего населения, избыток последовательно деградирует с приоритетом старших стадий. При возвращении в планетные стадии используются реальные карликовые звёзды, при отсутствии хозяина население остаётся свободным.
Порядок полного шага: производство за интервал → непрерывные потоки существующих цивилизаций → поколения звёзд и биологической жизни и бласты → итоговые ограничения среды → тренды. Возникшие из C0.1 цивилизации C0.2 получают равномерный профиль, смешанный с существующим населением, и начинают непрерывную эволюцию со следующего шага. Разные размеры полного шага могут отличаться из-за этого порядка, ограничений среды и нелинейных эффектов; чистая непрерывная цепочка без ограничений согласована при разделении интервала. Порядок полного шага: производство за интервал → непрерывные потоки существующих цивилизаций → поколения звёзд и биологической жизни и бласты → итоговые ограничения среды → тренды. Возникшие из C0.1 цивилизации C0.2 получают равномерный профиль, смешанный с существующим населением, и начинают непрерывную эволюцию со следующего шага. Разные размеры полного шага могут отличаться из-за этого порядка, ограничений среды и нелинейных эффектов; чистая непрерывная цепочка без ограничений согласована при разделении интервала.
+6
View File
@@ -238,6 +238,12 @@ godot --path Owniverse.GI --script res://tests/benchmark_civilizations.gd -- ui
## Расчёт между кадрами ## Расчёт между кадрами
`test_flow_commit.gd` проверяет консолидированное применение переходов и колонизации: точные квоты получателей, сохранение количества у каждого владельца, профили этик, статистику создания и отсутствие привязок у Ct. Проверки используют несколько владельцев и численность 103, 10^40 + 103 и 10^400 + 103.
```sh
godot --headless --path Owniverse.GI --script res://tests/test_flow_commit.gd
```
`godot --headless --path Owniverse.GI --script res://tests/test_frame_evolution.gd` `godot --headless --path Owniverse.GI --script res://tests/test_frame_evolution.gd`
Stage вызывает `evolute_frame`: транзакция получает бюджет 40 мс и уступает Stage вызывает `evolute_frame`: транзакция получает бюджет 40 мс и уступает
@@ -379,3 +379,36 @@ godot --headless --path . --script res://tests/benchmark_large_star_cohorts.gd -
```sh ```sh
godot --headless --path . --script res://tests/benchmark_many_objects.gd -- cycles=40 repeats=2 godot --headless --path . --script res://tests/benchmark_many_objects.gd -- cycles=40 repeats=2
``` ```
## Консолидация применения потоков и колонизации — 2026-10-08
Первый этап: исходное население распределяется по строкам владельцев одной таблицы переходов. Свободные стадии применяют готовые целые квоты напрямую. При колонизации списания планет объединяются по владельцам; каждая стадия колоний получает одну суммарную выдачу. Общие целые количества и статистика сохранены. Изменён порядок распределения единичных остатков между владельцами; полного совпадения состояния с прежним порядком округления не заявляется. Ct остаётся свободной от привязок.
До и после загружались одни и те же диагностические файлы с теми же количествами и партиями. Скорость 6, полный интервал 0,2 секунды (20 000 игровых лет), CPU-профилирование включено, UI отсутствует. По два прогона из исходного состояния, каждый по 20 циклов: до → после → после → до. На каждую модель и сохранение обработано 800 000 игровых лет. Валидация начальных и конечных состояний прошла; SHA-256 входных файлов до и после совпали. Сохранение игрока не использовалось.
| Сохранение | До, мс | После, мс | Сокращение времени | Полных циклов/с после |
|---|---:|---:|---:|---:|
| Все объекты | 385,263 | 377,222 | 2,09% | 2,651 |
| Много объектов и партий | 506,888 | 443,303 | 12,54% | 2,256 |
Для тяжёлого сохранения:
| Участок | До, мс/цикл | После, мс/цикл |
|---|---:|---:|
| Применение переходов | 113,011 | 91,591 |
| Колонизация | 53,487 | 11,520 |
| Подготовка потоков | 114,808 | 114,742 |
| Поколения звёзд и жизни | 116,085 | 115,999 |
Первый этап сокращает полный тяжёлый цикл на 12.54%, но цель пяти циклов в секунду не достигнута. До 200 мс нужно ещё примерно 2.22-кратное ускорение. Следующие крупные затраты — подготовка цивилизационных потоков, поколения и оставшееся применение переходов.
Проверки: целочисленные квоты и владельцы, включая 10^400 + 103 — 1068; цивилизационные потоки — 1895; состояние цивилизаций — 70; кадровый планировщик — 38. Всего 3071 проверка без ошибок. Проверены сохранение населения каждого владельца при планетных переходах, точные поступления по стадиям, замена планет колониями, статистика и Ct.
Полные профили, отдельные прогоны и контрольные суммы: `flow_commit_step1_results.json`. Базовая версия: `413f2be9044090c878c2909dd9d0646f77513a5f`.
Для замера текущей версии на тех же входных файлах:
```sh
godot --headless --path . --script res://tests/benchmark_civilization_flows.gd -- res://tests/artifacts/all_objects_speed6.adventure.json 0.2 profile cycles=20
godot --headless --path . --script res://tests/benchmark_civilization_flows.gd -- res://tests/artifacts/many_objects_fragmented_stars_cycle.adventure.json 0.2 profile cycles=20
```
@@ -0,0 +1,763 @@
{
"delta_seconds": 0.2,
"input_sha256": {
"all_objects_speed6": "44bee3b3753fa51615f766d8f800a10cc2b70c8e7fb4ea12b6e18da7f07a7d67",
"many_objects_fragmented_stars_cycle": "1e95ef398923f26b912a305c84f9b3eb502d24cb14e8ae687a4becccf047324a"
},
"order": "before-after-after-before",
"profile_enabled": true,
"runs": [
{
"cycles": 20,
"max_ms": 407.447,
"mean_ms": 382.1863,
"min_ms": 338.878,
"model": "before",
"profile": {
"assign_homes": [
23607,
80
],
"civilization_flow_capacity": [
119827,
20
],
"civilization_flow_colonies": [
100257,
20
],
"civilization_flow_finish": [
167710,
20
],
"civilization_flow_ledger": [
1031564,
20
],
"civilization_flow_plan": [
2148765,
20
],
"civilization_operator_build": [
60291,
20
],
"civilization_operator_solve": [
665230,
860
],
"enforce_capacity": [
338749,
80
],
"generation_products": [
187536,
40
],
"generation_withdrawal": [
520136,
60
],
"generations": [
2614358,
20
],
"initialize": [
112213,
20
],
"newborn_batches": [
118460,
3016
],
"physical_amounts_build": [
478063,
360
],
"production": [
537724,
20
],
"trends": [
349106,
20
]
},
"run": 1,
"save": "all_objects_speed6",
"validation": "",
"years": 400000
},
{
"cycles": 20,
"max_ms": 406.895,
"mean_ms": 376.23395,
"min_ms": 324.14,
"model": "after",
"profile": {
"assign_homes": [
23735,
80
],
"civilization_flow_capacity": [
120538,
20
],
"civilization_flow_colonies": [
50071,
20
],
"civilization_flow_finish": [
169132,
20
],
"civilization_flow_ledger": [
892018,
20
],
"civilization_flow_plan": [
2165803,
20
],
"civilization_operator_build": [
60679,
20
],
"civilization_operator_solve": [
670541,
860
],
"enforce_capacity": [
340289,
80
],
"generation_products": [
188663,
40
],
"generation_withdrawal": [
534195,
60
],
"generations": [
2649493,
20
],
"initialize": [
113482,
20
],
"newborn_batches": [
118902,
3016
],
"physical_amounts_build": [
489793,
360
],
"production": [
548119,
20
],
"trends": [
350991,
20
]
},
"run": 1,
"save": "all_objects_speed6",
"validation": "",
"years": 400000
},
{
"cycles": 20,
"max_ms": 413.636,
"mean_ms": 378.2097,
"min_ms": 326.044,
"model": "after",
"profile": {
"assign_homes": [
23901,
80
],
"civilization_flow_capacity": [
121116,
20
],
"civilization_flow_colonies": [
50174,
20
],
"civilization_flow_finish": [
170232,
20
],
"civilization_flow_ledger": [
899208,
20
],
"civilization_flow_plan": [
2173515,
20
],
"civilization_operator_build": [
61133,
20
],
"civilization_operator_solve": [
672190,
860
],
"enforce_capacity": [
341889,
80
],
"generation_products": [
190021,
40
],
"generation_withdrawal": [
537089,
60
],
"generations": [
2668071,
20
],
"initialize": [
113520,
20
],
"newborn_batches": [
120186,
3016
],
"physical_amounts_build": [
491086,
360
],
"production": [
549548,
20
],
"trends": [
352056,
20
]
},
"run": 2,
"save": "all_objects_speed6",
"validation": "",
"years": 400000
},
{
"cycles": 20,
"max_ms": 410.851,
"mean_ms": 388.3392,
"min_ms": 346.936,
"model": "before",
"profile": {
"assign_homes": [
23780,
80
],
"civilization_flow_capacity": [
121258,
20
],
"civilization_flow_colonies": [
101386,
20
],
"civilization_flow_finish": [
171098,
20
],
"civilization_flow_ledger": [
1048869,
20
],
"civilization_flow_plan": [
2182234,
20
],
"civilization_operator_build": [
61215,
20
],
"civilization_operator_solve": [
676192,
860
],
"enforce_capacity": [
343586,
80
],
"generation_products": [
191018,
40
],
"generation_withdrawal": [
528628,
60
],
"generations": [
2654320,
20
],
"initialize": [
113911,
20
],
"newborn_batches": [
120518,
3016
],
"physical_amounts_build": [
485368,
360
],
"production": [
550201,
20
],
"trends": [
354035,
20
]
},
"run": 2,
"save": "all_objects_speed6",
"validation": "",
"years": 400000
},
{
"cycles": 20,
"max_ms": 540.275,
"mean_ms": 504.8664,
"min_ms": 445.592,
"model": "before",
"profile": {
"assign_homes": [
17920,
60
],
"civilization_flow_capacity": [
136151,
20
],
"civilization_flow_colonies": [
1064307,
20
],
"civilization_flow_finish": [
195983,
20
],
"civilization_flow_ledger": [
2251097,
20
],
"civilization_flow_plan": [
2284423,
20
],
"civilization_operator_build": [
62124,
20
],
"civilization_operator_solve": [
678656,
860
],
"enforce_capacity": [
288140,
60
],
"generation_products": [
77749,
20
],
"generation_withdrawal": [
795116,
40
],
"generations": [
2321187,
20
],
"initialize": [
140958,
20
],
"newborn_batches": [
54878,
1490
],
"physical_amounts_build": [
746203,
300
],
"production": [
744868,
20
],
"trends": [
355994,
20
]
},
"run": 1,
"save": "many_objects_fragmented_stars_cycle",
"validation": "",
"years": 400000
},
{
"cycles": 20,
"max_ms": 462.134,
"mean_ms": 441.9819,
"min_ms": 389.761,
"model": "after",
"profile": {
"assign_homes": [
17865,
60
],
"civilization_flow_capacity": [
137136,
20
],
"civilization_flow_colonies": [
229748,
20
],
"civilization_flow_finish": [
196611,
20
],
"civilization_flow_ledger": [
1828223,
20
],
"civilization_flow_plan": [
2286989,
20
],
"civilization_operator_build": [
62177,
20
],
"civilization_operator_solve": [
678566,
860
],
"enforce_capacity": [
288805,
60
],
"generation_products": [
78203,
20
],
"generation_withdrawal": [
805143,
40
],
"generations": [
2312257,
20
],
"initialize": [
141951,
20
],
"newborn_batches": [
55057,
1490
],
"physical_amounts_build": [
746690,
300
],
"production": [
749636,
20
],
"trends": [
355174,
20
]
},
"run": 1,
"save": "many_objects_fragmented_stars_cycle",
"validation": "",
"years": 400000
},
{
"cycles": 20,
"max_ms": 467.425,
"mean_ms": 444.6246,
"min_ms": 389.196,
"model": "after",
"profile": {
"assign_homes": [
18039,
60
],
"civilization_flow_capacity": [
137286,
20
],
"civilization_flow_colonies": [
231059,
20
],
"civilization_flow_finish": [
197868,
20
],
"civilization_flow_ledger": [
1835420,
20
],
"civilization_flow_plan": [
2302705,
20
],
"civilization_operator_build": [
62638,
20
],
"civilization_operator_solve": [
680863,
860
],
"enforce_capacity": [
289856,
60
],
"generation_products": [
78541,
20
],
"generation_withdrawal": [
808144,
40
],
"generations": [
2327718,
20
],
"initialize": [
142049,
20
],
"newborn_batches": [
55184,
1490
],
"physical_amounts_build": [
752516,
300
],
"production": [
752648,
20
],
"trends": [
358218,
20
]
},
"run": 2,
"save": "many_objects_fragmented_stars_cycle",
"validation": "",
"years": 400000
},
{
"cycles": 20,
"max_ms": 532.382,
"mean_ms": 508.9093,
"min_ms": 451.042,
"model": "before",
"profile": {
"assign_homes": [
18208,
60
],
"civilization_flow_capacity": [
137370,
20
],
"civilization_flow_colonies": [
1075170,
20
],
"civilization_flow_finish": [
198272,
20
],
"civilization_flow_ledger": [
2269355,
20
],
"civilization_flow_plan": [
2307886,
20
],
"civilization_operator_build": [
62940,
20
],
"civilization_operator_solve": [
683671,
860
],
"enforce_capacity": [
291171,
60
],
"generation_products": [
78498,
20
],
"generation_withdrawal": [
807187,
40
],
"generations": [
2322215,
20
],
"initialize": [
143015,
20
],
"newborn_batches": [
55362,
1490
],
"physical_amounts_build": [
749843,
300
],
"production": [
753597,
20
],
"trends": [
360333,
20
]
},
"run": 2,
"save": "many_objects_fragmented_stars_cycle",
"validation": "",
"years": 400000
}
],
"speed": 6,
"reference_commit": "413f2be9044090c878c2909dd9d0646f77513a5f",
"reference_script_sha256": "b2a04cc50ad1e77b45762264846cb546b013b09f4c204ba22e6472a38728e5b7",
"candidate_script_sha256": "cbc65eaf4ab0e67fb9d6d5d7ad11757bdeb5f0b1e0ffb2f2631bfe3dcd9e962e",
"summary": {
"all_objects_speed6": {
"before": {
"mean_ms": 385.26275,
"cycles_per_second": 2.5956311634073113,
"profile_ms": {
"assign_homes": 1.184675,
"civilization_flow_capacity": 6.027125,
"civilization_flow_colonies": 5.041075,
"civilization_flow_finish": 8.4702,
"civilization_flow_ledger": 52.010825,
"civilization_flow_plan": 108.27497500000001,
"civilization_operator_build": 3.03765,
"civilization_operator_solve": 33.53555,
"enforce_capacity": 17.058375,
"generation_products": 9.46385,
"generation_withdrawal": 26.219099999999997,
"generations": 131.71695000000003,
"initialize": 5.6531,
"newborn_batches": 5.97445,
"physical_amounts_build": 24.085775,
"production": 27.198125,
"trends": 17.578525000000003
}
},
"after": {
"mean_ms": 377.22182499999997,
"cycles_per_second": 2.6509600816442687,
"profile_ms": {
"assign_homes": 1.1909,
"civilization_flow_capacity": 6.04135,
"civilization_flow_colonies": 2.506125,
"civilization_flow_finish": 8.4841,
"civilization_flow_ledger": 44.78065,
"civilization_flow_plan": 108.48295,
"civilization_operator_build": 3.0453,
"civilization_operator_solve": 33.568275,
"enforce_capacity": 17.05445,
"generation_products": 9.4671,
"generation_withdrawal": 26.7821,
"generations": 132.9391,
"initialize": 5.675050000000001,
"newborn_batches": 5.9772,
"physical_amounts_build": 24.521974999999998,
"production": 27.441675,
"trends": 17.576175
}
},
"reduction_percent": 2.0871275512621024
},
"many_objects_fragmented_stars_cycle": {
"before": {
"mean_ms": 506.88784999999996,
"cycles_per_second": 1.9728229824407906,
"profile_ms": {
"assign_homes": 0.9032,
"civilization_flow_capacity": 6.838025,
"civilization_flow_colonies": 53.486925,
"civilization_flow_finish": 9.856375,
"civilization_flow_ledger": 113.0113,
"civilization_flow_plan": 114.807725,
"civilization_operator_build": 3.1266,
"civilization_operator_solve": 34.058175000000006,
"enforce_capacity": 14.482775,
"generation_products": 3.906175,
"generation_withdrawal": 40.057575,
"generations": 116.08505000000001,
"initialize": 7.099324999999999,
"newborn_batches": 2.756,
"physical_amounts_build": 37.40115,
"production": 37.461625,
"trends": 17.908175
}
},
"after": {
"mean_ms": 443.30325,
"cycles_per_second": 2.255792169355853,
"profile_ms": {
"assign_homes": 0.8976000000000001,
"civilization_flow_capacity": 6.86055,
"civilization_flow_colonies": 11.520175,
"civilization_flow_finish": 9.861975000000001,
"civilization_flow_ledger": 91.591075,
"civilization_flow_plan": 114.74235,
"civilization_operator_build": 3.120375,
"civilization_operator_solve": 33.985724999999995,
"enforce_capacity": 14.466524999999999,
"generation_products": 3.9186,
"generation_withdrawal": 40.332175,
"generations": 115.999375,
"initialize": 7.1,
"newborn_batches": 2.756025,
"physical_amounts_build": 37.48015,
"production": 37.5571,
"trends": 17.834799999999998
}
},
"reduction_percent": 12.544116020930463
}
}
}
+61
View File
@@ -0,0 +1,61 @@
extends "res://tests/test_civilization_evolution.gd"
func run():
var rules: Dictionary = root.get_node("Global").CivilizationRules
for count in [103, E.of("10000000000000000000000000000000000000103"), E.add(E.of("1e400"), 103)]:
var world = fixture()
var third: Variant = E.floor_value(E.divide(count, 3))
var free: Variant = E.subtract(count, E.multiply(third, 2))
world.state.amount["C0.2"] = free
world.state.binds.RD = {"C0.2": third}
world.state.binds.OD = {"C0.2": third}
var counts: Dictionary = {"C0.2": third, "C0.3": third, "HP": free}
var profile: Dictionary = {"m": 0.4, "l": 0.2, "w": 0.1, "d": 0.1, "c": 0.1, "b": 0.1}
var profiles: Dictionary = {}
for id in counts: profiles[id] = profile
var physical: Dictionary = world.ledger.physical_amounts(world.state)
world.civilization_flow._commit(world.state, physical, {"C0.2": {"counts": counts, "profiles": profiles}}, rules, 100)
for id in counts:
check(E.eq(world.ledger.physical_amount(world.state, id), counts[id]), "Commit preserves exact destination quota: " + id)
for owner in ["RD", "OD"]:
var held: Variant = 0
for id in counts: held = E.add(held, world.state.binds[owner].get(id, 0))
check(E.eq(held, third), "Commit preserves each owner's exact population")
for id in ["C0.2", "C0.3"]:
for ethic in profile: check(absf(world.state.ethics[id][ethic] - profile[ethic]) < 1e-12, "Commit preserves incoming ethical profile")
invariants(world)
dispose(world)
world = fixture()
world.state.binds.RD = {"C4.4": count}
var terminal: Dictionary = {"Ct": State.empty_profile()}
world.civilization_flow._commit(world.state, world.ledger.physical_amounts(world.state), {"C4.4": {"counts": {"Ct": count}, "profiles": terminal}}, rules, 100)
check(E.eq(world.state.amount.Ct, count), "Transcendent population is entirely free")
check(E.eq(world.state.binds.RD.get("Ct", 0), 0), "Transcendent population cannot retain a stellar owner")
dispose(world)
world = fixture()
var original_owners: Dictionary = {}
for owner in ["RD", "OD", "YD", "WYD"]:
world.state.binds[owner] = {}
for resource in ["GG", "AB", "RP", "HP"]:
world.state.binds[owner][resource] = count
original_owners[owner] = E.multiply(count, 4)
var colonies: Dictionary = {
"C1.4": {"counts": {"C1.4": E.multiply(count, 3)}, "profiles": {"C1.4": profile}},
"C1.5": {"counts": {"C1.5": E.multiply(count, 5)}, "profiles": {"C1.5": profile}}
}
world.civilization_flow._colonize(world.state, colonies, 100, rules)
check(E.eq(world.ledger.physical_amount(world.state, "C1.4"), E.multiply(count, 3)), "Colonization credits exact first quota")
check(E.eq(world.ledger.physical_amount(world.state, "C1.5"), E.multiply(count, 5)), "Colonization credits exact second quota")
check(E.eq(world.state.stats.total_amount["C1.4"], E.multiply(count, 3)), "Consolidated credit preserves creation statistics")
check(E.eq(world.state.civilization_stats.colonies, E.multiply(count, 8)), "Colonization statistics equal exact granted total")
for owner in original_owners:
var held: Variant = 0
for id in ["GG", "AB", "RP", "HP", "C1.4", "C1.5"]: held = E.add(held, world.state.binds[owner].get(id, 0))
check(E.eq(held, original_owners[owner]), "Colonization preserves each owner's occupied worlds")
var remaining_planets: Variant = 0
for id in ["GG", "AB", "RP", "HP"]: remaining_planets = E.add(remaining_planets, world.ledger.physical_amount(world.state, id))
check(E.eq(remaining_planets, E.multiply(count, 8)), "Planet loss equals colonies created")
invariants(world)
dispose(world)
print("Flow commit: %d checks, %d failures" % [checks, failures])
quit(1 if failures else 0)
+1
View File
@@ -0,0 +1 @@
uid://bfgdsxoycr20e
+78 -32
View File
@@ -306,53 +306,84 @@ func _limit(plans: Dictionary, budget: Dictionary, rules: Dictionary) -> void:
plans[source].counts[destination] = allowed plans[source].counts[destination] = allowed
_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.
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) -> void:
var credits: Dictionary = {} var credits: Dictionary = {}
var contributions: Dictionary = {} var contributions: Dictionary = {}
var births: Dictionary = {} var births: Dictionary = {}
var ownership: Dictionary = {}
var space_stages: Dictionary = {"Ct": true}
for id in rules: space_stages[id] = id == "Ct" or str(rules[id].home) != ""
for source in plans:
ownership[source] = {}
var free: Variant = target.amount.get(source, 0)
if E.gt(free, 0): ownership[source][""] = free
for owner in target.binds:
for source in target.binds[owner]:
if not ownership.has(source): continue
var held: Variant = target.binds[owner][source]
if E.gt(held, 0): ownership[source][owner] = held
for source in plans: for source in plans:
var plan: Dictionary = plans[source] var plan: Dictionary = plans[source]
var slots: Dictionary = {"": target.amount.get(source, 0)} var remaining: Dictionary = {}
for owner in target.binds:
var held: Variant = target.binds[owner].get(source, 0)
if E.gt(held, 0): slots[owner] = held
for destination in plan.counts: for destination in plan.counts:
var amount: Variant = plan.counts[destination] var amount: Variant = plan.counts[destination]
if E.le(amount, 0): continue if E.le(amount, 0): continue
remaining[destination] = amount
if not contributions.has(destination): contributions[destination] = [] if not contributions.has(destination): contributions[destination] = []
contributions[destination].append({"amount": amount, "profile": plan.profiles[destination]}) contributions[destination].append({"amount": amount, "profile": plan.profiles[destination]})
if destination == source: continue var slots: Dictionary = ownership[source]
var taken: Dictionary = Q.allocate_profiled(slots, amount) target.amount[source] = 0
for owner in taken: var owners: Array = slots.keys()
if E.le(taken[owner], 0): continue if owners.size() == 1 and owners[0] == "":
slots[owner] = E.subtract(slots[owner], taken[owner]) target.amount[source] = remaining.get(source, 0)
var host: String = owner for destination in remaining:
if State.is_space_civilization(destination, rules) or destination == "Ct": host = "" if destination == source: continue
elif owner == "" and State.is_space_civilization(source, rules): host = "@host" var host: String = "@host" if space_stages.get(source, false) and not space_stages.get(destination, false) else ""
if not credits.has(destination): credits[destination] = {} if not credits.has(destination): credits[destination] = {}
credits[destination][host] = E.add(credits[destination].get(host, 0), taken[owner]) credits[destination][host] = E.add(credits[destination].get(host, 0), remaining[destination])
target.amount[source] = slots[""] continue
for owner in slots: for i in owners.size():
if owner != "": target.binds[owner][source] = slots[owner] var owner: String = owners[i]
# Last row receives the remaining exact quotas; the free-only path
# therefore needs no proportional allocation or repeated subtraction.
var allocation: Dictionary = remaining if i == owners.size() - 1 else Q.allocate_profiled(remaining, slots[owner])
for destination in allocation:
var amount: Variant = allocation[destination]
if E.le(amount, 0): continue
if i != owners.size() - 1:
remaining[destination] = E.subtract(remaining[destination], amount)
if destination == source:
if owner == "": target.amount[source] = amount
else: target.binds[owner][source] = amount
continue
var host: String = owner
if space_stages.get(destination, false): host = ""
elif owner == "" and space_stages.get(source, false): host = "@host"
if not credits.has(destination): credits[destination] = {}
credits[destination][host] = E.add(credits[destination].get(host, 0), amount)
if owner != "" and E.le(allocation.get(source, 0), 0): target.binds[owner][source] = 0
var hosts: Dictionary = {} var hosts: Dictionary = {}
for id in physical: for id in physical:
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 []
for destination in credits: for destination in credits:
var created: Variant = world.ledger.credit(target, destination, Q.sum(credits[destination])) var created: Variant = world.ledger.credit(target, destination, Q.sum(credits[destination]))
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]
if owner == "" or E.le(count, 0): continue if owner == "" or E.le(count, 0): continue
if owner == "@host": if owner == "@host":
if hosts.is_empty(): continue if hosts.is_empty(): continue
var ownership: Dictionary = Q.split_profile(target.civilization_remainders, "flow:hosts:" + destination, count, hosts.keys(), _host_shares(hosts)) var distributed: Dictionary = Q.split_profile(target.civilization_remainders, "flow:hosts:" + destination, count, hosts.keys(), host_shares)
for host in ownership: for host in distributed:
if E.le(ownership[host], 0): continue if E.le(distributed[host], 0): continue
if not target.binds.has(host): target.binds[host] = {} if not target.binds.has(host): target.binds[host] = {}
target.binds[host][destination] = E.add(target.binds[host].get(destination, 0), ownership[host]) target.binds[host][destination] = E.add(target.binds[host].get(destination, 0), distributed[host])
target.amount[destination] = E.subtract(target.amount[destination], count)
else: else:
target.binds[owner][destination] = E.add(target.binds[owner].get(destination, 0), count) target.binds[owner][destination] = E.add(target.binds[owner].get(destination, 0), count)
target.amount[destination] = E.subtract(target.amount[destination], count) bound = E.add(bound, count)
target.amount[destination] = E.subtract(target.amount[destination], bound)
if not rules.has(destination): births[destination] = created if not rules.has(destination): births[destination] = created
var final: Dictionary = world.ledger.physical_amounts(target) var final: Dictionary = world.ledger.physical_amounts(target)
for destination in contributions: for destination in contributions:
@@ -387,7 +418,7 @@ func _colonize(target: Dictionary, colonies: Dictionary, year: Variant, rules: D
var total: Variant = Q.sum(granted) var total: Variant = Q.sum(granted)
if E.le(total, 0): return if E.le(total, 0): return
var taken_resources: Dictionary = Q.allocate_profiled(stocks, total) var taken_resources: Dictionary = Q.allocate_profiled(stocks, total)
var remaining: Dictionary = granted.duplicate() var removed_by_owner: Dictionary = {}
for resource in taken_resources: for resource in taken_resources:
var removed: Variant = taken_resources[resource] var removed: Variant = taken_resources[resource]
if E.le(removed, 0): continue if E.le(removed, 0): continue
@@ -401,15 +432,30 @@ func _colonize(target: Dictionary, colonies: Dictionary, year: Variant, rules: D
if E.le(amount, 0): continue if E.le(amount, 0): continue
if owner == "": target.amount[resource] = E.subtract(target.amount[resource], amount) if owner == "": target.amount[resource] = E.subtract(target.amount[resource], amount)
else: target.binds[owner][resource] = E.subtract(target.binds[owner][resource], amount) else: target.binds[owner][resource] = E.subtract(target.binds[owner][resource], amount)
var allocation: Dictionary = Q.allocate_profiled(remaining, amount) removed_by_owner[owner] = E.add(removed_by_owner.get(owner, 0), amount)
for destination in allocation: var remaining: Dictionary = granted.duplicate()
var count: Variant = allocation[destination] var placements: Dictionary = {}
if E.le(count, 0): continue var owners: Array = removed_by_owner.keys()
remaining[destination] = E.subtract(remaining[destination], count) for i in owners.size():
var created: Variant = world.ledger.credit(target, destination, count) var owner: String = owners[i]
if owner != "" and not State.is_space_civilization(destination, rules): var allocation: Dictionary = remaining if i == owners.size() - 1 else Q.allocate_profiled(remaining, removed_by_owner[owner])
target.amount[destination] = E.subtract(target.amount[destination], created) for destination in allocation:
target.binds[owner][destination] = E.add(target.binds[owner].get(destination, 0), created) var count: Variant = allocation[destination]
if E.le(count, 0): continue
if i != owners.size() - 1: remaining[destination] = E.subtract(remaining[destination], count)
if owner == "" or State.is_space_civilization(destination, rules): continue
if not placements.has(destination): placements[destination] = {}
placements[destination][owner] = count
for destination in granted:
var count: Variant = granted[destination]
if E.le(count, 0): continue
world.ledger.credit(target, destination, count)
var bound: Variant = 0
for owner in placements.get(destination, {}):
var held: Variant = placements[destination][owner]
target.binds[owner][destination] = E.add(target.binds[owner].get(destination, 0), held)
bound = E.add(bound, held)
target.amount[destination] = E.subtract(target.amount[destination], bound)
for id in granted: for id in granted:
if E.le(granted[id], 0): continue if E.le(granted[id], 0): continue
var profile: Dictionary = colonies[id].profiles[id] var profile: Dictionary = colonies[id].profiles[id]