Add exact arithmetic implementation and tests

- Introduced exact arithmetic with support for large integers and fractions in `exact.gd`, `big_integer.gd`, and `exact_value.gd`.
- Implemented a comprehensive test suite in `test_exact_math.gd` to validate arithmetic operations against expected results.
- Added a README in `math` directory to document the exact arithmetic system and its usage.
- Ensured that all arithmetic operations maintain precision without floating-point errors, using a custom representation for large numbers.
- Included methods for encoding and decoding exact values for serialization.
This commit is contained in:
Kirill
2026-09-30 08:08:27 +03:00
parent f2111a376a
commit fb8a3f5658
58 changed files with 5139 additions and 2162 deletions
+98 -101
View File
@@ -1,4 +1,5 @@
extends SceneTree
const E = preload("res://world/owniverse/math/exact.gd")
const CivilizationState = preload("res://world/owniverse/civilization_state.gd")
const CivilizationEvolution = preload("res://world/owniverse/civilization_evolution.gd")
@@ -11,9 +12,9 @@ func _initialize() -> void:
call_deferred("run")
func check(condition: bool, message: String) -> void:
checks += 1
if not condition:
failures += 1
checks = E.add(checks, 1)
if (not condition):
failures = E.add(failures, 1)
push_error(message)
func fixture():
@@ -21,11 +22,11 @@ func fixture():
for id in root.get_node("Global").OwniverseEntities:
var entity = load("res://world/owniverse/owniverse_entity.gd").new(root.get_node("Global").OwniverseEntities[id])
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.state.amount[id] = 0
world.state.stats.total_amount[id] = 0
if E.ne(entity.group_as, ""):
world.state.amount[entity.group_as] = 0
world.state.stats.total_amount[entity.group_as] = 0
return world
func dispose(world) -> void:
@@ -35,116 +36,116 @@ func dispose(world) -> void:
func run() -> void:
var global: Node = root.get_node("Global")
var world = fixture()
world.state.amount.RD = 400.0
world.batches.append_batch(world.state, "RD", 400.0, 0.0, 0.0)
var end: float = world.state.batches.RD[0].end_year
world.environment.modify_world_parameter("Bur", 9.0)
check(is_equal_approx(world.state.batches.RD[0].end_year, end / 10.0), "Burnability accelerates existing cohorts")
var count: float = world.state.batches.RD[0].amount
world.environment.modify_world_parameter("Bur", -9.0)
check(is_equal_approx(world.state.batches.RD[0].end_year, end) and world.state.batches.RD[0].amount == count, "Changing burnability back preserves population and lifetime")
world.environment.modify_world_parameter("Bur", 9.0)
world.batches.append_batch(world.state, "YD", 10.0, 0.0, 0.0)
check(is_equal_approx(world.state.batches.YD[0].start_year, world.catalog.entities.YD.lifespan / 10.0), "New stars use the same burnability")
world.state.time = {"datetime": end / 10.0 - 0.1, "year": end / 10.0 - 0.1}
world.state.amount.RD = 400
world.batches.append_batch(world.state, "RD", 400, 0, 0)
var end: Variant = world.state.batches.RD[0].end_year
world.environment.modify_world_parameter("Bur", 9)
check(E.near(world.state.batches.RD[0].end_year, E.divide(end, 10)), "Burnability accelerates existing cohorts")
var count: Variant = world.state.batches.RD[0].amount
world.environment.modify_world_parameter("Bur", E.neg(9))
check((E.near(world.state.batches.RD[0].end_year, end) and E.eq(world.state.batches.RD[0].amount, count)), "Changing burnability back preserves population and lifetime")
world.environment.modify_world_parameter("Bur", 9)
world.batches.append_batch(world.state, "YD", 10, 0, 0)
check(E.near(world.state.batches.YD[0].start_year, E.divide(world.catalog.entities.YD.lifespan, 10)), "New stars use the same burnability")
world.state.time = {"datetime": E.subtract(E.divide(end, 10), E.of("0.1")), "year": E.subtract(E.divide(end, 10), E.of("0.1"))}
world.batches.withdraw_batches(world.state, "RD", world.state.time.year)
world.environment.modify_world_parameter("Bur", -18.0)
check(world.state.batches.RD[0].end_year >= world.state.time.year, "Slowing a partially evolved cohort keeps its remaining window in the future")
world.environment.modify_world_parameter("Exp", 9.0)
check(world.stellar.evolution_products(world.state, world.catalog.entities.SN, 2.0).blast == 180.0, "Explosion power includes world explosiveness")
var products: Dictionary = world.stellar.evolution_products(world.state, world.catalog.entities.HRG, 1000.0)
check(products.get("GRB", 0.0) == 100.0 and products.get("SN", 0.0) == 900.0, "Explosiveness raises rare-event probability without duplicating stars")
world.catalog.entities.HRG.output_or.chance = 50.0
products = world.stellar.evolution_products(world.state, world.catalog.entities.HRG, 100.0)
check(products.get("GRB", 0.0) == 100.0 and products.get("SN", 0.0) == 0.0, "Rare-event probability is bounded at one")
world.environment.modify_world_parameter("Bur", E.neg(18))
check(E.ge(world.state.batches.RD[0].end_year, world.state.time.year), "Slowing a partially evolved cohort keeps its remaining window in the future")
world.environment.modify_world_parameter("Exp", 9)
check(E.eq(world.stellar.evolution_products(world.state, world.catalog.entities.SN, 2).blast, 180), "Explosion power includes world explosiveness")
var products: Dictionary = world.stellar.evolution_products(world.state, world.catalog.entities.HRG, 1000)
check((E.eq(products.get("GRB", 0), 100) and E.eq(products.get("SN", 0), 900)), "Explosiveness raises rare-event probability without duplicating stars")
world.catalog.entities.HRG.output_or.chance = 50
products = world.stellar.evolution_products(world.state, world.catalog.entities.HRG, 100)
check((E.eq(products.get("GRB", 0), 100) and E.eq(products.get("SN", 0), 0)), "Rare-event probability is bounded at one")
dispose(world)
world = fixture()
var ordinary: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1000000.0)
var ordinary: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1000000)
world.state.life_remainders.clear()
world.environment.modify_world_parameter("Agg", 9.0)
var aggressive: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1000000.0)
check(aggressive.EON1 > ordinary.EON1, "Aggression accelerates primitive evolution")
world.environment.modify_world_parameter("Agg", 9)
var aggressive: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1000000)
check(E.gt(aggressive.EON1, ordinary.EON1), "Aggression accelerates primitive evolution")
world.state.life_remainders.clear()
world.state.world_parameters.Order = 10.0
var orderly: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1000000.0)
check(orderly.RP < aggressive.RP, "Order reduces degradation")
world.state.world_parameters.Order = 10
var orderly: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1000000)
check(E.lt(orderly.RP, aggressive.RP), "Order reduces degradation")
world.state.life_remainders.clear()
world.state.world_parameters.Enlightenment = 10.0
var enlightened: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1000000.0)
check(enlightened.EON1 > orderly.EON1, "Enlightenment increases primitive progress")
world.state.world_parameters.Enlightenment = 10
var enlightened: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1000000)
check(E.gt(enlightened.EON1, orderly.EON1), "Enlightenment increases primitive progress")
dispose(world)
world = fixture()
var hotter = fixture()
for simulation in [world, hotter]:
simulation.state.amount.RD = 1000000.0
simulation.batches.append_batch(simulation.state, "RD", 1000000.0, 0.0, 0.0)
hotter.environment.modify_world_parameter("Bur", 9.0)
world.evolute(1000.0)
hotter.evolute(1000.0)
check(hotter.state.amount.S > world.state.amount.S, "Burnability changes actual space production")
check(hotter.state.energy + hotter.state.amount.S == Parameters.INITIAL_ENERGY, "Enhanced production still spends the GI energy budget")
simulation.state.amount.RD = 1000000
simulation.batches.append_batch(simulation.state, "RD", 1000000, 0, 0)
hotter.environment.modify_world_parameter("Bur", 9)
world.evolute(1000)
hotter.evolute(1000)
check(E.gt(hotter.state.amount.S, world.state.amount.S), "Burnability changes actual space production")
check(E.eq(E.add(hotter.state.energy, hotter.state.amount.S), Parameters.INITIAL_ENERGY), "Enhanced production still spends the GI energy budget")
dispose(world)
dispose(hotter)
world = fixture()
world.state.amount["C0.2"] = 1000000.0
world.state.ethics["C0.2"] = {"m": 1000000.0}
world.state.amount["C0.2"] = 1000000
world.state.ethics["C0.2"] = {"m": 1000000}
var baseline: Dictionary = world.state.duplicate(true)
var advanced: Dictionary = baseline.duplicate(true)
advanced.world_parameters.Agg = 19.0
advanced.world_parameters.Agg = 19
var next_stage: String = global.CivilizationRules["C0.2"].next
var outcomes: Array = []
for target in [baseline, advanced]:
var expired: Dictionary = {"C0.2": [{"amount": 1000000.0, "from": 0.0, "to": 1.0}]}
var expired: Dictionary = {"C0.2": [{"amount": 1000000, "from": 0, "to": 1}]}
var budget: Dictionary = CivilizationState.stellar_budget(world, target, global.CivilizationRules)
CivilizationEvolution.prepare(world, target, expired, 1.0, budget, global.CivilizationRules)
outcomes.append(expired["C0.2"][0].products.get(next_stage, 0.0))
check(outcomes[1] > outcomes[0], "Aggression changes actual ethical civilization transitions")
CivilizationEvolution.prepare(world, target, expired, 1, budget, global.CivilizationRules)
outcomes.append(expired["C0.2"][0].products.get(next_stage, 0))
check(E.gt(outcomes[1], outcomes[0]), "Aggression changes actual ethical civilization transitions")
dispose(world)
# Trends use ethical proportions and stage weights, not population magnitude.
world = fixture()
var rules: Dictionary = {"C2.0": {"home": "DwStar"}, "C2.1": {"home": "DwStar"}}
world.state.ethics = {"C2.0": {"d": 100.0}, "C2.1": {"d": 100.0}}
world.state.ethics = {"C2.0": {"d": 100}, "C2.1": {"d": 100}}
var source: Dictionary = world.state.duplicate(true)
var single: Dictionary = source.duplicate(true)
var split: Dictionary = source.duplicate(true)
Parameters.advance(source, single, rules, 1.0e6)
for i in 10: Parameters.advance(source, split, rules, 1.0e5)
check(is_equal_approx(single.trends.Integration, 0.018), "Trend integrates elapsed years and doubled stage weights")
Parameters.advance(source, single, rules, 1000000)
for i in 10: Parameters.advance(source, split, rules, 100000)
check(E.near(single.trends.Integration, E.of("0.018")), "Trend integrates elapsed years and doubled stage weights")
for key in Parameters.TRENDS:
check(is_equal_approx(single.trends[key], split.trends[key]), "Trend is independent of frame partition: " + key)
source.ethics["C2.0"].d = 1000000.0
check(E.near(single.trends[key], split.trends[key]), E.add("Trend is independent of frame partition: ", key))
source.ethics["C2.0"].d = 1000000
var larger: Dictionary = world.state.duplicate(true)
Parameters.advance(source, larger, rules, 1.0e6)
check(is_equal_approx(larger.trends.Integration, single.trends.Integration), "Population proportions determine trend influence")
world.state.trends.Integration = 0.99
Parameters.advance(source, world.state, rules, 1.0e8)
check(world.state.civilization_stats["trend:Integration"] == 2.0, "Long steps process every crossed threshold")
check(is_equal_approx(world.state.trends.Integration, 0.79), "Threshold crossing preserves the fractional remainder")
check(world.state.world_parameters.Federation > 1.0 or world.state.world_parameters.Enlightenment > 1.0 or world.state.world_parameters.Order > 1.0, "Integration events change the world")
var restored: Dictionary = bytes_to_var(var_to_bytes(world.state))
Parameters.advance(source, world.state, rules, 1.0e8)
Parameters.advance(source, restored, rules, 1.0e8)
check(world.state == restored, "Trend events resume identically after serialization")
Parameters.advance(source, world.state, rules, 1.0e12)
check(world.state.trends.Integration >= 0.0 and world.state.trends.Integration < 1.0, "Very large intervals finish with a valid trend fraction")
check(world.state.world_parameters.Agg >= 1.0, "Repeated integration events cannot reduce aggression below the scale")
var payload: Dictionary = {"format_version": 2, "app_version": "test", "ui": {"current_page": 1}, "state": world.state}
Parameters.advance(source, larger, rules, 1000000)
check(E.near(larger.trends.Integration, single.trends.Integration), "Population proportions determine trend influence")
world.state.trends.Integration = E.of("0.99")
Parameters.advance(source, world.state, rules, 100000000)
check(E.eq(world.state.civilization_stats["trend:Integration"], 2), "Long steps process every crossed threshold")
check(E.near(world.state.trends.Integration, E.of("0.79")), "Threshold crossing preserves the fractional remainder")
check(((E.gt(world.state.world_parameters.Federation, 1) or E.gt(world.state.world_parameters.Enlightenment, 1)) or E.gt(world.state.world_parameters.Order, 1)), "Integration events change the world")
var restored: Dictionary = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
Parameters.advance(source, world.state, rules, 100000000)
Parameters.advance(source, restored, rules, 100000000)
check(E.eq(world.state, restored), "Trend events resume identically after serialization")
Parameters.advance(source, world.state, rules, E.of("1.0e12"))
check((E.ge(world.state.trends.Integration, 0) and E.lt(world.state.trends.Integration, 1)), "Very large intervals finish with a valid trend fraction")
check(E.ge(world.state.world_parameters.Agg, 1), "Repeated integration events cannot reduce aggression below the scale")
var payload: Dictionary = {"format_version": 3, "app_version": "test", "ui": {"current_page": 1}, "state": world.state}
var manager: Node = root.get_node("SaveManager")
check(manager.validate_data(payload) == "", "New parameters and trends pass save validation")
check(E.eq(manager.validate_data(payload), ""), "New parameters and trends pass save validation")
var old: Dictionary = payload.duplicate(true)
old.state.erase("world_parameters")
old.state.erase("trends")
check(manager.validate_data(old) == "", "Existing current-format saves remain readable")
payload.state.world_parameters.Agg = 20.0
check(manager.validate_data(payload) != "", "Out-of-range scales are rejected")
check(E.eq(manager.validate_data(old), ""), "Existing current-format saves remain readable")
payload.state.world_parameters.Agg = 20
check(E.ne(manager.validate_data(payload), ""), "Out-of-range scales are rejected")
dispose(world)
for sample in [[1e-7, 0.0], [1e-6, 0.5], [1e-5, 1.0], [0.9, 0.0], [0.95, 0.5], [1.0, 1.0]]:
check(is_equal_approx(Parameters.density_display(sample[0]).position, sample[1]), "Density boundary " + str(sample[0]))
for sample in [[E.of("1e-7"), 0], [E.of("1e-6"), E.of("0.5")], [E.of("1e-5"), 1], [E.of("0.9"), 0], [E.of("0.95"), E.of("0.5")], [1, 1]]:
check(E.near(Parameters.density_display(sample[0]).position, sample[1]), E.add("Density boundary ", str(sample[0])))
# The fixture overrides saving/loading; the player's save is never accessed.
var scene = load("res://world/world.tscn").instantiate()
var stage = scene.get_node("Stage")
@@ -153,40 +154,36 @@ func run() -> void:
root.add_child(scene)
await process_frame
await process_frame
stage.owniverse.state.energy = Parameters.INITIAL_ENERGY / 4.0
stage.owniverse.state.world_parameters.Bur = 19.0
stage.owniverse.state.trends.Integration = 0.75
stage.owniverse.state.energy = E.divide(Parameters.INITIAL_ENERGY, 4)
stage.owniverse.state.world_parameters.Bur = 19
stage.owniverse.state.trends.Integration = E.of("0.75")
stage.world_indicators.update(stage.owniverse)
var warning: TextureRect = scene.get_node("Background/WarningBorder")
check(warning.mouse_filter == Control.MOUSE_FILTER_IGNORE, "Warning overlay does not block input")
var environments: Array = [
[1.0e-7, "ff0000ff"], [1.0e-6, "df2020bf"], [1.0e-5, "c040407f"],
[0.1, "00000000"], [0.9, "40a0c07f"], [0.97, "20b0dfbf"], [1.0, "00c0ffff"],
[0.1, "00000000"],
]
check(E.eq(warning.mouse_filter, Control.MOUSE_FILTER_IGNORE), "Warning overlay does not block input")
var environments: Array = [[E.of("1.0e-7"), "ff0000ff"], [E.of("1.0e-6"), "df2020bf"], [E.of("1.0e-5"), "c040407f"], [E.of("0.1"), "00000000"], [E.of("0.9"), "40a0c07f"], [E.of("0.97"), "20b0dfbf"], [1, "00c0ffff"], [E.of("0.1"), "00000000"]]
for sample in environments:
check(Parameters.density_warning_color(sample[0]).is_equal_approx(Color.html(sample[1])), "Unity warning threshold: " + str(sample[0]))
stage.owniverse.state.amount.H = 1e14
check(Parameters.density_warning_color(sample[0]).is_equal_approx(Color.html(sample[1])), E.add("Unity warning threshold: ", str(sample[0])))
stage.owniverse.state.amount.H = E.of("1e14")
stage.world_indicators.update(stage.owniverse)
check(warning.self_modulate.is_equal_approx(Color.html("00c0ffff")), "Compression warning uses current universe density")
stage.owniverse.state.amount.H = 0.0
stage.owniverse.state.amount.S = 1e24
stage.owniverse.state.amount.H = 0
stage.owniverse.state.amount.S = E.of("1e24")
stage.world_indicators.update(stage.owniverse)
check(warning.self_modulate.is_equal_approx(Color.html("ff0000ff")), "Rip warning replaces compression warning")
stage.owniverse.state.amount.S = 0.0
stage.owniverse.state.amount.S = 0
stage.world_indicators.update(stage.owniverse)
check(warning.self_modulate.a == 0.0, "Warning clears when density returns to normal")
check(E.eq(warning.self_modulate.a, 0), "Warning clears when density returns to normal")
for bar in stage.world_indicators.energy_bars:
check(bar.scale.x == 0.25, "Energy fill is synchronized on both pages")
check(E.eq(bar.scale.x, E.of("0.25")), "Energy fill is synchronized on both pages")
var pointer: Control = stage.world_indicators.pointers.Bur
check(is_equal_approx(pointer.position.x + pointer.size.x / 2.0, 304.0), "Burnability pointer reaches the correct scale endpoint")
check(stage.world_indicators.trend_bars[1].scale.y == 0.75, "Trend bar displays its stored level")
check(E.near(E.add(pointer.position.x, E.divide(pointer.size.x, 2)), 304), "Burnability pointer reaches the correct scale endpoint")
check(E.eq(stage.world_indicators.trend_bars[1].scale.y, E.of("0.75")), "Trend bar displays its stored level")
for locale in ["en", "ru"]:
global.Locale = locale
for id in ["Energy", "Density", "Burnability", "Explosiveness", "Aggression", "Trends"]:
var description: String = Descriptions.render(stage.owniverse, global, id, "Indicator")
check(not description.contains("—") and not description.contains("%value"), "Resolved indicator description: " + locale + "/" + id)
check(((not description.contains("—")) and (not description.contains("%value"))), E.add(E.add(E.add("Resolved indicator description: ", locale), "/"), id))
dispose(stage.owniverse)
scene.free()
print("World indicators: %d checks, %d failures" % [checks, failures])
quit(0 if failures == 0 else 1)
print(("World indicators: %d checks, %d failures" % [checks, failures]))
quit((0 if E.eq(failures, 0) else 1))