Files
owniverse/tests/test_simulation_optimizations.gd
Kirill 3fcc7211ce Implement civilization flow mechanics and profiling for resource allocation
- Added `civilization_flow.gd` to manage continuous expected flows and resource allocation within civilizations.
- Introduced `allocate_profiled` and `whole_quotas` functions in `quantity_math.gd` for precise stock allocation based on float shares.
- Enhanced `credit` logic in `object_ledger.gd` to handle production remainders more effectively.
- Updated `append_batch` in `object_batches.gd` to prevent batch creation for invalid IDs.
- Introduced `ratio_approx` function in `exact.gd` for better handling of float profiles.
- Added a benchmark script `benchmark_civilization_flows.gd` to evaluate the performance of civilization flow mechanics.
- Refactored various functions to improve clarity and efficiency in handling resource flows and allocations.
2026-10-07 22:51:55 +03:00

112 lines
6.9 KiB
GDScript

extends SceneTree
const E = preload("res://world/owniverse/math/exact.gd")
const Civ = preload("res://world/owniverse/civilization_evolution.gd")
const Q = preload("res://world/owniverse/quantity_math.gd")
const Fixture = preload("res://tests/fixtures/civilization_benchmark.gd")
var checks: int = 0
var failures: int = 0
func _initialize(): call_deferred("run")
func check(value: bool, message: String):
checks += 1
if not value:
failures += 1
push_error(message)
func run():
for count in [0, 1, 7, 999999999, E.of("1e40"), E.of("1e400")]:
var carry: Dictionary = {}
for round_index in 20:
var weights: Dictionary = {"m": round_index + 1.0, "l": 0.0, "w": 9.0, "d": 4.0, "c": 137.0, "b": 2.0}
var profile: Dictionary = Civ.State.normalize_profile(weights)
var shares: Array = []
for ethic in Civ.ETHICS: shares.append(profile[ethic])
var result: Dictionary = Q.split_profile(carry, "profile", count, Civ.ETHICS, shares)
check(E.eq(Q.sum(result), count), "Profile quotas conserve arbitrary-size whole totals")
for value in result.values(): check(E.ge(value, 0) and E.eq(value, E.floor_value(value)), "Profile quotas stay nonnegative whole")
for value in carry.values(): check(is_finite(value) and value >= -1.0 and value <= 1.0, "Signed profile carries stay bounded")
check(absf(E.ratio_approx(E.of("1e400"), E.of("3e400")) - 1.0 / 3.0) < 1e-15, "Float profile ratio handles values beyond float range")
for count in [1, 101, E.of("1e40"), E.of("1e400")]:
var stocks: Dictionary = {"a": count, "b": E.add(count, 1), "c": 0}
var total: Variant = Q.sum(stocks)
for limit in [0, count, E.subtract(total, 1), total]:
var allocated: Dictionary = Q.allocate_profiled(stocks, limit)
check(E.eq(Q.sum(allocated), limit), "Profiled stock allocation retains the exact granted total")
for id in stocks: check(E.ge(allocated[id], 0) and E.le(allocated[id], stocks[id]), "Profiled stock allocation respects every owner ceiling")
for probability in [0.1, 1.0 / 3.0, 1.23456789]:
var carries: Dictionary = {}
var old_carry: Variant = 0
for iteration in 20:
var expected: Variant = E.add(E.multiply(count, probability), old_carry)
var whole: Variant = E.floor_value(expected)
old_carry = E.maximum(0, E.subtract(expected, whole))
var actual: Variant = Q.accumulate_rate(carries, "rate", count, probability)
check(E.eq(actual, whole), "Fast float carries preserve the whole result of rate accumulation")
check(E.near(carries.rate, old_carry), "Fast float carries preserve the fractional remainder")
for speed in [1, 4, 6]:
var world = load("res://world/owniverse/owniverse.gd").new()
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
Fixture.seed(world, root.get_node("Global"), true, true)
var rules: Dictionary = root.get_node("Global").CivilizationRules
var id: String = rules.keys()[0]
var modifiers: Dictionary = {"progress": 1, "degradation": 1, "war": 1, "colonization": 1}
var original: Dictionary = Civ.coefficients(world, rules, id, modifiers).duplicate(true)
modifiers.progress = 2
var changed: Dictionary = Civ.coefficients(world, rules, id, modifiers)
var row: Dictionary = rules[id].ethics.m
var p: Variant = E.maximum(0, E.multiply(E.of(row.progress), 2))
var d: Variant = E.maximum(0, E.of(row.degradation))
check(E.near(changed.m.pro, E.divide(p, E.maximum(1, E.add(p, d)))), "Coefficient cache follows modifiers")
modifiers.progress = 1
check(E.eq(Civ.coefficients(world, rules, id, modifiers), original), "Coefficient cache restores original rates")
var local_rules: Dictionary = rules.duplicate(true)
local_rules[id].ethics.m.progress = "0.375"
changed = Civ.coefficients(world, local_rules, id, modifiers)
check(E.near(changed.m.pro, E.divide(E.of("0.375"), E.maximum(1, E.add(E.of("0.375"), d)))), "Coefficient cache follows rule edits")
var target: Dictionary = {"ethics": {}}
target.amount = {}
target.binds = {}
target.stats = {"total_amount": {}}
target.amount[id] = 60
target.stats.total_amount[id] = 60
Civ.State.reconcile(world, target, rules, {id: 60})
target.ethics[id] = {"m": 1.0, "l": 0.0, "w": 0.0, "d": 0.0, "c": 0.0, "b": 0.0}
Civ.State.reconcile(world, target, rules, {id: 60})
check(E.eq(target.ethics[id].m, 1.0), "Reconcile cache detects changed ethics")
Civ.State.reconcile(world, target, rules, {id: 120})
check(E.eq(target.ethics[id].m, 1.0), "Reconcile cache detects changed population")
target.amount[id] = 120
Civ.worship(world, target, rules)
target.ethics[id].m = 0.5
target.ethics[id].l = 0.5
var expected: float = 0.0
for stage in rules:
if rules[stage].worships:
for ethic in target.ethics[stage]:
var n: float = (0.0 + target.ethics[stage][ethic]) * E.approx(E.of(rules[stage].ethics[ethic].worship))
n *= E.approx(target.amount.get(stage, 0))
expected += n
check(E.near(Civ.worship(world, target, rules), expected), "Worship cache detects changed ethics")
var worship_rules: Dictionary = rules.duplicate(true)
for stage in worship_rules:
if worship_rules[stage].worships:
worship_rules[stage].ethics.m.worship = "0.375"
expected = 0.0
for stage in worship_rules:
if worship_rules[stage].worships:
for ethic in target.ethics[stage]:
var n: float = (0.0 + target.ethics[stage][ethic]) * E.approx(E.of(worship_rules[stage].ethics[ethic].worship))
n *= E.approx(target.amount.get(stage, 0))
expected += n
check(E.near(Civ.worship(world, target, worship_rules), expected), "Worship cache detects changed rules")
world.ledger = load("res://tests/fixtures/audit_rounding_ledger.gd").new(world)
# Civilizations no longer trigger batch generations. Seed an actual
# stellar completion so the selective-rounding path is exercised.
world.state.batches.BG = [{"amount": world.state.amount.BG, "start_year": 1, "end_year": 1}]
world.speed = speed
for i in 3: world.evolute(1.0 if speed == 1 else 0.1)
check(world.ledger.checks > 0 and world.ledger.failures == 0, "Selective and full rounding agree at speed %d" % speed)
for entity in world.catalog.entities.values(): entity.free()
world.free()
print("Simulation optimizations: %d checks, %d failures" % [checks, failures])
quit(1 if failures else 0)