2026-09-22 22:50:54 +03:00
|
|
|
extends SceneTree
|
2026-09-30 08:08:27 +03:00
|
|
|
const E = preload("res://world/owniverse/math/exact.gd")
|
2026-09-22 22:50:54 +03:00
|
|
|
var checks: int = 0
|
|
|
|
|
var failures: int = 0
|
|
|
|
|
|
|
|
|
|
func _initialize() -> void:
|
|
|
|
|
call_deferred("run")
|
|
|
|
|
|
|
|
|
|
func check(condition: bool, message: String) -> void:
|
2026-09-30 08:08:27 +03:00
|
|
|
checks = E.add(checks, 1)
|
|
|
|
|
if (not condition):
|
|
|
|
|
failures = E.add(failures, 1)
|
2026-09-22 22:50:54 +03:00
|
|
|
push_error(message)
|
|
|
|
|
|
|
|
|
|
func fixture(legacy: bool):
|
2026-09-30 08:08:27 +03:00
|
|
|
var world = load(("res://tests/fixtures/legacy_stellar_events.gd" if legacy else "res://world/owniverse/owniverse.gd")).new()
|
2026-09-22 22:50:54 +03:00
|
|
|
for id in root.get_node("Global").OwniverseEntities:
|
2026-09-29 20:23:20 +03:00
|
|
|
world.catalog.entities[id] = load("res://world/owniverse/owniverse_entity.gd").new(root.get_node("Global").OwniverseEntities[id])
|
2026-09-30 08:08:27 +03:00
|
|
|
world.state.amount[id] = 0
|
|
|
|
|
world.state.stats.total_amount[id] = 0
|
|
|
|
|
if E.ne(world.catalog.entities[id].group_as, ""):
|
|
|
|
|
world.state.amount[world.catalog.entities[id].group_as] = 0
|
|
|
|
|
world.state.stats.total_amount[world.catalog.entities[id].group_as] = 0
|
2026-09-22 22:50:54 +03:00
|
|
|
return world
|
|
|
|
|
|
|
|
|
|
func dispose(world) -> void:
|
2026-09-29 20:23:20 +03:00
|
|
|
for entity in world.catalog.entities.values(): entity.free()
|
2026-09-22 22:50:54 +03:00
|
|
|
world.free()
|
|
|
|
|
|
|
|
|
|
func equivalent(a: Variant, b: Variant) -> bool:
|
2026-09-30 08:08:27 +03:00
|
|
|
if ((a is Dictionary) and (b is Dictionary)):
|
|
|
|
|
if E.ne(a.size(), b.size()): return false
|
2026-09-22 22:50:54 +03:00
|
|
|
for key in a:
|
2026-09-30 08:08:27 +03:00
|
|
|
if ((not b.has(key)) or (not equivalent(a[key], b[key]))): return false
|
2026-09-22 22:50:54 +03:00
|
|
|
return true
|
2026-09-30 08:08:27 +03:00
|
|
|
if ((a is Array) and (b is Array)):
|
|
|
|
|
if E.ne(a.size(), b.size()): return false
|
2026-09-22 22:50:54 +03:00
|
|
|
for i in a.size():
|
2026-09-30 08:08:27 +03:00
|
|
|
if (not equivalent(a[i], b[i])): return false
|
2026-09-22 22:50:54 +03:00
|
|
|
return true
|
2026-10-01 23:43:34 +03:00
|
|
|
if E.numeric(a) and E.numeric(b):
|
|
|
|
|
# Counts must match exactly. Fractional float remainders can differ
|
|
|
|
|
# when the analytic integral replaces thousands of subtractions.
|
|
|
|
|
if not E.eq(E.floor_value(a), E.floor_value(b)): return false
|
|
|
|
|
return E.le(E.absolute(E.subtract(a, b)), E.of("1e-8"))
|
2026-09-30 08:08:27 +03:00
|
|
|
return E.eq(a, b)
|
2026-09-22 22:50:54 +03:00
|
|
|
|
|
|
|
|
func run() -> void:
|
|
|
|
|
for parent in ["LRG", "HRG", "RSG", "RHG"]:
|
2026-09-30 08:08:27 +03:00
|
|
|
for year in [101, E.of("101.5"), E.of("104.75"), E.of("1767.3"), 3005]:
|
2026-09-22 22:50:54 +03:00
|
|
|
var fast = fixture(false)
|
|
|
|
|
var legacy = fixture(true)
|
2026-09-30 08:08:27 +03:00
|
|
|
fast.state.amount[parent] = 20000
|
|
|
|
|
fast.state.amount.RD = 100000
|
|
|
|
|
fast.state.amount.HP = 50
|
|
|
|
|
fast.state.amount.Met = 100
|
|
|
|
|
fast.state.binds.RD = {"HP": 2000, "C0.1": 100}
|
|
|
|
|
fast.state.binds[parent] = {"HP": 10}
|
|
|
|
|
fast.state.batches[parent] = [{"amount": 20000, "start_year": E.of("100.3"), "end_year": E.of("3000.7")}]
|
2026-09-22 22:50:54 +03:00
|
|
|
# Existing short-lived populations test integration with new arrivals.
|
2026-09-30 08:08:27 +03:00
|
|
|
fast.state.amount.SN = 3
|
|
|
|
|
fast.batches.append_batch(fast.state, "SN", 3, E.of("100.5"), E.of("100.5"))
|
|
|
|
|
if E.eq(year, E.of("104.75")): fast.state.energy = 7
|
2026-09-22 22:50:54 +03:00
|
|
|
legacy.state = fast.state.duplicate(true)
|
2026-09-29 20:23:20 +03:00
|
|
|
fast.evolution.evolve_batches(fast.state, year)
|
|
|
|
|
legacy.evolution.evolve_batches(legacy.state, year)
|
2026-09-22 22:50:54 +03:00
|
|
|
for field in fast.state:
|
2026-09-30 08:08:27 +03:00
|
|
|
check(equivalent(fast.state[field], legacy.state[field]), ("%s at %s: %s matches materialized events" % [parent, year, field]))
|
2026-09-22 22:50:54 +03:00
|
|
|
for id in ["SN", "DSN", "BSN", "HN", "GRB", "SGRB", "LGRB", "HGRB"]:
|
2026-09-30 08:08:27 +03:00
|
|
|
check(E.le(fast.state.batches.get(id, []).size(), 3), "Only surviving event windows remain")
|
2026-09-22 22:50:54 +03:00
|
|
|
dispose(fast)
|
|
|
|
|
dispose(legacy)
|
|
|
|
|
var fast = fixture(false)
|
|
|
|
|
var pending: Dictionary = {}
|
2026-09-30 08:08:27 +03:00
|
|
|
fast.batches.append_batch(fast.state, "SN", 10000, E.of("0.25"), E.of("1667.25"), pending, E.of("1667.25"))
|
|
|
|
|
check((E.le(fast.state.batches.SN.size(), 2) and E.eq(pending.SN.size(), 1)), "Completed windows aggregate without transient batch allocation")
|
|
|
|
|
var total: Variant = pending.SN[0].amount
|
|
|
|
|
for batch in fast.state.batches.SN: total = E.add(total, batch.amount)
|
|
|
|
|
check(E.eq(total, 10000), "Direct completions plus survivors conserve event count")
|
2026-09-22 22:50:54 +03:00
|
|
|
dispose(fast)
|
2026-10-01 23:43:34 +03:00
|
|
|
# Dense completed events must not iterate a million elapsed windows.
|
|
|
|
|
# An aligned, fully completed cohort produces exactly count * lifespan.
|
|
|
|
|
fast = fixture(false)
|
|
|
|
|
pending = {}
|
|
|
|
|
var lifespan: Variant = fast.catalog.entities.SN.lifespan
|
|
|
|
|
var window: Variant = E.divide(lifespan, root.get_node("Global").batch_count) if E.gt(lifespan, 10) else lifespan
|
|
|
|
|
var creation_end: Variant = E.multiply(window, 1000000)
|
|
|
|
|
var dense_count: Variant = E.of("1e20")
|
|
|
|
|
var integral: Variant = fast.batches.append_batch(fast.state, "SN", dense_count, 0, creation_end, pending, E.add(creation_end, lifespan))
|
|
|
|
|
check(E.eq(pending.SN[0].amount, dense_count), "Million-window aggregation conserves every event")
|
|
|
|
|
check(E.eq(integral, E.multiply(dense_count, lifespan)), "Million-window aggregation preserves lifetime integral")
|
|
|
|
|
check(fast.state.batches.SN.is_empty(), "Completed million-window cohort leaves no transient batches")
|
|
|
|
|
dispose(fast)
|
2026-09-30 22:46:26 +03:00
|
|
|
# Sparse explosions: thousands of time windows but very few actual events.
|
|
|
|
|
# Exact interval edges and partial final windows exercise the jump boundary.
|
|
|
|
|
for count in [1, 3, 17]:
|
|
|
|
|
for year in [500, 1001]:
|
|
|
|
|
fast = fixture(false)
|
|
|
|
|
var legacy = fixture(true)
|
|
|
|
|
fast.state.amount.RHG = count
|
|
|
|
|
fast.state.batches.RHG = [{"amount": count, "start_year": E.of("0.25"), "end_year": E.of("1000.25")}]
|
|
|
|
|
legacy.state = fast.state.duplicate(true)
|
|
|
|
|
fast.evolution.evolve_batches(fast.state, year)
|
|
|
|
|
legacy.evolution.evolve_batches(legacy.state, year)
|
|
|
|
|
for field in fast.state:
|
|
|
|
|
check(equivalent(fast.state[field], legacy.state[field]), "Sparse %s at %s: %s" % [count, year, field])
|
|
|
|
|
dispose(fast)
|
|
|
|
|
dispose(legacy)
|
2026-09-30 08:08:27 +03:00
|
|
|
print(("Short-lived events: %d checks, %d failures" % [checks, failures]))
|
|
|
|
|
quit((0 if E.eq(failures, 0) else 1))
|