02c9843e1b
- 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.
569 lines
32 KiB
GDScript
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
|