2026-09-29 19:55:20 +03:00
|
|
|
extends SceneTree
|
2026-09-30 08:08:27 +03:00
|
|
|
const E = preload("res://world/owniverse/math/exact.gd")
|
2026-09-29 19:55:20 +03:00
|
|
|
|
|
|
|
|
const Math = preload("res://world/owniverse/quantity_math.gd")
|
2026-10-01 23:43:34 +03:00
|
|
|
const Legacy = preload("res://tests/fixtures/legacy_quantity_math.gd")
|
2026-09-29 19:55:20 +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-29 19:55:20 +03:00
|
|
|
push_error(message)
|
|
|
|
|
|
|
|
|
|
func run() -> void:
|
2026-10-01 23:43:34 +03:00
|
|
|
var rng = RandomNumberGenerator.new()
|
|
|
|
|
rng.seed = 65432
|
|
|
|
|
var old_remainders: Dictionary = {}
|
|
|
|
|
var new_remainders: Dictionary = {}
|
|
|
|
|
for sample in 300:
|
|
|
|
|
var requests: Dictionary = {}
|
|
|
|
|
var weights: Dictionary = {}
|
|
|
|
|
for id in ["w", "m", "b", "c", "d", "l"]:
|
|
|
|
|
requests[id] = E.of(str(rng.randi_range(-10, 100)) + "." + str(rng.randi_range(0, 9)))
|
|
|
|
|
weights[id] = rng.randi_range(0, 100)
|
|
|
|
|
if sample % 3 == 0:
|
|
|
|
|
for id in requests: requests[id] = E.multiply(requests[id], E.of("1e40"))
|
|
|
|
|
var limit: Variant = E.multiply(rng.randi_range(0, 600), E.of("1e40")) if sample % 3 == 0 else rng.randi_range(0, 600)
|
|
|
|
|
check(E.eq(Math.allocate_whole(requests, limit), Legacy.allocate_whole(requests, limit)), "Allocation matches legacy with limits, ties and big integers")
|
|
|
|
|
var count: Variant = limit
|
|
|
|
|
check(E.eq(Math.split(new_remainders, "population", count, weights), Legacy.split(old_remainders, "population", count, weights)), "Split matches legacy rounding")
|
|
|
|
|
check(E.eq(new_remainders, old_remainders), "Signed remainder debt matches legacy")
|
|
|
|
|
check_ledger_cache()
|
2026-09-29 19:55:20 +03:00
|
|
|
var Catalog = load("res://world/owniverse/entity_catalog.gd")
|
2026-09-30 08:08:27 +03:00
|
|
|
check(E.eq(Math.allocate_whole({}, 10), {}), "Empty allocation")
|
|
|
|
|
check(E.eq(Math.allocate_whole({"b": 1, "a": 1}, 1), {"a": 1, "b": 0}), "Stable lexical tie")
|
2026-09-29 19:55:20 +03:00
|
|
|
for limit in range(30):
|
2026-09-30 08:08:27 +03:00
|
|
|
var requests: Dictionary = {"a": E.of("3.9"), "b": E.of("9.1"), "c": E.neg(2), "d": 7}
|
2026-09-29 19:55:20 +03:00
|
|
|
var result: Dictionary = Math.allocate_whole(requests, limit)
|
2026-09-30 08:08:27 +03:00
|
|
|
check(E.eq(Math.sum(result), E.minimum(limit, 19)), "Conserve bounded allocation")
|
2026-09-29 19:55:20 +03:00
|
|
|
for id in result:
|
2026-09-30 08:08:27 +03:00
|
|
|
check(((E.eq(result[id], E.floor_value(result[id])) and E.ge(result[id], 0)) and E.le(result[id], E.floor_value(E.maximum(0, requests[id])))), "Whole bounded branch")
|
|
|
|
|
check(E.eq(Math.normalize_distribution({"a": E.neg(1), "b": 0}), {}), "Nonpositive weights")
|
|
|
|
|
check(E.eq(Math.normalize_distribution({"a": 2, "b": 6}), {"a": E.of("0.25"), "b": E.of("0.75")}), "Normalize weights")
|
|
|
|
|
check(E.eq(Math.production_step([E.of("0.25"), E.of("0.1"), 2]), 20), "Common whole production step")
|
|
|
|
|
check(E.eq(Math.production_step([]), 1), "Free production step")
|
2026-09-29 19:55:20 +03:00
|
|
|
var target: Dictionary = {}
|
2026-09-30 08:08:27 +03:00
|
|
|
var produced: Variant = 0
|
2026-09-29 19:55:20 +03:00
|
|
|
for i in 10:
|
2026-09-30 08:08:27 +03:00
|
|
|
produced = E.add(produced, Math.accumulate_whole(target, "remainders", "a", E.of("0.1")))
|
|
|
|
|
check(E.eq(produced, 1), "Fractional production carries")
|
2026-09-29 19:55:20 +03:00
|
|
|
var remainders: Dictionary = {}
|
2026-09-30 08:08:27 +03:00
|
|
|
var totals: Dictionary = {"a": 0, "b": 0, "c": 0}
|
2026-09-29 19:55:20 +03:00
|
|
|
for i in 30:
|
2026-09-30 08:08:27 +03:00
|
|
|
var result: Dictionary = Math.split(remainders, "split", 1, {"a": 1, "b": 1, "c": 1})
|
|
|
|
|
check(E.eq(Math.sum(result), 1), "Split conserves count")
|
|
|
|
|
for id in result: totals[id] = E.add(totals[id], result[id])
|
|
|
|
|
check(E.eq(totals, {"a": 10, "b": 10, "c": 10}), "Rounding debt distributes over time")
|
|
|
|
|
produced = 0
|
|
|
|
|
for i in 10: produced = E.add(produced, Math.accumulate_rate(remainders, "rate", 1, E.of("0.1")))
|
|
|
|
|
check(E.eq(produced, 1), "Rate carries")
|
2026-09-29 19:55:20 +03:00
|
|
|
var catalog = Catalog.new()
|
|
|
|
|
var rows: Dictionary = root.get_node("Global").OwniverseEntities
|
|
|
|
|
catalog.load_definitions(rows)
|
2026-09-30 08:08:27 +03:00
|
|
|
check(E.eq(catalog.entities.size(), rows.size()), "Catalog loads all definitions")
|
2026-09-29 19:55:20 +03:00
|
|
|
for id in catalog.entities:
|
|
|
|
|
var entity = catalog.entities[id]
|
2026-09-30 08:08:27 +03:00
|
|
|
check(E.eq(catalog.automatic_producers.has(entity), (E.ne(entity.product_per_year_type, ""))), "Producer index")
|
|
|
|
|
check(E.eq(catalog.unlockable_entities.has(entity), ((not entity.unlock.is_empty()))), "Unlock index")
|
|
|
|
|
if E.ne(entity.group_as, ""):
|
|
|
|
|
check((catalog.groups[entity.group_as].has(id) and catalog.members(entity.group_as).has(id)), "Group index")
|
|
|
|
|
if E.ne(entity.event_group, ""):
|
2026-09-29 19:55:20 +03:00
|
|
|
check(catalog.event_groups[entity.event_group].has(id), "Event index")
|
|
|
|
|
var members: Array = catalog.members("Star")
|
|
|
|
|
members.clear()
|
2026-09-30 08:08:27 +03:00
|
|
|
check((not catalog.members("Star").is_empty()), "Group results are independent")
|
2026-09-29 19:55:20 +03:00
|
|
|
check(Catalog.new().entities.is_empty(), "Catalog instances are independent")
|
|
|
|
|
for entity in catalog.entities.values(): entity.free()
|
2026-09-30 08:08:27 +03:00
|
|
|
print(("Quantity and catalog: %d checks, %d failures" % [checks, failures]))
|
|
|
|
|
quit((1 if failures else 0))
|
2026-10-01 23:43:34 +03:00
|
|
|
|
|
|
|
|
func check_ledger_cache() -> void:
|
|
|
|
|
var world = load("res://world/owniverse/owniverse.gd").new()
|
|
|
|
|
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
|
|
|
|
|
world.state.amount.RD = E.of("1e40")
|
|
|
|
|
world.state.binds.GEM = {"RD": 7}
|
|
|
|
|
check(E.eq(world.ledger.physical_amounts(world.state).RD, E.add(E.of("1e40"), 7)), "Initial cached physical amount")
|
|
|
|
|
var returned: Dictionary = world.ledger.physical_amounts(world.state)
|
|
|
|
|
returned.RD = 0
|
|
|
|
|
check(E.gt(world.ledger.physical_amounts(world.state).RD, 0), "Caller cannot mutate cached physical totals")
|
|
|
|
|
world.state.binds.GEM.RD = 11
|
|
|
|
|
check(E.eq(world.ledger.physical_amounts(world.state).RD, E.add(E.of("1e40"), 11)), "Nested binding change invalidates physical cache")
|
|
|
|
|
world.state.amount.RD = 2
|
|
|
|
|
check(E.eq(world.ledger.physical_amounts(world.state).RD, 13), "Direct count change invalidates physical cache")
|
|
|
|
|
var other: Dictionary = world.state.duplicate(true)
|
|
|
|
|
other.binds.clear()
|
|
|
|
|
check(E.eq(world.ledger.physical_amounts(other).RD, 2), "Alternate state has independent physical totals")
|
|
|
|
|
check(E.eq(world.ledger.physical_amounts(world.state).RD, 13), "Returning to original state refreshes cache")
|
|
|
|
|
var revision: int = world.catalog.index_revision
|
|
|
|
|
world.catalog.rebuild_group_index()
|
|
|
|
|
check(world.catalog.index_revision > revision, "Catalog rebuild invalidates cached membership")
|
|
|
|
|
check(E.eq(world.ledger.physical_amounts(world.state).RD, 13), "Rebuild retains correct physical totals")
|
|
|
|
|
var rules: Dictionary = root.get_node("Global").CivilizationRules.duplicate(true)
|
|
|
|
|
var original_stages: int = world.catalog.civilization_layout(rules).stages.size()
|
|
|
|
|
rules["C2.0"].home = ""
|
|
|
|
|
world.catalog.invalidate_civilization_layout()
|
|
|
|
|
check(world.catalog.civilization_layout(rules).stages.size() == original_stages - 1, "Explicit rules change rebuilds civilization layout")
|
|
|
|
|
for entity in world.catalog.entities.values(): entity.free()
|
|
|
|
|
world.free()
|