Files
owniverse/world/owniverse/civilization_flow.gd
T
Kirill 02c9843e1b Enhance civilization flow mechanics and adaptive quality control
- Implemented runtime-only adaptive quality controller to optimize simulation performance based on CPU time.
- Added adaptive quality thresholds and recovery cycles to dynamically adjust simulation fidelity.
- Introduced single-step solving method for civilization transitions to improve performance under high loads.
- Updated benchmark scripts to include speed parameter for testing various simulation speeds.
- Added tests for adaptive quality adjustments and their effects on civilization flow calculations.
- Documented results of optimization attempts and their impact on performance metrics.
2026-10-08 22:05:08 +03:00

569 lines
32 KiB
GDScript

extends "res://world/owniverse/simulation_module.gd"
## Continuous expected flows. Only ledger quantities and rounding carries are exact.
const E = preload("res://world/owniverse/math/exact.gd")
const State = preload("res://world/owniverse/civilization_state.gd")
const Q = preload("res://world/owniverse/quantity_math.gd")
const Civ = preload("res://world/owniverse/civilization_evolution.gd")
const Parameters = preload("res://world/owniverse/world_parameters.gd")
const ETHICS = State.ETHICS
var _generator_key: String = ""
var _generator: Dictionary = {}
var _solutions: Dictionary = {}
func _edges(target: Dictionary, rules: Dictionary, physical: Dictionary = {}) -> Dictionary:
var modifiers: Dictionary = Parameters.life_modifiers(target)
var order: float = maxf(1e-12, E.approx(Parameters.value(target, "Order")))
var stages: Array = rules.keys()
stages.sort()
var rates: Dictionary = {}
var spans: Dictionary = {}
var progression: Dictionary = {}
var budget: Dictionary = State.stellar_budget(world, target, rules, physical)
var signature: String = str(modifiers) + ":" + str(order)
for id in stages.duplicate():
if rules[id].ethics.is_empty(): continue
rates[id] = Civ.coefficients(world, rules, id, modifiers)
progression[id] = _can_progress(id, rules[id].next, budget, rules)
spans[id] = maxf(1e-12, E.approx(State.effective_span(world, target, id, rules, physical)))
signature += id + str(spans[id]) + str(rates[id]) + str(rules[id]) + str(progression[id])
for row: Dictionary in rules[id].ethics.values():
for destination in Q.rule_weights(str(row.fall)):
if not stages.has(destination): stages.append(destination)
if signature == _generator_key: return _generator
var build_started: int = world.profile_start()
var index: Dictionary = {}
for i in stages.size(): index[stages[i]] = i
var rows: Array = []
var rewards: Array = []
var maximum: float = 0.0
for id in stages:
for e in 6:
var edges: Dictionary = {}
var reward: Dictionary = {"war": 0.0, "col": 0.0, "colony": "", "previous": ""}
if rates.has(id):
var ethic: String = ETHICS[e]
var r: Dictionary = rates[id][ethic]
var span: float = spans[id]
var war: float = minf(0.5, E.approx(r.war)) if rules[id].fights else 0.0
var progress: float = (E.approx(r.pro) + war * 17.0 / 36.0) / span if progression[id] else 0.0
var degradation: float = (E.approx(r.deg) + war * 31.0 / 18.0 / order) / span
var next_node: int = index[rules[id].next] * 6 + e
edges[next_node] = progress
var falls: Dictionary = Q.rule_weights(str(rules[id].ethics[ethic].fall))
if falls.is_empty(): falls = {rules[id].previous: 1}
var fall_total: float = 0.0
for weight in falls.values(): fall_total += E.approx(weight)
for destination in falls:
var node: int = index[destination] * 6 + e
edges[node] = float(edges.get(node, 0.0)) + degradation * E.approx(falls[destination]) / fall_total
reward = {"war": war / span, "col": E.approx(r.col) / span if rules[id].colonizes else 0.0,
"colony": rules[id].colony, "previous": rules[id].previous}
var outgoing: float = 0.0
var self_node: int = index[id] * 6 + e
edges.erase(self_node)
for rate_value in edges.values(): outgoing += rate_value
maximum = maxf(maximum, outgoing)
rows.append({"edges": edges, "outgoing": outgoing})
rewards.append(reward)
_generator_key = signature
_solutions.clear()
var targets: Array = []
var probabilities: Array = []
for node in rows.size():
var destinations: PackedInt32Array = []
var weights: PackedFloat64Array = []
destinations.append(node)
weights.append(maxf(0.0, 1.0 - rows[node].outgoing / maximum) if maximum > 0.0 else 1.0)
for destination in rows[node].edges:
destinations.append(destination)
weights.append(rows[node].edges[destination] / maximum if maximum > 0.0 else 0.0)
targets.append(destinations)
probabilities.append(weights)
_generator = {"stages": stages, "index": index, "rows": rows, "rewards": rewards, "lambda": maximum, "order": order, "targets": targets, "probabilities": probabilities}
world.profile_end("civilization_operator_build", build_started)
return _generator
func _can_progress(source: String, destination: String, budget: Dictionary, rules: Dictionary) -> bool:
var home: String = rules[destination].home
if home != "":
var old_cost: Variant = E.maximum(1, rules[source].stars) if rules[source].home == home else 0
var net: Variant = E.subtract(E.maximum(1, rules[destination].stars), old_cost)
if E.gt(net, 0) and E.lt(budget.free.get(home, 0), net): return false
var new_req: Dictionary = budget.habitats.requirements.get(destination, {})
var old_req: Dictionary = budget.habitats.requirements.get(source, {})
if not new_req.is_empty():
var net: Variant = E.subtract(new_req.units, old_req.get("units", 0) if old_req.get("id", "") == new_req.id else 0)
var free: Variant = E.subtract(budget.habitats.capacity.get(new_req.id, 0), budget.habitats.requested.get(new_req.id, 0))
if E.gt(net, 0) and E.lt(free, net): return false
return true
## Uniformization computes exp(Q*t) through a Poisson series of sparse steps.
## Absorbing biological stages and Ct retain their mass. No one-transition cap.
func _solve(generator: Dictionary, source: String, profile: Dictionary, years: float) -> Dictionary:
var approximation: int = world.adaptive_quality
var key: String = source + ":" + str(years) + ":" + str(approximation)
var basis: Dictionary = _solutions.get(key, {})
if basis.is_empty():
var started: int = world.profile_start()
basis = _solve_single_step(generator, source, years) if approximation > 0 else _solve_basis(generator, source, years)
world.profile_end("civilization_operator_solve", started)
if _solutions.size() >= 512: _solutions.clear()
_solutions[key] = basis
var mass: PackedFloat64Array = basis.mass.duplicate()
var occupation: PackedFloat64Array = basis.occupation.duplicate()
var shares: PackedFloat64Array = []
for ethic in ETHICS: shares.append(profile[ethic])
for node in mass.size():
mass[node] *= shares[node % 6]
occupation[node] *= shares[node % 6]
return {"mass": mass, "occupation": occupation, "occupation_scale": 1.0}
## Load-shedding approximation: each ethic cohort can make at most one
## transition during this interval. The exact Markov solve remains the default.
func _solve_single_step(generator: Dictionary, source: String, years: float) -> Dictionary:
var size: int = generator.rows.size()
var mass: PackedFloat64Array = []
var occupation: PackedFloat64Array = []
mass.resize(size)
occupation.resize(size)
var offset: int = generator.index[source] * 6
for ethic in 6:
var node: int = offset + ethic
var row: Dictionary = generator.rows[node]
var outgoing: float = row.outgoing
if outgoing <= 0.0:
mass[node] = 1.0
occupation[node] = years
continue
var moved: float = clampf(1.0 - exp(-outgoing * years), 0.0, 1.0)
var stayed: float = 1.0 - moved
mass[node] += stayed
occupation[node] += years * (1.0 - moved * 0.5)
for destination in row.edges:
var rate: float = row.edges[destination]
if rate <= 0.0: continue
var share: float = moved * rate / outgoing
mass[destination] += share
occupation[destination] += years * share * 0.5
return {"mass": mass, "occupation": occupation}
func _solve_basis(generator: Dictionary, source: String, years: float) -> Dictionary:
var size: int = generator.rows.size()
var values: PackedFloat64Array = []
values.resize(size)
var active: PackedInt32Array = []
var offset: int = generator.index[source] * 6
for e in 6:
values[offset + e] = 1.0
active.append(offset + e)
var mass: PackedFloat64Array = []
var occupation: PackedFloat64Array = []
mass.resize(size)
occupation.resize(size)
var frequency: float = generator.lambda
if frequency <= 0.0:
mass = values.duplicate()
for node in active: occupation[node] = years
return {"mass": mass, "occupation": occupation}
var mean: float = frequency * years
var weight: float = exp(-mean)
var cumulative: float = 0.0
var limit: int = maxi(32, ceili(mean + 12.0 * sqrt(mean) + 32.0))
for step in limit:
cumulative = minf(1.0, cumulative + weight)
var time_weight: float = maxf(0.0, 1.0 - cumulative) / frequency
for node in active:
mass[node] += weight * values[node]
occupation[node] += time_weight * values[node]
if step > mean and weight < 1e-18: break
var next: PackedFloat64Array = []
next.resize(size)
var next_active: PackedInt32Array = []
for node in active:
var destinations: PackedInt32Array = generator.targets[node]
var probabilities: PackedFloat64Array = generator.probabilities[node]
for i in destinations.size():
var contribution: float = values[node] * probabilities[i]
if contribution <= 0.0: continue
var destination: int = destinations[i]
if next[destination] == 0.0: next_active.append(destination)
next[destination] += contribution
values = next
active = next_active
weight *= mean / float(step + 1)
var integrated: PackedFloat64Array = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
for node in size: integrated[node % 6] += occupation[node]
for e in 6:
if integrated[e] <= 0.0:
occupation[offset + e] = years
else:
var factor: float = years / integrated[e]
for node in range(e, size, 6): occupation[node] *= factor
return {"mass": mass, "occupation": occupation}
func _drift(target: Dictionary, rules: Dictionary, generator: Dictionary, years: float, physical: Dictionary = {}) -> void:
for id in rules:
if rules[id].ethics.is_empty() or not target.ethics.has(id): continue
var profile: Dictionary = State.normalize_profile(target.ethics[id])
var span: float = maxf(1e-12, E.approx(State.effective_span(world, target, id, rules, physical)))
var duration: float = years / span
var pair_decay: float = exp(-0.03 * duration)
var opposite_decay: float = exp(-0.02 * duration)
var result: Dictionary = {}
for ethic in ETHICS:
var opposite: float = profile[Civ.OPPOSITE[ethic]]
result[ethic] = 1.0 / 6.0 + pair_decay * ((profile[ethic] + opposite) * 0.5 - 1.0 / 6.0) + opposite_decay * (profile[ethic] - opposite) * 0.5
if rules[id].diplomacy:
var weighted: Dictionary = {}
for ethic in ETHICS: weighted[ethic] = result[ethic] * maxf(0.0, E.approx(E.of(rules[id].ethics[ethic].diplomacy_rate)))
weighted = State.normalize_profile(weighted)
var fraction: float = 1.0 - exp(-duration)
for ethic in ETHICS: result[ethic] = lerpf(result[ethic], weighted[ethic], fraction)
target.ethics[id] = State.normalize_profile(result)
func _profile(vector: Array) -> Dictionary:
var result: Dictionary = {}
for e in 6: result[ETHICS[e]] = vector[e]
return State.normalize_profile(result)
func _merge(plan: Dictionary, destination: String, count: Variant, profile: Dictionary) -> void:
if E.le(count, 0): return
var previous: Variant = plan.counts.get(destination, 0)
plan.profiles[destination] = State.merge_profiles(plan.profiles.get(destination, {}), previous, profile, count)
plan.counts[destination] = E.add(previous, count)
func _plans(target: Dictionary, physical: Dictionary, rules: Dictionary, generator: Dictionary, years: float) -> Dictionary:
var plans: Dictionary = {}
var colonies: Dictionary = {}
var damages: Dictionary = {}
var wars: Variant = 0
for source in rules:
var count: Variant = physical.get(source, 0)
if E.le(count, 0) or rules[source].ethics.is_empty(): continue
var solved: Dictionary = _solve(generator, source, State.normalize_profile(target.ethics.get(source, {})), years)
var vectors: Dictionary = {}
for node in solved.mass.size():
if solved.mass[node] <= 0.0: continue
var destination: String = generator.stages[int(node) / 6]
if not vectors.has(destination): vectors[destination] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
vectors[destination][int(node) % 6] += solved.mass[node]
var ids: Array = []
var shares: Array = []
var profiles: Dictionary = {}
var retained: float = 0.0
for destination in vectors:
var share: float = 0.0
for value in vectors[destination]: share += value
profiles[destination] = _profile(vectors[destination])
if destination == source or share < 0.5e-15:
retained += share
else:
ids.append(destination)
shares.append(share)
ids.append(source)
shares.append(retained)
if not profiles.has(source): profiles[source] = State.normalize_profile(target.ethics.get(source, {}))
var counts: Dictionary = Q.split_profile(target.civilization_remainders, "flow:" + source, count, ids, shares)
plans[source] = {"counts": counts, "profiles": profiles}
var colony_vectors: Dictionary = {}
var damage: Dictionary = {}
var war_reward: float = 0.0
for node in solved.occupation.size():
if solved.occupation[node] <= 0.0: continue
var reward: Dictionary = generator.rewards[node]
var duration: float = solved.occupation[node] * solved.occupation_scale
war_reward += duration * reward.war
if reward.war > 0.0:
damage[reward.previous] = float(damage.get(reward.previous, 0.0)) + duration * reward.war * 40.0 / 3.0 / generator.order
if reward.col <= 0.0: continue
if not colony_vectors.has(reward.colony): colony_vectors[reward.colony] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
colony_vectors[reward.colony][int(node) % 6] += duration * reward.col
wars = E.add(wars, Q.accumulate_rate(target.civilization_remainders, "flow:wars:" + source, count, war_reward))
for destination in damage:
damages[destination] = E.add(damages.get(destination, 0), Q.accumulate_rate(target.civilization_remainders, "flow:damage:" + source + ":" + destination, count, damage[destination]))
for destination in colony_vectors:
var reward: float = 0.0
for value in colony_vectors[destination]: reward += value
var quantity: Variant = Q.accumulate_rate(target.civilization_remainders, "flow:colony:" + source + ":" + destination, count, reward)
if not colonies.has(destination): colonies[destination] = {"counts": {}, "profiles": {}}
_merge(colonies[destination], destination, quantity, _profile(colony_vectors[destination]))
return {"plans": plans, "colonies": colonies, "damages": damages, "wars": wars}
## Positive resource costs share the same frozen free budget. Rejected movement
## stays at its source. Releases become available after this transaction commits.
func _limit(plans: Dictionary, budget: Dictionary, rules: Dictionary) -> void:
var demands: Dictionary = {}
var costs: Dictionary = {}
for source in plans:
costs[source] = {}
for destination in plans[source].counts:
if destination == source or not rules.has(destination): continue
var amount: Variant = plans[source].counts[destination]
if E.le(amount, 0): continue
var resource_costs: Dictionary = {}
var home: String = rules[destination].home
if home != "":
var old_cost: Variant = rules[source].stars if rules[source].home == home else 0
var net: Variant = E.maximum(0, E.subtract(E.maximum(1, rules[destination].stars), old_cost))
if E.gt(net, 0): resource_costs["star:" + home] = net
var housing: Dictionary = budget.habitats
var new_req: Dictionary = housing.requirements.get(destination, {})
var old_req: Dictionary = housing.requirements.get(source, {})
if not new_req.is_empty():
var net: Variant = E.maximum(0, E.subtract(new_req.units, old_req.get("units", 0) if old_req.get("id", "") == new_req.id else 0))
if E.gt(net, 0): resource_costs["house:" + new_req.id] = net
costs[source][destination] = resource_costs
for resource in resource_costs:
demands[resource] = E.add(demands.get(resource, 0), E.multiply(amount, resource_costs[resource]))
var factors: Dictionary = {}
for resource in demands:
var available: Variant
if resource.begins_with("star:"):
available = budget.free.get(resource.trim_prefix("star:"), 0)
else:
var id: String = resource.trim_prefix("house:")
available = E.maximum(0, E.subtract(budget.habitats.capacity.get(id, 0), budget.habitats.requested.get(id, 0)))
factors[resource] = [available, demands[resource]]
for source in costs:
for destination in costs[source]:
var requested: Variant = plans[source].counts[destination]
var allowed: Variant = requested
for resource in costs[source][destination]:
var ratio: Array = factors[resource]
if E.lt(ratio[0], ratio[1]): allowed = E.minimum(allowed, E.floor_value(E.mul_div(requested, ratio[0], ratio[1])))
var rejected: Variant = E.subtract(requested, allowed)
if E.le(rejected, 0): continue
plans[source].counts[destination] = allowed
_merge(plans[source], source, rejected, plans[source].profiles[destination])
## Apply one conserved source-by-owner table, then credit each destination once.
func _commit(target: Dictionary, physical: Dictionary, plans: Dictionary, rules: Dictionary, year: Variant) -> Dictionary:
var credits: Dictionary = {}
var contributions: Dictionary = {}
var births: Dictionary = {}
var ownership: Dictionary = {}
# Each source quota replaces its old physical population. Transfers between
# owners do not change these exact totals, and credits are applied below.
var final: Dictionary = physical.duplicate()
for source in plans:
if final.has(source): final[source] = plans[source].counts.get(source, 0)
var space_stages: Dictionary = {"Ct": true}
for id in rules: space_stages[id] = id == "Ct" or str(rules[id].home) != ""
for source in plans:
ownership[source] = {}
var free: Variant = target.amount.get(source, 0)
if E.gt(free, 0): ownership[source][""] = free
for owner in target.binds:
for source in target.binds[owner]:
if not ownership.has(source): continue
var held: Variant = target.binds[owner][source]
if E.gt(held, 0): ownership[source][owner] = held
for source in plans:
var plan: Dictionary = plans[source]
var remaining: Dictionary = {}
for destination in plan.counts:
var amount: Variant = plan.counts[destination]
if E.le(amount, 0): continue
remaining[destination] = amount
if not contributions.has(destination): contributions[destination] = []
contributions[destination].append({"amount": amount, "profile": plan.profiles[destination]})
var slots: Dictionary = ownership[source]
target.amount[source] = 0
var owners: Array = slots.keys()
if owners.size() == 1 and owners[0] == "":
target.amount[source] = remaining.get(source, 0)
for destination in remaining:
if destination == source: continue
var host: String = "@host" if space_stages.get(source, false) and not space_stages.get(destination, false) else ""
if not credits.has(destination): credits[destination] = {}
credits[destination][host] = E.add(credits[destination].get(host, 0), remaining[destination])
continue
for i in owners.size():
var owner: String = owners[i]
# Last row receives the remaining exact quotas; the free-only path
# therefore needs no proportional allocation or repeated subtraction.
var allocation: Dictionary = remaining if i == owners.size() - 1 else Q.allocate_profiled(remaining, slots[owner])
for destination in allocation:
var amount: Variant = allocation[destination]
if E.le(amount, 0): continue
if i != owners.size() - 1:
remaining[destination] = E.subtract(remaining[destination], amount)
if destination == source:
if owner == "": target.amount[source] = amount
else: target.binds[owner][source] = amount
continue
var host: String = owner
if space_stages.get(destination, false): host = ""
elif owner == "" and space_stages.get(source, false): host = "@host"
if not credits.has(destination): credits[destination] = {}
credits[destination][host] = E.add(credits[destination].get(host, 0), amount)
if owner != "" and E.le(allocation.get(source, 0), 0): target.binds[owner][source] = 0
var hosts: Dictionary = {}
for id in physical:
if world.catalog.entities[id].event_group == "DwStar" and E.gt(physical[id], 0): hosts[id] = physical[id]
var host_shares: Array = _host_shares(hosts) if not hosts.is_empty() else []
for destination in credits:
var requested: Variant = Q.sum(credits[destination])
var created: Variant = world.ledger.credit(target, destination, requested)
if final.has(destination): final[destination] = E.add(final[destination], created)
var bound: Variant = 0
for owner in credits[destination]:
var count: Variant = credits[destination][owner]
if owner == "" or E.le(count, 0): continue
if owner == "@host":
if hosts.is_empty(): continue
var distributed: Dictionary = Q.split_profile(target.civilization_remainders, "flow:hosts:" + destination, count, hosts.keys(), host_shares)
for host in distributed:
if E.le(distributed[host], 0): continue
if not target.binds.has(host): target.binds[host] = {}
target.binds[host][destination] = E.add(target.binds[host].get(destination, 0), distributed[host])
else:
target.binds[owner][destination] = E.add(target.binds[owner].get(destination, 0), count)
bound = E.add(bound, count)
target.amount[destination] = E.subtract(target.amount[destination], bound)
if not rules.has(destination): births[destination] = created
for destination in contributions:
if not rules.has(destination) or E.le(final.get(destination, 0), 0): continue
var profile: Dictionary = {}
for ethic in ETHICS: profile[ethic] = 0.0
var existing: Variant = physical.get(destination, 0) if not plans.has(destination) else 0
if E.gt(existing, 0):
var original: Dictionary = State.normalize_profile(target.ethics.get(destination, {}))
var ratio: float = E.ratio_approx(existing, final[destination])
for ethic in ETHICS: profile[ethic] += original[ethic] * ratio
for entry: Dictionary in contributions[destination]:
var ratio: float = E.ratio_approx(entry.amount, final[destination])
for ethic in ETHICS: profile[ethic] += entry.profile[ethic] * ratio
target.ethics[destination] = State.normalize_profile(profile)
for destination in births: world.batches.append_batch(target, destination, births[destination], year, year)
return final
func _host_shares(hosts: Dictionary) -> Array:
var total: Variant = Q.sum(hosts)
var result: Array = []
for value in hosts.values(): result.append(E.ratio_approx(value, total))
return result
func _colonize(target: Dictionary, colonies: Dictionary, year: Variant, rules: Dictionary, physical: Dictionary = {}) -> Dictionary:
if physical.is_empty(): physical = world.ledger.physical_amounts(target)
if colonies.is_empty(): return physical
var stocks: Dictionary = {}
for id in ["GG", "AB", "RP", "HP"]: stocks[id] = physical.get(id, 0)
var requests: Dictionary = {}
for id in colonies: requests[id] = colonies[id].counts.get(id, 0)
var granted: Dictionary = Q.allocate_profiled(requests, Q.sum(stocks))
var total: Variant = Q.sum(granted)
if E.le(total, 0): return physical
var taken_resources: Dictionary = Q.allocate_profiled(stocks, total)
var removed_by_owner: Dictionary = {}
for resource in taken_resources:
var removed: Variant = taken_resources[resource]
if E.le(removed, 0): continue
var owners: Dictionary = {"": target.amount.get(resource, 0)}
for owner in target.binds:
if E.gt(target.binds[owner].get(resource, 0), 0): owners[owner] = target.binds[owner][resource]
var taken: Dictionary = Q.allocate_profiled(owners, removed)
world.batches.scale_batches(target, resource, E.subtract(1, E.divide(removed, stocks[resource])))
for owner in taken:
var amount: Variant = taken[owner]
if E.le(amount, 0): continue
if owner == "": target.amount[resource] = E.subtract(target.amount[resource], amount)
else: target.binds[owner][resource] = E.subtract(target.binds[owner][resource], amount)
removed_by_owner[owner] = E.add(removed_by_owner.get(owner, 0), amount)
var remaining: Dictionary = granted.duplicate()
var placements: Dictionary = {}
var final: Dictionary = physical.duplicate()
var owners: Array = removed_by_owner.keys()
for i in owners.size():
var owner: String = owners[i]
var allocation: Dictionary = remaining if i == owners.size() - 1 else Q.allocate_profiled(remaining, removed_by_owner[owner])
for destination in allocation:
var count: Variant = allocation[destination]
if E.le(count, 0): continue
if i != owners.size() - 1: remaining[destination] = E.subtract(remaining[destination], count)
if owner == "" or State.is_space_civilization(destination, rules): continue
if not placements.has(destination): placements[destination] = {}
placements[destination][owner] = count
for destination in granted:
var count: Variant = granted[destination]
if E.le(count, 0): continue
var created: Variant = world.ledger.credit(target, destination, count)
if final.has(destination): final[destination] = E.add(final[destination], created)
var bound: Variant = 0
for owner in placements.get(destination, {}):
var held: Variant = placements[destination][owner]
target.binds[owner][destination] = E.add(target.binds[owner].get(destination, 0), held)
bound = E.add(bound, held)
target.amount[destination] = E.subtract(target.amount[destination], bound)
for id in granted:
if E.le(granted[id], 0): continue
var profile: Dictionary = colonies[id].profiles[id]
var drifted: Dictionary = {}
for ethic in ETHICS:
drifted[ethic] = 0.0
for source in ETHICS:
drifted[ethic] += profile[source] * (0.96 if source == ethic else (0.004 if Civ.OPPOSITE[source] == ethic else 0.009))
target.ethics[id] = State.merge_profiles(target.ethics.get(id, {}), physical.get(id, 0), drifted, granted[id])
target.civilization_stats.colonies = E.add(target.civilization_stats.get("colonies", 0), total)
for resource in taken_resources:
final[resource] = E.subtract(final.get(resource, 0), taken_resources[resource])
return final
func evolve(target: Dictionary, from_year: Variant, to_year: Variant, timer: RefCounted = null) -> void:
var keep_alive: RefCounted = self
var years: float = E.approx(E.subtract(to_year, from_year))
if years <= 0.0: return
var rules: Dictionary = Global.CivilizationRules
var generator: Dictionary = {}
# Bound the Poisson mean, processing the entire requested interval.
var maximum_years: float = years
var cursor: Variant = from_year
var physical: Dictionary = world.ledger.physical_amounts(target)
while E.lt(cursor, to_year):
if timer != null and not await timer.pause(): return
generator = _edges(target, rules, physical)
maximum_years = 32.0 / generator.lambda if generator.lambda > 0.0 else years
var end: Variant = E.minimum(to_year, E.add(cursor, E.of(maximum_years)))
var started: int = world.profile_start()
var transaction: Dictionary = _plans(target, physical, rules, generator, E.approx(E.subtract(end, cursor)))
world.profile_end("civilization_flow_plan", started)
if timer != null and not await timer.pause(): return
var plans: Dictionary = transaction.plans
var damage_total: Variant = 0
for source in transaction.damages:
if not plans.has(source): continue
var retained: Variant = plans[source].counts.get(source, 0)
var amount: Variant = E.minimum(retained, transaction.damages[source])
if E.le(amount, 0): continue
var profile: Dictionary = plans[source].profiles[source]
var weights: Dictionary = {}
for ethic in ETHICS:
var falls: Dictionary = Q.rule_weights(str(rules[source].ethics[ethic].fall))
if falls.is_empty(): falls = {rules[source].previous: 1}
var sum: float = 0.0
for weight in falls.values(): sum += E.approx(weight)
for destination in falls: weights[destination] = float(weights.get(destination, 0.0)) + profile[ethic] * E.approx(falls[destination]) / sum
var split: Dictionary = Q.split_profile(target.civilization_remainders, "flow:collateral:" + source, amount, weights.keys(), weights.values())
plans[source].counts[source] = E.subtract(retained, amount)
for destination in split: _merge(plans[source], destination, split[destination], profile)
damage_total = E.add(damage_total, amount)
started = world.profile_start()
_limit(plans, State.stellar_budget(world, target, rules, physical), rules)
world.profile_end("civilization_flow_capacity", started)
started = world.profile_start()
physical = _commit(target, physical, plans, rules, end)
world.profile_end("civilization_flow_ledger", started)
if timer != null and not await timer.pause(): return
started = world.profile_start()
physical = _colonize(target, transaction.colonies, end, rules, physical)
world.profile_end("civilization_flow_colonies", started)
started = world.profile_start()
_drift(target, rules, generator, E.approx(E.subtract(end, cursor)), physical)
target.civilization_stats.wars = E.add(target.civilization_stats.get("wars", 0), transaction.wars)
target.civilization_stats.collateral = E.add(target.civilization_stats.get("collateral", 0), damage_total)
physical = State.enforce_capacity(world, target, rules, end, physical)
State.reconcile(world, target, rules, physical)
target.civilization_stats.worship = Civ.worship(world, target, rules, physical)
world.profile_end("civilization_flow_finish", started)
cursor = end