Add exact arithmetic implementation and tests

- Introduced exact arithmetic with support for large integers and fractions in `exact.gd`, `big_integer.gd`, and `exact_value.gd`.
- Implemented a comprehensive test suite in `test_exact_math.gd` to validate arithmetic operations against expected results.
- Added a README in `math` directory to document the exact arithmetic system and its usage.
- Ensured that all arithmetic operations maintain precision without floating-point errors, using a custom representation for large numbers.
- Included methods for encoding and decoding exact values for serialization.
This commit is contained in:
Kirill
2026-09-30 08:08:27 +03:00
parent f2111a376a
commit fb8a3f5658
58 changed files with 5139 additions and 2162 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
- `quantity_math.gd`: нормализация положительных весов, ограниченное целочисленное распределение методом наибольших остатков, распределение с накоплением ошибки округления, накопление дробного выпуска и минимальный целый размер производственной партии.
- `entity_catalog.gd`: загрузка определений, индексы групп и событий, списки автоматических производителей и разблокируемых сущностей.
Математика не обращается к дереву сцены, Global или Owniverse. Методы накопления изменяют переданный словарь остатков; остальные методы возвращают результат. Для распределения `split` количество должно быть целым, веса — неотрицательными. Равные остатки разрешаются по строковому ID. Сохранены прежние точности округления: 1e-12 для выпуска и 1e-10 для событий цивилизаций.
Математика не обращается к дереву сцены, Global или Owniverse. Методы накопления изменяют переданный словарь остатков; остальные методы возвращают результат. Для распределения `split` количество должно быть целым, веса — неотрицательными. Равные остатки разрешаются по строковому ID. Старые поправки `snappedf` удалены: количества считаются целочисленно, дробные вычисления выполняет [модуль точной арифметики](math/README.md). Там же описаны границы точности и формат сохранения 3.
Каталог получает словарь исходных строк через `load_definitions(rows)`, вызываемый один раз для нового экземпляра. Конструктор OwniverseEntity по-прежнему использует настройки Global. Каталог не хранит количества, партии или состояние интерфейса. Определения пока остаются Node: освобождает их владелец мира, как до рефакторинга.
+109 -112
View File
@@ -1,58 +1,58 @@
extends "res://world/owniverse/simulation_module.gd"
const E = preload("res://world/owniverse/math/exact.gd")
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
func yearly_products(entity: OwniverseEntity) -> Dictionary:
var products: Dictionary = {}
if not entity.life_product.is_empty() and entity.lifespan > 0.0:
if ((not entity.life_product.is_empty()) and E.gt(entity.lifespan, 0)):
for resource in entity.life_product:
products[resource] = maxf(0.0, entity.life_product[resource]) / entity.lifespan
elif entity.product_per_year_type != "":
products[entity.product_per_year_type] = maxf(0.0, entity.product_per_year_amount)
products[resource] = E.divide(E.maximum(0, entity.life_product[resource]), entity.lifespan)
elif E.ne(entity.product_per_year_type, ""):
products[entity.product_per_year_type] = E.maximum(0, entity.product_per_year_amount)
return products
func produce_for_period(previous: Dictionary, target: Dictionary, quantities: Dictionary,
distribution: Dictionary, from_year: float, to_year: float) -> void:
func produce_for_period(previous: Dictionary, target: Dictionary, quantities: Dictionary, distribution: Dictionary, from_year: Variant, to_year: Variant) -> void:
var basic: Dictionary = {}
var automatic: Dictionary = {}
for id in quantities:
var entity: OwniverseEntity = world.catalog.entities[id]
var object_years: float = world.batches.object_years(previous, id, quantities[id], from_year, to_year)
var auto_share: float = clampf(entity.auto, 0.0, 1.0)
var object_years: Variant = world.batches.object_years(previous, id, quantities[id], from_year, to_year)
var auto_share: Variant = E.clamp_value(entity.auto, 0, 1)
var products: Dictionary = yearly_products(entity)
for product in products:
var amount: float = object_years * products[product] * WorldParameters.production(previous, entity)
basic[product] = basic.get(product, 0.0) + amount * (1.0 - auto_share)
automatic[product] = automatic.get(product, 0.0) + amount * auto_share
var amount: Variant = E.multiply(E.multiply(object_years, products[product]), WorldParameters.production(previous, entity))
basic[product] = E.add(basic.get(product, 0), E.multiply(amount, (E.subtract(1, auto_share))))
automatic[product] = E.add(automatic.get(product, 0), E.multiply(amount, auto_share))
# Basic and automatic output each retain their unproduced remainder.
if not target.has("resource_remainders"):
if (not target.has("resource_remainders")):
target.resource_remainders = {"basic": {}, "automatic": {}}
var pending: Dictionary = target.resource_remainders
var requests: Dictionary = {}
for id in basic:
pending.basic[id] = pending.basic.get(id, 0.0) + basic[id]
pending.automatic[id] = pending.automatic.get(id, 0.0) + automatic[id]
pending.basic[id] = E.add(pending.basic.get(id, 0), basic[id])
pending.automatic[id] = E.add(pending.automatic.get(id, 0), automatic[id])
var totals: Dictionary = {}
for channel in ["basic", "automatic"]:
for id in pending[channel]:
totals[id] = maxf(0.0, pending.basic.get(id, 0.0) + pending.automatic.get(id, 0.0))
totals[id] = E.maximum(0, E.add(pending.basic.get(id, 0), pending.automatic.get(id, 0)))
for id in totals:
if id == "S" or id == "H":
requests[id] = floorf(totals[id])
var funded: Dictionary = QuantityMath.allocate_whole(requests, floorf(maxf(0.0, previous.energy)))
if (E.eq(id, "S") or E.eq(id, "H")):
requests[id] = E.floor_value(totals[id])
var funded: Dictionary = QuantityMath.allocate_whole(requests, E.floor_value(E.maximum(0, previous.energy)))
automatic = {}
target.energy = previous.energy
for id in totals:
var amount: float = floorf(totals[id])
if id == "S" or id == "H":
amount = funded.get(id, 0.0)
target.energy -= amount
var amount: Variant = E.floor_value(totals[id])
if (E.eq(id, "S") or E.eq(id, "H")):
amount = funded.get(id, 0)
target.energy = E.subtract(target.energy, amount)
# Split the actual whole output, retaining the allocation error for next time.
var auto_ratio: float = clampf(pending.automatic.get(id, 0.0) / totals[id], 0.0, 1.0) if totals[id] > 0.0 else 0.0
var auto_amount: float = floorf(amount * auto_ratio)
pending.automatic[id] = pending.automatic.get(id, 0.0) - auto_amount
pending.basic[id] = pending.basic.get(id, 0.0) - (amount - auto_amount)
var auto_ratio: Variant = (E.clamp_value(E.divide(pending.automatic.get(id, 0), totals[id]), 0, 1) if E.gt(totals[id], 0) else 0)
var auto_amount: Variant = E.floor_value(E.multiply(amount, auto_ratio))
pending.automatic[id] = E.subtract(pending.automatic.get(id, 0), auto_amount)
pending.basic[id] = E.subtract(pending.basic.get(id, 0), (E.subtract(amount, auto_amount)))
automatic[id] = auto_amount
amount = world.ledger.credit(target, id, amount)
world.batches.append_batch(target, id, amount, from_year, to_year)
@@ -60,22 +60,22 @@ func produce_for_period(previous: Dictionary, target: Dictionary, quantities: Di
# Keep unused automatic resources across cycles; they are part of free amounts.
var budget: Dictionary = previous.get("auto_resources", {}).duplicate(true)
for id in automatic:
budget[id] = budget.get(id, 0.0) + automatic[id]
budget[id] = E.add(budget.get(id, 0), automatic[id])
for id in budget:
budget[id] = minf(maxf(0.0, budget[id]), world.ledger.available(target, id))
budget[id] = E.minimum(E.maximum(0, budget[id]), world.ledger.available(target, id))
target.auto_resources = budget
produce_brown_dwarfs(target, automatic.get("H", 0.0), from_year, to_year)
produce_brown_dwarfs(target, automatic.get("H", 0), from_year, to_year)
budget = ordinary_star_budget(target)
# One common multiplier preserves the initial count ratios, even under scarcity.
var candidates: Dictionary = {}
for id in distribution:
var entity: OwniverseEntity = world.catalog.entities[id]
if entity.group_as != "Star":
if E.ne(entity.group_as, "Star"):
continue
var has_cost: bool = false
for resource in entity.cost:
if entity.cost[resource] > 0.0:
if E.gt(entity.cost[resource], 0):
has_cost = true
if has_cost:
candidates[id] = distribution[id]
@@ -85,88 +85,87 @@ func produce_for_period(previous: Dictionary, target: Dictionary, quantities: Di
var costs: Dictionary = {}
var needs: Dictionary = {}
for id in candidates:
if auto_capacity(target, world.catalog.entities[id], budget) <= 0.0:
if E.le(auto_capacity(target, world.catalog.entities[id], budget), 0):
continue
for resource in world.catalog.entities[id].cost:
var cost: float = maxf(0.0, world.catalog.entities[id].cost[resource]) * candidates[id]
costs[resource] = costs.get(resource, 0.0) + cost
needs[resource] = needs.get(resource, 0.0) + cost
var cost: Variant = E.multiply(E.maximum(0, world.catalog.entities[id].cost[resource]), candidates[id])
costs[resource] = E.add(costs.get(resource, 0), cost)
needs[resource] = E.add(needs.get(resource, 0), cost)
for resource in world.catalog.entities[id].bind:
needs[resource] = needs.get(resource, 0.0) + maxf(0.0, world.catalog.entities[id].bind[resource]) * candidates[id]
var scale: float = INF
needs[resource] = E.add(needs.get(resource, 0), E.multiply(E.maximum(0, world.catalog.entities[id].bind[resource]), candidates[id]))
var scale: Variant = INF
for resource in costs:
if costs[resource] > 0.0:
scale = minf(scale, budget.get(resource, 0.0) / costs[resource])
if E.gt(costs[resource], 0):
scale = E.minimum(scale, E.divide(budget.get(resource, 0), costs[resource]))
for resource in needs:
if needs[resource] > 0.0:
scale = minf(scale, world.ledger.available(target, resource) / needs[resource])
if E.gt(needs[resource], 0):
scale = E.minimum(scale, E.divide(world.ledger.available(target, resource), needs[resource]))
var concrete_needs: Dictionary = world.ledger.expand_requirements(target, needs)
for resource in concrete_needs:
if concrete_needs[resource] > 0.0:
scale = minf(scale, world.ledger.available(target, resource) / concrete_needs[resource])
if not is_finite(scale) or scale <= 0.0:
if E.gt(concrete_needs[resource], 0):
scale = E.minimum(scale, E.divide(world.ledger.available(target, resource), concrete_needs[resource]))
if ((not E.finite(scale)) or E.le(scale, 0)):
return
# Rare types are served first; signed count deficits compensate later cycles.
if not target.has("auto_star_deficits"):
if (not target.has("auto_star_deficits")):
target.auto_star_deficits = {}
var deficits: Dictionary = target.auto_star_deficits
var order: Array = candidates.keys()
order.sort_custom(func(a: String, b: String) -> bool:
return candidates[a] < candidates[b] if candidates[a] != candidates[b] else a < b)
order.sort_custom(func ( a : String , b : String ) -> bool : return (E.lt(candidates[a], candidates[b]) if E.ne(candidates[a], candidates[b]) else E.lt(a, b)))
var created: Dictionary = {}
var metallicity: float = world.environment.stellar_environment(target).metallicity
var total_created: float = 0.0
var metallicity: Variant = world.environment.stellar_environment(target).metallicity
var total_created: Variant = 0
for id in order:
var entity: OwniverseEntity = world.catalog.entities[id]
var step: float = world.production.production_step(entity)
if not is_finite(step):
var step: Variant = world.production.production_step(entity)
if (not E.finite(step)):
continue
var quota: float = maxf(0.0, candidates[id] * scale + deficits.get(id, 0.0))
var desired: float = floorf(quota / step) * step
var actual: float = minf(desired, auto_capacity(target, entity, budget))
if actual <= 0.0:
var quota: Variant = E.maximum(0, E.add(E.multiply(candidates[id], scale), deficits.get(id, 0)))
var desired: Variant = E.multiply(E.floor_value(E.divide(quota, step)), step)
var actual: Variant = E.minimum(desired, auto_capacity(target, entity, budget))
if E.le(actual, 0):
continue
if not world.production.try_production(target, entity, actual, true):
if (not world.production.try_production(target, entity, actual, true)):
continue
budget = ordinary_star_budget(target)
created[id] = actual
total_created += actual
total_created = E.add(total_created, actual)
for id in candidates:
# Credit only actual production, so retained resources aren't counted twice.
deficits[id] = deficits.get(id, 0.0) + candidates[id] * total_created - created.get(id, 0.0)
deficits[id] = E.subtract(E.add(deficits.get(id, 0), E.multiply(candidates[id], total_created)), created.get(id, 0))
for id in created:
world.ledger.credit(target, id, created[id])
world.batches.append_batch(target, id, created[id], from_year, to_year)
world.stellar.create_planets(target, id, created[id], from_year, to_year, metallicity)
func produce_brown_dwarfs(target: Dictionary, new_hydrogen: float, from_year: float, to_year: float) -> void:
if not world.catalog.entities.has("HBD"):
func produce_brown_dwarfs(target: Dictionary, new_hydrogen: Variant, from_year: Variant, to_year: Variant) -> void:
if (not world.catalog.entities.has("HBD")):
return
var entity: OwniverseEntity = world.catalog.entities.HBD
var cost: float = entity.cost.get("H", 0.0)
if cost <= 0.0:
var cost: Variant = entity.cost.get("H", 0)
if E.le(cost, 0):
return
var quota: float = target.auto_star_deficits.get("HBD", 0.0) + maxf(0.0, new_hydrogen) * 0.001 / cost
var fuel: float = maxf(0.0, target.auto_star_deficits.get("HBD_fuel", 0.0))
var available: float = minf(target.auto_resources.get("H", 0.0), world.ledger.available(target, "H"))
quota = minf(quota, (available + fuel) / cost)
var expected: float = maxf(0.0, snappedf(quota, 1e-9))
var amount: float = floorf(expected)
var quota: Variant = E.add(target.auto_star_deficits.get("HBD", 0), E.divide(E.multiply(E.maximum(0, new_hydrogen), E.of("0.001")), cost))
var fuel: Variant = E.maximum(0, target.auto_star_deficits.get("HBD_fuel", 0))
var available: Variant = E.minimum(target.auto_resources.get("H", 0), world.ledger.available(target, "H"))
quota = E.minimum(quota, E.divide((E.add(available, fuel)), cost))
var expected: Variant = E.maximum(0, quota)
var amount: Variant = E.floor_value(expected)
# Do not retry a fractional chance on frames with no new automatic fuel.
if new_hydrogen > 0.0 and expected > amount:
var random_state: float = target.auto_star_deficits.get("HBD_random", 1234567.0)
random_state = fmod(random_state * 48271.0, 2147483647.0)
if (E.gt(new_hydrogen, 0) and E.gt(expected, amount)):
var random_state: Variant = target.auto_star_deficits.get("HBD_random", 1234567)
random_state = E.remainder(E.multiply(random_state, 48271), E.of("2147483647.0"))
target.auto_star_deficits.HBD_random = random_state
if random_state / 2147483647.0 < expected - amount:
amount += 1.0
amount = minf(amount, floorf(snappedf((available + fuel) / cost, 1e-9)))
var charge: float = ceilf(maxf(0.0, snappedf(amount * cost - fuel, 1e-9)))
if amount > 0.0 and charge <= available:
if E.lt(E.divide(random_state, E.of("2147483647.0")), E.subtract(expected, amount)):
amount = E.add(amount, 1)
amount = E.minimum(amount, E.floor_value(E.divide((E.add(available, fuel)), cost)))
var charge: Variant = E.ceil_value(E.maximum(0, E.subtract(E.multiply(amount, cost), fuel)))
if (E.gt(amount, 0) and E.le(charge, available)):
world.ledger.take_available(target, "H", charge)
target.auto_resources.H = maxf(0.0, target.auto_resources.get("H", 0.0) - charge)
target.auto_star_deficits.HBD_fuel = maxf(0.0, snappedf(fuel + charge - amount * cost, 1e-9))
quota -= amount
target.auto_resources.H = E.maximum(0, E.subtract(target.auto_resources.get("H", 0), charge))
target.auto_star_deficits.HBD_fuel = E.maximum(0, E.subtract(E.add(fuel, charge), E.multiply(amount, cost)))
quota = E.subtract(quota, amount)
world.ledger.credit(target, "HBD", amount)
world.batches.append_batch(target, "HBD", amount, from_year, to_year)
target.auto_star_deficits.HBD = quota
@@ -175,52 +174,50 @@ func produce_brown_dwarfs(target: Dictionary, new_hydrogen: float, from_year: fl
func ordinary_star_budget(target: Dictionary) -> Dictionary:
var budget: Dictionary = target.auto_resources.duplicate()
if world.catalog.entities.has("HBD"):
var reserved: float = maxf(0.0, target.auto_star_deficits.get("HBD", 0.0) * world.catalog.entities.HBD.cost.get("H", 0.0) - target.auto_star_deficits.get("HBD_fuel", 0.0))
budget.H = maxf(0.0, budget.get("H", 0.0) - reserved)
var reserved: Variant = E.maximum(0, E.subtract(E.multiply(target.auto_star_deficits.get("HBD", 0), world.catalog.entities.HBD.cost.get("H", 0)), target.auto_star_deficits.get("HBD_fuel", 0)))
budget.H = E.maximum(0, E.subtract(budget.get("H", 0), reserved))
return budget
func auto_capacity(source: Dictionary, entity: OwniverseEntity, budget: Dictionary) -> float:
var capacity: float = INF
func auto_capacity(source: Dictionary, entity: OwniverseEntity, budget: Dictionary) -> Variant:
var capacity: Variant = INF
var needs: Dictionary = {}
for resource in entity.cost:
var cost: float = maxf(0.0, entity.cost[resource])
var cost: Variant = E.maximum(0, entity.cost[resource])
needs[resource] = cost
if cost > 0.0:
capacity = minf(capacity, floorf(budget.get(resource, 0.0) / cost))
if E.gt(cost, 0):
capacity = E.minimum(capacity, E.floor_value(E.divide(budget.get(resource, 0), cost)))
for resource in entity.bind:
needs[resource] = needs.get(resource, 0.0) + maxf(0.0, entity.bind[resource])
needs[resource] = E.add(needs.get(resource, 0), E.maximum(0, entity.bind[resource]))
for resource in needs:
if needs[resource] > 0.0:
capacity = minf(capacity, floorf(world.ledger.available(source, resource) / needs[resource]))
if E.gt(needs[resource], 0):
capacity = E.minimum(capacity, E.floor_value(E.divide(world.ledger.available(source, resource), needs[resource])))
var concrete: Dictionary = world.ledger.expand_requirements(source, needs)
for resource in concrete:
if concrete[resource] > 0.0:
capacity = minf(capacity, floorf(world.ledger.available(source, resource) / concrete[resource]))
var step: float = world.production.production_step(entity)
return floorf(capacity / step) * step if is_finite(capacity) and is_finite(step) else 0.0
if E.gt(concrete[resource], 0):
capacity = E.minimum(capacity, E.floor_value(E.divide(world.ledger.available(source, resource), concrete[resource])))
var step: Variant = world.production.production_step(entity)
return (E.multiply(E.floor_value(E.divide(capacity, step)), step) if (E.finite(capacity) and E.finite(step)) else 0)
func produce_newborn_lifetime(target: Dictionary, id: String, count: float,
born_from: float, born_to: float, to_year: float, before_batches: Array,
remaining_object_years: float = NAN) -> void:
func produce_newborn_lifetime(target: Dictionary, id: String, count: Variant, born_from: Variant, born_to: Variant, to_year: Variant, before_batches: Array, remaining_object_years: Variant = NAN) -> void:
var products: Dictionary = yearly_products(world.catalog.entities[id])
if products.is_empty() or count <= 0.0:
if (products.is_empty() or E.le(count, 0)):
return
var object_years: float = remaining_object_years if is_finite(remaining_object_years) else count * maxf(0.0, to_year - (born_from + born_to) * 0.5)
var object_years: Variant = (remaining_object_years if E.finite(remaining_object_years) else E.multiply(count, E.maximum(0, E.subtract(to_year, E.multiply((E.add(born_from, born_to)), E.of("0.5"))))))
var previous_amounts: Dictionary = {}
for old: Dictionary in before_batches:
var start: float = old.get("created_from", -INF)
if not previous_amounts.has(start):
previous_amounts[start] = old.amount
for batch: Dictionary in target.batches.get(id, []):
var added: float = batch.amount - previous_amounts.get(batch.get("created_from", INF), 0.0)
object_years -= maxf(0.0, added) * world.batches.expired_years(to_year, batch.start_year, batch.end_year)
object_years = maxf(0.0, object_years)
for old : Dictionary in before_batches:
var start: Variant = old.get("created_from", E.neg(INF))
if (not previous_amounts.has(str(start))):
previous_amounts[str(start)] = old.amount
for batch : Dictionary in target.batches.get(id, []):
var added: Variant = E.subtract(batch.amount, previous_amounts.get(str(batch.get("created_from", INF)), 0))
object_years = E.subtract(object_years, E.multiply(E.maximum(0, added), world.batches.expired_years(to_year, batch.start_year, batch.end_year)))
object_years = E.maximum(0, object_years)
for resource in products:
var output: float = object_years * products[resource] * WorldParameters.production(target, world.catalog.entities[id])
if resource in ["H", "S"]:
output = minf(output, target.energy)
var actual: float = world.ledger.credit(target, resource, output)
if resource in ["H", "S"]:
target.energy = maxf(0.0, target.energy - actual)
var output: Variant = E.multiply(E.multiply(object_years, products[resource]), WorldParameters.production(target, world.catalog.entities[id]))
if E.contains(["H", "S"], resource):
output = E.minimum(output, target.energy)
var actual: Variant = world.ledger.credit(target, resource, output)
if E.contains(["H", "S"], resource):
target.energy = E.maximum(0, E.subtract(target.energy, actual))
+100 -101
View File
@@ -1,4 +1,5 @@
extends RefCounted
const E = preload("res://world/owniverse/math/exact.gd")
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
@@ -7,43 +8,42 @@ const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
const ETHICS = State.ETHICS
const OPPOSITE = {"m": "l", "l": "m", "w": "d", "d": "w", "c": "b", "b": "c"}
static func total(pop: Dictionary) -> float:
static func total(pop: Dictionary) -> Variant:
return QuantityMath.sum(pop)
static func add(to: Dictionary, from: Dictionary) -> void:
for key in from: to[key] = to.get(key, 0.0) + from[key]
for key in from: to[key] = E.add(to.get(key, 0), from[key])
static func take(world, pop: Dictionary, count: float) -> Dictionary:
static func take(world, pop: Dictionary, count: Variant) -> Dictionary:
var taken: Dictionary = QuantityMath.allocate_whole(pop, count)
for key in taken: pop[key] -= taken[key]
for key in taken: pop[key] = E.subtract(pop[key], taken[key])
return taken
static func rate(target: Dictionary, key: String, count: float, probability: float) -> float:
if not target.has("civilization_remainders"): target.civilization_remainders = {}
static func rate(target: Dictionary, key: String, count: Variant, probability: Variant) -> Variant:
if (not target.has("civilization_remainders")): target.civilization_remainders = {}
return QuantityMath.accumulate_rate(target.civilization_remainders, key, count, probability)
# Fixed-size largest-remainder apportionment with saved signed rounding errors.
# Runtime depends on the number of branches, never the number of civilizations.
static func split(target: Dictionary, key: String, count: float, weights: Dictionary) -> Dictionary:
if count <= 0.0 or QuantityMath.sum(weights) <= 0.0: return {}
if not target.has("civilization_remainders"): target.civilization_remainders = {}
static func split(target: Dictionary, key: String, count: Variant, weights: Dictionary) -> Dictionary:
if (E.le(count, 0) or E.le(QuantityMath.sum(weights), 0)): return {}
if (not target.has("civilization_remainders")): target.civilization_remainders = {}
return QuantityMath.split(target.civilization_remainders, key, count, weights)
static func drift(target: Dictionary, key: String, pop: Dictionary, rows: Dictionary,
diplomacy: bool = false, colony: bool = false) -> Dictionary:
static func drift(target: Dictionary, key: String, pop: Dictionary, rows: Dictionary, diplomacy: bool = false, colony: bool = false) -> Dictionary:
var weights: Dictionary = {}
for ethic in pop:
var influence: float = float(rows[ethic].diplomacy_rate) if diplomacy else 1.0
var influence: Variant = (E.of(rows[ethic].diplomacy_rate) if diplomacy else 1)
for other in ETHICS:
var weight: float = 9.0 if colony else 1.0
if other == ethic: weight = 960.0 if colony else 196.0
elif other == OPPOSITE[ethic]: weight = 4.0 if colony else 0.0
weights[other] = weights.get(other, 0.0) + pop[ethic] * influence * weight
if total(weights) <= 0.0: return pop.duplicate()
var weight: Variant = (9 if colony else 1)
if E.eq(other, ethic): weight = (960 if colony else 196)
elif E.eq(other, OPPOSITE[ethic]): weight = (4 if colony else 0)
weights[other] = E.add(weights.get(other, 0), E.multiply(E.multiply(pop[ethic], influence), weight))
if E.le(total(weights), 0): return pop.duplicate()
return split(target, key, total(pop), weights)
static func emit(plan: Dictionary, destination: String, pop: Dictionary) -> void:
if not plan.outputs.has(destination): plan.outputs[destination] = {}
if (not plan.outputs.has(destination)): plan.outputs[destination] = {}
add(plan.outputs[destination], pop)
static func fall(target: Dictionary, plan: Dictionary, rules: Dictionary, pop: Dictionary) -> void:
@@ -51,51 +51,50 @@ static func fall(target: Dictionary, plan: Dictionary, rules: Dictionary, pop: D
var weights: Dictionary = {}
for term in str(rules[plan.id].ethics[ethic].fall).split("^", false):
var pair: PackedStringArray = term.split(":")
weights[pair[0]] = float(pair[1])
if weights.is_empty(): weights[rules[plan.id].previous] = 1.0
var shares: Dictionary = split(target, "fall:" + plan.id + ":" + ethic, pop[ethic], weights)
weights[pair[0]] = E.of(pair[1])
if weights.is_empty(): weights[rules[plan.id].previous] = 1
var shares: Dictionary = split(target, E.add(E.add(E.add("fall:", plan.id), ":"), ethic), pop[ethic], weights)
for stage in shares: emit(plan, stage, {ethic: shares[stage]})
static func progress(world, target: Dictionary, rules: Dictionary, budget: Dictionary,
plan: Dictionary, pop: Dictionary) -> Dictionary:
static func progress(world, target: Dictionary, rules: Dictionary, budget: Dictionary, plan: Dictionary, pop: Dictionary) -> Dictionary:
var successor: String = rules[plan.id].next
var admitted: float = State.admit_transition(budget, rules, plan.id, successor, total(pop))
var admitted: Variant = State.admit_transition(budget, rules, plan.id, successor, total(pop))
var accepted: Dictionary = take(world, pop, admitted)
emit(plan, successor, accepted)
return pop
static func new_plan(id: String, year: float) -> Dictionary:
return {"id": id, "from": year, "to": -INF, "amount": 0.0, "outputs": {}}
static func new_plan(id: String, year: Variant) -> Dictionary:
return {"id": id, "from": year, "to": E.neg(INF), "amount": 0, "outputs": {}}
static func consume_untriggered(world, target: Dictionary, plan: Dictionary, pop: Dictionary, count: float, year: float) -> Dictionary:
var old_total: float = total(pop)
static func consume_untriggered(world, target: Dictionary, plan: Dictionary, pop: Dictionary, count: Variant, year: Variant) -> Dictionary:
var old_total: Variant = total(pop)
var removed: Dictionary = take(world, pop, count)
var actual: float = total(removed)
if actual > 0.0:
world.batches.scale_batches(target, plan.id, (old_total - actual) / old_total)
plan.amount += actual
plan.to = maxf(plan.to, year)
var actual: Variant = total(removed)
if E.gt(actual, 0):
world.batches.scale_batches(target, plan.id, E.divide((E.subtract(old_total, actual)), old_total))
plan.amount = E.add(plan.amount, actual)
plan.to = E.maximum(plan.to, year)
return removed
static func prepare(world, target: Dictionary, expired: Dictionary, year: float, budget: Dictionary, rules: Dictionary) -> Dictionary:
static func prepare(world, target: Dictionary, expired: Dictionary, year: Variant, budget: Dictionary, rules: Dictionary) -> Dictionary:
State.reconcile(world, target, rules)
var remaining: Dictionary = target.ethics.duplicate(true)
var plans: Dictionary = {}
var colonies: Array = []
var collateral: Dictionary = {}
var modifiers: Dictionary = WorldParameters.life_modifiers(target)
var order: float = WorldParameters.value(target, "Order")
if not target.has("civilization_stats"): target.civilization_stats = {}
var order: Variant = WorldParameters.value(target, "Order")
if (not target.has("civilization_stats")): target.civilization_stats = {}
var ids: Array = expired.keys()
ids.sort()
for id in ids:
if not rules.has(id) or expired[id].is_empty(): continue
if ((not rules.has(id)) or expired[id].is_empty()): continue
var plan: Dictionary = new_plan(id, year)
plans[id] = plan
for part: Dictionary in expired[id]:
plan.amount += part.amount
plan.from = minf(plan.from, part.from)
plan.to = maxf(plan.to, part.to)
for part : Dictionary in expired[id]:
plan.amount = E.add(plan.amount, part.amount)
plan.from = E.minimum(plan.from, part.from)
plan.to = E.maximum(plan.to, part.to)
var triggered: Dictionary = take(world, remaining[id], plan.amount)
var successes: Dictionary = {}
var failures: Dictionary = {}
@@ -103,54 +102,54 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: float,
var settlers: Dictionary = {}
for ethic in triggered:
var row: Dictionary = rules[id].ethics[ethic]
var p: float = maxf(0.0, float(row.progress) * modifiers.progress)
var d: float = maxf(0.0, float(row.degradation) * modifiers.degradation)
var scale: float = maxf(1.0, p + d)
var pro: float = rate(target, id + ":" + ethic + ":pro", triggered[ethic], p / scale)
var deg: float = rate(target, id + ":" + ethic + ":deg", triggered[ethic], d / scale)
var p: Variant = E.maximum(0, E.multiply(E.of(row.progress), modifiers.progress))
var d: Variant = E.maximum(0, E.multiply(E.of(row.degradation), modifiers.degradation))
var scale: Variant = E.maximum(1, E.add(p, d))
var pro: Variant = rate(target, E.add(E.add(E.add(id, ":"), ethic), ":pro"), triggered[ethic], E.divide(p, scale))
var deg: Variant = rate(target, E.add(E.add(E.add(id, ":"), ethic), ":deg"), triggered[ethic], E.divide(d, scale))
var bounded: Dictionary = QuantityMath.allocate_whole({"pro": pro, "deg": deg}, triggered[ethic])
if rules[id].fights: warriors[ethic] = rate(target, id + ":" + ethic + ":war", triggered[ethic], minf(1.0, float(row.war) * modifiers.war))
if rules[id].colonizes: settlers[ethic] = rate(target, id + ":" + ethic + ":col", triggered[ethic], minf(1.0, float(row.colonization) * modifiers.colonization))
if rules[id].fights: warriors[ethic] = rate(target, E.add(E.add(E.add(id, ":"), ethic), ":war"), triggered[ethic], E.minimum(1, E.multiply(E.of(row.war), modifiers.war)))
if rules[id].colonizes: settlers[ethic] = rate(target, E.add(E.add(E.add(id, ":"), ethic), ":col"), triggered[ethic], E.minimum(1, E.multiply(E.of(row.colonization), modifiers.colonization)))
successes[ethic] = bounded.pro
failures[ethic] = bounded.deg
triggered[ethic] -= bounded.pro + bounded.deg
triggered[ethic] = E.subtract(triggered[ethic], E.add(bounded.pro, bounded.deg))
add(triggered, progress(world, target, rules, budget, plan, successes))
fall(target, plan, rules, failures)
if rules[id].fights:
for ethic in warriors: warriors[ethic] = minf(warriors[ethic], triggered[ethic])
var wars: float = minf(total(warriors), floorf((total(triggered) + total(remaining[id])) / 2.0))
for ethic in warriors: warriors[ethic] = E.minimum(warriors[ethic], triggered[ethic])
var wars: Variant = E.minimum(total(warriors), E.floor_value(E.divide((E.add(total(triggered), total(remaining[id]))), 2)))
warriors = QuantityMath.allocate_whole(warriors, wars)
for ethic in warriors: triggered[ethic] -= warriors[ethic]
var outcomes: Dictionary = split(target, id + ":wars", wars, {"deg": 1.0, "pro25": 2.0, "pro50": 3.0, "pro75": 3.0})
var win_expected: float = outcomes.get("pro25", 0.0) * 0.25 + outcomes.get("pro50", 0.0) * 0.5 + outcomes.get("pro75", 0.0) * 0.75
var loss_expected: float = outcomes.get("deg", 0.0) * 2.0 + outcomes.get("pro25", 0.0) + outcomes.get("pro50", 0.0) * 1.5 + outcomes.get("pro75", 0.0) * 2.0
var winners: Dictionary = take(world, warriors, rate(target, id + ":wins", win_expected, 1.0))
var attackers_lost: Dictionary = take(world, warriors, rate(target, id + ":attackers_lost", outcomes.get("deg", 0.0) / order, 1.0))
for ethic in warriors: triggered[ethic] = E.subtract(triggered[ethic], warriors[ethic])
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 winners: Dictionary = take(world, warriors, rate(target, E.add(id, ":wins"), win_expected, 1))
var attackers_lost: Dictionary = take(world, warriors, rate(target, E.add(id, ":attackers_lost"), E.divide(outcomes.get("deg", 0), order), 1))
add(triggered, progress(world, target, rules, budget, plan, winners))
fall(target, plan, rules, attackers_lost)
var losses: float = minf(rate(target, id + ":loss", loss_expected / order, 1.0), total(triggered) + total(remaining[id]))
var losses: Variant = E.minimum(rate(target, E.add(id, ":loss"), E.divide(loss_expected, order), 1), E.add(total(triggered), total(remaining[id])))
var sides: Dictionary = QuantityMath.allocate_whole({"due": total(triggered), "later": total(remaining[id])}, losses)
var victims: Dictionary = take(world, triggered, sides.due)
add(victims, consume_untriggered(world, target, plan, remaining[id], sides.later, year))
fall(target, plan, rules, victims)
add(triggered, warriors)
var damage: float = rate(target, id + ":collateral", (outcomes.get("pro50", 0.0) * 10.0 + outcomes.get("pro75", 0.0) * 30.0) / order, 1.0)
collateral[rules[id].previous] = collateral.get(rules[id].previous, 0.0) + damage
target.civilization_stats.wars = target.civilization_stats.get("wars", 0.0) + wars
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)
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)
emit(plan, id, triggered)
if total(settlers) > 0.0: colonies.append({"stage": rules[id].colony, "source": id, "population": settlers})
if E.gt(total(settlers), 0): colonies.append({"stage": rules[id].colony, "source": id, "population": settlers})
# Collateral is applied after all stages planned their own attempts.
for id in collateral:
if not rules.has(id): continue
if not plans.has(id): plans[id] = new_plan(id, year)
if (not rules.has(id)): continue
if (not plans.has(id)): plans[id] = new_plan(id, year)
var plan: Dictionary = plans[id]
var retry: Dictionary = plan.outputs.get(id, {})
var amount: float = minf(collateral[id], total(retry) + total(remaining[id]))
var amount: Variant = E.minimum(collateral[id], E.add(total(retry), total(remaining[id])))
var sides: Dictionary = QuantityMath.allocate_whole({"due": total(retry), "later": total(remaining[id])}, amount)
var victims: Dictionary = take(world, retry, sides.due)
add(victims, consume_untriggered(world, target, plan, remaining[id], sides.later, year))
fall(target, plan, rules, victims)
target.civilization_stats.collateral = target.civilization_stats.get("collateral", 0.0) + total(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:
@@ -159,70 +158,70 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: float,
for destination in plan.outputs:
var pop: Dictionary = plan.outputs[destination]
if rules.has(destination):
if destination != "Ct":
pop = drift(target, id + ":" + destination + ":drift", pop, rules[id].ethics, destination == id and rules[id].diplomacy)
if not incoming.has(destination): incoming[destination] = {}
if E.ne(destination, "Ct"):
pop = drift(target, E.add(E.add(E.add(id, ":"), destination), ":drift"), pop, rules[id].ethics, (E.eq(destination, id) and rules[id].diplomacy))
if (not incoming.has(destination)): incoming[destination] = {}
add(incoming[destination], pop)
products[destination] = total(pop)
if State.is_space_civilization(id, rules) and world.catalog.entities[destination].group_as == "Planet":
planetary[destination] = planetary.get(destination, 0.0) + total(pop)
expired[id] = [{"amount": plan.amount, "from": plan.from, "to": maxf(plan.from, plan.to), "products": products}]
if (State.is_space_civilization(id, rules) and E.eq(world.catalog.entities[destination].group_as, "Planet")):
planetary[destination] = E.add(planetary.get(destination, 0), total(pop))
expired[id] = [{"amount": plan.amount, "from": plan.from, "to": E.maximum(plan.from, plan.to), "products": products}]
return {"remaining": remaining, "incoming": incoming, "planetary": planetary, "colonies": colonies}
static func finish(world, target: Dictionary, transaction: Dictionary, year: float, rules: Dictionary) -> void:
static func finish(world, target: Dictionary, transaction: Dictionary, year: Variant, rules: Dictionary) -> void:
target.ethics = transaction.remaining
for id in transaction.incoming:
if not target.ethics.has(id): target.ethics[id] = {}
if (not target.ethics.has(id)): target.ethics[id] = {}
add(target.ethics[id], transaction.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 = {}
for id in physical:
if world.catalog.entities[id].event_group == "DwStar" and physical[id] > 0.0: hosts[id] = physical[id]
if (E.eq(world.catalog.entities[id].event_group, "DwStar") and E.gt(physical[id], 0)): hosts[id] = physical[id]
for id in transaction.planetary:
var shares: Dictionary = split(target, "return:" + id, minf(transaction.planetary[id], target.amount.get(id, 0.0)), hosts)
var shares: Dictionary = split(target, E.add("return:", id), E.minimum(transaction.planetary[id], target.amount.get(id, 0)), hosts)
for owner in shares:
if not target.binds.has(owner): target.binds[owner] = {}
target.binds[owner][id] = target.binds[owner].get(id, 0.0) + shares[owner]
target.amount[id] -= shares[owner]
if (not target.binds.has(owner)): target.binds[owner] = {}
target.binds[owner][id] = E.add(target.binds[owner].get(id, 0), shares[owner])
target.amount[id] = E.subtract(target.amount[id], shares[owner])
colonize(world, target, transaction.colonies, year, rules)
State.reconcile(world, target, rules)
var worship: float = 0.0
var worship: Variant = 0
for id in rules:
if not rules[id].worships: continue
for ethic in target.ethics[id]: worship += target.ethics[id][ethic] * float(rules[id].ethics[ethic].worship)
if (not rules[id].worships): continue
for ethic in target.ethics[id]: worship = E.add(worship, E.multiply(target.ethics[id][ethic], E.of(rules[id].ethics[ethic].worship)))
target.civilization_stats.worship = worship
static func colonize(world, target: Dictionary, colonies: Array, year: float, rules: Dictionary) -> void:
static func colonize(world, target: Dictionary, colonies: Array, year: Variant, rules: Dictionary) -> void:
var physical: Dictionary = world.ledger.physical_amounts(target)
var stock: Dictionary = {}
for id in ["GG", "AB", "RP", "HP"]: stock[id] = physical.get(id, 0.0)
for id in ["GG", "AB", "RP", "HP"]: stock[id] = physical.get(id, 0)
var requests: Dictionary = {}
for i in colonies.size(): requests[str(i)] = total(colonies[i].population)
var granted: Dictionary = QuantityMath.allocate_whole(requests, total(stock))
for i in colonies.size():
var colony: Dictionary = colonies[i]
var population: Dictionary = QuantityMath.allocate_whole(colony.population, granted[str(i)])
population = drift(target, colony.source + ":colonies", population, rules[colony.source].ethics, false, true)
var count: float = total(population)
population = drift(target, E.add(colony.source, ":colonies"), population, rules[colony.source].ethics, false, true)
var count: Variant = total(population)
var targets: Dictionary = QuantityMath.allocate_whole(stock, count)
for id in targets:
var amount: float = targets[id]
if amount <= 0.0: continue
var slots: Dictionary = {"": target.amount.get(id, 0.0)}
for owner in target.binds: slots[owner] = target.binds[owner].get(id, 0.0)
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 taken: Dictionary = QuantityMath.allocate_whole(slots, amount)
world.batches.scale_batches(target, id, 1.0 - amount / stock[id])
stock[id] -= 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 taken[owner] <= 0.0: continue
if owner == "": target.amount[id] -= taken[owner]
else: target.binds[owner][id] -= taken[owner]
var created: float = world.ledger.credit(target, colony.stage, taken[owner])
if owner != "":
target.amount[colony.stage] -= created
target.binds[owner][colony.stage] = target.binds[owner].get(colony.stage, 0.0) + created
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] = {}
if (not target.ethics.has(colony.stage)): target.ethics[colony.stage] = {}
add(target.ethics[colony.stage], population)
target.civilization_stats.colonies = target.civilization_stats.get("colonies", 0.0) + count
target.civilization_stats.colonies = E.add(target.civilization_stats.get("colonies", 0), count)
+102 -105
View File
@@ -1,41 +1,42 @@
extends RefCounted
const E = preload("res://world/owniverse/math/exact.gd")
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
const ETHICS = ["m", "l", "w", "d", "c", "b"]
static func is_space_civilization(id: String, rules: Dictionary) -> bool:
return rules.has(id) and str(rules[id].home) != ""
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.
static func reconcile(world, target: Dictionary, rules: Dictionary) -> void:
if not target.has("ethics"):
if (not target.has("ethics")):
target.ethics = {}
var physical: Dictionary = world.ledger.physical_amounts(target)
for id in rules:
var count: float = physical.get(id, 0.0)
var count: Variant = physical.get(id, 0)
var current: Dictionary = target.ethics.get(id, {})
var total: float = 0.0
var total: Variant = 0
for ethic in ETHICS:
current[ethic] = maxf(0.0, current.get(ethic, 0.0))
total += current[ethic]
if total > count:
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 total < count:
var missing: float = count - total
var base: float = floorf(missing / ETHICS.size())
var rest: int = int(missing - base * ETHICS.size())
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]] += base + (1.0 if i < rest else 0.0)
current[ETHICS[i]] = E.add(current[ETHICS[i]], E.add(base, ((1 if E.lt(i, rest) else 0))))
target.ethics[id] = current
static func detach_space_civilizations(target: Dictionary, rules: Dictionary) -> void:
for held: Dictionary in target.binds.values():
for held : Dictionary in target.binds.values():
for id in held:
if is_space_civilization(id, rules):
target.amount[id] = target.amount.get(id, 0.0) + held[id]
held[id] = 0.0
target.amount[id] = E.add(target.amount.get(id, 0), held[id])
held[id] = 0
# A shared budget per stellar family, including stars bound into structures.
# Reservations affect capacity, not the physical stock or its production.
@@ -45,65 +46,64 @@ static func stellar_budget(world, target: Dictionary, rules: Dictionary) -> Dict
for id in physical:
var entity = world.catalog.entities[id]
for home in ["DwStar", "GiStar", "WD", "NS", "MGT", "BH"]:
if entity.event_group == home or entity.count_as == home or id == home:
capacity[home] = capacity.get(home, 0.0) + physical[id]
capacity["SMBH"] = maxf(0.0, target.amount.get("SMBH", 0.0))
for held: Dictionary in target.binds.values():
capacity["SMBH"] += maxf(0.0, held.get("SMBH", 0.0))
if ((E.eq(entity.event_group, home) or E.eq(entity.count_as, home)) or E.eq(id, home)):
capacity[home] = E.add(capacity.get(home, 0), physical[id])
capacity["SMBH"] = E.maximum(0, target.amount.get("SMBH", 0))
for held : Dictionary in target.binds.values():
capacity["SMBH"] = E.add(capacity["SMBH"], E.maximum(0, held.get("SMBH", 0)))
var populations: Dictionary = {}
var requested: Dictionary = {}
for id in rules:
if not is_space_civilization(id, rules):
if (not is_space_civilization(id, rules)):
continue
populations[id] = physical.get(id, 0.0)
populations[id] = physical.get(id, 0)
var home: String = rules[id].home
requested[home] = requested.get(home, 0.0) + populations[id] * maxf(1.0, rules[id].stars)
requested[home] = E.add(requested.get(home, 0), E.multiply(populations[id], E.maximum(1, rules[id].stars)))
var free: Dictionary = {}
for home in capacity:
free[home] = maxf(0.0, capacity[home] - requested.get(home, 0.0))
free[home] = E.maximum(0, E.subtract(capacity[home], requested.get(home, 0)))
return {"capacity": capacity, "requested": requested, "free": free, "populations": populations, "habitats": habitat_budget(world, target, rules)}
# Account for the source's existing reservation when changing stages. A failed
# transition retains its population at the source, rather than losing objects.
static func admit_transition(budget: Dictionary, rules: Dictionary, source: String,
destination: String, requested: float) -> float:
if source == destination or not is_space_civilization(destination, rules):
static func admit_transition(budget: Dictionary, rules: Dictionary, source: String, destination: String, requested: Variant) -> Variant:
if (E.eq(source, destination) or (not is_space_civilization(destination, rules))):
return requested
var home: String = rules[destination].home
var cost: float = maxf(1.0, rules[destination].stars)
var cost: Variant = E.maximum(1, rules[destination].stars)
var old_home: String = ""
var old_cost: float = 0.0
var old_cost: Variant = 0
if is_space_civilization(source, rules):
old_home = rules[source].home
old_cost = maxf(1.0, rules[source].stars)
old_cost = E.maximum(1, rules[source].stars)
# Existing overcapacity is handled by the separate population policy.
if budget.requested.get(home, 0.0) > budget.capacity.get(home, 0.0):
return 0.0
var net_cost: float = cost - old_cost if old_home == home else cost
var admitted: float = floorf(maxf(0.0, requested))
if net_cost > 0.0:
admitted = minf(admitted, floorf(budget.free.get(home, 0.0) / net_cost))
if E.gt(budget.requested.get(home, 0), budget.capacity.get(home, 0)):
return 0
var net_cost: Variant = (E.subtract(cost, old_cost) if E.eq(old_home, home) else cost)
var admitted: Variant = E.floor_value(E.maximum(0, requested))
if E.gt(net_cost, 0):
admitted = E.minimum(admitted, E.floor_value(E.divide(budget.free.get(home, 0), net_cost)))
if budget.has("habitats"):
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_units: float = new_req.units - (old_req.units if old_req.get("id", "") == new_req.id else 0.0)
if net_units > 0.0:
var free_units: float = maxf(0.0, housing.capacity.get(new_req.id, 0.0) - housing.requested.get(new_req.id, 0.0))
admitted = minf(admitted, floorf(free_units / net_units))
if not old_req.is_empty(): housing.requested[old_req.id] = maxf(0.0, housing.requested.get(old_req.id, 0.0) - admitted * old_req.units)
if not new_req.is_empty(): housing.requested[new_req.id] = housing.requested.get(new_req.id, 0.0) + admitted * new_req.units
if old_home != "":
budget.requested[old_home] = maxf(0.0, budget.requested.get(old_home, 0.0) - admitted * old_cost)
budget.free[old_home] = maxf(0.0, budget.capacity.get(old_home, 0.0) - budget.requested[old_home])
budget.requested[home] = budget.requested.get(home, 0.0) + admitted * cost
budget.free[home] = maxf(0.0, budget.capacity.get(home, 0.0) - budget.requested[home])
if (not new_req.is_empty()):
var net_units: Variant = E.subtract(new_req.units, ((old_req.units if E.eq(old_req.get("id", ""), new_req.id) else 0)))
if E.gt(net_units, 0):
var free_units: Variant = E.maximum(0, E.subtract(housing.capacity.get(new_req.id, 0), housing.requested.get(new_req.id, 0)))
admitted = E.minimum(admitted, E.floor_value(E.divide(free_units, net_units)))
if (not old_req.is_empty()): housing.requested[old_req.id] = E.maximum(0, E.subtract(housing.requested.get(old_req.id, 0), E.multiply(admitted, old_req.units)))
if (not new_req.is_empty()): housing.requested[new_req.id] = E.add(housing.requested.get(new_req.id, 0), E.multiply(admitted, new_req.units))
if E.ne(old_home, ""):
budget.requested[old_home] = E.maximum(0, E.subtract(budget.requested.get(old_home, 0), E.multiply(admitted, old_cost)))
budget.free[old_home] = E.maximum(0, E.subtract(budget.capacity.get(old_home, 0), budget.requested[old_home]))
budget.requested[home] = E.add(budget.requested.get(home, 0), E.multiply(admitted, cost))
budget.free[home] = E.maximum(0, E.subtract(budget.capacity.get(home, 0), budget.requested[home]))
return admitted
# Resolve shortages from the most advanced stages downwards. This gives existing
# advanced populations priority and lets a demotion cascade in the same cycle.
static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year: float) -> void:
static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year: Variant) -> void:
detach_space_civilizations(target, rules)
reconcile(world, target, rules)
var budget: Dictionary = stellar_budget(world, target, rules)
@@ -114,50 +114,50 @@ static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year:
if is_space_civilization(id, rules):
stages.append(id)
stages.sort()
for index in range(stages.size() - 1, -1, -1):
for index in range(E.subtract(stages.size(), 1), E.neg(1), E.neg(1)):
var id: String = stages[index]
var home: String = rules[id].home
var cost: float = maxf(1.0, rules[id].stars)
var count: float = target.amount.get(id, 0.0)
var supported: float = minf(count, floorf(remaining.get(home, 0.0) / cost))
var cost: Variant = E.maximum(1, rules[id].stars)
var count: Variant = target.amount.get(id, 0)
var supported: Variant = E.minimum(count, E.floor_value(E.divide(remaining.get(home, 0), cost)))
var requirement: Dictionary = budget.habitats.requirements.get(id, {})
if not requirement.is_empty():
supported = minf(supported, floorf(remaining_homes.get(requirement.id, 0.0) / requirement.units))
remaining_homes[requirement.id] = maxf(0.0, remaining_homes.get(requirement.id, 0.0) - supported * requirement.units)
remaining[home] = maxf(0.0, remaining.get(home, 0.0) - supported * cost)
var excess: float = count - supported
if excess <= 0.0:
if (not requirement.is_empty()):
supported = E.minimum(supported, E.floor_value(E.divide(remaining_homes.get(requirement.id, 0), requirement.units)))
remaining_homes[requirement.id] = E.maximum(0, E.subtract(remaining_homes.get(requirement.id, 0), E.multiply(supported, requirement.units)))
remaining[home] = E.maximum(0, E.subtract(remaining.get(home, 0), E.multiply(supported, cost)))
var excess: Variant = E.subtract(count, supported)
if E.le(excess, 0):
continue
var previous: String = stages[index - 1] if index > 0 else "C1.9"
world.batches.scale_batches(target, id, supported / count)
var previous: String = (stages[E.subtract(index, 1)] if E.gt(index, 0) else "C1.9")
world.batches.scale_batches(target, id, E.divide(supported, count))
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):
if (not target.ethics.has(previous)):
target.ethics[previous] = {}
for ethic in ETHICS:
target.ethics[id][ethic] -= removed[ethic]
target.ethics[previous][ethic] = target.ethics[previous].get(ethic, 0.0) + removed[ethic]
if previous == "C1.9":
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])
if E.eq(previous, "C1.9"):
var stars: Dictionary = {}
var physical: Dictionary = world.ledger.physical_amounts(target)
for star in physical:
if world.catalog.entities[star].event_group == "DwStar" and physical[star] > 0.0:
if (E.eq(world.catalog.entities[star].event_group, "DwStar") and E.gt(physical[star], 0)):
stars[star] = physical[star]
var weights: Dictionary = QuantityMath.normalize_distribution(stars)
# Worlds are aggregates: a star may host several inhabited planets.
var assigned: float = 0.0
var assigned: Variant = 0
var owners: Array = weights.keys()
owners.sort()
for i in owners.size():
var owner: String = owners[i]
var amount: float = floorf(excess * weights[owner]) if i + 1 < owners.size() else excess - assigned
assigned += amount
if not target.binds.has(owner):
var amount: Variant = (E.floor_value(E.multiply(excess, weights[owner])) if E.lt(E.add(i, 1), owners.size()) else E.subtract(excess, assigned))
assigned = E.add(assigned, amount)
if (not target.binds.has(owner)):
target.binds[owner] = {}
target.binds[owner][previous] = target.binds[owner].get(previous, 0.0) + amount
target.amount[previous] -= amount
target.binds[owner][previous] = E.add(target.binds[owner].get(previous, 0), amount)
target.amount[previous] = E.subtract(target.amount[previous], amount)
reconcile(world, target, rules)
assign_homes(world, target, rules)
@@ -168,13 +168,10 @@ static func habitat_budget(world, target: Dictionary, rules: Dictionary) -> Dict
var structures: Array = []
var capacity: Dictionary = {}
for id in world.catalog.entities:
if world.catalog.entities[id].claim != "":
if E.ne(world.catalog.entities[id].claim, ""):
structures.append(id)
capacity[id] = physical.get(id, 0.0)
structures.sort_custom(func(a, b):
var left: float = world.catalog.entities[a].civ_bonus
var right: float = world.catalog.entities[b].civ_bonus
return left > right if left != right else str(a) < str(b))
capacity[id] = physical.get(id, 0)
structures.sort_custom(func ( a , b ) : return (E.gt(world.catalog.entities[a].civ_bonus, world.catalog.entities[b].civ_bonus) if E.ne(world.catalog.entities[a].civ_bonus, world.catalog.entities[b].civ_bonus) else E.lt(str(a), str(b))))
var stages: Array = []
for id in rules:
if is_space_civilization(id, rules): stages.append(id)
@@ -182,23 +179,23 @@ static func habitat_budget(world, target: Dictionary, rules: Dictionary) -> Dict
var requirements: Dictionary = {}
var anchors: Dictionary = {}
var anchor: String = ""
var units: float = 1.0
var units: Variant = 1
for id in stages:
var direct: String = ""
for structure in structures:
if world.catalog.entities[structure].claim == id: direct = structure
if direct != "":
if E.eq(world.catalog.entities[structure].claim, id): direct = structure
if E.ne(direct, ""):
anchor = direct
units = 1.0
units = 1
else:
units *= 10.0
units = E.multiply(units, 10)
anchors[id] = anchor
if id.begins_with("C3.") or id.begins_with("C4."):
if (id.begins_with("C3.") or id.begins_with("C4.")):
requirements[id] = {"id": anchor, "units": units}
var requested: Dictionary = {}
for id in requirements:
var req: Dictionary = requirements[id]
requested[req.id] = requested.get(req.id, 0.0) + physical.get(id, 0.0) * req.units
requested[req.id] = E.add(requested.get(req.id, 0), E.multiply(physical.get(id, 0), req.units))
return {"capacity": capacity, "requested": requested, "requirements": requirements, "structures": structures, "anchors": anchors}
static func assign_homes(world, target: Dictionary, rules: Dictionary) -> void:
@@ -209,29 +206,29 @@ static func assign_homes(world, target: Dictionary, rules: Dictionary) -> void:
stages.sort()
stages.reverse()
for id in stages:
var left: float = target.amount.get(id, 0.0)
var left: Variant = target.amount.get(id, 0)
var homes: Dictionary = {}
var allowed: bool = budget.anchors[id] == ""
var allowed: bool = E.eq(budget.anchors[id], "")
for structure in budget.structures:
if structure == budget.anchors[id]: allowed = true
if not allowed or budget.capacity[structure] <= 0.0: continue
var amount: float = left
if world.catalog.entities[structure].claim == id:
amount = minf(amount, maxf(0.0, budget.capacity[structure] - used.get(structure, 0.0)))
used[structure] = used.get(structure, 0.0) + amount
if amount > 0.0: homes[structure] = amount
left -= amount
if left <= 0.0: break
if E.eq(structure, budget.anchors[id]): allowed = true
if ((not allowed) or E.le(budget.capacity[structure], 0)): continue
var amount: Variant = left
if E.eq(world.catalog.entities[structure].claim, id):
amount = E.minimum(amount, E.maximum(0, E.subtract(budget.capacity[structure], used.get(structure, 0))))
used[structure] = E.add(used.get(structure, 0), amount)
if E.gt(amount, 0): homes[structure] = amount
left = E.subtract(left, amount)
if E.le(left, 0): break
target.civilization_homes[id] = homes
static func effective_span(world, target: Dictionary, id: String, rules: Dictionary) -> float:
if not rules.has(id): return world.catalog.entities[id].lifespan
var base: float = rules[id].span
if not is_space_civilization(id, rules): return base
static func effective_span(world, target: Dictionary, id: String, rules: Dictionary) -> Variant:
if (not rules.has(id)): return world.catalog.entities[id].lifespan
var base: Variant = rules[id].span
if (not is_space_civilization(id, rules)): return base
assign_homes(world, target, rules)
var population: float = target.amount.get(id, 0.0)
if population <= 0.0: return base
var weighted: float = population
var population: Variant = target.amount.get(id, 0)
if E.le(population, 0): return base
var weighted: Variant = population
for home in target.civilization_homes.get(id, {}):
weighted += target.civilization_homes[id][home] * (maxf(0.0, world.catalog.entities[home].civ_bonus) - 1.0)
return base * population / weighted if weighted > 0.0 else base
weighted = E.add(weighted, E.multiply(target.civilization_homes[id][home], (E.subtract(E.maximum(0, world.catalog.entities[home].civ_bonus), 1))))
return (E.divide(E.multiply(base, population), weighted) if E.gt(weighted, 0) else base)
+56 -55
View File
@@ -1,4 +1,5 @@
extends "res://world/owniverse/simulation_module.gd"
const E = preload("res://world/owniverse/math/exact.gd")
const CivilizationEvolution = preload("res://world/owniverse/civilization_evolution.gd")
const CivilizationState = preload("res://world/owniverse/civilization_state.gd")
const SimulationTime = preload("res://world/owniverse/simulation_time.gd")
@@ -12,26 +13,26 @@ func initialize_life_batches() -> void:
for id in world.catalog.entities:
if world.catalog.entities[id].life_rule.is_empty():
continue
var tracked: float = 0.0
for batch: Dictionary in world.state.batches.get(id, []):
tracked += batch.amount
world.batches.append_batch(world.state, id, maxf(0.0, physical.get(id, 0.0) - tracked), world.state.time.datetime, world.state.time.datetime)
var tracked: Variant = 0
for batch : Dictionary in world.state.batches.get(id, []):
tracked = E.add(tracked, batch.amount)
world.batches.append_batch(world.state, id, E.maximum(0, E.subtract(physical.get(id, 0), tracked)), world.state.time.datetime, world.state.time.datetime)
func evolute_to_datetime(datetime: float) -> void:
func evolute_to_datetime(datetime: Variant, clock: Dictionary = {}) -> void:
# Reconcile only when simulation advances.
# Fractional frame updates cannot change capacity or populations.
var processed_year: float = SimulationTime.processed_year(datetime)
if processed_year > world.state.time.year:
var processed_year: Variant = SimulationTime.processed_year(datetime)
if E.gt(processed_year, world.state.time.year):
initialize_life_batches()
var previous: Dictionary = world.state
var next: Dictionary = previous.duplicate_deep()
next.version = SimulationTime.STATE_VERSION
next.speed = clampi(int(previous.speed), 1, Global.SPEED_LIMIT)
next.speed_in_year = pow(10.0, next.speed - 1)
var from_year: float = previous.time.year
var to_year: float = processed_year
if to_year > from_year:
next.speed = clampi(E.integer(previous.speed), 1, Global.SPEED_LIMIT)
next.speed_in_year = E.power(10, E.subtract(next.speed, 1))
var from_year: Variant = previous.time.year
var to_year: Variant = processed_year
if E.gt(to_year, from_year):
var quantities: Dictionary = world.ledger.physical_amounts(previous)
var distribution: Dictionary = QuantityMath.normalize_distribution(quantities)
next.distribution = distribution
@@ -40,23 +41,23 @@ func evolute_to_datetime(datetime: float) -> void:
evolve_batches(next, to_year)
world.ledger.refresh_groups(next)
world.ledger.clamp_auto_resources(next)
WorldParameters.advance(previous, next, Global.CivilizationRules, to_year - from_year)
next.time = {"datetime": datetime, "year": to_year}
WorldParameters.advance(previous, next, Global.CivilizationRules, E.subtract(to_year, from_year))
next.time = clock if not clock.is_empty() else {"datetime": datetime, "year": to_year}
# Publish only after every phase has finished; previous stays untouched.
world.state = next
if to_year > from_year:
world.state = E.bound_state(next)
if E.gt(to_year, from_year):
world.presentation.refresh(world.state, world.catalog)
func evolve_batches(target: Dictionary, to_year: float) -> void:
func evolve_batches(target: Dictionary, to_year: Variant) -> void:
# A long cycle may contain several generations; preserve actual birth intervals.
var completed: Dictionary = {}
while evolve_batch_generation(target, to_year, completed):
pass
func evolve_batch_generation(target: Dictionary, to_year: float, completed: Dictionary = {}) -> bool:
func evolve_batch_generation(target: Dictionary, to_year: Variant, completed: Dictionary = { }) -> bool:
# Apply direct completions at the original next-generation boundary, so
# binding transfers, the blast environment and capacity checks keep order.
var completed_now: Dictionary = completed.duplicate(true)
@@ -65,81 +66,81 @@ func evolve_batch_generation(target: Dictionary, to_year: float, completed: Dict
var expired: Dictionary = {}
var preview: Dictionary = {"batches": {}}
for id in target.batches:
if world.catalog.entities.has(id) and world.catalog.entities[id].destroy_level > 0:
if (world.catalog.entities.has(id) and E.gt(world.catalog.entities[id].destroy_level, 0)):
preview.batches[id] = target.batches[id].duplicate(true)
for id in target.batches.keys():
if not world.catalog.entities.has(id) or world.catalog.entities[id].destroy_level <= 0:
if ((not world.catalog.entities.has(id)) or E.le(world.catalog.entities[id].destroy_level, 0)):
continue
var dying: float = 0.0
for part: Dictionary in completed_now.get(id, []):
dying += part.amount
for part: Dictionary in world.batches.withdraw_batches(preview, id, to_year):
dying += part.amount
if quantities.get(id, 0.0) > 0.0:
world.stellar.destroy_planets(target, id, dying / quantities[id], world.catalog.entities[id].destroy_level)
var dying: Variant = 0
for part : Dictionary in completed_now.get(id, []):
dying = E.add(dying, part.amount)
for part : Dictionary in world.batches.withdraw_batches(preview, id, to_year):
dying = E.add(dying, part.amount)
if E.gt(quantities.get(id, 0), 0):
world.stellar.destroy_planets(target, id, E.divide(dying, quantities[id]), world.catalog.entities[id].destroy_level)
quantities = world.ledger.physical_amounts(target)
# Withdraw all types before creating successors: iteration order cannot affect them.
for id in target.batches.keys():
if world.catalog.entities.has(id):
expired[id] = completed_now.get(id, []) + world.batches.withdraw_batches(target, id, to_year)
expired[id] = E.add(completed_now.get(id, []), world.batches.withdraw_batches(target, id, to_year))
var has_expired: bool = false
for parts: Array in expired.values():
if not parts.is_empty():
for parts : Array in expired.values():
if (not parts.is_empty()):
has_expired = true
break
if not has_expired:
if (not has_expired):
return false
var fractions: Dictionary = {}
var outputs: Dictionary = {}
var star_budget: Dictionary = CivilizationState.stellar_budget(world, target, Global.CivilizationRules)
var civilization_transaction: Dictionary = CivilizationEvolution.prepare(world, target, expired, to_year, star_budget, Global.CivilizationRules)
for id in expired:
var total: float = 0.0
for part: Dictionary in expired[id]:
total += part.amount
fractions[id] = clampf(total / quantities[id], 0.0, 1.0) if quantities.get(id, 0.0) > 0.0 else 0.0
var total: Variant = 0
for part : Dictionary in expired[id]:
total = E.add(total, part.amount)
fractions[id] = (E.clamp_value(E.divide(total, quantities[id]), 0, 1) if E.gt(quantities.get(id, 0), 0) else 0)
var combined: Dictionary = {}
for part: Dictionary in expired[id]:
if not part.has("products"):
for part : Dictionary in expired[id]:
if (not part.has("products")):
part.products = world.stellar.evolution_products(target, world.catalog.entities[id], part.amount)
for successor in part.products.keys():
if Global.CivilizationRules.has(id):
continue
var requested: float = part.products[successor]
var admitted: float = CivilizationState.admit_transition(star_budget, Global.CivilizationRules, id, successor, requested)
var requested: Variant = part.products[successor]
var admitted: Variant = CivilizationState.admit_transition(star_budget, Global.CivilizationRules, id, successor, requested)
part.products[successor] = admitted
if admitted < requested:
part.products[id] = part.products.get(id, 0.0) + requested - admitted
if E.lt(admitted, requested):
part.products[id] = E.subtract(E.add(part.products.get(id, 0), requested), admitted)
for successor in part.products:
combined[successor] = combined.get(successor, 0.0) + part.products[successor]
combined[successor] = E.add(combined.get(successor, 0), part.products[successor])
outputs[id] = {}
if total > 0.0:
if E.gt(total, 0):
for successor in combined:
outputs[id][successor] = combined[successor] / total
outputs[id][successor] = E.divide(combined[successor], total)
var incoming: Dictionary = world.ledger.transfer_evolution(target, fractions, outputs)
var new_owners: Dictionary = {}
var blast_power: float = 0.0
var blast_power: Variant = 0
var environment: Dictionary = world.environment.stellar_environment(target)
for id in expired:
for part: Dictionary in expired[id]:
for part : Dictionary in expired[id]:
for successor in part.products:
var amount: float = part.products[successor]
if successor == "blast":
blast_power += amount
var amount: Variant = part.products[successor]
if E.eq(successor, "blast"):
blast_power = E.add(blast_power, amount)
continue
if not world.catalog.entities.has(successor):
if (not world.catalog.entities.has(successor)):
continue
if successor == id and (not world.catalog.entities[id].life_rule.is_empty() or Global.CivilizationRules.has(id)):
target.amount[id] = target.amount.get(id, 0.0) + amount
if (E.eq(successor, id) and (((not world.catalog.entities[id].life_rule.is_empty()) or Global.CivilizationRules.has(id)))):
target.amount[id] = E.add(target.amount.get(id, 0), amount)
else:
amount = world.ledger.credit(target, successor, amount)
var before_batches: Array = target.batches.get(successor, []).duplicate(true)
var cutoff: float = to_year if world.stellar.uses_short_lived_fast_path(successor) else -INF
var integral: float = world.batches.append_batch(target, successor, amount, part.from, part.to, completed, cutoff)
var cutoff: Variant = (to_year if world.stellar.uses_short_lived_fast_path(successor) else E.neg(INF))
var integral: Variant = world.batches.append_batch(target, successor, amount, part.from, part.to, completed, cutoff)
world.automatic.produce_newborn_lifetime(target, successor, amount, part.from, part.to, to_year, before_batches, integral)
new_owners[successor] = new_owners.get(successor, 0.0) + amount
new_owners[successor] = E.add(new_owners.get(successor, 0), amount)
world.ledger.accept_transfers(target, incoming)
world.ledger.bind_new_owners(target, new_owners)
world.ledger.round_object_allocations(target)
+43 -54
View File
@@ -1,101 +1,90 @@
extends RefCounted
const E = preload("res://world/owniverse/math/exact.gd")
const GREEN = "#40cc40"
const RED = "#ff6060"
const GREY = "#888888"
const Parameters = preload("res://world/owniverse/world_parameters.gd")
static func number(value: float, digits: int = 0) -> String:
if not is_finite(value):
return "—"
if absf(value) >= 1e5:
var exponent: int = int(floor(log(absf(value)) / log(10.0)))
return "%.4fE%+d" % [value / pow(10.0, exponent), exponent]
return "%.*f" % [digits, value]
static func number(value, digits: int = 0) -> String:
if not E.finite(value): return "—"
return E.scientific(value, 4) if E.ge(E.absolute(value), 100000) else E.fixed(value, digits)
static func colored(text: String, color: String) -> String:
return "[color=" + color + "]" + text + "[/color]"
return E.add(E.add(E.add(E.add("[color=", color), "]"), text), "[/color]")
static func matches(world, id: String) -> Array:
var result: Array = []
for key in world.catalog.entities:
var entity = world.catalog.entities[key]
if key == id or entity.count_as == id or entity.group_as == id:
if not world.catalog.is_group(key):
if ((E.eq(key, id) or E.eq(entity.count_as, id)) or E.eq(entity.group_as, id)):
if (not world.catalog.is_group(key)):
result.append(key)
return result
static func quantities(world, id: String) -> Dictionary:
var free: float = 0.0
var total: float = 0.0
var free: Variant = 0
var total: Variant = 0
var physical: Dictionary = world.ledger.physical_amounts(world.state)
for key in matches(world, id):
free += world.state.amount.get(key, 0.0)
total += physical.get(key, 0.0)
if id == "SMBH":
free = world.state.amount.get(id, 0.0)
free = E.add(free, world.state.amount.get(key, 0))
total = E.add(total, physical.get(key, 0))
if E.eq(id, "SMBH"):
free = world.state.amount.get(id, 0)
total = free
return {"free": free, "total": total}
static func costs(world, global: Node, id: String) -> String:
if not world.catalog.entities.has(id):
if (not world.catalog.entities.has(id)):
return ""
var lines: PackedStringArray = []
for requirements in [world.catalog.entities[id].cost, world.catalog.entities[id].bind]:
for resource in requirements:
var required: float = requirements[resource]
if required < 0.0:
var required: Variant = requirements[resource]
if E.lt(required, 0):
continue
var available: float = floorf(world.ledger.available(world.state, resource))
var available: Variant = E.floor_value(world.ledger.available(world.state, resource))
var name: String = str(global.get_template(resource, "Entity", "Name").get("text", resource))
lines.append(colored(name + ": " + number(required) + " (" + number(available) + ")", GREEN if available >= required else RED))
lines.append(colored(E.add(E.add(E.add(E.add(E.add(name, ": "), number(required)), " ("), number(available)), ")"), (GREEN if E.ge(available, required) else RED)))
return "\n".join(lines)
static func variables(world, global: Node, id: String, rate: float = 0.0) -> Dictionary:
static func variables(world, global: Node, id: String, rate: Variant = 0.0) -> Dictionary:
var counts: Dictionary = quantities(world, id)
var show_total: bool = false
for key in matches(world, id):
show_total = show_total or world.catalog.entities[key].group_as == "Planet"
var amount: String = number(counts.total if show_total else counts.free) if world.catalog.entities.has(id) else "—"
if not show_total and counts.total > counts.free:
amount += " " + colored("(" + number(counts.total) + ")", GREY)
if rate != 0.0:
amount += " | " + colored(("−" if rate < 0.0 else "+") + number(absf(rate), 1), RED if rate < 0.0 else GREEN)
show_total = (show_total or E.eq(world.catalog.entities[key].group_as, "Planet"))
var amount: String = (number((counts.total if show_total else counts.free)) if world.catalog.entities.has(id) else "—")
if ((not show_total) and E.gt(counts.total, counts.free)):
amount = E.add(amount, E.add(" ", colored(E.add(E.add("(", number(counts.total)), ")"), GREY)))
if E.ne(rate, 0):
amount = E.add(amount, E.add(" | ", colored(E.add((("−" if E.lt(rate, 0) else "+")), number(E.absolute(rate), 1)), (RED if E.lt(rate, 0) else GREEN))))
var physical: Dictionary = world.ledger.physical_amounts(world.state)
var voids: float = 0.0
var voids: Variant = 0
for key in physical:
voids += physical[key] * world.catalog.entities[key].voids
var he: float = world.state.amount.get("He", 0.0)
var h: float = world.state.amount.get("H", 0.0)
voids = E.add(voids, E.multiply(physical[key], world.catalog.entities[key].voids))
var he: Variant = world.state.amount.get("He", 0)
var h: Variant = world.state.amount.get("H", 0)
var mixture: String = "—"
if he + h > 0.0:
mixture = "He: " + number(100.0 * he / (he + h), 2) + "% / H: " + number(100.0 * h / (he + h), 2) + "%"
if E.gt(E.add(he, h), 0):
mixture = E.add(E.add(E.add(E.add("He: ", number(E.divide(E.multiply(100, he), (E.add(he, h))), 2)), "% / H: "), number(E.divide(E.multiply(100, h), (E.add(he, h))), 2)), "%")
var environment: Dictionary = world.environment.stellar_environment(world.state)
return {
"amount": amount, "colored costs": costs(world, global, id),
"lifespan": number(world.catalog.entities[id].lifespan) if world.catalog.entities.has(id) else "—",
"voids": number(voids), "he / h": "\n" + mixture,
"metallicity": number(environment.metallicity * 100.0, 2) + "%",
"энергия": number(world.state.energy), "плотность": number(environment.density, 6),
"лет в секунду": number(world.state.speed_in_year),
"amount1": "—", "amount2": "—", "amount3": "—", "amount4": "—",
}
return {"amount": amount, "colored costs": costs(world, global, id), "lifespan": (number(world.catalog.entities[id].lifespan) if world.catalog.entities.has(id) else "—"), "voids": number(voids), "he / h": E.add("\n", mixture), "metallicity": E.add(number(E.multiply(environment.metallicity, 100), 2), "%"), "энергия": number(world.state.energy), "плотность": number(environment.density, 6), "лет в секунду": number(world.state.speed_in_year), "amount1": "—", "amount2": "—", "amount3": "—", "amount4": "—"}
static func render(world, global: Node, id: String, category: String = "Entity", rate: float = 0.0) -> String:
static func render(world, global: Node, id: String, category: String = "Entity", rate: Variant = 0.0) -> String:
var values: Dictionary = variables(world, global, id, rate)
if category == "Indicator":
if id == "Density":
if E.eq(category, "Indicator"):
if E.eq(id, "Density"):
id = Parameters.density_display(world.environment.stellar_environment(world.state).density).mode
var scales: Dictionary = {"Burnability": "Bur", "Explosiveness": "Exp", "Aggression": "Agg"}
if scales.has(id):
var level: float = Parameters.value(world.state, scales[id])
values.amount = number(level, 1 if level != floorf(level) else 0)
values.scale = global.render_template(global.get_template("Scale" + str(clampi(roundi(level), 1, 19)), "Indicator", "Scale"), {})
if id == "Trends":
var level: Variant = Parameters.value(world.state, scales[id])
values.amount = number(level, (1 if E.ne(level, E.floor_value(level)) else 0))
values.scale = global.render_template(global.get_template(E.add("Scale", str(clampi(E.integer(E.round_value(level)), 1, 19))), "Indicator", "Scale"), {})
if E.eq(id, "Trends"):
for trend in Parameters.TRENDS:
values[trend.to_lower()] = number(world.state.get("trends", {}).get(trend, 0.0) * 100.0, 2) + "%"
if id == "Energy":
values["энергия"] = number(world.state.energy) + " (" + number(clampf(world.state.energy / Parameters.INITIAL_ENERGY, 0.0, 1.0) * 100.0, 2) + "%)"
values[trend.to_lower()] = E.add(number(E.multiply(world.state.get("trends", {}).get(trend, 0), 100), 2), "%")
if E.eq(id, "Energy"):
values["энергия"] = E.add(E.add(E.add(number(world.state.energy), " ("), number(E.multiply(E.clamp_value(E.divide(world.state.energy, Parameters.INITIAL_ENERGY), 0, 1), 100), 2)), "%)")
var title: String = global.render_template(global.get_template(id, category, "Name"), values)
var body: String = global.render_template(global.get_template(id, category, "Description"), values)
return "[b]" + (title if title != "" else id) + "[/b]\n" + body
return E.add(E.add(E.add("[b]", ((title if E.ne(title, "") else id))), "[/b]\n"), body)
+22 -21
View File
@@ -1,54 +1,55 @@
extends RefCounted
const E = preload("res://world/owniverse/math/exact.gd")
## Read-only projections of simulation state. No controls or simulation mutations.
var pending_amounts: Dictionary[String, float] = {}
var pending_amounts: Dictionary [ String , Variant ] = {}
var deferred_updates: bool = false
var dirty: bool = true
func refresh(state: Dictionary, catalog, force: bool = false) -> void:
dirty = true
if deferred_updates and not force:
if (deferred_updates and (not force)):
return
pending_amounts = amounts(state, catalog)
dirty = false
## Aliases are presentation-only; group counters are not counted twice.
static func amounts(state: Dictionary, catalog, total_produced: bool = false) -> Dictionary[String, float]:
var source: Dictionary = state.stats.total_amount if total_produced else state.amount
if not total_produced:
static func amounts(state: Dictionary, catalog, total_produced: bool = false) -> Dictionary [ String, Variant ]:
var source: Dictionary = (state.stats.total_amount if total_produced else state.amount)
if (not total_produced):
source = source.duplicate()
var physical: Dictionary = {}
for id in catalog.entities:
if not catalog.is_group(id):
physical[id] = maxf(0.0, state.amount.get(id, 0.0))
for bindings: Dictionary in state.binds.values():
if (not catalog.is_group(id)):
physical[id] = E.maximum(0, state.amount.get(id, 0))
for bindings : Dictionary in state.binds.values():
for id in bindings:
if physical.has(id):
physical[id] += maxf(0.0, bindings[id])
var planets: float = 0.0
physical[id] = E.add(physical[id], E.maximum(0, bindings[id]))
var planets: Variant = 0
for id in physical:
if catalog.entities[id].group_as == "Planet":
if E.eq(catalog.entities[id].group_as, "Planet"):
source[id] = physical[id]
planets += physical[id]
planets = E.add(planets, physical[id])
source["Planet"] = planets
var result: Dictionary[String, float] = {}
var result: Dictionary [ String , Variant ] = {}
for id in source:
result[id] = 0.0
result[id] = 0
for id in source:
# group_as counters are already sums and must not be counted a second time.
if catalog.is_group(id):
continue
var item_id: String = id
if catalog.entities.has(id) and catalog.entities[id].count_as != "":
if (catalog.entities.has(id) and E.ne(catalog.entities[id].count_as, "")):
item_id = catalog.entities[id].count_as
# Detailed icons need their own count as well as the summary alias.
# Aggregate only from source, never from these presentation entries.
if item_id != id:
result[id] = result.get(id, 0.0) + float(source[id])
result[item_id] = result.get(item_id, 0.0) + float(source[id])
if E.ne(item_id, id):
result[id] = E.add(result.get(id, 0), E.of(source[id]))
result[item_id] = E.add(result.get(item_id, 0), E.of(source[id]))
for id in source:
if catalog.is_group(id):
result[id] = float(source[id])
result[id] = E.of(source[id])
return result
@@ -63,12 +64,12 @@ static func unlocks(state: Dictionary, catalog, locked: Array) -> Dictionary:
var entity = catalog.entities[id]
var eligible: bool = true
for requirement in entity.unlock:
if maxf(state.stats.total_amount.get(requirement, 0.0), display_totals.get(requirement, 0.0)) < entity.unlock[requirement]:
if E.lt(E.maximum(state.stats.total_amount.get(requirement, 0), display_totals.get(requirement, 0)), entity.unlock[requirement]):
eligible = false
break
if eligible:
for item_id in [id, entity.count_as]:
if item_id != "" and not visible.has(item_id):
if (E.ne(item_id, "") and (not visible.has(item_id))):
visible.append(item_id)
else:
remaining.append(id)
+29 -31
View File
@@ -1,58 +1,56 @@
extends "res://world/owniverse/simulation_module.gd"
const E = preload("res://world/owniverse/math/exact.gd")
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
func life_products(target: Dictionary, entity: OwniverseEntity, count: float) -> Dictionary:
func life_products(target: Dictionary, entity: OwniverseEntity, count: Variant) -> Dictionary:
var rule: Dictionary = entity.life_rule
var pending: Dictionary = {}
var modifiers: Dictionary = WorldParameters.life_modifiers(target)
var probabilities: Dictionary = {
"progress": float(rule.progress) * modifiers.progress * WorldParameters.value(target, "Enlightenment"),
"degradation": float(rule.degradation) * modifiers.degradation,
}
var probability_sum: float = maxf(1.0, probabilities.progress + probabilities.degradation)
if not target.has("life_remainders"):
var probabilities: Dictionary = {"progress": E.multiply(E.multiply(E.of(rule.progress), modifiers.progress), WorldParameters.value(target, "Enlightenment")), "degradation": E.multiply(E.of(rule.degradation), modifiers.degradation)}
var probability_sum: Variant = E.maximum(1, E.add(probabilities.progress, probabilities.degradation))
if (not target.has("life_remainders")):
target.life_remainders = {}
for channel in ["progress", "degradation"]:
var key: String = entity.id + ":" + channel
var expected: float = count * probabilities[channel] / probability_sum + target.life_remainders.get(key, 0.0)
var key: String = E.add(E.add(entity.id, ":"), channel)
var expected: Variant = E.add(E.divide(E.multiply(count, probabilities[channel]), probability_sum), target.life_remainders.get(key, 0))
target.life_remainders[key] = expected
pending[channel] = floorf(snappedf(expected, 1.0e-12))
pending[channel] = E.floor_value(expected)
var assigned: Dictionary = QuantityMath.allocate_whole(pending, count)
for channel in assigned:
var key: String = entity.id + ":" + channel
target.life_remainders[key] = maxf(0.0, target.life_remainders[key] - assigned[channel])
var result: Dictionary = {entity.id: count - assigned.progress - assigned.degradation}
var key: String = E.add(E.add(entity.id, ":"), channel)
target.life_remainders[key] = E.maximum(0, E.subtract(target.life_remainders[key], assigned[channel]))
var result: Dictionary = {entity.id: E.subtract(E.subtract(count, assigned.progress), assigned.degradation)}
result[rule.next] = assigned.progress
var weights: Dictionary = {}
var weight_sum: float = 0.0
var weight_sum: Variant = 0
for term in str(rule.fall).split("^"):
var pair: PackedStringArray = term.split(":")
weights[pair[0]] = float(pair[1])
weight_sum += float(pair[1])
weights[pair[0]] = E.of(pair[1])
weight_sum = E.add(weight_sum, E.of(pair[1]))
var requests: Dictionary = {}
for id in weights:
var key: String = entity.id + ":fall:" + id
var expected: float = assigned.degradation * weights[id] / weight_sum + target.life_remainders.get(key, 0.0)
var key: String = E.add(E.add(entity.id, ":fall:"), id)
var expected: Variant = E.add(E.divide(E.multiply(assigned.degradation, weights[id]), weight_sum), target.life_remainders.get(key, 0))
target.life_remainders[key] = expected
requests[id] = maxf(0.0, floorf(snappedf(expected, 1.0e-12)))
requests[id] = E.maximum(0, E.floor_value(expected))
var falls: Dictionary = QuantityMath.allocate_whole(requests, assigned.degradation)
var distributed: float = 0.0
var distributed: Variant = 0
for id in falls:
distributed += falls[id]
result[id] = result.get(id, 0.0) + falls[id]
var key: String = entity.id + ":fall:" + id
target.life_remainders[key] -= falls[id]
distributed = E.add(distributed, falls[id])
result[id] = E.add(result.get(id, 0), falls[id])
var key: String = E.add(E.add(entity.id, ":fall:"), id)
target.life_remainders[key] = E.subtract(target.life_remainders[key], falls[id])
# Fill sub-unit fall quotas deterministically, retaining signed allocation errors.
while distributed < assigned.degradation:
while E.lt(distributed, assigned.degradation):
var selected: String = ""
var largest: float = -INF
var largest: Variant = E.neg(INF)
for id in weights:
var rest: float = target.life_remainders[entity.id + ":fall:" + id]
if rest > largest:
var rest: Variant = target.life_remainders[E.add(E.add(entity.id, ":fall:"), id)]
if E.gt(rest, largest):
largest = rest
selected = id
result[selected] = result.get(selected, 0.0) + 1.0
target.life_remainders[entity.id + ":fall:" + selected] -= 1.0
distributed += 1.0
result[selected] = E.add(result.get(selected, 0), 1)
target.life_remainders[E.add(E.add(entity.id, ":fall:"), selected)] = E.subtract(target.life_remainders[E.add(E.add(entity.id, ":fall:"), selected)], 1)
distributed = E.add(distributed, 1)
return result
+93 -94
View File
@@ -1,10 +1,10 @@
extends "res://world/owniverse/simulation_module.gd"
const E = preload("res://world/owniverse/math/exact.gd")
const CivilizationState = preload("res://world/owniverse/civilization_state.gd")
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
func try_production(target: Dictionary, entity: OwniverseEntity, amount: float,
automatic: bool = false) -> bool:
if amount <= 0.0 or amount != floorf(amount):
func try_production(target: Dictionary, entity: OwniverseEntity, amount: Variant, automatic: bool = false) -> bool:
if (E.le(amount, 0) or E.ne(amount, E.floor_value(amount))):
return false
var trial: Dictionary = target.duplicate()
trial.amount = target.amount.duplicate()
@@ -13,26 +13,26 @@ func try_production(target: Dictionary, entity: OwniverseEntity, amount: float,
trial.batches = target.batches.duplicate()
var copied_batches: Dictionary = {}
for resource in entity.cost:
var cost: float = roundf(maxf(0.0, entity.cost[resource]) * amount)
if automatic and trial.auto_resources.get(resource, 0.0) < cost:
var cost: Variant = E.round_value(E.multiply(E.maximum(0, entity.cost[resource]), amount))
if (automatic and E.lt(trial.auto_resources.get(resource, 0), cost)):
return false
var before: Dictionary = world.ledger.physical_amounts(trial)
var taken: Dictionary = world.ledger.take_available(trial, resource, cost)
var total: float = 0.0
var total: Variant = 0
for quantity in taken.values():
total += quantity
if total != cost:
total = E.add(total, quantity)
if E.ne(total, cost):
return false
if automatic:
trial.auto_resources[resource] = maxf(0.0, trial.auto_resources.get(resource, 0.0) - cost)
trial.auto_resources[resource] = E.maximum(0, E.subtract(trial.auto_resources.get(resource, 0), cost))
for id in taken:
# Isolate only batches actually touched by this transaction.
if trial.batches.has(id) and not copied_batches.has(id):
if (trial.batches.has(id) and (not copied_batches.has(id))):
trial.batches[id] = trial.batches[id].duplicate(true)
copied_batches[id] = true
var factor: float = 1.0 - taken[id] / before[id] if before.get(id, 0.0) > 0.0 else 0.0
var factor: Variant = (E.subtract(1, E.divide(taken[id], before[id])) if E.gt(before.get(id, 0), 0) else 0)
world.batches.scale_batches(trial, id, factor)
if not world.ledger.bind_new_owners(trial, {entity.id: amount}, true):
if (not world.ledger.bind_new_owners(trial, {entity.id: amount}, true)):
return false
world.ledger.refresh_groups(trial)
world.ledger.clamp_auto_resources(trial)
@@ -46,72 +46,71 @@ func smbh_sources() -> Dictionary:
var masses: Dictionary = {}
for id in world.catalog.entities:
var entity: OwniverseEntity = world.catalog.entities[id]
if entity.count_as == "BH" and entity.mass > 0.0:
masses[id] = floorf(maxf(0.0, world.state.amount.get(id, 0.0))) * entity.mass
if (E.eq(entity.count_as, "BH") and E.gt(entity.mass, 0)):
masses[id] = E.multiply(E.floor_value(E.maximum(0, world.state.amount.get(id, 0))), entity.mass)
return masses
func smbh_available() -> float:
var mass: float = 0.0
func smbh_available() -> Variant:
var mass: Variant = 0
for value in smbh_sources().values():
mass += value
return floorf(mass)
mass = E.add(mass, value)
return E.floor_value(mass)
func plan_smbh(desired_amount: float) -> Dictionary:
var empty: Dictionary = {"amount": 0.0, "taken": {}}
if desired_amount <= 0.0 or not is_finite(desired_amount):
func plan_smbh(desired_amount: Variant) -> Dictionary:
var empty: Dictionary = {"amount": 0, "taken": {}}
if (E.le(desired_amount, 0) or (not E.finite(desired_amount))):
return empty
var total_mass: float = smbh_available()
if total_mass <= 0.0:
var total_mass: Variant = smbh_available()
if E.le(total_mass, 0):
return empty
var requested: float = minf(floorf(desired_amount), total_mass)
if requested <= 0.0:
var requested: Variant = E.minimum(E.floor_value(desired_amount), total_mass)
if E.le(requested, 0):
return empty
var masses: Dictionary = smbh_sources()
var distribution: Dictionary = QuantityMath.normalize_distribution(masses)
var taken: Dictionary = {}
var order: Array = []
var consumed_mass: float = 0.0
var consumed_mass: Variant = 0
for id in distribution:
var expected: float = requested * distribution[id] / world.catalog.entities[id].mass
taken[id] = minf(floorf(expected), floorf(world.state.amount[id]))
consumed_mass += taken[id] * world.catalog.entities[id].mass
order.append({"id": id, "remainder": expected - taken[id]})
order.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
return a.remainder > b.remainder if a.remainder != b.remainder else str(a.id) < str(b.id))
var expected: Variant = E.divide(E.multiply(requested, distribution[id]), world.catalog.entities[id].mass)
taken[id] = E.minimum(E.floor_value(expected), E.floor_value(world.state.amount[id]))
consumed_mass = E.add(consumed_mass, E.multiply(taken[id], world.catalog.entities[id].mass))
order.append({"id": id, "remainder": E.subtract(expected, taken[id])})
order.sort_custom(func ( a : Dictionary , b : Dictionary ) -> bool : return (E.gt(a.remainder, b.remainder) if E.ne(a.remainder, b.remainder) else E.lt(str(a.id), str(b.id))))
# Consume whole BH only; all excess mass becomes SMBH, never disappears.
for candidate: Dictionary in order:
if consumed_mass >= requested:
for candidate : Dictionary in order:
if E.ge(consumed_mass, requested):
break
var id: String = candidate.id
if taken[id] < floorf(world.state.amount[id]):
taken[id] += 1.0
consumed_mass += world.catalog.entities[id].mass
if consumed_mass <= 0.0 or consumed_mass != floorf(consumed_mass):
if E.lt(taken[id], E.floor_value(world.state.amount[id])):
taken[id] = E.add(taken[id], 1)
consumed_mass = E.add(consumed_mass, world.catalog.entities[id].mass)
if (E.le(consumed_mass, 0) or E.ne(consumed_mass, E.floor_value(consumed_mass))):
return empty
return {"amount": consumed_mass, "taken": taken}
func produce_smbh(desired_amount: float) -> float:
func produce_smbh(desired_amount: Variant) -> Variant:
var plan: Dictionary = plan_smbh(desired_amount)
var consumed_mass: float = plan.amount
var consumed_mass: Variant = plan.amount
var taken: Dictionary = plan.taken
if consumed_mass <= 0.0:
return 0.0
if E.le(consumed_mass, 0):
return 0
var next: Dictionary = world.state.duplicate(true)
var physical: Dictionary = world.ledger.physical_amounts(world.state)
for id in taken:
if taken[id] <= 0.0:
if E.le(taken[id], 0):
continue
next.amount[id] -= taken[id]
var fraction: float = taken[id] / physical[id]
world.batches.scale_batches(next, id, 1.0 - fraction)
next.amount[id] = E.subtract(next.amount[id], taken[id])
var fraction: Variant = E.divide(taken[id], physical[id])
world.batches.scale_batches(next, id, E.subtract(1, fraction))
# Release the consumed owners' share of any resources they held.
for resource in next.binds.get(id, {}):
var released: float = next.binds[id][resource] * fraction
next.binds[id][resource] -= released
next.amount[resource] = next.amount.get(resource, 0.0) + released
var released: Variant = E.multiply(next.binds[id][resource], fraction)
next.binds[id][resource] = E.subtract(next.binds[id][resource], released)
next.amount[resource] = E.add(next.amount.get(resource, 0), released)
world.ledger.credit(next, "SMBH", consumed_mass)
world.batches.append_batch(next, "SMBH", consumed_mass, world.state.time.datetime, world.state.time.datetime)
world.ledger.round_object_allocations(next)
@@ -122,29 +121,29 @@ func produce_smbh(desired_amount: float) -> float:
return consumed_mass
func produce(item_id: String, desired_amount: float) -> float:
if item_id == "SMBH":
func produce(item_id: String, desired_amount: Variant) -> Variant:
if E.eq(item_id, "SMBH"):
return produce_smbh(desired_amount)
# Use special production for S and H
if item_id == "S" or item_id == "H":
if (E.eq(item_id, "S") or E.eq(item_id, "H")):
return produce_hs(item_id, desired_amount)
var entity: OwniverseEntity = world.catalog.entities[item_id]
if desired_amount <= 0:
return 0.0
var max_producible = floorf(calculate_max_producible(entity, desired_amount))
if E.le(desired_amount, 0):
return 0
var max_producible = E.floor_value(calculate_max_producible(entity, desired_amount))
var budget: Dictionary = CivilizationState.stellar_budget(world, world.state, Global.CivilizationRules)
max_producible = CivilizationState.admit_transition(budget, Global.CivilizationRules, "", item_id, max_producible)
if max_producible <= 0:
return 0.0
var metallicity: float = world.environment.stellar_environment(world.state).metallicity
if not try_production(world.state, entity, max_producible):
return 0.0
if E.le(max_producible, 0):
return 0
var metallicity: Variant = world.environment.stellar_environment(world.state).metallicity
if (not try_production(world.state, entity, max_producible)):
return 0
# Add produced entities
world.ledger.process_entity(item_id, max_producible)
world.batches.add_entity_to_batch(item_id, max_producible, world.state.time.datetime)
@@ -154,49 +153,49 @@ func produce(item_id: String, desired_amount: float) -> float:
return max_producible
func produce_hs(entity_id: String, value: float) -> float:
if value <= 0:
return 0.0
var to_produce: float = floorf(value)
to_produce = minf(to_produce, floorf(world.state.energy))
if to_produce <= 0:
return 0.0
world.state.energy -= to_produce
func produce_hs(entity_id: String, value: Variant) -> Variant:
if E.le(value, 0):
return 0
var to_produce: Variant = E.floor_value(value)
to_produce = E.minimum(to_produce, E.floor_value(world.state.energy))
if E.le(to_produce, 0):
return 0
world.state.energy = E.subtract(world.state.energy, to_produce)
world.ledger.process_entity(entity_id, to_produce)
return to_produce
func production_step(entity: OwniverseEntity) -> float:
return QuantityMath.production_step(entity.cost.values() + entity.bind.values())
func production_step(entity: OwniverseEntity) -> Variant:
return QuantityMath.production_step(E.add(entity.cost.values(), entity.bind.values()))
func calculate_max_producible(entity: OwniverseEntity, desired_amount: float = 1.0E150) -> float:
if entity.id == "SMBH":
func calculate_max_producible(entity: OwniverseEntity, desired_amount: Variant = 1.0E150) -> Variant:
if E.eq(entity.id, "SMBH"):
return plan_smbh(desired_amount).amount
var requirements: Dictionary = {}
for resource in entity.cost:
requirements[resource] = maxf(0.0, entity.cost[resource])
requirements[resource] = E.maximum(0, entity.cost[resource])
for resource in entity.bind:
requirements[resource] = requirements.get(resource, 0.0) + maxf(0.0, entity.bind[resource])
requirements[resource] = E.add(requirements.get(resource, 0), E.maximum(0, entity.bind[resource]))
var has_requirements: bool = false
for quantity in requirements.values():
if quantity > 0.0:
if E.gt(quantity, 0):
has_requirements = true
break
# No recipe means unavailable for production, not unlimited free objects.
if not has_requirements:
return 0.0
var available: float = maxf(0.0, desired_amount)
if (not has_requirements):
return 0
var available: Variant = E.maximum(0, desired_amount)
for resource in requirements:
if requirements[resource] > 0.0:
available = minf(available, floorf(world.ledger.available(world.state, resource) / requirements[resource]))
if E.gt(requirements[resource], 0):
available = E.minimum(available, E.floor_value(E.divide(world.ledger.available(world.state, resource), requirements[resource])))
var concrete: Dictionary = world.ledger.expand_requirements(world.state, requirements)
for resource in concrete:
if concrete[resource] > 0.0:
available = minf(available, floorf(world.ledger.available(world.state, resource) / concrete[resource]))
var step: float = production_step(entity)
return floorf(available / step) * step if is_finite(step) else 0.0
if E.gt(concrete[resource], 0):
available = E.minimum(available, E.floor_value(E.divide(world.ledger.available(world.state, resource), concrete[resource])))
var step: Variant = production_step(entity)
return (E.multiply(E.floor_value(E.divide(available, step)), step) if E.finite(step) else 0)
+82
View File
@@ -0,0 +1,82 @@
# Числа симуляции
Количество объектов, запасы, энергия, расходы, привязки и статистика считаются
целочисленно, без перехода через `float`. Значения до `10^27` сохраняют каждую
единицу; промежуточные произведения могут превышать этот диапазон.
`big_integer.gd` хранит модуль числа массивом разрядов по основанию `10^9`,
младший разряд первым. Переносы при умножении не переполняют `int64`.
Деление использует нормализованную оценку цифры частного.
`exact.gd` — единый API арифметики. Малые значения остаются обычными `int`,
большие числа и дроби представлены `exact_value.gd`: знак, целый числитель,
целый знаменатель. Дроби сокращаются. Операции создают новые значения;
поля `ExactValue` и массивы его разрядов нельзя изменять после создания.
```gdscript
const E = preload("res://world/owniverse/math/exact.gd")
var stock = E.of("1e27") # Строка, не литерал float 1e27!
stock = E.subtract(stock, 1) # 999999999999999999999999999
var price = E.of("0.125") # Точно 1/8
var cost = E.multiply(price, 8) # Точно 1
if E.ge(stock, cost):
stock = E.subtract(stock, cost)
```
Использовать `E.add/subtract/multiply/divide`, `E.eq/lt/le/gt/ge`,
`E.floor_value/ceil_value/round_value`. Перегрузки операторов в GDScript нет:
обычные `+`, `==`, `min`, `float(...)` для чисел симуляции не подходят.
Сигнатуры принимают `Variant`, а не `float`. `E.integer` допускается только
для значений в диапазоне `int64`, например индексов и состояния генератора.
Для поиска чисел в массиве используется `E.contains`, поскольку сравнение
объектов по ссылке не является сравнением числовых значений.
## Границы точности
- Десятичные параметры CSV разбираются из строк точно. Стоимости, проверка
доступности и размер целой производственной партии не округляются.
- Суммы распределений и переносы целых объектов сохраняются точно.
- Промежуточная рациональная арифметика точна. Перед публикацией следующего
состояния знаменатели сохранённых дробей ограничиваются `10^36`.
Если знаменатель больше, значение усекается к нулю с ошибкой меньше
`10^-36`. Целые числа эта операция не меняет. Это предотвращает рост
знаменателей при многократном накоплении меняющихся долей. Такая точность
относится к каждой операции ограничения, а не является обещанием нулевой
суммарной ошибки при бесконечном числе циклов.
- Логарифмы, корни, нецелые степени и нормальная случайная выборка остаются
приближёнными. Результаты трансцендентных функций переводятся обратно в
рациональные коэффициенты с 12 знаками после точки. Статистическая выборка
не обеспечивает равномерный выбор каждого отдельного целого около `10^27`;
последующий учёт выбранного количества и остатка точен.
- Возраст хранится в целых тиках (`10^9` тиков на год). Малый остаток кадрового
времени остаётся `float` и переносится между кадрами; большой возраст
никогда не складывается с малым приращением через `float`.
- Интерфейс преобразует в `float` только координаты индикаторов. Текст чисел,
доступное количество и значения кнопок формируются из точных чисел.
`E.of(float)` сохраняет фактическое двоичное значение входного числа: он
не может восстановить единицы, уже потерянные предыдущими вычислениями.
## Сохранение
Формат сохранения — **3**, версия состояния — **0.2.0**. На главном потоке
`E.encode` строит независимый снимок, затем существующий фоновый писатель
сохраняет его. Большие целые и дроби кодируются как `{"$exact": "число"}`;
для дробей используется строка `числитель/знаменатель`. Целые за пределами
точного диапазона JSON/float тоже кодируются строкой. Зашифрованный файл и
диагностический JSON используют один снимок. После проверки `E.decode`
восстанавливает числа, без загрузки сериализованных объектов GDScript.
Миграции старой арифметики нет. Старый формат несовместим: существующий
механизм загрузки сохраняет его в `.incompatible.bak` и начинает новую игру.
Старое сохранение игрока не используется тестами и не изменялось при разработке.
## Проверки
`godot --headless --path Owniverse.GI --script res://tests/test_exact_math.gd`
Независимый эталон `tests/fixtures/exact_oracle.json` получен через Python
`fractions.Fraction` (120 пар, seed 4321). Тест сравнивает строковые результаты
суммы, разности, произведения, деления и округления вниз. Дополнительно
проверяются границы `int64`, JSON, рецепты, распределения, жизнь при `10^27`,
единичный расход энергии, тик времени и 100 циклов с меняющимися долями звёзд.
+102
View File
@@ -0,0 +1,102 @@
extends RefCounted
## Unsigned little-endian base 10^9 arithmetic. Inputs are never mutated.
const BASE: int = 1000000000
static func trim(a: Array) -> Array:
while a.size() > 1 and a.back() == 0: a.pop_back()
return a
static func parse(text: String) -> Array:
var result: Array = []
var end: int = text.length()
while end > 0:
var start: int = maxi(0, end - 9)
result.append(int(text.substr(start, end - start)))
end = start
return trim(result if not result.is_empty() else [0])
static func decimal(a: Array) -> String:
var result: String = str(a.back())
for i in range(a.size() - 2, -1, -1): result += "%09d" % a[i]
return result
static func compare(a: Array, b: Array) -> int:
if a.size() != b.size(): return 1 if a.size() > b.size() else -1
for i in range(a.size() - 1, -1, -1):
if a[i] != b[i]: return 1 if a[i] > b[i] else -1
return 0
static func add(a: Array, b: Array) -> Array:
var result: Array = []
var carry: int = 0
for i in maxi(a.size(), b.size()):
var value: int = carry + (a[i] if i < a.size() else 0) + (b[i] if i < b.size() else 0)
result.append(value % BASE)
carry = value / BASE
if carry: result.append(carry)
return result
static func subtract(a: Array, b: Array) -> Array:
assert(compare(a, b) >= 0, "Unsigned subtraction underflow")
var result: Array = []
var borrow: int = 0
for i in a.size():
var value: int = a[i] - borrow - (b[i] if i < b.size() else 0)
borrow = 1 if value < 0 else 0
result.append(value + BASE if borrow else value)
return trim(result)
static func multiply(a: Array, b: Array) -> Array:
var result: Array = []
result.resize(a.size() + b.size())
result.fill(0)
for i in a.size():
var carry: int = 0
for j in b.size():
# < BASE^2 + 2*BASE: comfortably inside signed int64.
var value: int = a[i] * b[j] + result[i + j] + carry
result[i + j] = value % BASE
carry = value / BASE
result[i + b.size()] = carry
return trim(result)
static func divmod(a: Array, b: Array) -> Array:
assert(b != [0], "Division by zero")
if compare(a, b) < 0: return [[0], a.duplicate()]
if b.size() == 1:
var quotient: Array = []
quotient.resize(a.size())
var carry: int = 0
for i in range(a.size() - 1, -1, -1):
var value: int = carry * BASE + a[i]
quotient[i] = value / b[0]
carry = value % b[0]
return [trim(quotient), [carry]]
# Normalize the leading divisor limb so the two-limb quotient estimate
# needs at most two downward corrections (Knuth's division estimate).
var factor: int = BASE / (b.back() + 1)
a = multiply(a, [factor])
b = multiply(b, [factor])
var quotient: Array = []
quotient.resize(a.size())
var remainder: Array = [0]
for i in range(a.size() - 1, -1, -1):
remainder.push_front(a[i])
trim(remainder)
var high: int = remainder[b.size()] if remainder.size() > b.size() else 0
var low: int = remainder[b.size() - 1] if remainder.size() >= b.size() else 0
var estimate: int = mini(BASE - 1, (high * BASE + low) / b.back())
var product: Array = multiply(b, [estimate])
while compare(product, remainder) > 0:
estimate -= 1
product = subtract(product, b)
quotient[i] = estimate
remainder = subtract(remainder, product)
return [trim(quotient), divmod(remainder, [factor])[0]]
static func gcd(a: Array, b: Array) -> Array:
while b != [0]:
var remainder: Array = divmod(a, b)[1]
a = b
b = remainder
return a
+1
View File
@@ -0,0 +1 @@
uid://6wfbh3vq5smd
+322
View File
@@ -0,0 +1,322 @@
extends RefCounted
## Exact signed rational arithmetic. Native small integers are the fast path.
const Big = preload("res://world/owniverse/math/big_integer.gd")
const Value = preload("res://world/owniverse/math/exact_value.gd")
const SMALL: int = 1000000000
const MAX_INT: int = 9223372036854775807
const MIN_INT: int = -9223372036854775807 - 1
const CARRY_SCALE: Array = [0, 0, 0, 0, 1] # 10^36
static func make(n: Array, d: Array = [1], polarity: int = 1):
assert(d != [0], "Zero denominator")
if n == [0]: return 0
if d != [1]:
var common: Array = Big.gcd(n, d)
n = Big.divmod(n, common)[0]
d = Big.divmod(d, common)[0]
if d == [1] and (n.size() == 1 or (n.size() == 2 and n[1] < SMALL)):
return polarity * (n[0] + (n[1] * SMALL if n.size() == 2 else 0))
if d == [1] and n.size() == 3:
var limit: Array = [854775807 if polarity > 0 else 854775808, 223372036, 9]
if Big.compare(n, limit) <= 0:
return (("-" if polarity < 0 else "") + Big.decimal(n)).to_int()
var result = Value.new()
result.numerator = n
result.denominator = d
result.polarity = polarity
return result
static func of(value):
if value is Value or value is int: return value
if value is float:
if not is_finite(value): return value
if value == floorf(value) and absf(value) < 1e18: return int(value)
# Imports the actual binary float, not an invented decimal rounding.
var data := PackedByteArray()
data.resize(8)
data.encode_double(0, value)
var bits: int = data.decode_s64(0)
var exponent: int = (bits >> 52) & 2047
var mantissa: int = bits & 4503599627370495
if exponent: mantissa += 4503599627370496
var power: int = exponent - 1075 if exponent else -1074
var n: Array = Big.parse(str(mantissa))
var d: Array = [1]
for i in absi(power):
if power > 0: n = Big.multiply(n, [2])
else: d = Big.multiply(d, [2])
return make(n, d, -1 if value < 0 else 1)
var text: String = str(value).strip_edges().to_lower()
if text == "": return 0
if "/" in text:
var terms = text.split("/")
assert(terms.size() == 2)
return divide(of(terms[0]), of(terms[1]))
var polarity: int = -1 if text.begins_with("-") else 1
text = text.trim_prefix("-").trim_prefix("+")
var terms = text.split("e")
var power: int = int(terms[1]) if terms.size() == 2 else 0
text = terms[0]
var point: int = text.find(".")
if point >= 0: power -= text.length() - point - 1
text = text.replace(".", "").replace("_", "")
assert(text.is_valid_int(), "Invalid exact number: " + str(value))
assert(absi(power) <= 10000, "Unreasonable decimal exponent")
return make(Big.parse(text + "0".repeat(maxi(0, power))), Big.parse("1" + "0".repeat(maxi(0, -power))), polarity)
static func parts(value) -> Array:
value = of(value)
if value is int: return [Big.parse(str(value).trim_prefix("-")), [1], -1 if value < 0 else 1]
return [value.numerator, value.denominator, value.polarity]
static func numeric(value) -> bool:
return value is int or value is float or value is Value
static func finite(value) -> bool:
return value is Value or value is int or (value is float and is_finite(value))
static func approx(value) -> float:
if not value is Value: return float(value)
var a: String = Big.decimal(value.numerator)
var b: String = Big.decimal(value.denominator)
return value.polarity * float(a.left(16)) / float(b.left(16)) * pow(10.0, maxi(0, a.length() - 16) - maxi(0, b.length() - 16))
static func compare(a, b) -> int:
if a is int and b is int: return 0 if a == b else (1 if a > b else -1)
if not finite(a) or not finite(b):
var x: float = approx(a)
var y: float = approx(b)
return 0 if x == y else (1 if x > y else -1)
var x: Array = parts(a)
var y: Array = parts(b)
if x[2] != y[2]: return 1 if x[2] > y[2] else -1
return x[2] * Big.compare(Big.multiply(x[0], y[1]), Big.multiply(y[0], x[1]))
static func eq(a, b) -> bool:
if a == null: return b == null
if b == null: return false
if numeric(a) and numeric(b):
if (a is float and is_nan(a)) or (b is float and is_nan(b)): return false
return compare(a, b) == 0
if a is Dictionary and b is Dictionary:
if a.size() != b.size(): return false
for k in a:
if not b.has(k) or not eq(a[k], b[k]): return false
return true
if a is Array and b is Array:
if a.size() != b.size(): return false
for i in a.size():
if not eq(a[i], b[i]): return false
return true
if (a is String or a is StringName) and (b is String or b is StringName):
return str(a) == str(b)
if typeof(a) != typeof(b): return false
return a == b
static func ne(a, b) -> bool: return not eq(a, b)
static func lt(a, b) -> bool: return compare(a, b) < 0 if numeric(a) and numeric(b) else a < b
static func le(a, b) -> bool: return compare(a, b) <= 0 if numeric(a) and numeric(b) else a <= b
static func gt(a, b) -> bool: return compare(a, b) > 0 if numeric(a) and numeric(b) else a > b
static func ge(a, b) -> bool: return compare(a, b) >= 0 if numeric(a) and numeric(b) else a >= b
static func minimum(a, b): return a if le(a, b) else b
static func maximum(a, b): return a if ge(a, b) else b
static func clamp_value(a, low, high): return minimum(maximum(a, low), high)
static func neg(a): return subtract(0, a)
static func absolute(a): return neg(a) if lt(a, 0) else a
static func add(a, b):
if not numeric(a) or not numeric(b): return a + b
if a is int and b is int:
if (b >= 0 and a <= MAX_INT - b) or (b < 0 and a >= MIN_INT - b): return a + b
if not finite(a) or not finite(b): return approx(a) + approx(b)
if a is int and a == 0: return of(b)
if b is int and b == 0: return of(a)
var x: Array = parts(a)
var y: Array = parts(b)
var common: Array = Big.gcd(x[1], y[1])
var xd: Array = Big.divmod(x[1], common)[0]
var yd: Array = Big.divmod(y[1], common)[0]
var left: Array = Big.multiply(x[0], yd)
var right: Array = Big.multiply(y[0], xd)
var denominator: Array = Big.multiply(xd, y[1])
if x[2] == y[2]: return make(Big.add(left, right), denominator, x[2])
var order: int = Big.compare(left, right)
return make(Big.subtract(left, right) if order >= 0 else Big.subtract(right, left), denominator, x[2] if order >= 0 else y[2])
static func subtract(a, b):
if not numeric(a) or not numeric(b): return a - b
if a is int and b is int:
if (b >= 0 and a >= MIN_INT + b) or (b < 0 and a <= MAX_INT + b): return a - b
if not finite(a) or not finite(b): return approx(a) - approx(b)
if b is int and b == 0: return of(a)
var y: Array = parts(b)
return add(a, make(y[0], y[1], -y[2]))
static func multiply(a, b):
if not numeric(a) or not numeric(b): return a * b
if not finite(a) or not finite(b): return approx(a) * approx(b)
if (a is int and a == 0) or (b is int and b == 0): return 0
if a is int and a == 1: return of(b)
if b is int and b == 1: return of(a)
if a is int and b is int and a != MIN_INT and b != MIN_INT:
if absi(a) <= MAX_INT / absi(b): return a * b
var x: Array = parts(a)
var y: Array = parts(b)
var g1: Array = Big.gcd(x[0], y[1])
var g2: Array = Big.gcd(y[0], x[1])
return make(Big.multiply(Big.divmod(x[0], g1)[0], Big.divmod(y[0], g2)[0]), Big.multiply(Big.divmod(x[1], g2)[0], Big.divmod(y[1], g1)[0]), x[2] * y[2])
static func divide(a, b):
if not numeric(a) or not numeric(b): return a / b
if not finite(a) or not finite(b): return approx(a) / approx(b)
if b is int and b == 1: return of(a)
if a is int and b is int and b != 0 and not (a == MIN_INT and b == -1):
if a % b == 0: return a / b
var y: Array = parts(b)
assert(y[0] != [0], "Exact division by zero")
return multiply(a, make(y[1], y[0], y[2]))
static func floor_value(a):
if not finite(a): return a
if a is int: return a
var x: Array = parts(a)
var qr: Array = Big.divmod(x[0], x[1])
if x[2] < 0 and qr[1] != [0]: qr[0] = Big.add(qr[0], [1])
return make(qr[0], [1], x[2])
static func ceil_value(a): return neg(floor_value(neg(a)))
static func round_value(a): return multiply(-1 if lt(a, 0) else 1, floor_value(add(absolute(a), of("0.5"))))
static func remainder(a, b): return subtract(a, multiply(truncate(divide(a, b)), b))
static func truncate(a): return ceil_value(a) if lt(a, 0) else floor_value(a)
static func integer(a) -> int:
var result = truncate(of(a))
assert(result is int, "Value does not fit native int64")
return result
## Bound persistent fractions, never integer quantities or recipe arithmetic.
## Truncation towards zero changes a value by less than 10^-36.
static func bounded(value):
if not value is Value or Big.compare(value.denominator, CARRY_SCALE) <= 0:
return value
var units: Array = Big.divmod(Big.multiply(value.numerator, CARRY_SCALE), value.denominator)[0]
return make(units, CARRY_SCALE, value.polarity)
## Only call on a newly owned state snapshot, never on a published old state.
static func bound_state(value):
if value is Value: return bounded(value)
if value is Dictionary:
for key in value: value[key] = bound_state(value[key])
elif value is Array:
for i in value.size(): value[i] = bound_state(value[i])
return value
static func power(a, b):
if finite(b) and eq(b, floor_value(b)) and le(absolute(b), 10000):
var exponent: int = integer(absolute(b))
var result = 1
while exponent:
if exponent % 2: result = multiply(result, a)
exponent /= 2
if exponent: a = multiply(a, a)
return divide(1, result) if lt(b, 0) else result
return coefficient(pow(approx(a), approx(b)))
static func coefficient(value: float):
# Only transcendental/RNG boundaries are approximate: 12 decimal places.
if not is_finite(value): return value
return of(String.num(value, 12))
static func square_root(a): return coefficient(sqrt(approx(a)))
static func logarithm(a): return coefficient(log(approx(a)))
static func near(a, b) -> bool:
return le(absolute(subtract(a, b)), multiply(of("0.00001"), maximum(1, maximum(absolute(a), absolute(b)))))
static func contains(container, value) -> bool:
if container is Array:
for item in container:
if eq(item, value): return true
return false
return value in container
static func fixed(value, digits: int = 0) -> String:
var scale = power(10, digits)
var rounded = round_value(multiply(value, scale))
var text: String = str(absolute(rounded))
if digits:
text = text.lpad(digits + 1, "0")
text = text.left(text.length() - digits) + "." + text.right(digits)
return ("-" if lt(rounded, 0) else "") + text
static func scientific(value, digits: int = 4) -> String:
if eq(value, 0): return fixed(0, digits) + "E+0"
var terms: Array = parts(absolute(value))
var exponent: int = Big.decimal(terms[0]).length() - Big.decimal(terms[1]).length()
if lt(absolute(value), power(10, exponent)): exponent -= 1
var mantissa = divide(value, power(10, exponent))
if ge(absolute(round_value(multiply(mantissa, power(10, digits)))), multiply(10, power(10, digits))):
mantissa = divide(mantissa, 10)
exponent += 1
return fixed(mantissa, digits) + ("E+%d" if exponent >= 0 else "E%d") % exponent
static func grouped(value, digits: int = 0) -> String:
if eq(value, 0): return ""
if ge(absolute(value), 1000000000): return scientific(value, digits)
var text: String = fixed(absolute(value), digits)
var parts_text: PackedStringArray = text.split(".")
var integer_text: String = parts_text[0]
var result: String = ""
while integer_text.length() > 3:
result = " " + integer_text.right(3) + result
integer_text = integer_text.left(integer_text.length() - 3)
return ("-" if lt(value, 0) else "") + integer_text + result + ("." + parts_text[1] if parts_text.size() > 1 else "")
static func encode(value):
if value is Value: return {"$exact": str(value)}
if value is int and (value > 9007199254740991 or value < -9007199254740991):
return {"$exact": str(value)}
if value is Dictionary:
var result: Dictionary = {}
for k in value: result[k] = encode(value[k])
return result
if value is Array:
var result: Array = []
for item in value: result.append(encode(item))
return result
return value
static func decode(value):
if value is Dictionary:
if value.size() == 1 and value.has("$exact"): return of(value["$exact"])
var result: Dictionary = {}
for k in value: result[k] = decode(value[k])
return result
if value is Array:
var result: Array = []
for item in value: result.append(decode(item))
return result
return value
static func valid_encoded(value) -> bool:
if value is Dictionary:
if value.has("$exact"):
if value.size() != 1 or not value["$exact"] is String: return false
var text: String = value["$exact"]
if text.length() > 4096: return false
var terms: PackedStringArray = text.split("/")
if terms.size() > 2: return false
var n: String = terms[0].trim_prefix("-")
if n.is_empty(): return false
for ch in n:
if ch < "0" or ch > "9": return false
if terms.size() == 2:
if terms[1].is_empty(): return false
for ch in terms[1]:
if ch < "0" or ch > "9": return false
if Big.parse(terms[1]) == [0]: return false
return true
for k in value:
if not valid_encoded(value[k]): return false
if value is Array:
for item in value:
if not valid_encoded(item): return false
return true
+1
View File
@@ -0,0 +1 @@
uid://cfxfoprykygry
+9
View File
@@ -0,0 +1,9 @@
extends RefCounted
## Immutable by contract; arithmetic creates new values. Signed reduced fraction.
var polarity: int = 1
var numerator: Array = [0]
var denominator: Array = [1]
func _to_string() -> String:
const Big = preload("res://world/owniverse/math/big_integer.gd")
return ("-" if polarity < 0 else "") + Big.decimal(numerator) + ("/" + Big.decimal(denominator) if denominator != [1] else "")
+1
View File
@@ -0,0 +1 @@
uid://qv2rdp4s0d36
+111 -123
View File
@@ -1,181 +1,169 @@
extends "res://world/owniverse/simulation_module.gd"
const E = preload("res://world/owniverse/math/exact.gd")
const CivilizationState = preload("res://world/owniverse/civilization_state.gd")
const SimulationTime = preload("res://world/owniverse/simulation_time.gd")
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
func scale_batches(target: Dictionary, id: String, factor: float) -> void:
func scale_batches(target: Dictionary, id: String, factor: Variant) -> void:
var batches: Array = target.batches.get(id, [])
var total: float = 0.0
var assigned: float = 0.0
var total: Variant = 0
var assigned: Variant = 0
var shares: Array = []
for batch: Dictionary in batches:
var expected: float = batch.amount * maxf(0.0, factor)
total += expected
batch.amount = floorf(expected)
assigned += batch.amount
shares.append({"batch": batch, "rest": expected - batch.amount})
shares.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
return a.rest > b.rest)
for share: Dictionary in shares:
if assigned < roundf(total):
share.batch.amount += 1.0
assigned += 1.0
share.batch.withdraw_remainder = minf(share.batch.get("withdraw_remainder", 0.0), share.batch.amount)
for batch : Dictionary in batches:
var expected: Variant = E.multiply(batch.amount, E.maximum(0, factor))
total = E.add(total, expected)
batch.amount = E.floor_value(expected)
assigned = E.add(assigned, batch.amount)
shares.append({"batch": batch, "rest": E.subtract(expected, batch.amount)})
shares.sort_custom(func ( a : Dictionary , b : Dictionary ) -> bool : return E.gt(a.rest, b.rest))
for share : Dictionary in shares:
if E.lt(assigned, E.round_value(total)):
share.batch.amount = E.add(share.batch.amount, 1)
assigned = E.add(assigned, 1)
share.batch.withdraw_remainder = E.minimum(share.batch.get("withdraw_remainder", 0), share.batch.amount)
func append_batch(target: Dictionary, id: String, amount: float,
created_from: float, created_to: float, completed: Dictionary = {},
completed_before: float = -INF) -> float:
if amount <= 0.0 or not world.catalog.entities.has(id) or world.catalog.entities[id].lifespan <= 0.0:
func append_batch(target: Dictionary, id: String, amount: Variant, created_from: Variant, created_to: Variant, completed: Dictionary = { }, completed_before: Variant = - INF) -> Variant:
if ((E.le(amount, 0) or (not world.catalog.entities.has(id))) or E.le(world.catalog.entities[id].lifespan, 0)):
return NAN
if created_to < created_from:
if E.lt(created_to, created_from):
return NAN
if not target.batches.has(id):
if (not target.batches.has(id)):
target.batches[id] = []
if not target.has("batch_origins"):
if (not target.has("batch_origins")):
target.batch_origins = {}
var batches: Array = target.batches[id]
var lifespan: float = CivilizationState.effective_span(world, target, id, Global.CivilizationRules)
if world.catalog.entities[id].group_as == "Star":
lifespan /= WorldParameters.effective(target, "Bur")
var batch_duration: float = lifespan / Global.batch_count if lifespan > 10.0 else lifespan
if batch_duration <= 0.0:
var lifespan: Variant = CivilizationState.effective_span(world, target, id, Global.CivilizationRules)
if E.eq(world.catalog.entities[id].group_as, "Star"):
lifespan = E.divide(lifespan, WorldParameters.effective(target, "Bur"))
var batch_duration: Variant = (E.divide(lifespan, Global.batch_count) if E.gt(lifespan, 10) else lifespan)
if E.le(batch_duration, 0):
return NAN
if not target.batch_origins.has(id):
target.batch_origins[id] = batches[0].get("created_from", created_from) if not batches.is_empty() else created_from
var origin: float = target.batch_origins[id]
var cursor: float = created_from
var duration: float = created_to - created_from
var remaining_amount: float = amount
var completed_amount: float = 0.0
var completed_from: float = INF
var completed_to: float = -INF
# Subtract virtual windows in their original order to preserve floating-point
# rounding of lifetime production, rather than subtracting one large sum.
var remaining_object_years: float = amount * maxf(0.0, completed_before - (created_from + created_to) * 0.5) if is_finite(completed_before) else NAN
# Compute the index once. Recomputing floor from a rounded boundary can
# select the preceding window forever (e.g. fractional origins at large ages).
var window_index: float = floorf((cursor - origin) / batch_duration)
if origin + (window_index + 1.0) * batch_duration <= cursor:
window_index += 1.0
if (not target.batch_origins.has(id)):
target.batch_origins[id] = (batches[0].get("created_from", created_from) if (not batches.is_empty()) else created_from)
var origin: Variant = target.batch_origins[id]
var cursor: Variant = created_from
var duration: Variant = E.subtract(created_to, created_from)
var remaining_amount: Variant = amount
var completed_amount: Variant = 0
var completed_from: Variant = INF
var completed_to: Variant = E.neg(INF)
# Subtract each completed window's lifetime production exactly.
var remaining_object_years: Variant = (E.multiply(amount, E.maximum(0, E.subtract(completed_before, E.multiply((E.add(created_from, created_to)), E.of("0.5"))))) if E.finite(completed_before) else NAN)
# Advance an exact window index rather than repeatedly finding a boundary.
var window_index: Variant = E.floor_value(E.divide((E.subtract(cursor, origin)), batch_duration))
if E.le(E.add(origin, E.multiply((E.add(window_index, 1)), batch_duration)), cursor):
window_index = E.add(window_index, 1)
var batches_by_start: Dictionary = {}
for existing: Dictionary in batches:
var start: float = existing.get("created_from", -INF)
if not batches_by_start.has(start):
batches_by_start[start] = []
batches_by_start[start].append(existing)
for existing : Dictionary in batches:
var start: Variant = existing.get("created_from", E.neg(INF))
if (not batches_by_start.has(str(start))):
batches_by_start[str(start)] = []
batches_by_start[str(start)].append(existing)
while true:
var window_start: float = origin + window_index * batch_duration
var window_end: float = origin + (window_index + 1.0) * batch_duration
if window_end <= cursor:
push_error("Batch window exceeds floating-point time precision: " + id)
var window_start: Variant = E.add(origin, E.multiply(window_index, batch_duration))
var window_end: Variant = E.add(origin, E.multiply((E.add(window_index, 1)), batch_duration))
if E.le(window_end, cursor):
push_error("Batch window does not advance: " + id)
return remaining_object_years
var part_end: float = minf(created_to, window_end)
var part_amount: float = remaining_amount
if duration > 0.0 and part_end < created_to:
part_amount = floorf(amount * (part_end - created_from) / duration) - (amount - remaining_amount)
if window_end + lifespan <= completed_before:
var part_end: Variant = E.minimum(created_to, window_end)
var part_amount: Variant = remaining_amount
if (E.gt(duration, 0) and E.lt(part_end, created_to)):
part_amount = E.subtract(E.floor_value(E.divide(E.multiply(amount, (E.subtract(part_end, created_from))), duration)), (E.subtract(amount, remaining_amount)))
if E.le(E.add(window_end, lifespan), completed_before):
# Retain the original integer allocation and lifetime integral, but
# do not allocate thousands of already-dead batch dictionaries.
completed_amount += part_amount
completed_from = minf(completed_from, window_start + lifespan)
completed_to = maxf(completed_to, window_end + lifespan)
remaining_object_years -= part_amount * expired_years(completed_before, window_start + lifespan, window_end + lifespan)
completed_amount = E.add(completed_amount, part_amount)
completed_from = E.minimum(completed_from, E.add(window_start, lifespan))
completed_to = E.maximum(completed_to, E.add(window_end, lifespan))
remaining_object_years = E.subtract(remaining_object_years, E.multiply(part_amount, expired_years(completed_before, E.add(window_start, lifespan), E.add(window_end, lifespan))))
else:
var batch: Dictionary = {}
for existing: Dictionary in batches_by_start.get(window_start, []):
if existing.get("created_from", -INF) == window_start and existing.start_year == window_start + lifespan and existing.end_year == window_end + lifespan:
for existing : Dictionary in batches_by_start.get(str(window_start), []):
if ((E.eq(existing.get("created_from", E.neg(INF)), window_start) and E.eq(existing.start_year, E.add(window_start, lifespan))) and E.eq(existing.end_year, E.add(window_end, lifespan))):
batch = existing
break
if batch.is_empty():
batch = {
"created_from": window_start,
"created_to": window_end,
"start_year": window_start + lifespan,
"end_year": window_end + lifespan,
"amount": 0.0,
}
batch = {"created_from": window_start, "created_to": window_end, "start_year": E.add(window_start, lifespan), "end_year": E.add(window_end, lifespan), "amount": 0}
batches.append(batch)
if not batches_by_start.has(window_start):
batches_by_start[window_start] = []
batches_by_start[window_start].append(batch)
batch.amount += part_amount
remaining_amount -= part_amount
if duration == 0.0 or part_end >= created_to:
if (not batches_by_start.has(str(window_start))):
batches_by_start[str(window_start)] = []
batches_by_start[str(window_start)].append(batch)
batch.amount = E.add(batch.amount, part_amount)
remaining_amount = E.subtract(remaining_amount, part_amount)
if (E.eq(duration, 0) or E.ge(part_end, created_to)):
break
cursor = window_end
window_index += 1.0
batches.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
return a.created_from < b.created_from)
if completed_amount > 0.0:
if not completed.has(id): completed[id] = []
window_index = E.add(window_index, 1)
batches.sort_custom(func ( a : Dictionary , b : Dictionary ) -> bool : return E.lt(a.created_from, b.created_from))
if E.gt(completed_amount, 0):
if (not completed.has(id)): completed[id] = []
completed[id].append({"amount": completed_amount, "from": completed_from, "to": completed_to})
return remaining_object_years
func withdraw_batches(target: Dictionary, id: String, to_year: float) -> Array:
func withdraw_batches(target: Dictionary, id: String, to_year: Variant) -> Array:
var expired: Array = []
var remaining: Array = []
for batch: Dictionary in target.batches.get(id, []):
var start: float = maxf(batch.start_year, batch.get("processed_until", batch.start_year))
var end: float = batch.end_year
var amount: float = batch.amount
if amount <= 0.0:
for batch : Dictionary in target.batches.get(id, []):
var start: Variant = E.maximum(batch.start_year, batch.get("processed_until", batch.start_year))
var end: Variant = batch.end_year
var amount: Variant = batch.amount
if E.le(amount, 0):
continue
if to_year < start or (to_year == start and end > start):
if (E.lt(to_year, start) or ((E.eq(to_year, start) and E.gt(end, start)))):
remaining.append(batch)
continue
var consumed: float = amount
var until: float = minf(to_year, end)
if to_year < end:
var carry: float = batch.get("withdraw_remainder", 0.0)
var expected: float = carry + (amount - carry) * (to_year - start) / (end - start)
consumed = floorf(expected)
batch.withdraw_remainder = expected - consumed
batch.amount = maxf(0.0, amount - consumed)
var consumed: Variant = amount
var until: Variant = E.minimum(to_year, end)
if E.lt(to_year, end):
var carry: Variant = batch.get("withdraw_remainder", 0)
var expected: Variant = E.add(carry, E.divide(E.multiply((E.subtract(amount, carry)), (E.subtract(to_year, start))), (E.subtract(end, start))))
consumed = E.floor_value(expected)
batch.withdraw_remainder = E.subtract(expected, consumed)
batch.amount = E.maximum(0, E.subtract(amount, consumed))
batch.processed_until = to_year
remaining.append(batch)
if consumed > 0.0:
if E.gt(consumed, 0):
expired.append({"amount": consumed, "from": start, "to": until})
target.batches[id] = remaining
return expired
func object_years(source: Dictionary, id: String, quantity: float,
from_year: float, to_year: float) -> float:
var result: float = quantity * SimulationTime.elapsed_years(from_year, to_year)
for batch: Dictionary in source.batches.get(id, []):
var start: float = maxf(batch.start_year, batch.get("processed_until", batch.start_year))
var end: float = batch.end_year
result -= batch.amount * (expired_years(to_year, start, end) - expired_years(from_year, start, end))
return maxf(0.0, result)
func object_years(source: Dictionary, id: String, quantity: Variant, from_year: Variant, to_year: Variant) -> Variant:
var result: Variant = E.multiply(quantity, SimulationTime.elapsed_years(from_year, to_year))
for batch : Dictionary in source.batches.get(id, []):
var start: Variant = E.maximum(batch.start_year, batch.get("processed_until", batch.start_year))
var end: Variant = batch.end_year
result = E.subtract(result, E.multiply(batch.amount, (E.subtract(expired_years(to_year, start, end), expired_years(from_year, start, end)))))
return E.maximum(0, result)
func expired_years(year: float, start: float, end: float) -> float:
if year <= start:
return 0.0
if end <= start:
return year - start
if year >= end:
return year - end + (end - start) * 0.5
return (year - start) * (year - start) / (2.0 * (end - start))
func expired_years(year: Variant, start: Variant, end: Variant) -> Variant:
if E.le(year, start):
return 0
if E.le(end, start):
return E.subtract(year, start)
if E.ge(year, end):
return E.add(E.subtract(year, end), E.multiply((E.subtract(end, start)), E.of("0.5")))
return E.divide(E.multiply((E.subtract(year, start)), (E.subtract(year, start))), (E.multiply(2, (E.subtract(end, start)))))
func consume_from_batches(entity: OwniverseEntity, current_year: float) -> float:
var total: float = 0.0
func consume_from_batches(entity: OwniverseEntity, current_year: Variant) -> Variant:
var total: Variant = 0
var physical: Dictionary = world.ledger.physical_amounts(world.state)
for part: Dictionary in withdraw_batches(world.state, entity.id, current_year):
total += part.amount
var fraction: float = clampf(total / physical[entity.id], 0.0, 1.0) if physical.get(entity.id, 0.0) > 0.0 else 0.0
world.state.amount[entity.id] = world.state.amount.get(entity.id, 0.0) * (1.0 - fraction)
for part : Dictionary in withdraw_batches(world.state, entity.id, current_year):
total = E.add(total, part.amount)
var fraction: Variant = (E.clamp_value(E.divide(total, physical[entity.id]), 0, 1) if E.gt(physical.get(entity.id, 0), 0) else 0)
world.state.amount[entity.id] = E.multiply(world.state.amount.get(entity.id, 0), (E.subtract(1, fraction)))
for owner_id in world.state.binds:
if world.state.binds[owner_id].has(entity.id):
world.state.binds[owner_id][entity.id] *= 1.0 - fraction
world.state.binds[owner_id][entity.id] = E.multiply(world.state.binds[owner_id][entity.id], E.subtract(1, fraction))
world.ledger.round_object_allocations(world.state)
world.ledger.refresh_groups(world.state)
world.presentation.refresh(world.state, world.catalog)
return total
func add_entity_to_batch(entity_id: String, amount: float, current_year: float) -> void:
func add_entity_to_batch(entity_id: String, amount: Variant, current_year: Variant) -> void:
append_batch(world.state, entity_id, amount, current_year, current_year)
+117 -124
View File
@@ -1,23 +1,24 @@
extends "res://world/owniverse/simulation_module.gd"
const E = preload("res://world/owniverse/math/exact.gd")
const CivilizationState = preload("res://world/owniverse/civilization_state.gd")
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
func physical_amounts(source: Dictionary) -> Dictionary:
var amounts: Dictionary = {}
for id in world.catalog.entities:
if not world.catalog.is_group(id):
amounts[id] = maxf(0.0, source.amount.get(id, 0.0))
for bindings: Dictionary in source.binds.values():
if (not world.catalog.is_group(id)):
amounts[id] = E.maximum(0, source.amount.get(id, 0))
for bindings : Dictionary in source.binds.values():
for id in bindings:
if amounts.has(id):
amounts[id] += maxf(0.0, bindings[id])
amounts[id] = E.add(amounts[id], E.maximum(0, bindings[id]))
return amounts
func available(source: Dictionary, id: String) -> float:
var amount: float = 0.0
func available(source: Dictionary, id: String) -> Variant:
var amount: Variant = 0
for member in world.catalog.members(id):
amount += maxf(0.0, source.amount.get(member, 0.0))
amount = E.add(amount, E.maximum(0, source.amount.get(member, 0)))
return amount
@@ -26,42 +27,41 @@ func expand_requirements(source: Dictionary, requirements: Dictionary) -> Dictio
for id in requirements:
var weights: Dictionary = {}
for member in world.catalog.members(id):
weights[member] = source.amount.get(member, 0.0)
weights[member] = source.amount.get(member, 0)
weights = QuantityMath.normalize_distribution(weights)
for member in weights:
expanded[member] = expanded.get(member, 0.0) + maxf(0.0, requirements[id]) * weights[member]
expanded[member] = E.add(expanded.get(member, 0), E.multiply(E.maximum(0, requirements[id]), weights[member]))
return expanded
func take_available(target: Dictionary, id: String, amount: float) -> Dictionary:
func take_available(target: Dictionary, id: String, amount: Variant) -> Dictionary:
var quantities: Dictionary = {}
for member in world.catalog.members(id):
quantities[member] = target.amount.get(member, 0.0)
quantities[member] = target.amount.get(member, 0)
var distribution: Dictionary = QuantityMath.normalize_distribution(quantities)
var taken: Dictionary = {}
var total: float = minf(amount, available(target, id))
var discrete: bool = not distribution.is_empty()
var total: Variant = E.minimum(amount, available(target, id))
var discrete: bool = (not distribution.is_empty())
for member in distribution:
discrete = discrete and is_discrete(member)
discrete = (discrete and is_discrete(member))
if discrete:
total = floorf(total)
total = E.floor_value(total)
var shares: Array = []
var assigned: float = 0.0
var assigned: Variant = 0
for member in distribution:
var expected: float = total * distribution[member]
taken[member] = floorf(expected) if discrete else expected
assigned += taken[member]
shares.append({"id": member, "rest": expected - taken[member]})
var expected: Variant = E.multiply(total, distribution[member])
taken[member] = (E.floor_value(expected) if discrete else expected)
assigned = E.add(assigned, taken[member])
shares.append({"id": member, "rest": E.subtract(expected, taken[member])})
if discrete:
shares.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
return a.rest > b.rest if a.rest != b.rest else str(a.id) < str(b.id))
for share: Dictionary in shares:
if assigned >= total:
shares.sort_custom(func ( a : Dictionary , b : Dictionary ) -> bool : return (E.gt(a.rest, b.rest) if E.ne(a.rest, b.rest) else E.lt(str(a.id), str(b.id))))
for share : Dictionary in shares:
if E.ge(assigned, total):
break
taken[share.id] += 1.0
assigned += 1.0
taken[share.id] = E.add(taken[share.id], 1)
assigned = E.add(assigned, 1)
for member in taken:
target.amount[member] = maxf(0.0, target.amount.get(member, 0.0) - taken[member])
target.amount[member] = E.maximum(0, E.subtract(target.amount.get(member, 0), taken[member]))
return taken
@@ -69,45 +69,44 @@ func is_discrete(_id: String) -> bool:
return true
func credit(target: Dictionary, id: String, amount: float) -> float:
if amount <= 0.0:
return 0.0
func credit(target: Dictionary, id: String, amount: Variant) -> Variant:
if E.le(amount, 0):
return 0
if is_discrete(id):
if not target.has("production_remainders"):
if (not target.has("production_remainders")):
target.production_remainders = {}
var expected: float = amount + target.production_remainders.get(id, 0.0)
amount = floorf(expected)
target.production_remainders[id] = expected - amount
target.amount[id] = target.amount.get(id, 0.0) + amount
target.stats.total_amount[id] = target.stats.total_amount.get(id, 0.0) + amount
if world.catalog.entities.has(id) and world.catalog.entities[id].group_as != "" and world.catalog.entities[id].group_as != id:
var expected: Variant = E.add(amount, target.production_remainders.get(id, 0))
amount = E.floor_value(expected)
target.production_remainders[id] = E.subtract(expected, amount)
target.amount[id] = E.add(target.amount.get(id, 0), amount)
target.stats.total_amount[id] = E.add(target.stats.total_amount.get(id, 0), amount)
if ((world.catalog.entities.has(id) and E.ne(world.catalog.entities[id].group_as, "")) and E.ne(world.catalog.entities[id].group_as, id)):
var group: String = world.catalog.entities[id].group_as
target.stats.total_amount[group] = target.stats.total_amount.get(group, 0.0) + amount
target.stats.total_amount[group] = E.add(target.stats.total_amount.get(group, 0), amount)
return amount
func round_object_allocations(target: Dictionary) -> void:
var physical: Dictionary = physical_amounts(target)
for id in physical:
if not is_discrete(id):
if (not is_discrete(id)):
continue
var slots: Array = [ {"owner": "", "value": target.amount.get(id, 0.0)}]
var slots: Array = [{"owner": "", "value": target.amount.get(id, 0)}]
for owner_id in target.binds:
if target.binds[owner_id].has(id):
slots.append({"owner": owner_id, "value": target.binds[owner_id][id]})
var assigned: float = 0.0
for slot: Dictionary in slots:
slot.whole = floorf(slot.value)
slot.rest = slot.value - slot.whole
assigned += slot.whole
slots.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
return a.rest > b.rest if a.rest != b.rest else str(a.owner) < str(b.owner))
var left: float = roundf(physical[id]) - assigned
for slot: Dictionary in slots:
if left > 0.0:
slot.whole += 1.0
left -= 1.0
if slot.owner == "":
var assigned: Variant = 0
for slot : Dictionary in slots:
slot.whole = E.floor_value(slot.value)
slot.rest = E.subtract(slot.value, slot.whole)
assigned = E.add(assigned, slot.whole)
slots.sort_custom(func ( a : Dictionary , b : Dictionary ) -> bool : return (E.gt(a.rest, b.rest) if E.ne(a.rest, b.rest) else E.lt(str(a.owner), str(b.owner))))
var left: Variant = E.subtract(E.round_value(physical[id]), assigned)
for slot : Dictionary in slots:
if E.gt(left, 0):
slot.whole = E.add(slot.whole, 1)
left = E.subtract(left, 1)
if E.eq(slot.owner, ""):
target.amount[id] = slot.whole
else:
target.binds[slot.owner][id] = slot.whole
@@ -117,48 +116,42 @@ func refresh_groups(target: Dictionary) -> void:
var groups: Dictionary = {}
for id in world.catalog.entities:
var group: String = world.catalog.entities[id].group_as
if group != "":
groups[group] = groups.get(group, 0.0) + target.amount.get(id, 0.0)
if E.ne(group, ""):
groups[group] = E.add(groups.get(group, 0), target.amount.get(id, 0))
for group in groups:
target.amount[group] = groups[group]
func clamp_auto_resources(target: Dictionary) -> void:
for id in target.get("auto_resources", {}):
target.auto_resources[id] = minf(target.auto_resources[id], available(target, id))
target.auto_resources[id] = E.minimum(target.auto_resources[id], available(target, id))
func bind_full(target: Dictionary, amounts: Dictionary) -> bool:
# Binding only mutates free amounts and owners' holdings.
var trial: Dictionary = {
"amount": target.amount.duplicate(),
"binds": target.binds.duplicate(true),
}
var trial: Dictionary = {"amount": target.amount.duplicate(), "binds": target.binds.duplicate(true)}
var requests: Array = []
for owner_id in amounts:
if not world.catalog.entities.has(owner_id):
if (not world.catalog.entities.has(owner_id)):
return false
for id in world.catalog.entities[owner_id].bind:
var quantity: float = amounts[owner_id] * maxf(0.0, world.catalog.entities[owner_id].bind[id])
if not is_equal_approx(quantity, roundf(quantity)):
var quantity: Variant = E.multiply(amounts[owner_id], E.maximum(0, world.catalog.entities[owner_id].bind[id]))
if (not E.eq(quantity, E.round_value(quantity))):
return false
if quantity > 0.0:
requests.append({"owner": owner_id, "id": id, "amount": roundf(quantity), "members": world.catalog.members(id).size()})
requests.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
if a.members != b.members:
return a.members < b.members
return str(a.id) < str(b.id) if a.id != b.id else str(a.owner) < str(b.owner))
for request: Dictionary in requests:
if E.gt(quantity, 0):
requests.append({"owner": owner_id, "id": id, "amount": E.round_value(quantity), "members": world.catalog.members(id).size()})
requests.sort_custom(func ( a : Dictionary , b : Dictionary ) -> bool : return (E.lt(a.members, b.members) if E.ne(a.members, b.members) else ((E.lt(str(a.id), str(b.id)) if E.ne(a.id, b.id) else E.lt(str(a.owner), str(b.owner))))))
for request : Dictionary in requests:
var taken: Dictionary = take_available(trial, request.id, request.amount)
var total: float = 0.0
var total: Variant = 0
for quantity in taken.values():
total += quantity
if total != request.amount:
total = E.add(total, quantity)
if E.ne(total, request.amount):
return false
if not trial.binds.has(request.owner):
if (not trial.binds.has(request.owner)):
trial.binds[request.owner] = {}
for id in taken:
trial.binds[request.owner][id] = trial.binds[request.owner].get(id, 0.0) + taken[id]
trial.binds[request.owner][id] = E.add(trial.binds[request.owner].get(id, 0), taken[id])
target.amount = trial.amount
target.binds = trial.binds
return true
@@ -169,51 +162,51 @@ func bind_new_owners(target: Dictionary, amounts: Dictionary, require_full: bool
return bind_full(target, amounts)
var requests: Dictionary = {}
for owner_id in amounts:
if not world.catalog.entities.has(owner_id):
if (not world.catalog.entities.has(owner_id)):
continue
for id in world.catalog.entities[owner_id].bind:
var quantity: float = amounts[owner_id] * maxf(0.0, world.catalog.entities[owner_id].bind[id])
var quantity: Variant = E.multiply(amounts[owner_id], E.maximum(0, world.catalog.entities[owner_id].bind[id]))
var weights: Dictionary = {}
for member in world.catalog.members(id):
weights[member] = target.amount.get(member, 0.0)
weights[member] = target.amount.get(member, 0)
weights = QuantityMath.normalize_distribution(weights)
for member in weights:
if not requests.has(member):
if (not requests.has(member)):
requests[member] = {}
requests[member][owner_id] = requests[member].get(owner_id, 0.0) + quantity * weights[member]
requests[member][owner_id] = E.add(requests[member].get(owner_id, 0), E.multiply(quantity, weights[member]))
for id in requests:
var requested: float = 0.0
var requested: Variant = 0
for quantity in requests[id].values():
requested += quantity
var available: float = available(target, id)
var factor: float = minf(1.0, available / requested) if requested > 0.0 else 0.0
requested = E.add(requested, quantity)
var available: Variant = available(target, id)
var factor: Variant = (E.minimum(1, E.divide(available, requested)) if E.gt(requested, 0) else 0)
for owner_id in requests[id]:
var quantity: float = requests[id][owner_id] * factor
if not target.binds.has(owner_id):
var quantity: Variant = E.multiply(requests[id][owner_id], factor)
if (not target.binds.has(owner_id)):
target.binds[owner_id] = {}
target.binds[owner_id][id] = target.binds[owner_id].get(id, 0.0) + quantity
target.amount[id] = maxf(0.0, target.amount.get(id, 0.0) - quantity)
target.binds[owner_id][id] = E.add(target.binds[owner_id].get(id, 0), quantity)
target.amount[id] = E.maximum(0, E.subtract(target.amount.get(id, 0), quantity))
return true
func apply_costs(entity: OwniverseEntity, amount: float) -> void:
func apply_costs(entity: OwniverseEntity, amount: Variant) -> void:
var costs: Dictionary = {}
for resource in entity.cost:
costs[resource] = roundf(maxf(0.0, entity.cost[resource]) * amount)
costs[resource] = E.round_value(E.multiply(E.maximum(0, entity.cost[resource]), amount))
for resource in costs:
var quantities: Dictionary = physical_amounts(world.state)
var taken: Dictionary = take_available(world.state, resource, costs[resource])
for id in taken:
var factor: float = 1.0 - taken[id] / quantities[id] if quantities.get(id, 0.0) > 0.0 else 0.0
var factor: Variant = (E.subtract(1, E.divide(taken[id], quantities[id])) if E.gt(quantities.get(id, 0), 0) else 0)
world.batches.scale_batches(world.state, id, factor)
refresh_groups(world.state)
clamp_auto_resources(world.state)
world.presentation.refresh(world.state, world.catalog)
func apply_binds(item_id: String, _entity: OwniverseEntity, amount: float) -> bool:
if not bind_new_owners(world.state, {item_id: amount}, true):
func apply_binds(item_id: String, _entity: OwniverseEntity, amount: Variant) -> bool:
if (not bind_new_owners(world.state, {item_id: amount}, true)):
return false
refresh_groups(world.state)
clamp_auto_resources(world.state)
@@ -222,20 +215,20 @@ func apply_binds(item_id: String, _entity: OwniverseEntity, amount: float) -> bo
return true
func process_entity(entity_id: String, amount: float) -> void:
if amount == 0:
func process_entity(entity_id: String, amount: Variant) -> void:
if E.eq(amount, 0):
return
# Update state amount
world.state.amount[entity_id] = world.state.amount.get(entity_id, 0.0) + amount
world.state.stats.total_amount[entity_id] = world.state.stats.total_amount.get(entity_id, 0.0) + maxf(0.0, amount)
world.state.amount[entity_id] = E.add(world.state.amount.get(entity_id, 0), amount)
world.state.stats.total_amount[entity_id] = E.add(world.state.stats.total_amount.get(entity_id, 0), E.maximum(0, amount))
# Handle group updates if entity belongs to a group
var entity = world.catalog.entities[entity_id]
if entity.group_as != "":
world.state.amount[entity.group_as] = world.state.amount.get(entity.group_as, 0.0) + amount
world.state.stats.total_amount[entity.group_as] = world.state.stats.total_amount.get(entity.group_as, 0.0) + maxf(0.0, amount)
if E.ne(entity.group_as, ""):
world.state.amount[entity.group_as] = E.add(world.state.amount.get(entity.group_as, 0), amount)
world.state.stats.total_amount[entity.group_as] = E.add(world.state.stats.total_amount.get(entity.group_as, 0), E.maximum(0, amount))
world.presentation.refresh(world.state, world.catalog)
@@ -245,33 +238,33 @@ func transfer_evolution(target: Dictionary, fractions: Dictionary, outputs: Dict
var bindings: Dictionary = target.binds.duplicate(true)
var incoming: Dictionary = {}
for owner_id in bindings:
var owner_survival: float = 1.0 - fractions.get(owner_id, 0.0)
var owner_survival: Variant = E.subtract(1, fractions.get(owner_id, 0))
target.binds[owner_id] = {}
for id in bindings[owner_id]:
var held: float = bindings[owner_id][id]
var released: float = held * (1.0 - owner_survival)
target.amount[id] = target.amount.get(id, 0.0) + released
var heirs: Dictionary = world.stellar.stellar_successors(outputs.get(owner_id, {})) if world.catalog.entities.has(id) and world.catalog.entities[id].group_as == "Planet" else {}
var fraction: float = fractions.get(id, 0.0)
var held: Variant = bindings[owner_id][id]
var released: Variant = E.multiply(held, (E.subtract(1, owner_survival)))
target.amount[id] = E.add(target.amount.get(id, 0), released)
var heirs: Dictionary = (world.stellar.stellar_successors(outputs.get(owner_id, {})) if (world.catalog.entities.has(id) and E.eq(world.catalog.entities[id].group_as, "Planet")) else {})
var fraction: Variant = fractions.get(id, 0)
for heir in heirs:
if not incoming.has(heir):
if (not incoming.has(heir)):
incoming[heir] = {}
incoming[heir][id] = incoming[heir].get(id, 0.0) + released * (1.0 - fraction) * heirs[heir]
incoming[heir][id] = E.add(incoming[heir].get(id, 0), E.multiply(E.multiply(released, (E.subtract(1, fraction))), heirs[heir]))
for child in outputs.get(id, {}):
if world.catalog.entities.has(child) and not CivilizationState.is_space_civilization(child, Global.CivilizationRules):
incoming[heir][child] = incoming[heir].get(child, 0.0) + released * fraction * heirs[heir] * outputs[id][child]
var remaining: float = held * owner_survival * (1.0 - fraction)
target.binds[owner_id][id] = target.binds[owner_id].get(id, 0.0) + remaining
if (world.catalog.entities.has(child) and (not CivilizationState.is_space_civilization(child, Global.CivilizationRules))):
incoming[heir][child] = E.add(incoming[heir].get(child, 0), E.multiply(E.multiply(E.multiply(released, fraction), heirs[heir]), outputs[id][child]))
var remaining: Variant = E.multiply(E.multiply(held, owner_survival), (E.subtract(1, fraction)))
target.binds[owner_id][id] = E.add(target.binds[owner_id].get(id, 0), remaining)
for successor in outputs.get(id, {}):
if not world.catalog.entities.has(successor) or CivilizationState.is_space_civilization(successor, Global.CivilizationRules):
if ((not world.catalog.entities.has(successor)) or CivilizationState.is_space_civilization(successor, Global.CivilizationRules)):
continue
var transferred: float = held * owner_survival * fraction * maxf(0.0, outputs[id][successor])
if not incoming.has(owner_id):
var transferred: Variant = E.multiply(E.multiply(E.multiply(held, owner_survival), fraction), E.maximum(0, outputs[id][successor]))
if (not incoming.has(owner_id)):
incoming[owner_id] = {}
incoming[owner_id][successor] = incoming[owner_id].get(successor, 0.0) + transferred
incoming[owner_id][successor] = E.add(incoming[owner_id].get(successor, 0), transferred)
for id in fractions:
target.amount[id] = maxf(0.0, target.amount.get(id, 0.0) * (1.0 - fractions[id]))
target.amount[id] = E.maximum(0, E.multiply(target.amount.get(id, 0), (E.subtract(1, fractions[id]))))
return incoming
@@ -279,9 +272,9 @@ func transfer_evolution(target: Dictionary, fractions: Dictionary, outputs: Dict
## Commit reservations after newly evolved objects have been credited.
func accept_transfers(target: Dictionary, incoming: Dictionary) -> void:
for owner_id in incoming:
if not target.binds.has(owner_id):
if (not target.binds.has(owner_id)):
target.binds[owner_id] = {}
for id in incoming[owner_id]:
var transferred: float = minf(incoming[owner_id][id], target.amount.get(id, 0.0))
target.binds[owner_id][id] = target.binds[owner_id].get(id, 0.0) + transferred
target.amount[id] = maxf(0.0, target.amount.get(id, 0.0) - transferred)
var transferred: Variant = E.minimum(incoming[owner_id][id], target.amount.get(id, 0))
target.binds[owner_id][id] = E.add(target.binds[owner_id].get(id, 0), transferred)
target.amount[id] = E.maximum(0, E.subtract(target.amount.get(id, 0), transferred))
+10 -15
View File
@@ -1,5 +1,6 @@
extends Node
class_name Owniverse
const E = preload("res://world/owniverse/math/exact.gd")
## Owns the state and modules. Rules live in the modules below.
const SimulationTime = preload("res://world/owniverse/simulation_time.gd")
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
@@ -16,10 +17,7 @@ var evolution = preload("res://world/owniverse/evolution_coordinator.gd").new(se
var state: Dictionary = {
'version': SimulationTime.STATE_VERSION,
'time': {
'datetime': 0.0,
'year': 0.0,
},
'time': {'datetime': 0, 'year': 0},
'speed': 1,
'speed_in_year': 1,
'energy': WorldParameters.INITIAL_ENERGY,
@@ -43,22 +41,19 @@ var state: Dictionary = {
'civilization_stats': {},
'civilization_homes': {},
'ethics': {},
'stats': {
'total_amount': {},
}
'stats': {'total_amount': {}},
}
var speed: int:
var speed: int :
get:
return state.speed
set(value):
state.speed = clampi(value, 1, Global.SPEED_LIMIT)
state.speed_in_year = pow(10.0, state.speed - 1)
state.speed_in_year = E.power(10, E.subtract(state.speed, 1))
func evolute(delta: float) -> void:
if delta <= 0.0 or not is_finite(delta):
func evolute(delta: Variant) -> void:
if (E.le(delta, 0) or (not E.finite(delta))):
return
var datetime: float = state.time.datetime + delta * pow(10.0, speed - 1)
if not is_finite(datetime) or not is_finite(datetime * 10.0):
return
evolution.evolute_to_datetime(datetime)
var clock: Dictionary = SimulationTime.advance(state.time, E.approx(delta), speed)
if not E.finite(clock.datetime): return
evolution.evolute_to_datetime(clock.datetime, clock)
+41 -40
View File
@@ -1,23 +1,24 @@
extends Node
class_name OwniverseEntity
const E = preload("res://world/owniverse/math/exact.gd")
var id: String
var is_visible: bool = false
var unlock: Dictionary = {}
var cost: Dictionary = {}
var bind: Dictionary = {}
var mass: float
var mass: Variant
var space: int
var life_rule: Dictionary = {}
var lifespan: float
var lifespan_delta: float
var auto: float
var gamma: float
var lifespan: Variant
var lifespan_delta: Variant
var auto: Variant
var gamma: Variant
var blast: int
var blastability: bool = false
var life_product: Dictionary = {}
var product_per_year_type: String = ""
var product_per_year_amount: float = 0
var product_per_year_amount: Variant = 0
var voids: int
var output_or: Dictionary = {}
var output_and: Dictionary = {}
@@ -27,49 +28,49 @@ var planets: Dictionary = {}
var count_as: String
var group_as: String
var event_group: String
var space_bonus: float
var civ_bonus: float
var space_bonus: Variant
var civ_bonus: Variant
var claim: String
var amount: float = 0.0
var amount: Variant = 0
func _init(values: Dictionary) -> void:
id = values.id
is_visible = values.is_visible == "1"
is_visible = E.eq(values.is_visible, "1")
unlock = _parse_and_string(values.unlock)
cost = _parse_and_string(values.cost)
bind = _parse_and_string(values.bind)
#print(id, " | ", cost, " | ", bind)
mass = float(values.mass)
space = int(values.space)
mass = E.of(values.mass)
space = E.integer(values.space)
life_rule = Global.LifeRules.get(id, {}).duplicate(true)
if Global.CivilizationRules.has(id):
life_rule = {"ethical": true, "span": Global.CivilizationRules[id].span}
lifespan = float(life_rule.span) if not life_rule.is_empty() else float(values.lifespan)
lifespan_delta = lifespan / Global.batch_count if lifespan > 10 else lifespan
auto = float(values.auto)
gamma = float(values.gamma)
blast = int(values.blast)
blastability = str(values.get("blastability", "")) == "1"
lifespan = (E.of(life_rule.span) if (not life_rule.is_empty()) else E.of(values.lifespan))
lifespan_delta = (E.divide(lifespan, Global.batch_count) if E.gt(lifespan, 10) else lifespan)
auto = E.of(values.auto)
gamma = E.of(values.gamma if str(values.gamma).is_valid_float() else "0")
blast = E.integer(values.blast)
blastability = E.eq(str(values.get("blastability", "")), "1")
life_product = _parse_and_string(values.life_product)
for key in life_product.keys():
product_per_year_type = key
product_per_year_amount = life_product[key] / lifespan if lifespan != 0 else 0
product_per_year_amount = (E.divide(life_product[key], lifespan) if E.ne(lifespan, 0) else 0)
#print(id, " | ", lifespan_delta, " | ", lifespan)
voids = int(values.voids)
voids = E.integer(values.voids)
output_or = _parse_or_string(values.output)
output_and = _parse_and_string(values.output)
destroy_level = int(values.destroy_level)
destroy_level = E.integer(values.destroy_level)
degradation = _parse_and_string(values.degradation)
planets = _parse_planet_string(values.planets)
count_as = values.count_as
group_as = values.group_as
event_group = values.event_group
space_bonus = float(values.space_bonus)
civ_bonus = float(values.civ_bonus)
space_bonus = E.of(values.space_bonus)
civ_bonus = E.of(values.civ_bonus)
claim = values.claim
@@ -78,39 +79,39 @@ func _parse_or_string(source_line: String) -> Dictionary:
if source_line.contains("^"):
var first_split = source_line.split("^")
var second_split = first_split[1].split("%")
var probability = float(second_split[1])
var probability = E.of(second_split[1])
result["chance"] = probability
result["positive"] = second_split[0]
result["negative"] = first_split[0]
return result
func _parse_and_string(source_line: String) -> Dictionary:
var result = {}
var first_split = source_line.split("&")
for elem in first_split:
var second_split = elem.split(":")
if len(second_split) == 2:
result[second_split[0]] = float(second_split[1])
if E.eq(len(second_split), 2):
result[second_split[0]] = E.of(second_split[1])
return result
func _parse_planet_string(source_line: String) -> Dictionary:
var result = {}
for elem in source_line.split("&", false):
var parts: PackedStringArray = elem.strip_edges().split(":")
if parts.size() != 3 or parts[0].strip_edges().is_empty():
push_warning("Invalid planet definition for %s: %s" % [id, elem])
if (E.ne(parts.size(), 3) or parts[0].strip_edges().is_empty()):
push_warning(("Invalid planet definition for %s: %s" % [id, elem]))
continue
if not parts[1].is_valid_float() or not parts[2].is_valid_float():
push_warning("Invalid planet numbers for %s: %s" % [id, elem])
if ((not parts[1].is_valid_float()) or (not parts[2].is_valid_float())):
push_warning(("Invalid planet numbers for %s: %s" % [id, elem]))
continue
var count: float = float(parts[1])
var chance: float = float(parts[2])
if not is_finite(count) or not is_finite(chance) or count < 0.0 or count != floorf(count) or chance < 0.0 or chance > 1.0:
push_warning("Planet values out of range for %s: %s" % [id, elem])
var count: Variant = E.of(parts[1])
var chance: Variant = E.of(parts[2])
if ((((((not E.finite(count)) or (not E.finite(chance))) or E.lt(count, 0)) or E.ne(count, E.floor_value(count))) or E.lt(chance, 0)) or E.gt(chance, 1)):
push_warning(("Planet values out of range for %s: %s" % [id, elem]))
continue
# Planet chances are fractions (0.01 = 1%), unlike output_or percentages.
result[parts[0].strip_edges()] = {"amount": count, "chance": chance}
+57 -66
View File
@@ -1,60 +1,51 @@
extends RefCounted
const E = preload("res://world/owniverse/math/exact.gd")
## Quantity arithmetic without scene tree or global settings.
static func accumulate_whole(target: Dictionary, field: String, key: String, value: float) -> float:
if not target.has(field):
static func accumulate_whole(target: Dictionary, field: String, key: String, value: Variant) -> Variant:
if (not target.has(field)):
target[field] = {}
var expected: float = maxf(0.0, value) + target[field].get(key, 0.0)
var whole: float = floorf(snappedf(expected, 1.0e-12))
target[field][key] = maxf(0.0, expected - whole)
var expected: Variant = E.add(E.maximum(0, value), target[field].get(key, 0))
var whole: Variant = E.floor_value(expected)
target[field][key] = E.maximum(0, E.subtract(expected, whole))
return whole
static func allocate_whole(requests: Dictionary, limit: float) -> Dictionary:
var total: float = 0.0
static func allocate_whole(requests: Dictionary, limit: Variant) -> Dictionary:
var total: Variant = 0
for amount in requests.values():
total += floorf(maxf(0.0, amount))
var available: float = floorf(minf(maxf(0.0, limit), total))
total = E.add(total, E.floor_value(E.maximum(0, amount)))
var available: Variant = E.floor_value(E.minimum(E.maximum(0, limit), total))
var result: Dictionary = {}
var order: Array = []
var assigned: float = 0.0
var assigned: Variant = 0
for id in requests:
var expected: float = floorf(maxf(0.0, requests[id])) * available / total if total > 0.0 else 0.0
result[id] = floorf(expected)
assigned += result[id]
order.append({"id": id, "rest": expected - result[id]})
order.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
return a.rest > b.rest if a.rest != b.rest else str(a.id) < str(b.id))
for item: Dictionary in order:
if assigned >= available:
var expected: Variant = (E.divide(E.multiply(E.floor_value(E.maximum(0, requests[id])), available), total) if E.gt(total, 0) else 0)
result[id] = E.floor_value(expected)
assigned = E.add(assigned, result[id])
order.append({"id": id, "rest": E.subtract(expected, result[id])})
order.sort_custom(func ( a : Dictionary , b : Dictionary ) -> bool : return (E.gt(a.rest, b.rest) if E.ne(a.rest, b.rest) else E.lt(str(a.id), str(b.id))))
for item : Dictionary in order:
if E.ge(assigned, available):
break
if result[item.id] < floorf(requests[item.id]):
result[item.id] += 1.0
assigned += 1.0
if E.lt(result[item.id], E.floor_value(requests[item.id])):
result[item.id] = E.add(result[item.id], 1)
assigned = E.add(assigned, 1)
return result
static func production_step(costs: Array) -> float:
var step: float = 1.0
for value in costs:
var cost: float = maxf(0.0, value)
if cost == 0.0:
continue
var decimal: String = String.num(cost, 12)
var point: int = decimal.find(".")
var digits: int = decimal.length() - point - 1 if point >= 0 else 0
var denominator: float = pow(10.0, digits)
var numerator: float = roundf(cost * denominator)
if numerator == 0.0:
return INF
denominator /= whole_gcd(numerator, denominator)
step = step / whole_gcd(step, denominator) * denominator
static func production_step(costs: Array):
var step = 1
for cost in costs:
if E.le(cost, 0): continue
var denominator = E.make(E.parts(cost)[1])
step = E.multiply(E.divide(step, whole_gcd(step, denominator)), denominator)
return step
static func whole_gcd(a: float, b: float) -> float:
while b > 0.0:
var remainder: float = fmod(a, b)
static func whole_gcd(a: Variant, b: Variant) -> Variant:
while E.gt(b, 0):
var remainder: Variant = E.remainder(a, b)
a = b
b = remainder
return a
@@ -62,50 +53,50 @@ static func whole_gcd(a: float, b: float) -> float:
static func normalize_distribution(values: Dictionary) -> Dictionary:
var normalized_distribution: Dictionary = {}
var distribution_total: float = 0
var distribution_total: Variant = 0
for id in values:
if values[id] > 0:
distribution_total += values[id]
if E.gt(values[id], 0):
distribution_total = E.add(distribution_total, values[id])
normalized_distribution[id] = values[id]
for id in normalized_distribution:
normalized_distribution[id] /= distribution_total
normalized_distribution[id] = E.divide(normalized_distribution[id], distribution_total)
return normalized_distribution
static func sum(pop: Dictionary) -> float:
var result: float = 0.0
for n in pop.values(): result += n
static func sum(pop: Dictionary) -> Variant:
var result: Variant = 0
for n in pop.values(): result = E.add(result, n)
return result
static func split(remainders: Dictionary, key: String, count: float, weights: Dictionary) -> Dictionary:
static func split(remainders: Dictionary, key: String, count: Variant, weights: Dictionary) -> Dictionary:
var result: Dictionary = {}
var sum_weights: float = sum(weights)
if count <= 0.0 or sum_weights <= 0.0: return result
var sum_weights: Variant = sum(weights)
if (E.le(count, 0) or E.le(sum_weights, 0)): return result
var expected: Dictionary = {}
var positive_sum: float = 0.0
var positive_sum: Variant = 0
for id in weights:
expected[id] = count * weights[id] / sum_weights + remainders.get(key + ":" + id, 0.0)
positive_sum += maxf(0.0, expected[id])
expected[id] = E.add(E.divide(E.multiply(count, weights[id]), sum_weights), remainders.get(E.add(E.add(key, ":"), id), 0))
positive_sum = E.add(positive_sum, E.maximum(0, expected[id]))
var order: Array = []
var assigned: float = 0.0
var assigned: Variant = 0
for id in weights:
var normalized: float = maxf(0.0, expected[id]) * count / positive_sum if positive_sum > 0.0 else 0.0
result[id] = floorf(normalized)
assigned += result[id]
order.append({"id": id, "rest": normalized - result[id]})
order.sort_custom(func(a, b): return a.rest > b.rest if a.rest != b.rest else str(a.id) < str(b.id))
var normalized: Variant = (E.divide(E.multiply(E.maximum(0, expected[id]), count), positive_sum) if E.gt(positive_sum, 0) else 0)
result[id] = E.floor_value(normalized)
assigned = E.add(assigned, result[id])
order.append({"id": id, "rest": E.subtract(normalized, result[id])})
order.sort_custom(func ( a , b ) : return (E.gt(a.rest, b.rest) if E.ne(a.rest, b.rest) else E.lt(str(a.id), str(b.id))))
for entry in order:
if assigned >= count: break
result[entry.id] += 1.0
assigned += 1.0
if E.ge(assigned, count): break
result[entry.id] = E.add(result[entry.id], 1)
assigned = E.add(assigned, 1)
for id in weights:
remainders[key + ":" + id] = clampf(expected[id] - result[id], -1.0, 1.0)
remainders[E.add(E.add(key, ":"), id)] = E.clamp_value(E.subtract(expected[id], result[id]), E.neg(1), 1)
return result
static func accumulate_rate(remainders: Dictionary, key: String, count: float, probability: float) -> float:
var expected: float = count * maxf(0.0, probability) + remainders.get(key, 0.0)
var actual: float = floorf(snappedf(expected, 1e-10))
remainders[key] = maxf(0.0, expected - actual)
static func accumulate_rate(remainders: Dictionary, key: String, count: Variant, probability: Variant) -> Variant:
var expected: Variant = E.add(E.multiply(count, E.maximum(0, probability)), remainders.get(key, 0))
var actual: Variant = E.floor_value(expected)
remainders[key] = E.maximum(0, E.subtract(expected, actual))
return actual
+16 -10
View File
@@ -1,13 +1,19 @@
extends RefCounted
## datetime is accumulated time; year is the last processed tenth of a year.
const STATE_VERSION: String = "0.1.1"
const E = preload("res://world/owniverse/math/exact.gd")
const STATE_VERSION: String = "0.2.0"
const TICKS_PER_YEAR: int = 1000000000
static func processed_year(datetime: float) -> float:
# Remove only sub-nanotick accumulation noise at decimal boundaries.
# A relative epsilon would incorrectly advance large ages by whole years.
return floorf(snappedf(datetime * 10.0, 1e-9)) / 10.0
static func processed_year(datetime):
return E.divide(E.floor_value(E.multiply(datetime, 10)), 10)
static func elapsed_years(from_year: float, to_year: float) -> float:
# Subtract integer tenths before returning to float years. Otherwise 0.3-0.2
# can fall below 0.1 and postpone a whole unit of resource production.
return (roundf(to_year * 10.0) - roundf(from_year * 10.0)) / 10.0
static func elapsed_years(from_year, to_year):
return E.subtract(to_year, from_year)
## Frame fractions stay small; the universe age is an exact integer tick count.
static func advance(time: Dictionary, delta: float, speed: int) -> Dictionary:
var ticks = E.round_value(E.multiply(time.datetime, TICKS_PER_YEAR))
var increment: float = delta * pow(10.0, speed - 1) * TICKS_PER_YEAR + float(time.get("tick_remainder", 0.0))
var whole: float = roundf(increment) if absf(increment - roundf(increment)) < 1e-6 else floorf(increment)
ticks = E.add(ticks, E.of(whole))
var datetime = E.divide(ticks, TICKS_PER_YEAR)
return {"datetime": datetime, "year": processed_year(datetime), "ticks": ticks, "tick_remainder": increment - whole}
+50 -51
View File
@@ -1,118 +1,117 @@
extends "res://world/owniverse/simulation_module.gd"
const E = preload("res://world/owniverse/math/exact.gd")
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
## Testable optimization switch; both paths preserve the same event order.
var direct_completions: bool = true
func add_output_terms(outputs: Dictionary, terms: String, count: float) -> void:
if count <= 0.0:
func add_output_terms(outputs: Dictionary, terms: String, count: Variant) -> void:
if E.le(count, 0):
return
for term in terms.split("&"):
var parts: PackedStringArray = term.strip_edges().split(":")
var id: String = parts[0].strip_edges()
var amount: float = float(parts[1]) if parts.size() == 2 else 1.0
if id != "" and amount > 0.0:
outputs[id] = outputs.get(id, 0.0) + count * amount
var amount: Variant = (E.of(parts[1]) if E.eq(parts.size(), 2) else 1)
if (E.ne(id, "") and E.gt(amount, 0)):
outputs[id] = E.add(outputs.get(id, 0), E.multiply(count, amount))
func evolution_products(target: Dictionary, entity: OwniverseEntity, count: float) -> Dictionary:
if not entity.life_rule.is_empty():
func evolution_products(target: Dictionary, entity: OwniverseEntity, count: Variant) -> Dictionary:
if (not entity.life_rule.is_empty()):
return world.life.life_products(target, entity, count)
var outputs: Dictionary = {}
for id in entity.output_and:
outputs[id] = count * maxf(0.0, entity.output_and[id])
if not entity.output_or.is_empty():
outputs[id] = E.multiply(count, E.maximum(0, entity.output_and[id]))
if (not entity.output_or.is_empty()):
# The parser stores the percentage after '%', e.g. 0.1 means 0.1 percent.
var chance: float = clampf(entity.output_or.get("chance", 0.0) / 100.0 * WorldParameters.effective(target, "Exp"), 0.0, 1.0)
if not target.has("evolution_remainders"):
var chance: Variant = E.clamp_value(E.multiply(E.divide(entity.output_or.get("chance", 0), 100), WorldParameters.effective(target, "Exp")), 0, 1)
if (not target.has("evolution_remainders")):
target.evolution_remainders = {}
var expected: float = count * chance + target.evolution_remainders.get(entity.id, 0.0)
var positive: float = minf(count, floorf(snappedf(expected, 1.0e-12)))
target.evolution_remainders[entity.id] = maxf(0.0, expected - positive)
var expected: Variant = E.add(E.multiply(count, chance), target.evolution_remainders.get(entity.id, 0))
var positive: Variant = E.minimum(count, E.floor_value(expected))
target.evolution_remainders[entity.id] = E.maximum(0, E.subtract(expected, positive))
add_output_terms(outputs, str(entity.output_or.get("positive", "")), positive)
add_output_terms(outputs, str(entity.output_or.get("negative", "")), count - positive)
add_output_terms(outputs, str(entity.output_or.get("negative", "")), E.subtract(count, positive))
# Entity.blast is authoritative even if imported output still contains
# blast:N. Zero disables the event; never add both representations.
outputs.erase("blast")
if entity.blast > 0:
outputs["blast"] = count * entity.blast * WorldParameters.effective(target, "Exp")
if E.gt(entity.blast, 0):
outputs["blast"] = E.multiply(E.multiply(count, entity.blast), WorldParameters.effective(target, "Exp"))
return outputs
func create_planets(target: Dictionary, star_id: String, count: float,
from_year: float, to_year: float, metallicity: float) -> void:
if count <= 0.0 or world.catalog.entities[star_id].planets.is_empty():
func create_planets(target: Dictionary, star_id: String, count: Variant, from_year: Variant, to_year: Variant, metallicity: Variant) -> void:
if (E.le(count, 0) or world.catalog.entities[star_id].planets.is_empty()):
return
if not target.binds.has(star_id):
if (not target.binds.has(star_id)):
target.binds[star_id] = {}
for id in world.catalog.entities[star_id].planets:
if not world.catalog.entities.has(id):
if (not world.catalog.entities.has(id)):
continue
var rule: Dictionary = world.catalog.entities[star_id].planets[id]
var amount: float = QuantityMath.accumulate_whole(target, "planet_remainders", star_id + ":" + id,
count * rule.amount * rule.chance * maxf(0.0, metallicity))
var amount: Variant = QuantityMath.accumulate_whole(target, "planet_remainders", E.add(E.add(star_id, ":"), id), E.multiply(E.multiply(E.multiply(count, rule.amount), rule.chance), E.maximum(0, metallicity)))
amount = world.ledger.credit(target, id, amount)
target.amount[id] -= amount
target.binds[star_id][id] = target.binds[star_id].get(id, 0.0) + amount
target.amount[id] = E.subtract(target.amount[id], amount)
target.binds[star_id][id] = E.add(target.binds[star_id].get(id, 0), amount)
world.batches.append_batch(target, id, amount, from_year, to_year)
func stellar_successors(products: Dictionary) -> Dictionary:
var weights: Dictionary = {}
for id in products:
if world.catalog.entities.has(id) and (world.catalog.entities[id].group_as in ["Star", "BH"] or world.catalog.entities[id].event_group == "SNGRBB" or world.catalog.entities[id].count_as == "BD"):
if (world.catalog.entities.has(id) and (((E.contains(["Star", "BH"], world.catalog.entities[id].group_as) or E.eq(world.catalog.entities[id].event_group, "SNGRBB")) or E.eq(world.catalog.entities[id].count_as, "BD")))):
weights[id] = products[id]
return QuantityMath.normalize_distribution(weights)
func destroy_planets(target: Dictionary, owner_id: String, ratio: float, level: int, cause: String = "local") -> void:
if ratio <= 0.0 or level not in [1, 2]:
func destroy_planets(target: Dictionary, owner_id: String, ratio: Variant, level: int, cause: String = "local") -> void:
if (E.le(ratio, 0) or not E.contains([1, 2], level)):
return
var held: Dictionary = target.binds.get(owner_id, {})
var physical: Dictionary = world.ledger.physical_amounts(target)
var rocky: float = 0.0
var rocky: Variant = 0
for id in held.keys():
if not world.catalog.entities.has(id) or world.catalog.entities[id].group_as != "Planet":
if ((not world.catalog.entities.has(id)) or E.ne(world.catalog.entities[id].group_as, "Planet")):
continue
if level == 1 and not world.catalog.entities[id].blastability:
if (E.eq(level, 1) and (not world.catalog.entities[id].blastability)):
continue
var key: String = cause + ":" + str(level) + ":" + owner_id + ":" + id
var destroyed: float = minf(held[id], QuantityMath.accumulate_whole(target, "destruction_remainders", key, held[id] * minf(1.0, ratio)))
if destroyed == held[id]:
target.destruction_remainders[key] = 0.0
if destroyed <= 0.0:
var key: String = E.add(E.add(E.add(E.add(E.add(E.add(cause, ":"), str(level)), ":"), owner_id), ":"), id)
var destroyed: Variant = E.minimum(held[id], QuantityMath.accumulate_whole(target, "destruction_remainders", key, E.multiply(held[id], E.minimum(1, ratio))))
if E.eq(destroyed, held[id]):
target.destruction_remainders[key] = 0
if E.le(destroyed, 0):
continue
held[id] -= destroyed
world.batches.scale_batches(target, id, 1.0 - destroyed / physical[id])
if level == 1:
rocky += destroyed
if rocky > 0.0:
held[id] = E.subtract(held[id], destroyed)
world.batches.scale_batches(target, id, E.subtract(1, E.divide(destroyed, physical[id])))
if E.eq(level, 1):
rocky = E.add(rocky, destroyed)
if E.gt(rocky, 0):
rocky = world.ledger.credit(target, "RP", rocky)
target.amount.RP -= rocky
held.RP = held.get("RP", 0.0) + rocky
target.amount.RP = E.subtract(target.amount.RP, rocky)
held.RP = E.add(held.get("RP", 0), rocky)
func apply_blast(target: Dictionary, power: float, environment: Dictionary) -> void:
var hits: float = QuantityMath.accumulate_whole(target, "blast_remainders", "hits", power * environment.blast)
func apply_blast(target: Dictionary, power: Variant, environment: Dictionary) -> void:
var hits: Variant = QuantityMath.accumulate_whole(target, "blast_remainders", "hits", E.multiply(power, environment.blast))
var quantities: Dictionary = world.ledger.physical_amounts(target)
var weights: Dictionary = {}
for id in world.catalog.entities:
if not world.catalog.entities[id].planets.is_empty() and quantities.get(id, 0.0) > 0.0:
if ((not world.catalog.entities[id].planets.is_empty()) and E.gt(quantities.get(id, 0), 0)):
weights[id] = quantities[id]
var distribution: Dictionary = QuantityMath.normalize_distribution(weights)
# Fractional exposure is deterministic; destruction itself remains whole.
for id in distribution:
destroy_planets(target, id, hits * distribution[id] / quantities[id] * environment.density, 1, "blast")
destroy_planets(target, id, E.multiply(E.divide(E.multiply(hits, distribution[id]), quantities[id]), environment.density), 1, "blast")
func uses_short_lived_fast_path(id: String) -> bool:
if not direct_completions: return false
if (not direct_completions): return false
# Only terminal stellar events with linear outputs and no living successors.
var entity: OwniverseEntity = world.catalog.entities[id]
if entity.event_group != "SNGRBB" or not entity.output_or.is_empty() or not entity.life_rule.is_empty():
if ((E.ne(entity.event_group, "SNGRBB") or (not entity.output_or.is_empty())) or (not entity.life_rule.is_empty())):
return false
for successor in entity.output_and:
if world.catalog.entities.has(successor) and world.catalog.entities[successor].lifespan > 0.0:
if (world.catalog.entities.has(successor) and E.gt(world.catalog.entities[successor].lifespan, 0)):
return false
return true
+24 -27
View File
@@ -1,42 +1,39 @@
extends "res://world/owniverse/simulation_module.gd"
const E = preload("res://world/owniverse/math/exact.gd")
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
func stellar_environment(source: Dictionary) -> Dictionary:
var quantities: Dictionary = world.ledger.physical_amounts(source)
# SMBH is a self-group in GI; its quantity stores mass, not object count.
var mass: float = source.amount.get("SMBH", 0.0)
var volume: float = quantities.get("S", 0.0)
var mass: Variant = source.amount.get("SMBH", 0)
var volume: Variant = quantities.get("S", 0)
for id in quantities:
var entity: OwniverseEntity = world.catalog.entities[id]
if id in ["H", "He", "Met", "SMBH"]:
mass += quantities[id]
if E.contains(["H", "He", "Met", "SMBH"], id):
mass = E.add(mass, quantities[id])
else:
mass += quantities[id] * maxf(0.0, entity.mass)
volume += quantities[id] * maxf(0.0, entity.space)
volume += quantities[id] * maxf(0.0, entity.voids) * 1.0e22
var metallicity: float = 0.0
if mass > 0.0:
metallicity = maxf(0.0, 0.4 * log(100.0 * source.amount.get("Met", 0.0) / mass + 0.11) + 0.88)
var base_mass: float = 65536.0 * (pow(mass, 0.1) if mass > 0.0 and mass < 1.0e10 else (10.0 if mass >= 1.0e10 else 1.0))
return {
"metallicity": metallicity,
"density": 1.23456e-7 * sqrt(base_mass + mass) / pow(maxf(1.0, volume), 1.0 / 6.0),
"blast": clampf(source.amount.get("S", 0.0) / volume, 0.0, 1.0) if volume > 0.0 else 1.0,
}
mass = E.add(mass, E.multiply(quantities[id], E.maximum(0, entity.mass)))
volume = E.add(volume, E.multiply(quantities[id], E.maximum(0, entity.space)))
volume = E.add(volume, E.multiply(E.multiply(quantities[id], E.maximum(0, entity.voids)), E.of("1.0e22")))
var metallicity: Variant = 0
if E.gt(mass, 0):
metallicity = E.maximum(0, E.add(E.multiply(E.of("0.4"), E.logarithm(E.add(E.divide(E.multiply(100, source.amount.get("Met", 0)), mass), E.of("0.11")))), E.of("0.88")))
var base_mass: Variant = E.multiply(65536, ((E.power(mass, E.of("0.1")) if (E.gt(mass, 0) and E.lt(mass, E.of("1.0e10"))) else ((10 if E.ge(mass, E.of("1.0e10")) else 1)))))
return {"metallicity": metallicity, "density": E.divide(E.multiply(E.of("1.23456e-7"), E.square_root(E.add(base_mass, mass))), E.power(E.maximum(1, volume), E.divide(1, 6))), "blast": (E.clamp_value(E.divide(source.amount.get("S", 0), volume), 0, 1) if E.gt(volume, 0) else 1)}
func modify_world_parameter(key: String, change: float) -> void:
if key not in ["Bur", "Exp", "Agg"] or not is_finite(change): return
var old_multiplier: float = WorldParameters.effective(world.state, key)
if not world.state.has("world_parameters"): world.state.world_parameters = WorldParameters.defaults()
world.state.world_parameters[key] = clampf(WorldParameters.value(world.state, key) + change, 1.0, 19.0)
if key == "Bur":
var ratio: float = old_multiplier / WorldParameters.effective(world.state, key)
var now: float = world.state.time.year
func modify_world_parameter(key: String, change: Variant) -> void:
if (not E.contains(["Bur", "Exp", "Agg"], key) or (not E.finite(change))): return
var old_multiplier: Variant = WorldParameters.effective(world.state, key)
if (not world.state.has("world_parameters")): world.state.world_parameters = WorldParameters.defaults()
world.state.world_parameters[key] = E.clamp_value(E.add(WorldParameters.value(world.state, key), change), 1, 19)
if E.eq(key, "Bur"):
var ratio: Variant = E.divide(old_multiplier, WorldParameters.effective(world.state, key))
var now: Variant = world.state.time.year
for id in world.state.batches:
if not world.catalog.entities.has(id) or world.catalog.entities[id].group_as != "Star": continue
for batch: Dictionary in world.state.batches[id]:
if ((not world.catalog.entities.has(id)) or E.ne(world.catalog.entities[id].group_as, "Star")): continue
for batch : Dictionary in world.state.batches[id]:
# Only future survival time changes. Birth dates and the fractional
# withdrawal carry remain intact; already elapsed time is not replayed.
for field in ["start_year", "end_year", "processed_until"]:
if batch.has(field): batch[field] = now + maxf(0.0, batch[field] - now) * ratio
if batch.has(field): batch[field] = E.add(now, E.multiply(E.maximum(0, E.subtract(batch[field], now)), ratio))
+67 -73
View File
@@ -1,112 +1,106 @@
extends RefCounted
const E = preload("res://world/owniverse/math/exact.gd")
const INITIAL_ENERGY = 1.0e27
static var INITIAL_ENERGY = E.of("1.0e27")
const TRENDS = ["Antitheism", "Integration", "Xenocide", "Reconstruction", "Spirituality"]
const WEIGHTS = {
"m": [0, 2, 2, 0, 6], "l": [6, 1, 1, 2, 0],
"w": [2, 0, 6, 0, 2], "d": [0, 6, 0, 1, 3],
"c": [1, 3, 3, 3, 0], "b": [3, 0, 0, 6, 1],
}
const WEIGHTS = {"m": [0, 2, 2, 0, 6], "l": [6, 1, 1, 2, 0], "w": [2, 0, 6, 0, 2], "d": [0, 6, 0, 1, 3], "c": [1, 3, 3, 3, 0], "b": [3, 0, 0, 6, 1]}
static func defaults() -> Dictionary:
return {"Bur": 10.0, "Exp": 10.0, "Agg": 10.0,
"Federation": 1.0, "Enlightenment": 1.0, "Order": 1.0}
return {"Bur": 10, "Exp": 10, "Agg": 10, "Federation": 1, "Enlightenment": 1, "Order": 1}
static func trend_defaults() -> Dictionary:
return {"Antitheism": 0.0, "Integration": 0.0, "Xenocide": 0.0,
"Reconstruction": 0.0, "Spirituality": 0.0}
return {"Antitheism": 0, "Integration": 0, "Xenocide": 0, "Reconstruction": 0, "Spirituality": 0}
static func value(source: Dictionary, key: String) -> float:
return source.get("world_parameters", {}).get(key, defaults().get(key, 1.0))
static func value(source: Dictionary, key: String) -> Variant:
return source.get("world_parameters", {}).get(key, defaults().get(key, 1))
static func effective(source: Dictionary, key: String) -> float:
return pow(10.0, (clampf(value(source, key), 1.0, 19.0) - 10.0) / 9.0)
static func effective(source: Dictionary, key: String) -> Variant:
return E.power(10, E.divide((E.subtract(E.clamp_value(value(source, key), 1, 19), 10)), 9))
static func life_modifiers(source: Dictionary) -> Dictionary:
var aggression: float = effective(source, "Agg")
return {"progress": aggression,
"degradation": (1.0 - (1.0 - aggression) / 20.0) / value(source, "Order"),
"war": 1.0 - (1.0 - aggression) / 5.0,
"colonization": 1.0 - (1.0 - aggression) / 2.0}
var aggression: Variant = effective(source, "Agg")
return {"progress": aggression, "degradation": E.divide((E.subtract(1, E.divide((E.subtract(1, aggression)), 20))), value(source, "Order")), "war": E.subtract(1, E.divide((E.subtract(1, aggression)), 5)), "colonization": E.subtract(1, E.divide((E.subtract(1, aggression)), 2))}
static func production(source: Dictionary, entity) -> float:
if entity.event_group == "SNGRBB":
static func production(source: Dictionary, entity) -> Variant:
if E.eq(entity.event_group, "SNGRBB"):
return effective(source, "Exp")
if entity.group_as == "Star":
var burn: float = effective(source, "Bur")
return sqrt(burn) if burn > 1.0 else burn
return 1.0
if E.eq(entity.group_as, "Star"):
var burn: Variant = effective(source, "Bur")
return (E.square_root(burn) if E.gt(burn, 1) else burn)
return 1
static func density_display(density: float) -> Dictionary:
if density <= 1.0e-5:
static func density_display(density: Variant) -> Dictionary:
if E.le(density, E.of("1.0e-5")):
# Unity had log10(density) - 7 here; +7 maps 1e-7..1e-5 to 0..1.
return {"mode": "DensityRip", "position": clampf((log(maxf(density, 1e-7)) / log(10.0) + 7.0) / 2.0, 0.0, 1.0)}
if density >= 0.9:
return {"mode": "DensityCrunch", "position": clampf((density - 0.9) * 10.0, 0.0, 1.0)}
return {"mode": "Density", "position": clampf(log(density * 1.0e5) / log(9.0e4), 0.0, 1.0)}
return {"mode": "DensityRip", "position": E.clamp_value(E.divide((E.add(E.divide(E.logarithm(E.maximum(density, E.of("1e-7"))), E.logarithm(10)), 7)), 2), 0, 1)}
if E.ge(density, E.of("0.9")):
return {"mode": "DensityCrunch", "position": E.clamp_value(E.multiply((E.subtract(density, E.of("0.9"))), 10), 0, 1)}
return {"mode": "Density", "position": E.clamp_value(E.divide(E.logarithm(E.multiply(density, 100000)), E.logarithm(90000)), 0, 1)}
## Unity ApocalypseCheck thresholds and WarningUI colors; visual warning only.
static func density_warning_color(density: float) -> Color:
if density >= 1.0: return Color.html("00c0ffff")
if density >= 0.97: return Color.html("20b0dfbf")
if density >= 0.9: return Color.html("40a0c07f")
if density <= 1.0e-7: return Color.html("ff0000ff")
if density <= 1.0e-6: return Color.html("df2020bf")
if density <= 1.0e-5: return Color.html("c040407f")
return Color(0.0, 0.0, 0.0, 0.0)
static func density_warning_color(density: Variant) -> Color:
if E.ge(density, 1): return Color.html("00c0ffff")
if E.ge(density, E.of("0.97")): return Color.html("20b0dfbf")
if E.ge(density, E.of("0.9")): return Color.html("40a0c07f")
if E.le(density, E.of("1.0e-7")): return Color.html("ff0000ff")
if E.le(density, E.of("1.0e-6")): return Color.html("df2020bf")
if E.le(density, E.of("1.0e-5")): return Color.html("c040407f")
return Color(0, 0, 0, 0)
## Integrate the population present at the start of the simulation interval.
## Thresholds use whole event counts; no per-year loop at high speeds.
static func advance(previous: Dictionary, target: Dictionary, rules: Dictionary, years: float) -> void:
if years <= 0.0: return
if not target.has("world_parameters"): target.world_parameters = defaults()
if not target.has("trends"): target.trends = trend_defaults()
var rates: Array = [0.0, 0.0, 0.0, 0.0, 0.0]
static func advance(previous: Dictionary, target: Dictionary, rules: Dictionary, years: Variant) -> void:
if E.le(years, 0): return
if (not target.has("world_parameters")): target.world_parameters = defaults()
if (not target.has("trends")): target.trends = trend_defaults()
var rates: Array = [0, 0, 0, 0, 0]
var stages: Array = []
for id in rules:
if str(rules[id].home) != "": stages.append(id)
if E.ne(str(rules[id].home), ""): stages.append(id)
stages.sort()
var weight: float = 1.0
var weight: Variant = 1
for id in stages:
var population: Dictionary = previous.get("ethics", {}).get(id, {})
var count: float = 0.0
for n in population.values(): count += n
if count > 0.0:
var count: Variant = 0
for n in population.values(): count = E.add(count, n)
if E.gt(count, 0):
for ethic in population:
for i in TRENDS.size():
rates[i] += population[ethic] / count * WEIGHTS[ethic][i] * weight
weight *= 2.0
rates[i] = E.add(rates[i], E.multiply(E.multiply(E.divide(population[ethic], count), WEIGHTS[ethic][i]), weight))
weight = E.multiply(weight, 2)
for i in TRENDS.size():
var name: String = TRENDS[i]
var accumulated: float = target.trends[name] + rates[i] * years * 1.0e-9
var events: float = floorf(snappedf(accumulated, 1.0e-12))
target.trends[name] = maxf(0.0, accumulated - events)
if events <= 0.0: continue
var key: String = "trend:" + name
target.civilization_stats[key] = target.civilization_stats.get(key, 0.0) + events
if name == "Integration": _integration(target, events)
var accumulated: Variant = E.add(target.trends[name], E.multiply(E.multiply(rates[i], years), E.of("1.0e-9")))
var events: Variant = E.floor_value(accumulated)
target.trends[name] = E.maximum(0, E.subtract(accumulated, events))
if E.le(events, 0): continue
var key: String = E.add("trend:", name)
target.civilization_stats[key] = E.add(target.civilization_stats.get(key, 0), events)
if E.eq(name, "Integration"): _integration(target, events)
static func _integration(target: Dictionary, events: float) -> void:
static func _integration(target: Dictionary, events: Variant) -> void:
# Equal-probability outcomes in Unity. Use a saved RNG and bounded aggregate
# sampling for large steps, so thousands of triggers do not stall a frame.
var rng := RandomNumberGenerator.new()
var rng = RandomNumberGenerator.new()
rng.seed = 76123
if target.civilization_stats.has("trend_rng"):
rng.state = int(target.civilization_stats.trend_rng)
var counts: Array = [0.0, 0.0, 0.0]
if events <= 256.0:
for i in int(events): counts[rng.randi_range(0, 2)] += 1.0
rng.state = E.integer(target.civilization_stats.trend_rng)
var counts: Array = [0, 0, 0]
if E.le(events, 256):
for i in E.integer(events):
var chosen: int = rng.randi_range(0, 2)
counts[chosen] = E.add(counts[chosen], 1)
else:
counts[0] = clampf(roundf(rng.randfn(events / 3.0, sqrt(events * 2.0 / 9.0))), 0.0, events)
var rest: float = events - counts[0]
counts[1] = clampf(roundf(rng.randfn(rest / 2.0, sqrt(rest / 4.0))), 0.0, rest)
counts[2] = rest - counts[1]
counts[0] = E.clamp_value(E.round_value(E.of(rng.randfn(E.approx(E.divide(events, 3)), E.approx(E.square_root(E.divide(E.multiply(events, 2), 9)))))), 0, events)
var rest: Variant = E.subtract(events, counts[0])
counts[1] = E.clamp_value(E.round_value(E.of(rng.randfn(E.approx(E.divide(rest, 2)), E.approx(E.square_root(E.divide(rest, 4)))))), 0, rest)
counts[2] = E.subtract(rest, counts[1])
target.civilization_stats.trend_rng = rng.state
var parameters: Dictionary = target.world_parameters
parameters.Agg = maxf(1.0, parameters.Agg - counts[0] - counts[1])
parameters.Federation += counts[0] * 0.1
parameters.Enlightenment += counts[1] * 0.2
parameters.Order += counts[2] * 0.1
parameters.Agg = E.maximum(1, E.subtract(E.subtract(parameters.Agg, counts[0]), counts[1]))
parameters.Federation = E.add(parameters.Federation, E.multiply(counts[0], E.of("0.1")))
parameters.Enlightenment = E.add(parameters.Enlightenment, E.multiply(counts[1], E.of("0.2")))
parameters.Order = E.add(parameters.Order, E.multiply(counts[2], E.of("0.1")))
for i in 3:
var key: String = ["Federation", "Enlightenment", "Order"][i]
target.civilization_stats[key] = target.civilization_stats.get(key, 0.0) + counts[i]
target.civilization_stats[key] = E.add(target.civilization_stats.get(key, 0), counts[i])
+13 -8
View File
@@ -99,6 +99,7 @@ size_flags_vertical = 3
script = ExtResource("3_k83x5")
[node name="World" type="VBoxContainer" parent="Stage" unique_id=1878643860]
visible = false
layout_mode = 1
anchors_preset = 10
anchor_right = 1.0
@@ -1058,10 +1059,9 @@ texture = ExtResource("61_d36xh")
stretch_mode = 3
[node name="Credits" type="VBoxContainer" parent="Stage" unique_id=785042592]
visible = false
layout_mode = 0
offset_right = 40.0
offset_bottom = 40.0
offset_right = 1080.0
offset_bottom = 1920.0
script = ExtResource("62_o2i0n")
[node name="TopMenu" type="HBoxContainer" parent="Stage/Credits" unique_id=1983527467]
@@ -1088,15 +1088,20 @@ texture = ExtResource("62_vu83j")
custom_minimum_size = Vector2(1080, 0)
layout_mode = 2
[node name="Highlight" type="TextureRect" parent="Stage/Credits/Logo" unique_id=1035898922]
visible = false
layout_mode = 2
texture = ExtResource("64_rur7c")
[node name="Lamp" type="TextureButton" parent="Stage/Credits/Logo" unique_id=1059180893]
layout_mode = 2
texture_normal = ExtResource("65_y1rwn")
[node name="Highlight" type="TextureRect" parent="Stage/Credits/Logo" unique_id=1035898922]
visible = false
layout_mode = 2
mouse_filter = 2
texture = ExtResource("64_rur7c")
[node name="Control" type="Control" parent="Stage/Credits" unique_id=1835435348]
layout_mode = 2
size_flags_vertical = 3
[node name="ItemAmountUpdate" type="Timer" parent="Stage" unique_id=1953928024]
wait_time = 0.2
autostart = true