Add core simulation modules for owniverse mechanics

- Introduced `life_evolution.gd` to manage life evolution processes.
- Implemented `manual_production.gd` for handling production mechanics, including resource management and production steps.
- Created `object_batches.gd` to manage batches of objects, including scaling and appending batches.
- Developed `object_ledger.gd` for tracking physical amounts, available resources, and binding ownerships.
- Added `simulation_module.gd` as a base class for simulation modules, providing a weak reference to the world.
- Established `stellar_rules.gd` for managing stellar evolution and destruction mechanics.
- Introduced `universe_environment.gd` to calculate environmental parameters based on the current state of the universe.
This commit is contained in:
Kirill
2026-09-29 20:23:20 +03:00
parent da03ba11c0
commit f2111a376a
42 changed files with 1681 additions and 1704 deletions
+51 -51
View File
@@ -16,72 +16,72 @@ func fixture():
var global: Node = root.get_node("Global")
for id in global.OwniverseEntities:
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
world.entities[id] = entity
world.catalog.entities[id] = entity
world.state.amount[id] = 0.0
world.state.stats.total_amount[id] = 0.0
if entity.group_as != "":
world.state.amount[entity.group_as] = 0.0
world.state.stats.total_amount[entity.group_as] = 0.0
world.entities["N"].life_product = {}
world.entities["N"].product_per_year_type = "H"
world.entities["N"].product_per_year_amount = 10.0
world.entities["N"].auto = 0.0
world.catalog.entities["N"].life_product = {}
world.catalog.entities["N"].product_per_year_type = "H"
world.catalog.entities["N"].product_per_year_amount = 10.0
world.catalog.entities["N"].auto = 0.0
world.state.amount.N = 1.0
return world
func dispose(world) -> void:
for entity in world.entities.values():
for entity in world.catalog.entities.values():
entity.free()
world.free()
func run() -> void:
var world = fixture()
for id in ["DSN", "SN", "BSN", "HN", "SGRB", "GRB", "LGRB", "HGRB"]:
check(world._evolution_products(world.state, world.entities[id], 3.0).blast == 3.0 * world.entities[id].blast, "Entity blast determines power: " + id)
world.entities.SN.output_and.blast = 999.0
world.entities.SN.blast = 4
check(world._evolution_products(world.state, world.entities.SN, 2.0).blast == 8.0, "Imported output cannot override or double blast power")
world.entities.SN.blast = 0
check(not world._evolution_products(world.state, world.entities.SN, 2.0).has("blast"), "Zero entity blast disables external event")
world.entities.SN.output_or = {"positive": "blast:999", "negative": "blast:999", "chance": 50.0}
check(not world._evolution_products(world.state, world.entities.SN, 2.0).has("blast"), "Alternative output cannot restore disabled blast")
check(world.stellar.evolution_products(world.state, world.catalog.entities[id], 3.0).blast == 3.0 * world.catalog.entities[id].blast, "Entity blast determines power: " + id)
world.catalog.entities.SN.output_and.blast = 999.0
world.catalog.entities.SN.blast = 4
check(world.stellar.evolution_products(world.state, world.catalog.entities.SN, 2.0).blast == 8.0, "Imported output cannot override or double blast power")
world.catalog.entities.SN.blast = 0
check(not world.stellar.evolution_products(world.state, world.catalog.entities.SN, 2.0).has("blast"), "Zero entity blast disables external event")
world.catalog.entities.SN.output_or = {"positive": "blast:999", "negative": "blast:999", "chance": 50.0}
check(not world.stellar.evolution_products(world.state, world.catalog.entities.SN, 2.0).has("blast"), "Alternative output cannot restore disabled blast")
dispose(world)
world = fixture()
var rd = world.entities.RD
var rd = world.catalog.entities.RD
check(rd.planets.HP.chance == 0.01, "Preserve fractional planet probability")
check(rd.planets.GG.amount == 2.0 and rd.planets.GG.chance == 0.08, "Read count and chance separately")
check(rd._parse_planet_string("HP:1:0.01") == {"HP": {"amount": 1.0, "chance": 0.01}}, "Accept a single planet term")
check(rd._parse_planet_string("").is_empty(), "Empty planet definitions stay empty")
for id in ["HP", "EON1", "C0.9", "C1.2"]:
check(world.entities[id].blastability, "Blast vulnerable: " + id)
check(world.catalog.entities[id].blastability, "Blast vulnerable: " + id)
for id in ["RP", "GG", "AB", "C1.3", "C1.4", "C1.5"]:
check(not world.entities[id].blastability, "Blast protected: " + id)
check(not world.catalog.entities[id].blastability, "Blast protected: " + id)
# Actual GI table: HRG -> SN, with 1% GRB; no random rounding.
var totals: Dictionary = {}
for i in 100:
var products: Dictionary = world._evolution_products(world.state, world.entities.HRG, 1.0)
var products: Dictionary = world.stellar.evolution_products(world.state, world.catalog.entities.HRG, 1.0)
for id in products:
totals[id] = totals.get(id, 0.0) + products[id]
check(totals.get("SN") == 99.0 and totals.get("GRB") == 1.0, "GI rare outcomes accumulate deterministically")
var restored = fixture()
restored.state = bytes_to_var(var_to_bytes(world.state))
check(world._evolution_products(world.state, world.entities.HRG, 37.0) == restored._evolution_products(restored.state, restored.entities.HRG, 37.0), "Evolution carry survives serialization")
check(world.stellar.evolution_products(world.state, world.catalog.entities.HRG, 37.0) == restored.stellar.evolution_products(restored.state, restored.catalog.entities.HRG, 37.0), "Evolution carry survives serialization")
dispose(restored)
# Real RD lifespan, four stars spread over the existing lifespan/40 window.
world.state.amount.N = 0.0
world.state.amount.RD = 4.0
world._append_batch(world.state, "RD", 4.0, 0.0, 0.0)
world.batches.append_batch(world.state, "RD", 4.0, 0.0, 0.0)
var lifespan: float = rd.lifespan
var width: float = rd.lifespan_delta
world._evolve_batches(world.state, lifespan)
world.evolution.evolve_batches(world.state, lifespan)
check(world.state.amount.RD == 4.0, "Window start does not kill the whole batch")
world._evolve_batches(world.state, lifespan + width / 4.0)
world.evolution.evolve_batches(world.state, lifespan + width / 4.0)
check(world.state.amount.RD == 3.0 and world.state.amount.TWD == 1.0, "One quarter of a batch evolves")
world._evolve_batches(world.state, lifespan + width / 2.0)
world.evolution.evolve_batches(world.state, lifespan + width / 2.0)
check(world.state.amount.RD == 2.0 and world.state.amount.TWD == 2.0, "Half the batch evolves")
world._evolve_batches(world.state, lifespan + width)
world.evolution.evolve_batches(world.state, lifespan + width)
check(world.state.amount.RD == 0.0 and world.state.amount.TWD == 4.0, "Window end completes evolution")
check(world.state.batches.RD.is_empty(), "Completed batch removed")
dispose(world)
@@ -89,7 +89,7 @@ func run() -> void:
world = fixture()
world.state.amount.RD = 100.0
for i in 100:
world._create_planets(world.state, "RD", 1.0, 0.0, 0.0, 1.0)
world.stellar.create_planets(world.state, "RD", 1.0, 0.0, 0.0, 1.0)
check(world.state.binds.RD.HP == 1.0, "100 RD at metallicity 1 produce one HP")
check(world.state.binds.RD.RP == 21.0, "Rocky planet probability retains fractions")
check(world.state.binds.RD.GG == 16.0 and world.state.binds.RD.AB == 120.0, "Planet multiplicity applied")
@@ -98,8 +98,8 @@ func run() -> void:
check(saves.validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Planet carry save validates")
restored = fixture()
restored.state = bytes_to_var(var_to_bytes(world.state))
world._create_planets(world.state, "RD", 37.0, 0.0, 0.0, 0.7)
restored._create_planets(restored.state, "RD", 37.0, 0.0, 0.0, 0.7)
world.stellar.create_planets(world.state, "RD", 37.0, 0.0, 0.0, 0.7)
restored.stellar.create_planets(restored.state, "RD", 37.0, 0.0, 0.0, 0.7)
check(world.state == restored.state, "Planet carry continues after save")
dispose(restored)
dispose(world)
@@ -108,14 +108,14 @@ func run() -> void:
world = fixture()
world.state.amount.RD = 4.0
world.state.binds.RD = {"HP": 8.0, "RP": 4.0, "C1.2": 4.0, "C1.3": 4.0, "GG": 4.0}
world._append_batch(world.state, "RD", 4.0, 0.0, 0.0)
world._evolve_batches(world.state, lifespan + width / 2.0)
world.batches.append_batch(world.state, "RD", 4.0, 0.0, 0.0)
world.evolution.evolve_batches(world.state, lifespan + width / 2.0)
check(world.state.amount.RD == 2.0 and world.state.amount.TWD == 2.0, "Partial star transition with planets")
check(world.state.binds.RD.HP + world.state.binds.TWD.HP == 4.0, "Half of HP sterilized")
check(world.state.binds.RD["C1.3"] == 2.0 and world.state.binds.TWD["C1.3"] == 2.0, "Protected civilization splits between parent and heir")
check(world.state.binds.RD.RP + world.state.binds.TWD.RP == 10.0, "Sterilized planets become RP without loss")
check(world.state.amount.RP == 0.0 and world.state.amount["C1.3"] == 0.0, "Inherited planets never become free")
world._evolve_batches(world.state, lifespan + width)
world.evolution.evolve_batches(world.state, lifespan + width)
check(world.state.binds.RD["C1.3"] == 0.0 and world.state.binds.TWD["C1.3"] == 4.0, "No planets left at extinct parent type")
check(saves.validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Inherited state validates")
dispose(world)
@@ -123,9 +123,9 @@ func run() -> void:
world = fixture()
world.state.amount.WS = 2.0
world.state.binds.WS = {"HP": 4.0, "C1.3": 4.0, "RP": 4.0, "GG": 4.0}
world._append_batch(world.state, "WS", 2.0, 0.0, 0.0)
world._evolve_batches(world.state, world.entities.WS.lifespan + world.entities.WS.lifespan_delta)
var physical: Dictionary = world._physical_amounts(world.state)
world.batches.append_batch(world.state, "WS", 2.0, 0.0, 0.0)
world.evolution.evolve_batches(world.state, world.catalog.entities.WS.lifespan + world.catalog.entities.WS.lifespan_delta)
var physical: Dictionary = world.ledger.physical_amounts(world.state)
check(physical.HP == 0.0 and physical["C1.3"] == 0.0 and physical.RP == 0.0 and physical.GG == 0.0, "Level 2 removes all planets, including protected civilization")
check(world.state.amount.LRG == 2.0, "Giant evolves after local destruction")
dispose(world)
@@ -133,10 +133,10 @@ func run() -> void:
world = fixture()
world.state.amount.RD = 10.0
world.state.binds.RD = {"HP": 10.0, "C1.2": 10.0, "C1.3": 10.0, "RP": 0.0}
world._apply_blast(world.state, 5.0, {"blast": 1.0, "density": 1.0})
world.stellar.apply_blast(world.state, 5.0, {"blast": 1.0, "density": 1.0})
check(world.state.binds.RD.HP == 5.0 and world.state.binds.RD["C1.2"] == 5.0, "Blast damages vulnerable worlds proportionally")
check(world.state.binds.RD["C1.3"] == 10.0 and world.state.binds.RD.RP == 10.0, "Blast protects C1.3 and creates RP")
world._apply_blast(world.state, 1e6, {"blast": 1.0, "density": 1.0})
world.stellar.apply_blast(world.state, 1e6, {"blast": 1.0, "density": 1.0})
check(world.state.binds.RD.HP == 0.0 and world.state.binds.RD.RP == 20.0, "Overwhelming blast is capped at available planets")
check(not world.state.amount.has("blast"), "Blast is not a resource")
dispose(world)
@@ -145,15 +145,15 @@ func run() -> void:
world.state.amount.RD = 1.0
world.state.binds.RD = {"HP": 1.0}
for i in 10:
world._apply_blast(world.state, 1.0, {"blast": 1.0, "density": 0.1})
world.stellar.apply_blast(world.state, 1.0, {"blast": 1.0, "density": 0.1})
check(world.state.binds.RD.HP == 0.0 and world.state.binds.RD.RP == 1.0, "Small blast effects accumulate across cycles")
dispose(world)
# Actual supernova output: one SN gives a neutron star, resources, and an event.
world = fixture()
world.state.amount.SN = 1.0
world._append_batch(world.state, "SN", 1.0, 0.0, 0.0)
world._evolve_batches(world.state, 2.0)
world.batches.append_batch(world.state, "SN", 1.0, 0.0, 0.0)
world.evolution.evolve_batches(world.state, 2.0)
check(world.state.amount.SN == 0.0 and world.state.amount.LNS == 1.0, "Supernova produces a remnant")
check(world.state.amount.H == 8.0 and world.state.amount.He == 50.0 and world.state.amount.Met == 11.0, "Supernova returns table resources")
check(not world.state.amount.has("blast"), "Supernova dispatches blast output")
@@ -163,7 +163,7 @@ func run() -> void:
world = fixture()
world.state.amount.H = 1000.0
world.state.amount.Met = 100.0
check(world.produce("RD", 100.0) == 100.0, "Manual star creation succeeds")
check(world.production.produce("RD", 100.0) == 100.0, "Manual star creation succeeds")
check(world.state.binds.RD.HP >= 1.0, "Manual creation dispatches planet formation")
check(world.state.amount.H == 900.0, "Planet creation does not duplicate or spend stellar fuel")
dispose(world)
@@ -180,25 +180,25 @@ func run() -> void:
dispose(world)
world = fixture()
world.entities.N.auto = 1.0
world.catalog.entities.N.auto = 1.0
# 1000 newly produced automatic H reserve 1 H, enough for 10 HBD.
world.evolute(100.0)
check(world.state.amount.HBD == 10.0, "Brown dwarfs form without existing ordinary stars")
check(world.state.amount.H == 999.0 and world.state.auto_resources.H == 999.0, "Brown dwarfs consume exactly their reserved hydrogen")
check(world.get_item_amounts().BD == 10.0 and world.state.batches.HBD.size() > 0, "Brown dwarfs appear in counter and evolution batches")
world.entities.N.product_per_year_amount = 0.0
check(world.presentation.amounts(world.state, world.catalog).BD == 10.0 and world.state.batches.HBD.size() > 0, "Brown dwarfs appear in counter and evolution batches")
world.catalog.entities.N.product_per_year_amount = 0.0
world.evolute(100.0)
check(world.state.amount.HBD == 10.0, "Stored automatic budget is not taxed repeatedly")
dispose(world)
world = fixture()
world.entities.N.auto = 1.0
world.catalog.entities.N.auto = 1.0
world.state.amount.RD = 1.0
world.evolute(50.0)
check(world.state.amount.HBD == 5.0 and is_equal_approx(world.state.auto_star_deficits.HBD_fuel, 0.5), "Individual dwarfs retain prepaid fractional fuel")
check(world.state.amount.H == 0.0, "Prepaid fuel is not available to ordinary stars")
var resumed = fixture()
resumed.entities.N.auto = 1.0
resumed.catalog.entities.N.auto = 1.0
resumed.state = bytes_to_var(var_to_bytes(world.state))
world.evolute(50.0)
resumed.evolute(50.0)
@@ -206,8 +206,8 @@ func run() -> void:
check(world.state == resumed.state, "Brown dwarf quota survives serialization deterministically")
check(world.state.amount.RD == 1000.0 and world.state.amount.H == 0.0, "Shared automatic hydrogen is spent once")
world.state.batches.HBD = [{"amount": 10.0, "start_year": 101.0, "end_year": 101.0}]
world._evolve_batches(world.state, 101.0)
check(world.state.amount.HBD == 0.0 and world.state.amount.CBD == 10.0 and world.get_item_amounts().BD == 10.0, "Cooling preserves aggregate brown dwarf count")
world.evolution.evolve_batches(world.state, 101.0)
check(world.state.amount.HBD == 0.0 and world.state.amount.CBD == 10.0 and world.presentation.amounts(world.state, world.catalog).BD == 10.0, "Cooling preserves aggregate brown dwarf count")
dispose(resumed)
dispose(world)
@@ -217,7 +217,7 @@ func run() -> void:
world.state.amount.H += 1.0
world.state.auto_resources.H = world.state.auto_resources.get("H", 0.0) + 1.0
var before: float = world.state.amount.HBD
world._produce_brown_dwarfs(world.state, 1.0, float(i), float(i + 1))
world.automatic.produce_brown_dwarfs(world.state, 1.0, float(i), float(i + 1))
var born: float = world.state.amount.HBD - before
check(born == 0.0 or born == 1.0, "Small fuel increments produce individual dwarfs")
saw_individual = saw_individual or born == 1.0
@@ -228,23 +228,23 @@ func run() -> void:
for simulation in [world, resumed]:
simulation.state.amount.H += 1.0
simulation.state.auto_resources.H += 1.0
simulation._produce_brown_dwarfs(simulation.state, 1.0, float(i), float(i + 1))
simulation.automatic.produce_brown_dwarfs(simulation.state, 1.0, float(i), float(i + 1))
check(world.state == resumed.state, "Random sequence and prepaid fuel survive saving")
check(is_equal_approx(world.state.amount.H + world.state.amount.HBD * 0.1 + world.state.auto_star_deficits.HBD_fuel, 500.0), "Individual production conserves fuel including prepaid reserve")
check(is_equal_approx(world.state.amount.HBD + world.state.auto_star_deficits.HBD, 5.0), "Signed remainder preserves the 0.1 percent allocation")
var paused_count: float = world.state.amount.HBD
for i in 10: world._produce_brown_dwarfs(world.state, 0.0, 500.0, 501.0)
for i in 10: world.automatic.produce_brown_dwarfs(world.state, 0.0, 500.0, 501.0)
check(world.state.amount.HBD == paused_count, "No repeated random attempts without new fuel")
dispose(resumed)
dispose(world)
# Two simultaneous successors share surviving planets in the same 99:1 ratio.
world = fixture()
world.entities.HRG.destroy_level = 0
world.catalog.entities.HRG.destroy_level = 0
world.state.amount.HRG = 100.0
world.state.binds.HRG = {"C1.3": 100.0}
world.state.batches.HRG = [{"start_year": 0.1, "end_year": 0.1, "amount": 100.0}]
world._evolve_batches(world.state, 0.1)
world.evolution.evolve_batches(world.state, 0.1)
check(world.state.binds.SN["C1.3"] == 99.0 and world.state.binds.GRB["C1.3"] == 1.0, "Planets split among actual alternative successors")
check(world.state.binds.HRG["C1.3"] == 0.0, "Alternative evolution leaves no orphan bindings")
dispose(world)