Refactor civilization evolution and state management

- Enhanced the `civilization_evolution.gd` script to improve population handling and resource allocation during civilization transitions.
- Introduced new functions in `civilization_state.gd` for profile normalization and merging, ensuring consistent population representation across stages.
- Updated `evolution_coordinator.gd` to incorporate budget phases for better tracking of simulation performance.
- Improved `frame_budget.gd` to monitor maximum slice durations and phases during processing.
- Added a new benchmarking tool in `android_benchmark.gd` to measure performance metrics during simulation runs, including frame processing times and throughput.
- Created associated scene and resource files for the benchmarking tool to facilitate testing and performance evaluation.
This commit is contained in:
Kirill
2026-10-06 23:02:42 +03:00
parent e48d4bbe88
commit 6ee66a627d
23 changed files with 751 additions and 3009 deletions
+91 -33
View File
@@ -58,7 +58,7 @@ static func drift(target: Dictionary, key: String, pop: Dictionary, rows: Dictio
weights.append(E.maximum(0, E.subtract(E.add(common, E.multiply(contributions[ETHICS.find(ethic)], 951 if colony else 195)), E.multiply(contributions[ETHICS.find(OPPOSITE[ethic])], 5 if colony else 1))))
if E.le(QuantityMath.sum_array(weights), 0): return pop.duplicate()
if not target.has("civilization_remainders"): target.civilization_remainders = {}
return QuantityMath.split_arrays(target.civilization_remainders, key, population, ETHICS, weights, 0)
return QuantityMath.split_arrays(target.civilization_remainders, key, population, ETHICS, weights, 0, true)
static func diplomacy_rates(rows: Dictionary) -> Array:
var signature: String = ""
@@ -188,18 +188,20 @@ static func consume_untriggered(world, target: Dictionary, plan: Dictionary, pop
# Keeping the summation order preserves float fractional rounding exactly.
static func worship(world, target: Dictionary, rules: Dictionary) -> Variant:
var result: Variant = 0
var physical: Dictionary = world.ledger.physical_amounts(target)
for id in rules:
if not rules[id].worships: continue
var pop: Dictionary = target.ethics[id]
var pop: Dictionary = State.normalize_profile(target.ethics.get(id, {}))
var coefficients: Dictionary = {}
for ethic in pop: coefficients[ethic] = rules[id].ethics[ethic].worship
var cached: Dictionary = world.ledger.worship_products.get(id, {})
if cached.is_empty() or not E.eq(cached.population, pop) or not E.eq(cached.coefficients, coefficients):
var products: Dictionary = {}
for ethic in pop: products[ethic] = E.multiply(pop[ethic], E.of(coefficients[ethic]))
cached = {"population": pop.duplicate(), "coefficients": coefficients, "products": products}
var count: Variant = physical.get(id, 0)
if cached.is_empty() or not E.eq(cached.population, pop) or not E.eq(cached.coefficients, coefficients) or not E.eq(cached.count, count):
var worship_rate: float = 0.0
for ethic in pop: worship_rate += (0.0 + pop[ethic]) * E.approx(E.of(coefficients[ethic]))
cached = {"population": pop.duplicate(), "coefficients": coefficients, "count": count, "product": E.multiply(count, worship_rate)}
world.ledger.worship_products[id] = cached
for ethic in pop: result = E.add(result, cached.products[ethic])
result = E.add(result, cached.product)
return result
static func coefficients(world, rules: Dictionary, id: String, modifiers: Dictionary) -> Dictionary:
@@ -236,8 +238,13 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: Varian
for id in ids:
reconcile_ids.append(id)
if rules[id].fights and not reconcile_ids.has(rules[id].previous): reconcile_ids.append(rules[id].previous)
if budget_timer != null: budget_timer.call("set_phase", "civilization_reconcile")
State.reconcile(world, target, rules, physical, reconcile_ids)
var remaining: Dictionary = target.ethics.duplicate()
if physical.is_empty(): physical = world.ledger.physical_amounts(target)
var remaining: Dictionary = {}
for id in reconcile_ids:
if rules.has(id):
remaining[id] = State.materialize_profile(target, id, target.ethics.get(id, {}), physical.get(id, 0))
var copied: Dictionary = {}
var plans: Dictionary = {}
var colonies: Array = []
@@ -247,6 +254,7 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: Varian
if (not target.has("civilization_stats")): target.civilization_stats = {}
ids.sort()
for id in ids:
if budget_timer != null: budget_timer.call("set_phase", "civilization_stage:" + id)
if budget_timer != null and not await budget_timer.pause(): return {}
if ((not rules.has(id)) or expired[id].is_empty()): continue
var plan: Dictionary = new_plan(id, year)
@@ -265,6 +273,8 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: Varian
var settlers: Dictionary = {}
var stage_rates: Dictionary = coefficients(world, rules, id, modifiers)
for ethic in triggered:
if budget_timer != null: budget_timer.call("set_phase", "civilization_ethic:" + id)
if budget_timer != null and not await budget_timer.pause(): return {}
var probability: Dictionary = stage_rates[ethic]
var prefix: String = id + ":" + ethic
var pro: Variant = rate(target, prefix + ":pro", triggered[ethic], probability.pro)
@@ -277,11 +287,15 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: Varian
triggered[ethic] = E.subtract(triggered[ethic], E.add(bounded.pro, bounded.deg))
var profile_transitions: int = world.profile_start()
add(triggered, progress(world, target, rules, budget, plan, successes))
if budget_timer != null and not await budget_timer.pause(): return {}
fall(target, plan, rules, failures)
world.profile_end("civilization_transitions", profile_transitions)
if rules[id].fights:
var profile_wars: int = world.profile_start()
for ethic in warriors: warriors[ethic] = E.minimum(warriors[ethic], triggered[ethic])
for ethic in warriors:
if budget_timer != null: budget_timer.call("set_phase", "civilization_war:" + id)
if budget_timer != null and not await budget_timer.pause(): return {}
warriors[ethic] = E.minimum(warriors[ethic], triggered[ethic])
var warriors_total: Variant = total(warriors)
var triggered_total: Variant = total(triggered)
var remaining_total: Variant = total(remaining[id])
@@ -289,30 +303,36 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: Varian
warriors = QuantityMath.allocate_whole_with_total(warriors, wars, warriors_total)
for ethic in warriors: triggered[ethic] = E.subtract(triggered[ethic], warriors[ethic])
triggered_total = E.subtract(triggered_total, wars)
var outcomes: Dictionary = split(target, E.add(id, ":wars"), wars, {"deg": 1, "pro25": 2, "pro50": 3, "pro75": 3})
var win_expected: Variant = E.add(E.add(E.multiply(outcomes.get("pro25", 0), E.of("0.25")), E.multiply(outcomes.get("pro50", 0), E.of("0.5"))), E.multiply(outcomes.get("pro75", 0), E.of("0.75")))
var loss_expected: Variant = E.add(E.add(E.add(E.multiply(outcomes.get("deg", 0), 2), outcomes.get("pro25", 0)), E.multiply(outcomes.get("pro50", 0), E.of("1.5"))), E.multiply(outcomes.get("pro75", 0), 2))
var wins_requested: Variant = rate(target, E.add(id, ":wins"), win_expected, 1)
# Integrate the four equally weighted battle outcomes directly.
# Their expected coefficients are wins=17/36, deaths=1/9,
# total losses=29/18, and collateral=40/3 per battle. Rate
# remainders preserve fractional results between generations.
var wins_requested: Variant = rate(target, E.add(id, ":wins"), wars, E.of("17/36"))
var winners: Dictionary = take(world, warriors, wins_requested, wars)
var winner_total: Variant = E.floor_value(E.minimum(E.maximum(0, wins_requested), wars))
warriors_total = E.subtract(wars, winner_total)
var losses_requested: Variant = rate(target, E.add(id, ":attackers_lost"), E.divide(outcomes.get("deg", 0), order), 1)
var deaths_expected: Variant = E.divide(wars, 9)
var losses_requested: Variant = rate(target, E.add(id, ":attackers_lost"), E.divide(deaths_expected, order), 1)
var attackers_lost: Dictionary = take(world, warriors, losses_requested, warriors_total)
var attackers_lost_total: Variant = E.floor_value(E.minimum(E.maximum(0, losses_requested), warriors_total))
warriors_total = E.subtract(warriors_total, attackers_lost_total)
add(triggered, progress(world, target, rules, budget, plan, winners, winner_total))
triggered_total = total(triggered)
if budget_timer != null and not await budget_timer.pause(): return {}
fall(target, plan, rules, attackers_lost)
remaining_total = total(remaining[id])
var losses: Variant = E.minimum(rate(target, E.add(id, ":loss"), E.divide(loss_expected, order), 1), E.add(triggered_total, remaining_total))
var losses_expected: Variant = E.multiply(wars, E.of("29/18"))
var losses: Variant = E.minimum(rate(target, E.add(id, ":loss"), E.divide(losses_expected, order), 1), E.add(triggered_total, remaining_total))
var sides: Dictionary = QuantityMath.allocate_whole_with_total({"due": triggered_total, "later": remaining_total}, losses, E.add(triggered_total, remaining_total))
var victims: Dictionary = take(world, triggered, sides.due, triggered_total)
triggered_total = E.subtract(triggered_total, sides.due)
add(victims, consume_untriggered(world, target, plan, remaining[id], sides.later, year, remaining_total))
if budget_timer != null and not await budget_timer.pause(): return {}
fall(target, plan, rules, victims)
add(triggered, warriors)
triggered_total = E.add(triggered_total, warriors_total)
var damage: Variant = rate(target, E.add(id, ":collateral"), E.divide((E.add(E.multiply(outcomes.get("pro50", 0), 10), E.multiply(outcomes.get("pro75", 0), 30))), order), 1)
var damage_expected: Variant = E.multiply(wars, E.of("40/3"))
var damage: Variant = rate(target, E.add(id, ":collateral"), E.divide(damage_expected, order), 1)
collateral[rules[id].previous] = E.add(collateral.get(rules[id].previous, 0), damage)
target.civilization_stats.wars = E.add(target.civilization_stats.get("wars", 0), wars)
world.profile_end("wars", profile_wars)
@@ -333,15 +353,19 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: Varian
var sides: Dictionary = QuantityMath.allocate_whole_with_total({"due": retry_total, "later": remaining_total}, amount, E.add(retry_total, remaining_total))
var victims: Dictionary = take(world, retry, sides.due, retry_total)
add(victims, consume_untriggered(world, target, plan, remaining[id], sides.later, year, remaining_total))
if budget_timer != null and not await budget_timer.pause(): return {}
fall(target, plan, rules, victims)
target.civilization_stats.collateral = E.add(target.civilization_stats.get("collateral", 0), total(victims))
var incoming: Dictionary = {}
var planetary: Dictionary = {}
for id in plans:
if budget_timer != null: budget_timer.call("set_phase", "civilization_products:" + id)
if budget_timer != null and not await budget_timer.pause(): return {}
var plan: Dictionary = plans[id]
var products: Dictionary = {}
for destination in plan.outputs:
if budget_timer != null: budget_timer.call("set_phase", "civilization_drift:" + id)
if budget_timer != null and not await budget_timer.pause(): return {}
var pop: Dictionary = plan.outputs[destination]
var count: Variant = total(pop)
if rules.has(destination):
@@ -356,7 +380,7 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: Varian
planetary[destination] = E.add(planetary.get(destination, 0), count)
expired[id] = [{"amount": plan.amount, "from": plan.from, "to": E.maximum(plan.from, plan.to), "products": products}]
world.profile_end("civilization_prepare", profile_started)
return {"active": true, "remaining": remaining, "incoming": incoming, "planetary": planetary, "colonies": colonies}
return {"active": true, "remaining": remaining, "incoming": incoming, "planetary": planetary, "colonies": colonies, "base_physical": physical.duplicate()}
static func finish(world, target: Dictionary, transaction: Dictionary, year: Variant, rules: Dictionary, budget_timer: RefCounted = null) -> void:
var profile_started: int = world.profile_start()
@@ -366,10 +390,15 @@ static func finish(world, target: Dictionary, transaction: Dictionary, year: Var
target.civilization_stats.worship = worship(world, target, rules)
world.profile_end("civilization_finish", profile_started)
return
target.ethics = transaction.remaining
for id in transaction.incoming:
if (not target.ethics.has(id)): target.ethics[id] = {}
add(target.ethics[id], transaction.incoming[id])
var remaining: Dictionary = transaction.remaining
var incoming: Dictionary = transaction.incoming
for id in remaining:
var counts: Dictionary = remaining[id].duplicate()
add(counts, incoming.get(id, {}))
target.ethics[id] = State.profile_from_counts(counts)
for id in incoming:
if not remaining.has(id):
target.ethics[id] = State.merge_profile(target.ethics.get(id, {}), transaction.base_physical.get(id, 0), incoming[id])
# Space civilizations that fall back to planetary stages need a stellar host.
var physical: Dictionary = world.ledger.physical_amounts(target)
var hosts: Dictionary = {}
@@ -399,6 +428,16 @@ static func colonize(world, target: Dictionary, colonies: Array, year: Variant,
var requests: Dictionary = {}
for i in colonies.size(): requests[str(i)] = total(colonies[i].population)
var granted: Dictionary = QuantityMath.allocate_whole(requests, total(stock))
var additions: Dictionary = {}
var credit_events: Array = []
var batch_additions: Dictionary = {}
var debits: Dictionary = {}
var slots_by_resource: Dictionary = {}
for id in stock:
var slots: Dictionary = {"": target.amount.get(id, 0)}
for owner in target.binds: slots[owner] = target.binds[owner].get(id, 0)
slots_by_resource[id] = slots
debits[id] = {}
for i in colonies.size():
if budget_timer != null and not await budget_timer.pause(): return
var colony: Dictionary = colonies[i]
@@ -409,21 +448,40 @@ static func colonize(world, target: Dictionary, colonies: Array, year: Variant,
for id in targets:
var amount: Variant = targets[id]
if E.le(amount, 0): continue
var slots: Dictionary = {"": target.amount.get(id, 0)}
for owner in target.binds: slots[owner] = target.binds[owner].get(id, 0)
var slots: Dictionary = slots_by_resource[id]
var taken: Dictionary = QuantityMath.allocate_whole(slots, amount)
world.batches.scale_batches(target, id, E.subtract(1, E.divide(amount, stock[id])))
stock[id] = E.subtract(stock[id], amount)
for owner in taken:
if E.le(taken[owner], 0): continue
if E.eq(owner, ""): target.amount[id] = E.subtract(target.amount[id], taken[owner])
else: target.binds[owner][id] = E.subtract(target.binds[owner][id], taken[owner])
var created: Variant = world.ledger.credit(target, colony.stage, taken[owner])
if E.ne(owner, ""):
target.amount[colony.stage] = E.subtract(target.amount[colony.stage], created)
target.binds[owner][colony.stage] = E.add(target.binds[owner].get(colony.stage, 0), created)
world.batches.append_batch(target, colony.stage, amount, year, year)
if (not target.ethics.has(colony.stage)): target.ethics[colony.stage] = {}
add(target.ethics[colony.stage], population)
slots[owner] = E.subtract(slots.get(owner, 0), taken[owner])
debits[id][owner] = E.add(debits[id].get(owner, 0), taken[owner])
credit_events.append({"stage": colony.stage, "owner": owner, "amount": taken[owner]})
batch_additions[colony.stage] = E.add(batch_additions.get(colony.stage, 0), taken[owner])
if not additions.has(colony.stage): additions[colony.stage] = {}
add(additions[colony.stage], population)
target.civilization_stats.colonies = E.add(target.civilization_stats.get("colonies", 0), count)
# Commit each resource and owner once after all colonies have consumed their
# sequentially allocated shares. This keeps allocation order exact while
# avoiding repeated batch scaling and ledger mutations in the inner loop.
for id in debits:
var removed: Variant = E.subtract(physical.get(id, 0), stock[id])
if E.gt(physical.get(id, 0), 0):
world.batches.scale_batches(target, id, E.subtract(1, E.divide(removed, physical[id])))
for owner in debits[id]:
if E.eq(owner, ""):
target.amount[id] = E.subtract(target.amount.get(id, 0), debits[id][owner])
elif target.binds.has(owner):
target.binds[owner][id] = E.subtract(target.binds[owner].get(id, 0), debits[id][owner])
for event: Dictionary in credit_events:
var stage: String = event.stage
var owner: String = event.owner
var created: Variant = world.ledger.credit(target, stage, event.amount)
if E.ne(owner, ""):
target.amount[stage] = E.subtract(target.amount[stage], created)
if not target.binds.has(owner): target.binds[owner] = {}
target.binds[owner][stage] = E.add(target.binds[owner].get(stage, 0), created)
for stage in batch_additions:
world.batches.append_batch(target, stage, batch_additions[stage], year, year)
for id in additions:
target.ethics[id] = State.merge_profile(target.ethics.get(id, {}), physical.get(id, 0), additions[id])
world.profile_end("colonization", profile_colonies)
+68 -21
View File
@@ -5,11 +5,68 @@ const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
const ETHICS = ["m", "l", "w", "d", "c", "b"]
static func empty_profile() -> Dictionary:
var result: Dictionary = {}
for ethic in ETHICS: result[ethic] = 1.0 / ETHICS.size()
return result
static func normalize_profile(profile: Dictionary) -> Dictionary:
var result: Dictionary = {}
var total: float = 0.0
for ethic in ETHICS:
var value: float = maxf(0.0, E.approx(profile.get(ethic, 0.0)))
result[ethic] = value
total += value
if not is_finite(total) or total <= 0.0: return empty_profile()
if profile.size() == ETHICS.size() and absf(total - 1.0) <= 0.000000000001:
return result
var assigned: float = 0.0
for i in ETHICS.size() - 1:
var ethic: String = ETHICS[i]
result[ethic] = (0.0 + result[ethic]) / total
assigned += result[ethic]
result[ETHICS.back()] = maxf(0.0, 1.0 - assigned)
return result
static func profile_from_counts(population: Dictionary) -> Dictionary:
var total: Variant = QuantityMath.sum(population)
if E.le(total, 0): return empty_profile()
var profile: Dictionary = {}
for ethic in ETHICS:
profile[ethic] = E.approx(E.divide(population.get(ethic, 0), total))
return normalize_profile(profile)
static func materialize_profile(target: Dictionary, id: String, profile: Dictionary, count: Variant) -> Dictionary:
var normalized: Dictionary = normalize_profile(profile)
if not target.has("civilization_remainders"): target.civilization_remainders = {}
var shares: Array = []
for ethic in ETHICS: shares.append(normalized[ethic])
var result: Dictionary = QuantityMath.split_profile(target.civilization_remainders, "profile:" + id, count, ETHICS, shares)
for ethic in ETHICS:
if not result.has(ethic): result[ethic] = 0
return result
static func merge_profile(base: Dictionary, base_count: Variant, incoming: Dictionary) -> Dictionary:
var incoming_count: Variant = QuantityMath.sum(incoming)
return merge_profiles(base, base_count, profile_from_counts(incoming), incoming_count)
static func merge_profiles(base: Dictionary, base_count: Variant, incoming_profile: Dictionary, incoming_count: Variant) -> Dictionary:
var total: Variant = E.add(base_count, incoming_count)
if E.le(total, 0): return empty_profile()
var base_ratio: float = E.approx(E.divide(base_count, total)) if E.gt(base_count, 0) else 0.0
var incoming_ratio: float = E.approx(E.divide(incoming_count, total)) if E.gt(incoming_count, 0) else 0.0
var normalized_base: Dictionary = normalize_profile(base)
var result: Dictionary = {}
for ethic in ETHICS:
var incoming_share: float = E.approx(incoming_profile.get(ethic, 0.0)) if E.gt(incoming_count, 0) else 0.0
result[ethic] = (0.0 + normalized_base[ethic]) * base_ratio + incoming_share * incoming_ratio
return normalize_profile(result)
static func is_space_civilization(id: String, rules: Dictionary) -> bool:
return (rules.has(id) and E.ne(str(rules[id].home), ""))
# Preserve any existing ethics. New populations start equally distributed;
# reductions (including blasts) are proportional and never leave negative counts.
# Keep one six-value float profile per stage. Absolute population remains in
# the exact physical object ledger and is materialized only when a transition needs it.
static func reconcile(world, target: Dictionary, rules: Dictionary, physical: Dictionary = {}, only_ids: Array = []) -> void:
if (not target.has("ethics")):
target.ethics = {}
@@ -19,22 +76,10 @@ static func reconcile(world, target: Dictionary, rules: Dictionary, physical: Di
for id in ids:
if not rules.has(id): continue
var count: Variant = physical.get(id, 0)
var current: Dictionary = target.ethics.get(id, {})
var current: Dictionary = normalize_profile(target.ethics.get(id, {}))
var cached: Dictionary = world.ledger.reconciled_ethics.get(id, {})
if not cached.is_empty() and E.eq(cached.count, count) and E.eq(cached.population, current):
continue
var total: Variant = 0
for ethic in ETHICS:
current[ethic] = E.maximum(0, current.get(ethic, 0))
total = E.add(total, current[ethic])
if E.gt(total, count):
current = QuantityMath.allocate_whole(current, count)
elif E.lt(total, count):
var missing: Variant = E.subtract(count, total)
var base: Variant = E.floor_value(E.divide(missing, ETHICS.size()))
var rest: int = E.integer(E.subtract(missing, E.multiply(base, ETHICS.size())))
for i in ETHICS.size():
current[ETHICS[i]] = E.add(current[ETHICS[i]], E.add(base, ((1 if E.lt(i, rest) else 0))))
target.ethics[id] = current
world.ledger.reconciled_ethics[id] = {"count": count, "population": current.duplicate()}
@@ -115,6 +160,7 @@ static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year:
detach_space_civilizations(target, rules)
var physical: Dictionary = world.ledger.physical_amounts(target)
reconcile(world, target, rules, physical)
var ethical_totals: Dictionary = physical.duplicate()
var budget: Dictionary = stellar_budget(world, target, rules, physical)
var remaining: Dictionary = budget.capacity.duplicate()
var remaining_homes: Dictionary = budget.habitats.capacity.duplicate()
@@ -141,12 +187,13 @@ static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year:
target.amount[id] = supported
world.ledger.credit(target, previous, excess)
world.batches.append_batch(target, previous, excess, year, year)
var removed: Dictionary = QuantityMath.allocate_whole(target.ethics[id], excess)
if (not target.ethics.has(previous)):
target.ethics[previous] = {}
for ethic in ETHICS:
target.ethics[id][ethic] = E.subtract(target.ethics[id][ethic], removed[ethic])
target.ethics[previous][ethic] = E.add(target.ethics[previous].get(ethic, 0), removed[ethic])
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 source_profile: Dictionary = target.ethics.get(id, empty_profile())
target.ethics[previous] = merge_profiles(previous_profile, previous_count, source_profile, excess)
ethical_totals[id] = supported
ethical_totals[previous] = E.add(previous_count, excess)
# Capacity losses are proportional, so the surviving source keeps its profile.
if E.eq(previous, "C1.9"):
var stars: Dictionary = {}
var current_physical: Dictionary = world.ledger.physical_amounts(target)
+8
View File
@@ -30,10 +30,12 @@ func evolute_to_datetime(datetime: Variant, clock: Dictionary = {}, budget: RefC
# Fractional frame updates cannot change capacity or populations.
var processed_year: Variant = SimulationTime.processed_year(datetime)
if E.gt(processed_year, world.state.time.year):
if budget != null: budget.call("set_phase", "initialize_life_batches")
var profile_initialize: int = world.profile_start()
initialize_life_batches()
world.profile_end("initialize", profile_initialize)
var previous: Dictionary = world.state
if budget != null: budget.call("set_phase", "deep_copy_and_coarsen")
var next: Dictionary = previous.duplicate_deep()
next.version = SimulationTime.STATE_VERSION
next.speed = clampi(E.integer(previous.speed), 1, Global.SPEED_LIMIT)
@@ -87,6 +89,7 @@ func evolve_batch_generation(target: Dictionary, to_year: Variant, completed: Di
# Apply direct completions at the original next-generation boundary, so
# binding transfers, the blast environment and capacity checks keep order.
if budget != null and not await budget.pause(): return false
if budget != null: budget.call("set_phase", "generation_setup_and_withdrawal")
var completed_now: Dictionary = completed.duplicate()
completed.clear()
if context.is_empty():
@@ -132,7 +135,9 @@ func evolve_batch_generation(target: Dictionary, to_year: Variant, completed: Di
var fractions: Dictionary = {}
var outputs: Dictionary = {}
var current_quantities: Dictionary = quantities
if budget != null: budget.call("set_phase", "civilization_stellar_budget")
var star_budget: Dictionary = CivilizationState.stellar_budget(world, target, Global.CivilizationRules, current_quantities)
if budget != null: budget.call("set_phase", "civilization_prepare")
var civilization_transaction: Dictionary = await CivilizationEvolution.prepare(world, target, expired, to_year, star_budget, Global.CivilizationRules, current_quantities, budget, expired_totals)
if budget != null and budget.cancelled: return false
var profile_products: int = world.profile_start()
@@ -166,6 +171,7 @@ func evolve_batch_generation(target: Dictionary, to_year: Variant, completed: Di
affected[id] = true
for held in target.binds.get(id, {}): affected[held] = true
for successor in outputs[id]: affected[successor] = true
if budget != null: budget.call("set_phase", "transfer_evolution")
var incoming: Dictionary = world.ledger.transfer_evolution(target, fractions, outputs)
var new_owners: Dictionary = {}
@@ -177,6 +183,7 @@ func evolve_batch_generation(target: Dictionary, to_year: Variant, completed: Di
# blast retain the original snapshot before crediting newborn populations.
var environment: Dictionary = world.environment.stellar_environment(target) if E.gt(blast_power, 0) else {}
var resolution: Variant = world.batches.temporal_resolution(target)
if budget != null: budget.call("set_phase", "transfer_and_newborns")
for id in expired:
for part : Dictionary in expired[id]:
for successor in part.products:
@@ -214,6 +221,7 @@ func evolve_batch_generation(target: Dictionary, to_year: Variant, completed: Di
world.profile_end("newborn_batches", profile_newborn)
new_owners[successor] = E.add(new_owners.get(successor, 0), amount)
world.ledger.accept_transfers(target, incoming)
if budget != null: budget.call("set_phase", "finish_and_capacity")
world.ledger.bind_new_owners(target, new_owners)
for owner in new_owners:
for resource in world.catalog.entities[owner].bind:
+14
View File
@@ -5,18 +5,32 @@ var slice_usec: int = 40000
var deadline: int
var cancelled: bool = false
var waiting_usec: int = 0
var checkpoint_usec: int = 0
var max_slice_usec: int = 0
var max_slice_phase: String = "startup"
var phase: String = "startup"
func _init(scene_tree: SceneTree, milliseconds: int = 40):
tree = scene_tree
slice_usec = milliseconds * 1000
deadline = Time.get_ticks_usec() + slice_usec
checkpoint_usec = deadline - slice_usec
func set_phase(value: String) -> void:
phase = value
func pause() -> bool:
var keep_alive: RefCounted = self
if cancelled: return false
var now: int = Time.get_ticks_usec()
var work_usec: int = now - checkpoint_usec
if work_usec > max_slice_usec:
max_slice_usec = work_usec
max_slice_phase = phase
if Time.get_ticks_usec() >= deadline:
var waiting_from: int = Time.get_ticks_usec()
await tree.process_frame
keep_alive.waiting_usec += Time.get_ticks_usec() - waiting_from
deadline = Time.get_ticks_usec() + slice_usec
checkpoint_usec = Time.get_ticks_usec()
return not keep_alive.cancelled
+60 -2
View File
@@ -123,11 +123,69 @@ static func sum(pop: Dictionary) -> Variant:
static var _split_keys: Dictionary = {}
## Split a whole population using normalized float shares. Quantizing the six
## shares to 15 decimal places matches their stored float precision, while the
## integer denominator avoids a second expensive exact normalization pass.
static func split_profile(remainders: Dictionary, key: String, count: Variant, ids: Array, shares: Array) -> Dictionary:
var result: Dictionary = {}
if E.le(count, 0): return result
assert(E.eq(count, E.floor_value(count)) and ids.size() == shares.size() and not ids.is_empty())
const SCALE: int = 1000000000000000
var weights: Array = []
var assigned_weight: int = 0
for i in ids.size() - 1:
var weight: int = clampi(roundi(clampf(float(shares[i]), 0.0, 1.0) * SCALE), 0, SCALE)
weights.append(weight)
assigned_weight += weight
var last_weight: int = SCALE - assigned_weight
if last_weight < 0:
var excess: int = -last_weight
for i in range(weights.size() - 1, -1, -1):
var reduction: int = mini(excess, weights[i])
weights[i] -= reduction
excess -= reduction
if excess == 0: break
last_weight = 0
weights.append(last_weight)
var cached: Dictionary = _split_keys.get(key, {})
if cached.is_empty() or cached.ids != ids:
var keys: Array = []
for id in ids: keys.append(key + ":" + str(id))
if _split_keys.size() >= 512: _split_keys.clear()
cached = {"ids": ids.duplicate(), "keys": keys}
_split_keys[key] = cached
var expected: Array = E.mul_div_many(count, weights, SCALE)
var rests: Array = []
var order: Array = []
var total: Variant = 0
for i in ids.size():
expected[i] = E.add(expected[i], remainders.get(cached.keys[i], 0))
result[ids[i]] = E.floor_value(E.maximum(0, expected[i]))
total = E.add(total, result[ids[i]])
rests.append(E.subtract(expected[i], result[ids[i]]))
order.append(i)
order.sort_custom(func(a: int, b: int) -> bool: return E.gt(rests[a], rests[b]) if E.ne(rests[a], rests[b]) else str(ids[a]) < str(ids[b]))
if E.lt(total, count):
for i in order:
if E.ge(total, count): break
result[ids[i]] = E.add(result[ids[i]], 1)
total = E.add(total, 1)
elif E.gt(total, count):
order.reverse()
for i in order:
if E.le(total, count): break
if E.le(result[ids[i]], 0): continue
result[ids[i]] = E.subtract(result[ids[i]], 1)
total = E.subtract(total, 1)
for i in ids.size():
remainders[cached.keys[i]] = E.clamp_value(E.subtract(expected[i], result[ids[i]]), E.neg(1), 1)
return result
static func split(remainders: Dictionary, key: String, count: Variant, weights: Dictionary) -> Dictionary:
if E.le(count, 0): return {}
return split_arrays(remainders, key, count, weights.keys(), weights.values())
static func split_arrays(remainders: Dictionary, key: String, count: Variant, ids: Array, weights: Array, population_factor: int = 0) -> Dictionary:
static func split_arrays(remainders: Dictionary, key: String, count: Variant, ids: Array, weights: Array, population_factor: int = 0, skip_conserved_identity: bool = false) -> Dictionary:
var result: Dictionary = {}
if E.le(count, 0): return result
var sum_weights: Variant = 0
@@ -177,7 +235,7 @@ static func split_arrays(remainders: Dictionary, key: String, count: Variant, id
# Fractional identity ratios still round-trip through parts/make in the
# original algorithm. Retain them to preserve ties and saved errors exactly.
var normalized: Array = positives
if E.gt(positive_sum, 0) and (E.ne(positive_sum, count) or not whole_quotas):
if E.gt(positive_sum, 0) and (E.ne(positive_sum, count) or (not whole_quotas and not (skip_conserved_identity and E.eq(positive_sum, count)))):
normalized = E.mul_div_many(count, positives, positive_sum)
var order: Array = []
var rests: Array = []
+17 -7
View File
@@ -62,16 +62,26 @@ static func advance(previous: Dictionary, target: Dictionary, rules: Dictionary,
for id in rules:
if E.ne(str(rules[id].home), ""): stages.append(id)
stages.sort()
# Profiles remain in the save after a stage becomes empty. Only living
# populations (including populations bound to a host object) contribute.
var populated: Dictionary = {}
var amounts: Dictionary = previous.get("amount", {})
for id in stages:
if E.gt(amounts.get(id, 0), 0): populated[id] = true
for owner in previous.get("binds", {}):
for id in previous.binds[owner]:
if rules.has(id) and E.ne(str(rules[id].home), "") and E.gt(previous.binds[owner][id], 0):
populated[id] = true
var weight: Variant = 1
for id in stages:
if not populated.has(id):
weight = E.multiply(weight, 2)
continue
var population: Dictionary = previous.get("ethics", {}).get(id, {})
var count: Variant = 0
for n in population.values(): count = E.add(count, n)
if E.gt(count, 0):
for ethic in population:
var proportion: Variant = E.divide(population[ethic], count)
for i in TRENDS.size():
rates[i] = E.add(rates[i], E.multiply(E.multiply(proportion, WEIGHTS[ethic][i]), weight))
for ethic in population:
var proportion: float = maxf(0.0, float(population[ethic]))
for i in TRENDS.size():
rates[i] = E.add(rates[i], E.multiply(E.multiply(proportion, WEIGHTS[ethic][i]), weight))
weight = E.multiply(weight, 2)
for i in TRENDS.size():
var name: String = TRENDS[i]