Files
owniverse/world/owniverse/automatic_production.gd
T
Kirill c4e7302567 Add new tests and implement frame budget management
- Introduced `civilization_benchmark.gd` and `legacy_quantity_math.gd` for benchmarking and legacy quantity calculations.
- Created `test_dynamic_math.gd`, `test_frame_evolution.gd`, and `test_sparse_batches.gd` to validate mathematical operations and frame evolution logic.
- Implemented `frame_budget.gd` to manage cooperative work on the main thread, ensuring no partially calculated state is published.
- Added UID files for all new scripts to maintain unique identifiers.
2026-10-01 23:43:34 +03:00

226 lines
13 KiB
GDScript

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 E.gt(entity.lifespan, 0)):
for resource in entity.life_product:
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: Variant, to_year: Variant) -> void:
var basic: Dictionary = {}
var automatic: Dictionary = {}
for id in quantities:
var entity: OwniverseEntity = world.catalog.entities[id]
var products: Dictionary = yearly_products(entity)
if products.is_empty(): continue
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)
for product in products:
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")):
target.resource_remainders = {"basic": {}, "automatic": {}}
var pending: Dictionary = target.resource_remainders
var requests: Dictionary = {}
for id in basic:
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] = E.maximum(0, E.add(pending.basic.get(id, 0), pending.automatic.get(id, 0)))
for id in totals:
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: 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: 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)
# 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] = E.add(budget.get(id, 0), automatic[id])
for id in budget:
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), 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 E.ne(entity.group_as, "Star"):
continue
var has_cost: bool = false
for resource in entity.cost:
if E.gt(entity.cost[resource], 0):
has_cost = true
if has_cost:
candidates[id] = distribution[id]
candidates = QuantityMath.normalize_distribution(candidates)
if candidates.is_empty():
return
var costs: Dictionary = {}
var needs: Dictionary = {}
for id in candidates:
if E.le(auto_capacity(target, world.catalog.entities[id], budget), 0):
continue
for resource in world.catalog.entities[id].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] = 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 E.gt(costs[resource], 0):
scale = E.minimum(scale, E.divide(budget.get(resource, 0), costs[resource]))
for resource in needs:
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 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")):
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 (E.lt(candidates[a], candidates[b]) if E.ne(candidates[a], candidates[b]) else E.lt(a, b)))
var created: Dictionary = {}
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: Variant = world.production.production_step(entity)
if (not E.finite(step)):
continue
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)):
continue
budget = ordinary_star_budget(target)
created[id] = 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] = 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: 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: Variant = entity.cost.get("H", 0)
if E.le(cost, 0):
return
var quota: Variant = E.add(target.auto_star_deficits.get("HBD", 0), E.mul_div(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 (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 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 = 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
func ordinary_star_budget(target: Dictionary) -> Dictionary:
var budget: Dictionary = target.auto_resources.duplicate()
if world.catalog.entities.has("HBD"):
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) -> Variant:
var capacity: Variant = INF
var needs: Dictionary = {}
for resource in entity.cost:
var cost: Variant = E.maximum(0, entity.cost[resource])
needs[resource] = 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] = E.add(needs.get(resource, 0), E.maximum(0, entity.bind[resource]))
for resource in needs:
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 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: 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 E.le(count, 0)):
return
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: 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 coefficient: Variant = E.multiply(products[resource], WorldParameters.production(target, world.catalog.entities[id]))
var output: Variant = E.multiply(object_years, coefficient)
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))