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
+10
View File
@@ -1,5 +1,15 @@
# Проверки поэтапного переноса из Owniverse.U
## Точная арифметика
`godot --headless --path Owniverse.GI --script res://tests/test_exact_math.gd`
Независимый эталон Python Fraction, границы int64 и 10^27, точная сериализация,
рецепты с дробными стоимостями, сохранение сумм, время и 100 циклов с меняющимися
долями. Все остальные тесты используют числовое сравнение и сериализацию через
`E`, а не сравнение ссылок на большие числа. Границы точности и новый формат
сохранения описаны в [документации арифметики](../world/owniverse/math/README.md).
## Модули симуляции
`godot --headless --path Owniverse.GI --script res://tests/test_module_lifecycle.gd`
File diff suppressed because one or more lines are too long
+22 -21
View File
@@ -1,4 +1,5 @@
extends SceneTree
const E = preload("res://world/owniverse/math/exact.gd")
var failures: int = 0
var checks: int = 0
@@ -7,41 +8,41 @@ func _initialize() -> void:
call_deferred("run")
func check(value: bool, message: String) -> void:
checks += 1
if not value:
failures += 1
checks = E.add(checks, 1)
if (not value):
failures = E.add(failures, 1)
push_error(message)
func run() -> void:
var actions = load("res://ui/actions/actions.tscn").instantiate()
root.add_child(actions)
for maximum in [0.0, 0.1, 0.99, 1.0, 1.9, 10.0, 100.0, 123.0, 1000.0, 10000.0, 1e9, 1.1e9, 1e150]:
for maximum in [0, E.of("0.1"), E.of("0.99"), 1, E.of("1.9"), 10, 100, 123, 1000, 10000, E.of("1e9"), E.of("1.1e9"), E.of("1e150")]:
actions.set_actions("test", maximum)
var labels: Array = []
var values: Array = []
for i in 5:
var value: float = actions.action_values[i]
var value: Variant = actions.action_values[i]
var text: String = actions.action_labels[i].text
check(value == floorf(value) and value <= floorf(maximum), "Whole affordable request")
if value > 0.0:
check(not values.has(value) and not labels.has(text), "Unique requests and displayed labels")
check((E.eq(value, E.floor_value(value)) and E.le(value, E.floor_value(maximum))), "Whole affordable request")
if E.gt(value, 0):
check(((not values.has(value)) and (not labels.has(text))), "Unique requests and displayed labels")
values.append(value)
labels.append(text)
else:
check(text == "" and actions.action_labels[i].get_parent().get_node("TextureButton").disabled, "Unused button blank and disabled")
if maximum >= 1.0: check(values.back() == floorf(maximum), "Maximum retained")
actions.set_actions("test", 100.0)
check(actions.action_values.values() == [1.0, 10.0, 100.0, 0.0, 0.0], "Exact power of ten not duplicated")
actions.set_actions("SMBH", 25.0, func(_request): return 25.0)
check(actions.action_values.values() == [25.0, 0.0, 0.0, 0.0, 0.0], "Identical SMBH previews collapse to one button")
actions.set_actions("step", 123.0, func(request): return floorf(request / 5.0) * 5.0)
check(actions.action_values.values() == [10.0, 100.0, 123.0, 0.0, 0.0], "Unproducible small requests omitted")
check(actions.action_labels[2].text == "120", "Recipe preview displayed while original request retained")
check((E.eq(text, "") and actions.action_labels[i].get_parent().get_node("TextureButton").disabled), "Unused button blank and disabled")
if E.ge(maximum, 1): check(E.eq(values.back(), E.floor_value(maximum)), "Maximum retained")
actions.set_actions("test", 100)
check(E.eq(actions.action_values.values(), [1, 10, 100, 0, 0]), "Exact power of ten not duplicated")
actions.set_actions("SMBH", 25, func ( _request ) : return 25)
check(E.eq(actions.action_values.values(), [25, 0, 0, 0, 0]), "Identical SMBH previews collapse to one button")
actions.set_actions("step", 123, func ( request ) : return E.multiply(E.floor_value(E.divide(request, 5)), 5))
check(E.eq(actions.action_values.values(), [10, 100, 123, 0, 0]), "Unproducible small requests omitted")
check(E.eq(actions.action_labels[2].text, "120"), "Recipe preview displayed while original request retained")
var emitted: Array = []
actions.action_pressed.connect(func(_page, amount): emitted.append(amount))
actions.action_pressed.connect(func ( _page , amount ) : emitted.append(amount))
actions._on_action_button_pressed(2)
actions._on_action_button_pressed(4)
check(emitted == [123.0], "Only active button emits its matching request")
check(E.eq(emitted, [123]), "Only active button emits its matching request")
actions.queue_free()
print("Action values: %d checks, %d failures" % [checks, failures])
quit(0 if failures == 0 else 1)
print(("Action values: %d checks, %d failures" % [checks, failures]))
quit((0 if E.eq(failures, 0) else 1))
+44 -39
View File
@@ -1,4 +1,5 @@
extends SceneTree
const E = preload("res://world/owniverse/math/exact.gd")
var failures: int = 0
var checks: int = 0
@@ -8,89 +9,93 @@ func _initialize() -> void:
call_deferred("run")
func check(value: bool, message: String) -> void:
checks += 1
if not value:
failures += 1
checks = E.add(checks, 1)
if (not value):
failures = E.add(failures, 1)
push_error(message)
func run() -> void:
var manager = load("res://autoload/save_manager.gd").new()
var directory: String = "/tmp/owniverse-async-%s-%s" % [OS.get_process_id(), Time.get_ticks_usec()]
var directory: String = ("/tmp/owniverse-async-%s-%s" % [OS.get_process_id(), Time.get_ticks_usec()])
DirAccess.make_dir_recursive_absolute(directory)
manager.save_directory = directory
root.add_child(manager)
manager.save_completed.connect(func(filename, success, error): completions.append([filename, success, error]))
manager.save_completed.connect(func ( filename , success , error ) : completions.append([filename, success, error]))
var world = load("res://world/owniverse/owniverse.gd").new()
world.state.energy = E.subtract(world.state.energy, 1)
world.state.time = preload("res://world/owniverse/simulation_time.gd").advance({"datetime": E.of("1e27")}, 0.000000001, 1)
var data: Dictionary = {"state": world.state, "ui": {"current_page": 1}}
var initial_energy: float = world.state.energy
var initial_energy: Variant = world.state.energy
var global: Node = root.get_node("Global")
var old_plaintext: bool = global.save_plaintext_copy
global.save_plaintext_copy = true
check(manager.request_save("snapshot", "test-password", data), "Snapshot queued")
data.state.energy = 99.0
data.state.energy = 99
data.ui.current_page = 2
var frames: int = 0
var deadline: int = Time.get_ticks_msec() + 10000
while manager.is_saving() and Time.get_ticks_msec() < deadline:
frames += 1
var deadline: int = E.add(Time.get_ticks_msec(), 10000)
while (manager.is_saving() and E.lt(Time.get_ticks_msec(), deadline)):
frames = E.add(frames, 1)
await process_frame
check(not manager.is_saving() and frames > 0, "Frames continue while save is pending")
check(((not manager.is_saving()) and E.gt(frames, 0)), "Frames continue while save is pending")
var saved: Dictionary = manager.load_data("snapshot", "test-password")
check(saved.state.energy == initial_energy and saved.ui.current_page == 1, "Worker owns a deep snapshot")
check((E.eq(saved.state.energy, initial_energy) and E.eq(saved.ui.current_page, 1)), "Worker owns a deep snapshot")
check(E.eq(saved.state.time, world.state.time), "Encrypted save preserves exact ticks at huge age")
var plain: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(directory.path_join("snapshot.adventure.json")))
check(plain.state.energy == initial_energy, "Diagnostic JSON uses same snapshot")
check(completions.size() == 1 and completions[0][1], "Completion reports successful write")
check(E.eq(E.decode(plain.state.energy), initial_energy), "Diagnostic JSON uses same snapshot")
check(E.eq(E.decode(plain.state.time), saved.state.time), "Diagnostic JSON preserves fractional time")
check((E.eq(completions.size(), 1) and completions[0][1]), "Completion reports successful write")
completions.clear()
data.state.energy = 1.0
data.state.energy = 1
manager.request_save("latest", "test-password", data)
data.state.energy = 2.0
data.state.energy = 2
manager.request_save("latest", "test-password", data)
data.state.energy = 3.0
data.state.energy = 3
manager.request_save("latest", "test-password", data)
data.state.energy = 4.0
data.state.energy = 4
manager.flush_saves()
check(completions.size() == 2, "Repeated requests coalesce behind active write")
check(manager.load_data("latest", "test-password").state.energy == 3.0, "Last queued snapshot wins")
check(E.eq(completions.size(), 2), "Repeated requests coalesce behind active write")
check(E.eq(manager.load_data("latest", "test-password").state.energy, 3), "Last queued snapshot wins")
var incompatible_path: String = directory.path_join("incompatible.adventure")
var incompatible = FileAccess.open_encrypted_with_pass(incompatible_path, FileAccess.WRITE, "test-password")
incompatible.store_var({"format_version": -1, "state": {"unknown": true}}, false)
incompatible.store_var({"format_version": E.neg(1), "state": {"unknown": true}}, false)
incompatible.close()
var original: PackedByteArray = FileAccess.get_file_as_bytes(incompatible_path)
var previous_archive = FileAccess.open(incompatible_path + ".incompatible.bak", FileAccess.WRITE)
var previous_archive = FileAccess.open(E.add(incompatible_path, ".incompatible.bak"), FileAccess.WRITE)
previous_archive.store_string("keep previous backup")
previous_archive.close()
manager.blocked_saves.incompatible = true
check(manager.load_data("incompatible", "test-password").is_empty() and manager.last_error == "", "Unsupported format permits a fresh session")
check(not manager.blocked_saves.has("incompatible") and not FileAccess.file_exists(incompatible_path), "Incompatible file no longer blocks saving")
check(FileAccess.get_file_as_bytes(incompatible_path + ".incompatible.bak.1") == original, "Unsupported save archived without losing bytes")
check(FileAccess.get_file_as_string(incompatible_path + ".incompatible.bak") == "keep previous backup", "Earlier archive preserved")
check((manager.load_data("incompatible", "test-password").is_empty() and E.eq(manager.last_error, "")), "Unsupported format permits a fresh session")
check(((not manager.blocked_saves.has("incompatible")) and (not FileAccess.file_exists(incompatible_path))), "Incompatible file no longer blocks saving")
check(E.eq(FileAccess.get_file_as_bytes(E.add(incompatible_path, ".incompatible.bak.1")), original), "Unsupported save archived without losing bytes")
check(E.eq(FileAccess.get_file_as_string(E.add(incompatible_path, ".incompatible.bak")), "keep previous backup"), "Earlier archive preserved")
check(manager.request_save("incompatible", "test-password", data), "New session can queue a save")
manager.flush_saves()
check(manager.load_data("incompatible", "test-password").state == data.state, "New format saves and reloads after incompatible file")
check(E.eq(manager.load_data("incompatible", "test-password").state, data.state), "New format saves and reloads after incompatible file")
# Expected failures below must leave the last good encrypted file intact.
data.state.energy = -1.0
data.state.energy = E.neg(1)
manager.request_save("latest", "test-password", data)
manager.flush_saves()
check(not completions.back()[1] and manager.last_error.contains("Invalid save"), "Worker validation error reaches main thread")
check(manager.load_data("latest", "test-password").state.energy == 3.0, "Invalid snapshot preserves previous save")
check(((not completions.back()[1]) and manager.last_error.contains("Invalid save")), "Worker validation error reaches main thread")
check(E.eq(manager.load_data("latest", "test-password").state.energy, 3), "Invalid snapshot preserves previous save")
manager.blocked_saves.latest = true
check(not manager.request_save("latest", "test-password", data), "Unreadable save protection also applies to queue")
check((not manager.request_save("latest", "test-password", data)), "Unreadable save protection also applies to queue")
# A valid snapshot exercises an actual I/O failure, not validation.
data.state.energy = 5.0
data.state.energy = 5
manager.request_save("missing/failure", "test-password", data)
manager.flush_saves()
check(manager.last_error.contains("Cannot write"), "I/O error reported without hanging queue")
manager.request_save("shutdown", "test-password", data)
data.state.energy = 6.0
data.state.energy = 6
manager.request_save("shutdown", "test-password", data)
manager.free()
var reader = load("res://autoload/save_manager.gd").new()
reader.save_directory = directory
check(reader.load_data("shutdown", "test-password").state.energy == 6.0, "Teardown drains final pending snapshot")
check(E.eq(reader.load_data("shutdown", "test-password").state.energy, 6), "Teardown drains final pending snapshot")
reader.free()
world.free()
# Exercise the stage entry point too: manager tests alone do not catch a
@@ -100,12 +105,12 @@ func run() -> void:
shared_manager.save_directory = directory
var stage = load("res://stage/stage.gd").new()
stage.pending_page = 2
stage.owniverse.state.energy = 42.0
stage.owniverse.state.energy = 42
stage.save_data()
check(shared_manager.is_saving(), "Stage queues a background save instead of waiting for disk")
stage.owniverse.state.energy = 43.0
stage.owniverse.state.energy = 43
shared_manager.flush_saves()
check(shared_manager.load_data("owniverse", "qwerty").state.energy == 42.0, "Stage save writes an independent snapshot")
check(E.eq(shared_manager.load_data("owniverse", "qwerty").state.energy, 42), "Stage save writes an independent snapshot")
stage.owniverse.free()
stage.free()
shared_manager.save_directory = original_directory
@@ -113,5 +118,5 @@ func run() -> void:
for filename in DirAccess.get_files_at(directory):
DirAccess.remove_absolute(directory.path_join(filename))
DirAccess.remove_absolute(directory)
print("Async save: %d checks, %d failures (validation/block/I/O errors above are intentional)" % [checks, failures])
quit(0 if failures == 0 else 1)
print(("Async save: %d checks, %d failures (validation/block/I/O errors above are intentional)" % [checks, failures]))
quit((0 if E.eq(failures, 0) else 1))
+27 -26
View File
@@ -1,4 +1,5 @@
extends SceneTree
const E = preload("res://world/owniverse/math/exact.gd")
const Clock = preload("res://world/owniverse/simulation_time.gd")
var checks: int = 0
var failures: int = 0
@@ -7,9 +8,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():
@@ -18,16 +19,16 @@ func fixture():
for id in global.OwniverseEntities:
var entity = load("res://world/owniverse/owniverse_entity.gd").new(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
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
world.catalog.entities["N"].product_per_year_amount = 10
world.catalog.entities["N"].auto = 0
world.state.amount.N = 1
return world
func dispose(world) -> void:
@@ -37,21 +38,21 @@ func dispose(world) -> void:
func run() -> void:
var world = fixture()
world.state.batch_origins.LGRB = 10357034.80000000074505806
var boundary: float = 41215289.79999999701976776
for interval in [[boundary - 2.5, boundary + 2.25], [boundary, boundary + 2.0]]:
world.state.batch_origins.LGRB = E.of("10357034.80000000074505806")
var boundary: Variant = E.of("41215289.79999999701976776")
for interval in [[E.subtract(boundary, E.of("2.5")), E.add(boundary, E.of("2.25"))], [boundary, E.add(boundary, 2)]]:
world.state.batches.LGRB = []
world.batches.append_batch(world.state, "LGRB", 100.0, interval[0], interval[1])
var total: float = 0.0
var last: float = -INF
for batch: Dictionary in world.state.batches.LGRB:
total += batch.amount
check(batch.created_from > last, "Batch windows advance monotonically")
check(batch.created_to > batch.created_from, "Positive window duration")
check(batch.amount >= 0.0, "No negative batch amounts")
world.batches.append_batch(world.state, "LGRB", 100, interval[0], interval[1])
var total: Variant = 0
var last: Variant = E.neg(INF)
for batch : Dictionary in world.state.batches.LGRB:
total = E.add(total, batch.amount)
check(E.gt(batch.created_from, last), "Batch windows advance monotonically")
check(E.gt(batch.created_to, batch.created_from), "Positive window duration")
check(E.ge(batch.amount, 0), "No negative batch amounts")
last = batch.created_from
check(total == 100.0, "Boundary correction preserves all objects")
check(world.state.batches.LGRB.size() <= 7, "Finite number of windows")
check(E.eq(total, 100), "Boundary correction preserves all objects")
check(E.le(world.state.batches.LGRB.size(), 7), "Finite number of windows")
dispose(world)
print("Batch boundaries: %d checks, %d failures" % [checks, failures])
quit(0 if failures == 0 else 1)
print(("Batch boundaries: %d checks, %d failures" % [checks, failures]))
quit((0 if E.eq(failures, 0) else 1))
+82 -81
View File
@@ -1,4 +1,5 @@
extends SceneTree
const E = preload("res://world/owniverse/math/exact.gd")
const Clock = preload("res://world/owniverse/simulation_time.gd")
var checks: int = 0
var failures: int = 0
@@ -7,9 +8,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():
@@ -18,16 +19,16 @@ func fixture():
for id in global.OwniverseEntities:
var entity = load("res://world/owniverse/owniverse_entity.gd").new(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
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
world.catalog.entities["N"].product_per_year_amount = 10
world.catalog.entities["N"].auto = 0
world.state.amount.N = 1
return world
func dispose(world) -> void:
@@ -35,130 +36,130 @@ func dispose(world) -> void:
entity.free()
world.free()
func seed_population(world, id: String, count: float, time: float = 1.0) -> void:
world.state.amount.N = 0.0
func seed_population(world, id: String, count: Variant, time: Variant = 1.0) -> void:
world.state.amount.N = 0
world.state.amount[id] = count
world.state.batches[id] = [{"amount": count, "start_year": time, "end_year": time}]
world.evolution.initialize_life_batches()
func sum_ethics(world, id: String) -> float:
var result: float = 0.0
for n in world.state.ethics.get(id, {}).values(): result += n
func sum_ethics(world, id: String) -> Variant:
var result: Variant = 0
for n in world.state.ethics.get(id, {}).values(): result = E.add(result, n)
return result
func invariants(world) -> void:
var physical: Dictionary = world.ledger.physical_amounts(world.state)
for id in world.state.ethics:
check(sum_ethics(world, id) == physical.get(id, 0.0), "Ethics equal physical population for " + id)
check(E.eq(sum_ethics(world, id), physical.get(id, 0)), E.add("Ethics equal physical population for ", id))
for n in world.state.ethics[id].values():
if n < 0.0 or n != floorf(n): check(false, "Invalid ethic amount")
check(root.get_node("SaveManager").validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Civilization state validates")
if (E.lt(n, 0) or E.ne(n, E.floor_value(n))): check(false, "Invalid ethic amount")
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 3, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Civilization state validates")
func run() -> void:
var global: Node = root.get_node("Global")
var baseline: Dictionary = global.CivilizationRules.duplicate(true)
var world = fixture()
seed_population(world, "C0.2", 60000.0)
world.evolution.evolve_batches(world.state, 1.0)
check(world.state.amount["C0.3"] > 0.0, "C0.2 actually progresses")
check(world.state.amount["C0.1"] > 0.0, "C0.2 actually degrades")
check(world.state.civilization_stats.get("wars", 0.0) == 0.0, "Table disables early wars")
check(world.state.amount["C0.2"] + world.state.amount["C0.3"] + world.state.amount["C0.1"] == 60000.0, "Ordinary evolution conserves population")
check(world.state.stats.total_amount["C0.2"] == 0.0, "Survivors do not count as new civilizations")
check(world.state.civilization_stats.worship > 0.0, "Worship derived from surviving ethics")
seed_population(world, "C0.2", 60000)
world.evolution.evolve_batches(world.state, 1)
check(E.gt(world.state.amount["C0.3"], 0), "C0.2 actually progresses")
check(E.gt(world.state.amount["C0.1"], 0), "C0.2 actually degrades")
check(E.eq(world.state.civilization_stats.get("wars", 0), 0), "Table disables early wars")
check(E.eq(E.add(E.add(world.state.amount["C0.2"], world.state.amount["C0.3"]), world.state.amount["C0.1"]), 60000), "Ordinary evolution conserves population")
check(E.eq(world.state.stats.total_amount["C0.2"], 0), "Survivors do not count as new civilizations")
check(E.gt(world.state.civilization_stats.worship, 0), "Worship derived from surviving ethics")
invariants(world)
var restored = fixture()
restored.state = bytes_to_var(var_to_bytes(world.state))
world.evolution.evolve_batches(world.state, 2100.0)
restored.evolution.evolve_batches(restored.state, 2100.0)
check(world.state == restored.state, "Save resumes deterministic ethical evolution")
restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
world.evolution.evolve_batches(world.state, 2100)
restored.evolution.evolve_batches(restored.state, 2100)
check(E.eq(world.state, restored.state), "Save resumes deterministic ethical evolution")
invariants(world)
dispose(restored)
dispose(world)
world = fixture()
world.state.amount.RD = 1e8
seed_population(world, "C2.0", 60000.0)
world.state.amount["C1.9"] = 10000.0
world.state.amount.RD = 100000000
seed_population(world, "C2.0", 60000)
world.state.amount["C1.9"] = 10000
world.evolution.initialize_life_batches()
world.evolution.evolve_batches(world.state, 1.0)
check(world.state.civilization_stats.wars > 0.0, "Wars execute when enabled")
check(world.state.civilization_stats.collateral > 0.0, "Wars affect previous-stage populations")
check(world.state.amount["C2.1"] > 0.0, "Winners and peaceful progress reach next stage")
world.evolution.evolve_batches(world.state, 1)
check(E.gt(world.state.civilization_stats.wars, 0), "Wars execute when enabled")
check(E.gt(world.state.civilization_stats.collateral, 0), "Wars affect previous-stage populations")
check(E.gt(world.state.amount["C2.1"], 0), "Winners and peaceful progress reach next stage")
invariants(world)
dispose(world)
world = fixture()
world.state.amount.RD = 1.0
world.state.binds.RD = {"HP": 5.0, "GG": 5.0, "AB": 5.0, "RP": 5.0}
seed_population(world, "C1.6", 60000.0)
for row: Dictionary in global.CivilizationRules["C1.6"].ethics.values(): row.degradation = "0"
world.evolution.evolve_batches(world.state, 1.0)
world.state.amount.RD = 1
world.state.binds.RD = {"HP": 5, "GG": 5, "AB": 5, "RP": 5}
seed_population(world, "C1.6", 60000)
for row : Dictionary in global.CivilizationRules["C1.6"].ethics.values(): row.degradation = "0"
world.evolution.evolve_batches(world.state, 1)
var after: Dictionary = world.ledger.physical_amounts(world.state)
check(world.state.civilization_stats.colonies == 20.0, "Colonization limited to real targets")
check(after.HP + after.GG + after.AB + after.RP == 0.0, "Colonized objects consumed once")
check(world.state.binds.RD.get("C1.4", 0.0) == 20.0, "Colonies retain target host and use two-stages-earlier level")
var totals: float = 0.0
check(E.eq(world.state.civilization_stats.colonies, 20), "Colonization limited to real targets")
check(E.eq(E.add(E.add(E.add(after.HP, after.GG), after.AB), after.RP), 0), "Colonized objects consumed once")
check(E.eq(world.state.binds.RD.get("C1.4", 0), 20), "Colonies retain target host and use two-stages-earlier level")
var totals: Variant = 0
for id in after:
if world.catalog.entities[id].group_as == "Planet": totals += after[id]
check(totals == 60020.0, "Colonization replaces planets, does not create extra worlds")
if E.eq(world.catalog.entities[id].group_as, "Planet"): totals = E.add(totals, after[id])
check(E.eq(totals, 60020), "Colonization replaces planets, does not create extra worlds")
invariants(world)
dispose(world)
global.CivilizationRules = baseline.duplicate(true)
world = fixture()
world.state.amount.RD = 1e25
world.state.amount.BG = 1e25
world.state.amount.TWD = 1e25
world.state.amount.SNS = 1e25
world.state.amount.MGT = 1e25
world.state.amount.BH25 = 1e25
world.state.amount.SMBH = 1e26
world.state.amount.RD = E.of("1e25")
world.state.amount.BG = E.of("1e25")
world.state.amount.TWD = E.of("1e25")
world.state.amount.SNS = E.of("1e25")
world.state.amount.MGT = E.of("1e25")
world.state.amount.BH25 = E.of("1e25")
world.state.amount.SMBH = E.of("1e26")
for id in world.catalog.entities:
if world.catalog.entities[id].claim != "": world.state.amount[id] = 1e12
if E.ne(world.catalog.entities[id].claim, ""): world.state.amount[id] = E.of("1e12")
# Accelerated deterministic fixture traverses every enabled stage, including Ct.
for id in global.CivilizationRules:
if id == "Ct": continue
if E.eq(id, "Ct"): continue
global.CivilizationRules[id].fights = false
global.CivilizationRules[id].colonizes = false
global.CivilizationRules[id].diplomacy = false
for row: Dictionary in global.CivilizationRules[id].ethics.values():
for row : Dictionary in global.CivilizationRules[id].ethics.values():
row.progress = "1"
row.degradation = "0"
world.catalog.entities[id].lifespan = 1.0
world.catalog.entities[id].lifespan_delta = 1.0
global.CivilizationRules[id].span = 1.0
seed_population(world, "C0.2", 6.0)
world.evolution.evolve_batches(world.state, 1000.0)
check(world.state.amount.Ct == 6.0, "Complete C0.2 -> C4.4 -> Ct chain runs")
check(not world.state.batches.has("Ct"), "Transcendence is terminal")
world.catalog.entities[id].lifespan = 1
world.catalog.entities[id].lifespan_delta = 1
global.CivilizationRules[id].span = 1
seed_population(world, "C0.2", 6)
world.evolution.evolve_batches(world.state, 1000)
check(E.eq(world.state.amount.Ct, 6), "Complete C0.2 -> C4.4 -> Ct chain runs")
check((not world.state.batches.has("Ct")), "Transcendence is terminal")
invariants(world)
dispose(world)
global.CivilizationRules = baseline
world = fixture()
world.state.amount.RD = 10000.0
world.state.amount.SCS = 1.0
world.state.amount["C2.4"] = 1.0
world.state.amount.RD = 10000
world.state.amount.SCS = 1
world.state.amount["C2.4"] = 1
world.evolution.initialize_life_batches()
var span: float = world.state.batches["C2.4"][0].start_year
check(is_equal_approx(span, 20000.0 / 3.6), "Structure bonus shortens newly registered lifespan")
var span: Variant = world.state.batches["C2.4"][0].start_year
check(E.near(span, E.divide(20000, E.of("3.6"))), "Structure bonus shortens newly registered lifespan")
dispose(world)
var evolution = load("res://world/owniverse/civilization_evolution.gd")
var balanced: Dictionary = {"m": 10000.0, "l": 10000.0, "w": 10000.0, "d": 10000.0, "c": 10000.0, "b": 10000.0}
var balanced: Dictionary = {"m": 10000, "l": 10000, "w": 10000, "d": 10000, "c": 10000, "b": 10000}
var neutral: Dictionary = evolution.drift({}, "test", balanced, global.CivilizationRules["C2.0"].ethics)
var diplomatic: Dictionary = evolution.drift({}, "test", balanced, global.CivilizationRules["C2.0"].ethics, true)
check(neutral == balanced, "Ordinary drift preserves balanced ethics")
check(diplomatic.d > diplomatic.w, "Diplomacy uses per-ethic influence")
check(evolution.total(diplomatic) == 60000.0, "Diplomacy conserves population")
check(E.eq(neutral, balanced), "Ordinary drift preserves balanced ethics")
check(E.gt(diplomatic.d, diplomatic.w), "Diplomacy uses per-ethic influence")
check(E.eq(evolution.total(diplomatic), 60000), "Diplomacy conserves population")
world = fixture()
world.state.amount.RD = 1e15
world.state.amount.BG = 1e15
world.state.amount["C3.0"] = 6.0
world.state.amount.RD = E.of("1e15")
world.state.amount.BG = E.of("1e15")
world.state.amount["C3.0"] = 6
world.evolution.initialize_life_batches()
check(world.state.amount["C3.0"] == 0.0 and world.state.amount["C2.9"] == 6.0, "Missing mandatory structures degrade existing civilizations")
check((E.eq(world.state.amount["C3.0"], 0) and E.eq(world.state.amount["C2.9"], 6)), "Missing mandatory structures degrade existing civilizations")
invariants(world)
dispose(world)
print("Civilization evolution: %d checks, %d failures" % [checks, failures])
quit(0 if failures == 0 else 1)
print(("Civilization evolution: %d checks, %d failures" % [checks, failures]))
quit((0 if E.eq(failures, 0) else 1))
+69 -68
View File
@@ -1,4 +1,5 @@
extends SceneTree
const E = preload("res://world/owniverse/math/exact.gd")
const Clock = preload("res://world/owniverse/simulation_time.gd")
var checks: int = 0
var failures: int = 0
@@ -7,9 +8,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():
@@ -18,16 +19,16 @@ func fixture():
for id in global.OwniverseEntities:
var entity = load("res://world/owniverse/owniverse_entity.gd").new(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
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
world.catalog.entities["N"].product_per_year_amount = 10
world.catalog.entities["N"].auto = 0
world.state.amount.N = 1
return world
func dispose(world) -> void:
@@ -38,97 +39,97 @@ func dispose(world) -> void:
func run() -> void:
var world = fixture()
var global: Node = root.get_node("Global")
check(global.CivilizationRules.size() == 44, "Load all ethical civilization stages")
for row: Dictionary in global.CivilizationRules.values():
check(row.ethics.size() == 6 or row.span == 0.0, "Every ethical stage has six rules")
world.state.amount.RD = 12.0
world.state.amount["C0.2"] = 8.0
world.state.binds.RD = {"C2.0": 12.0, "C1.9": 3.0}
check(E.eq(global.CivilizationRules.size(), 44), "Load all ethical civilization stages")
for row : Dictionary in global.CivilizationRules.values():
check((E.eq(row.ethics.size(), 6) or E.eq(row.span, 0)), "Every ethical stage has six rules")
world.state.amount.RD = 12
world.state.amount["C0.2"] = 8
world.state.binds.RD = {"C2.0": 12, "C1.9": 3}
world.evolution.initialize_life_batches()
check(world.state.amount["C2.0"] == 12.0 and world.state.binds.RD["C2.0"] == 0.0, "Old space civilizations detach without loss")
check(world.state.binds.RD["C1.9"] == 3.0, "Planetary civilizations retain their host")
check(world.state.ethics["C0.2"] == {"m": 2.0, "l": 2.0, "w": 1.0, "d": 1.0, "c": 1.0, "b": 1.0}, "Migration uses equal ethics and deterministic remainder")
check(world.state.ethics["C2.0"].values().all(func(n): return n == 2.0), "Bound civilization ethics count physical population")
check((E.eq(world.state.amount["C2.0"], 12) and E.eq(world.state.binds.RD["C2.0"], 0)), "Old space civilizations detach without loss")
check(E.eq(world.state.binds.RD["C1.9"], 3), "Planetary civilizations retain their host")
check(E.eq(world.state.ethics["C0.2"], {"m": 2, "l": 2, "w": 1, "d": 1, "c": 1, "b": 1}), "Migration uses equal ethics and deterministic remainder")
check(world.state.ethics["C2.0"].values().all(func ( n ) : return E.eq(n, 2)), "Bound civilization ethics count physical population")
var before: Dictionary = world.state.duplicate(true)
world.evolution.initialize_life_batches()
check(world.state == before, "Repeated initialization preserves ethics")
world.state.ethics["C0.2"] = {"m": 8.0, "l": 0.0, "w": 0.0, "d": 0.0, "c": 0.0, "b": 0.0}
check(E.eq(world.state, before), "Repeated initialization preserves ethics")
world.state.ethics["C0.2"] = {"m": 8, "l": 0, "w": 0, "d": 0, "c": 0, "b": 0}
world.evolution.initialize_life_batches()
check(world.state.ethics["C0.2"].m == 8.0, "Existing unequal distribution is retained")
world.state.amount["C0.2"] = 3.0
check(E.eq(world.state.ethics["C0.2"].m, 8), "Existing unequal distribution is retained")
world.state.amount["C0.2"] = 3
world.evolution.initialize_life_batches()
check(world.state.ethics["C0.2"].m == 3.0, "Losses reduce ethics without inventing population")
check(root.get_node("SaveManager").validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Ethics state validates")
check(E.eq(world.state.ethics["C0.2"].m, 3), "Losses reduce ethics without inventing population")
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 3, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Ethics state validates")
var restored = fixture()
restored.state = bytes_to_var(var_to_bytes(world.state))
restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
restored.evolution.initialize_life_batches()
check(restored.state == world.state, "Save roundtrip preserves ethics and ownership")
check(E.eq(restored.state, world.state), "Save roundtrip preserves ethics and ownership")
dispose(restored)
dispose(world)
world = fixture()
world.state.amount.RD = 10.0
for row: Dictionary in global.CivilizationRules["C1.9"].ethics.values():
world.state.amount.RD = 10
for row : Dictionary in global.CivilizationRules["C1.9"].ethics.values():
row.progress = "1"
row.degradation = "0"
row.colonization = "0"
world.state.binds.RD = {"C1.9": 10.0}
world.state.batches["C1.9"] = [{"amount": 10.0, "start_year": 1.0, "end_year": 1.0}]
world.evolution.evolve_batches(world.state, 1.0)
check(world.state.amount["C2.0"] == 10.0 and world.state.binds.RD.get("C2.0", 0.0) == 0.0, "Space successor does not inherit planetary binding")
world.state.binds.RD = {"C1.9": 10}
world.state.batches["C1.9"] = [{"amount": 10, "start_year": 1, "end_year": 1}]
world.evolution.evolve_batches(world.state, 1)
check((E.eq(world.state.amount["C2.0"], 10) and E.eq(world.state.binds.RD.get("C2.0", 0), 0)), "Space successor does not inherit planetary binding")
dispose(world)
var capacity = load("res://world/owniverse/civilization_state.gd")
world = fixture()
world.state.amount.RD = 10.0
world.state.amount["C2.0"] = 8.0
world.state.amount.RD = 10
world.state.amount["C2.0"] = 8
var budget: Dictionary = capacity.stellar_budget(world, world.state, global.CivilizationRules)
check(capacity.admit_transition(budget, global.CivilizationRules, "C1.9", "C2.0", 5.0) == 2.0, "Growth consumes only remaining stellar capacity")
check(capacity.admit_transition(budget, global.CivilizationRules, "C1.9", "C2.0", 5.0) == 0.0, "A reservation cannot be spent twice")
world.state.amount["C2.0"] = 1.0
check(E.eq(capacity.admit_transition(budget, global.CivilizationRules, "C1.9", "C2.0", 5), 2), "Growth consumes only remaining stellar capacity")
check(E.eq(capacity.admit_transition(budget, global.CivilizationRules, "C1.9", "C2.0", 5), 0), "A reservation cannot be spent twice")
world.state.amount["C2.0"] = 1
budget = capacity.stellar_budget(world, world.state, global.CivilizationRules)
check(capacity.admit_transition(budget, global.CivilizationRules, "C2.0", "C2.1", 1.0) == 1.0, "A transition reuses the source reservation")
check(budget.free.DwStar == 0.0, "C2.1 reserves ten dwarf stars")
world.state.amount["C2.0"] = 0.0
world.state.binds.SC = {"RD": 10.0}
check(E.eq(capacity.admit_transition(budget, global.CivilizationRules, "C2.0", "C2.1", 1), 1), "A transition reuses the source reservation")
check(E.eq(budget.free.DwStar, 0), "C2.1 reserves ten dwarf stars")
world.state.amount["C2.0"] = 0
world.state.binds.SC = {"RD": 10}
budget = capacity.stellar_budget(world, world.state, global.CivilizationRules)
check(budget.capacity.DwStar == 20.0, "Stars inside structures are counted once")
check(E.eq(budget.capacity.DwStar, 20), "Stars inside structures are counted once")
dispose(world)
world = fixture()
world.state.amount.RD = 12.0
world.state.amount["C2.1"] = 2.0
world.state.amount["C2.0"] = 5.0
world.state.amount.RD = 12
world.state.amount["C2.1"] = 2
world.state.amount["C2.0"] = 5
world.evolution.initialize_life_batches()
check(world.state.amount["C2.1"] == 1.0 and world.state.amount["C2.0"] == 2.0, "Shared budget supports advanced civilizations first")
check(world.state.binds.RD["C1.9"] == 4.0, "Excess cascades to supported planetary stage")
check((E.eq(world.state.amount["C2.1"], 1) and E.eq(world.state.amount["C2.0"], 2)), "Shared budget supports advanced civilizations first")
check(E.eq(world.state.binds.RD["C1.9"], 4), "Excess cascades to supported planetary stage")
var snapshot: Dictionary = world.state.duplicate(true)
world.evolution.initialize_life_batches()
check(world.state == snapshot, "Capacity enforcement is idempotent")
world.state.amount.RD = 0.0
check(E.eq(world.state, snapshot), "Capacity enforcement is idempotent")
world.state.amount.RD = 0
world.evolution.initialize_life_batches()
check(world.state.amount["C2.1"] == 0.0 and world.state.amount["C2.0"] == 0.0, "Vanished stars force all unsupported space civilizations down")
check(world.ledger.physical_amounts(world.state)["C1.9"] == 7.0, "Forced degradation conserves civilizations")
check((E.eq(world.state.amount["C2.1"], 0) and E.eq(world.state.amount["C2.0"], 0)), "Vanished stars force all unsupported space civilizations down")
check(E.eq(world.ledger.physical_amounts(world.state)["C1.9"], 7), "Forced degradation conserves civilizations")
dispose(world)
world = fixture()
world.state.amount.LGW = 20.0
world.state.amount.SMBH = 2.0e22
world.state.amount["C4.2"] = 3.0
world.state.amount.LGW = 20
world.state.amount.SMBH = E.of("2.0e22")
world.state.amount["C4.2"] = 3
world.evolution.initialize_life_batches()
check(world.state.amount["C4.2"] == 2.0, "SMBH capacity uses stellar mass equivalent")
check(world.ledger.physical_amounts(world.state)["C1.9"] == 1.0, "Unsupported civilization degrades through missing habitat families")
check(root.get_node("SaveManager").validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Capacity-limited state validates")
check(E.eq(world.state.amount["C4.2"], 2), "SMBH capacity uses stellar mass equivalent")
check(E.eq(world.ledger.physical_amounts(world.state)["C1.9"], 1), "Unsupported civilization degrades through missing habitat families")
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 3, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Capacity-limited state validates")
dispose(world)
world = fixture()
world.state.amount.RD = 2.0
for row: Dictionary in global.CivilizationRules["C1.9"].ethics.values():
world.state.amount.RD = 2
for row : Dictionary in global.CivilizationRules["C1.9"].ethics.values():
row.progress = "1"
row.degradation = "0"
row.colonization = "0"
world.state.binds.RD = {"C1.9": 5.0}
world.state.batches["C1.9"] = [{"amount": 5.0, "start_year": 1.0, "end_year": 1.0}]
world.evolution.evolve_batches(world.state, 1.0)
check(world.state.amount["C2.0"] == 2.0 and world.state.binds.RD["C1.9"] == 3.0, "Unfunded transition retains the original planetary civilization")
world.state.binds.RD = {"C1.9": 5}
world.state.batches["C1.9"] = [{"amount": 5, "start_year": 1, "end_year": 1}]
world.evolution.evolve_batches(world.state, 1)
check((E.eq(world.state.amount["C2.0"], 2) and E.eq(world.state.binds.RD["C1.9"], 3)), "Unfunded transition retains the original planetary civilization")
dispose(world)
print("Civilization state: %d checks, %d failures" % [checks, failures])
quit(0 if failures == 0 else 1)
print(("Civilization state: %d checks, %d failures" % [checks, failures]))
quit((0 if E.eq(failures, 0) else 1))
+39 -38
View File
@@ -1,4 +1,5 @@
extends SceneTree
const E = preload("res://world/owniverse/math/exact.gd")
const Clock = preload("res://world/owniverse/simulation_time.gd")
var checks: int = 0
var failures: int = 0
@@ -7,9 +8,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():
@@ -18,16 +19,16 @@ func fixture():
for id in global.OwniverseEntities:
var entity = load("res://world/owniverse/owniverse_entity.gd").new(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
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
world.catalog.entities["N"].product_per_year_amount = 10
world.catalog.entities["N"].auto = 0
world.state.amount.N = 1
return world
func dispose(world) -> void:
@@ -39,39 +40,39 @@ func run() -> void:
var global: Node = root.get_node("Global")
var description = load("res://world/owniverse/item_description.gd")
var world = fixture()
world.state.amount.H = 8.0
world.state.amount.He = 2.0
world.state.amount.H = 8
world.state.amount.He = 2
for locale in ["en", "ru"]:
global.Locale = locale
var text: String = description.render(world, global, "RD")
check(text.contains("[b]") and not text.contains("%value"), "Localized template resolved: " + locale)
check(text.contains("#40cc40"), "Affordable cost green: " + locale)
check(text.contains("4.0000E+10"), "Lifespan substituted: " + locale)
check((text.contains("[b]") and (not text.contains("%value"))), E.add("Localized template resolved: ", locale))
check(text.contains("#40cc40"), E.add("Affordable cost green: ", locale))
check(text.contains("4.0000E+10"), E.add("Lifespan substituted: ", locale))
text = description.render(world, global, "He")
check(text.contains("He: 20.00% / H: 80.00%") and not text.contains("%value"), "Helium metadata and mixture: " + locale)
check((text.contains("He: 20.00% / H: 80.00%") and (not text.contains("%value"))), E.add("Helium metadata and mixture: ", locale))
text = description.render(world, global, "Shovelin", "Force")
check(text.count("]—[/color]") == 4, "Missing force levels use dash: " + locale)
check(E.eq(text.count("]—[/color]"), 4), E.add("Missing force levels use dash: ", locale))
text = description.render(world, global, "YD")
check(text.contains("#ff6060"), "Insufficient cost red: " + locale)
check(text.contains("#ff6060"), E.add("Insufficient cost red: ", locale))
var reordered: Dictionary = {"text": "%value0% / %value1%", "value0": "lifespan", "value1": "amount"}
check(global.render_template(reordered, {"amount": "12", "lifespan": "99"}) == "99 / 12", "Metadata chooses variables, not fixed argument positions")
world.state.amount.RD = 2.0
world.state.binds.G = {"RD": 3.0, "HP": 7.0}
var values: Dictionary = description.variables(world, global, "RD", -2.0)
check(values.amount.contains("2") and values.amount.contains("(5)") and values.amount.contains("#888888"), "Free and total counts with gray binding total")
check(values.amount.contains("#ff6060") and values.amount.contains("−2.0"), "Real-time decrease red")
check(E.eq(global.render_template(reordered, {"amount": "12", "lifespan": "99"}), "99 / 12"), "Metadata chooses variables, not fixed argument positions")
world.state.amount.RD = 2
world.state.binds.G = {"RD": 3, "HP": 7}
var values: Dictionary = description.variables(world, global, "RD", E.neg(2))
check(((values.amount.contains("2") and values.amount.contains("(5)")) and values.amount.contains("#888888")), "Free and total counts with gray binding total")
check((values.amount.contains("#ff6060") and values.amount.contains("−2.0")), "Real-time decrease red")
values = description.variables(world, global, "HP")
check(values.amount == "7", "Planet description includes bound planets")
world.state.amount.EON1 = 2.0
world.state.binds.G.EON2 = 3.0
check(E.eq(values.amount, "7"), "Planet description includes bound planets")
world.state.amount.EON1 = 2
world.state.binds.G.EON2 = 3
var saved_state: Dictionary = world.state.duplicate(true)
var counters: Dictionary = world.presentation.amounts(world.state, world.catalog)
check(counters.HP == 7.0 and counters.LP == 5.0 and counters.Planet == 12.0, "Planet counters include bound objects and aggregate eons")
check(counters.RD == 2.0, "Star counters retain available quantity")
check(world.state == saved_state, "Reading counters does not mutate simulation")
check(world.presentation.amounts(world.state, world.catalog, true).HP == 0.0, "Produced counters remain independent of current population")
values = description.variables(world, global, "RD", 2.0)
check(values.amount.contains("#40cc40") and values.amount.contains("+2.0"), "Increase green")
check(((E.eq(counters.HP, 7) and E.eq(counters.LP, 5)) and E.eq(counters.Planet, 12)), "Planet counters include bound objects and aggregate eons")
check(E.eq(counters.RD, 2), "Star counters retain available quantity")
check(E.eq(world.state, saved_state), "Reading counters does not mutate simulation")
check(E.eq(world.presentation.amounts(world.state, world.catalog, true).HP, 0), "Produced counters remain independent of current population")
values = description.variables(world, global, "RD", 2)
check((values.amount.contains("#40cc40") and values.amount.contains("+2.0")), "Increase green")
var stage_script = load("res://stage/stage.gd")
check(stage_script.can_instantiate(), "Stage integration compiles")
var stage = stage_script.new()
@@ -81,9 +82,9 @@ func run() -> void:
stage.g_item_descriptions = [label]
stage.description_targets[0] = {"id": "RD", "category": "Entity"}
stage._refresh_descriptions()
check(label.bbcode_enabled and label.text.contains("Красный карлик"), "Stage displays localized rich text")
check((label.bbcode_enabled and label.text.contains("Красный карлик")), "Stage displays localized rich text")
stage._update_descriptions(0.25)
world.state.amount.RD += 1.0
world.state.amount.RD = E.add(world.state.amount.RD, 1)
stage._update_descriptions(0.25)
check(label.text.contains("+4.0"), "Selected description refreshes with rate per real second")
global.Locale = "en"
@@ -92,5 +93,5 @@ func run() -> void:
label.free()
stage.free()
dispose(world)
print("Descriptions: %d checks, %d failures" % [checks, failures])
quit(0 if failures == 0 else 1)
print(("Descriptions: %d checks, %d failures" % [checks, failures]))
quit((0 if E.eq(failures, 0) else 1))
+76
View File
@@ -0,0 +1,76 @@
extends SceneTree
const E = preload("res://world/owniverse/math/exact.gd")
const Q = preload("res://world/owniverse/quantity_math.gd")
const Clock = preload("res://world/owniverse/simulation_time.gd")
var checks: int = 0
var failures: int = 0
func _initialize():
call_deferred("run")
func check(actual, expected: String, label: String):
checks += 1
if str(actual) != expected:
failures += 1
push_error(label + ": " + str(actual) + " != " + expected)
func run():
# Expected strings were generated independently with Python fractions.Fraction.
var cases = JSON.parse_string(FileAccess.get_file_as_string("res://tests/fixtures/exact_oracle.json"))
for row in cases:
var a = E.of(row[0])
var b = E.of(row[1])
check(E.add(a, b), row[2], "sum")
check(E.subtract(a, b), row[3], "difference")
check(E.multiply(a, b), row[4], "product")
check(E.divide(a, b), row[5], "quotient")
check(E.floor_value(E.divide(a, b)), row[6], "floor")
for text in ["9223372036854775807", "-9223372036854775808", "9007199254740993", "1000000000000000000000000001", "-1/10000000000000"]:
var value = E.of(text)
check(E.decode(JSON.parse_string(JSON.stringify(E.encode(value)))), text, "JSON round trip")
check(E.add(E.of("9223372036854775807"), 1), "9223372036854775808", "int64 overflow promoted")
check(E.subtract(E.of("-9223372036854775808"), 1), "-9223372036854775809", "int64 underflow promoted")
check(Q.production_step([E.of("1e27")]), "1", "large integral recipe")
check(Q.production_step([E.of("1e-13")]), "10000000000000", "tiny fractional recipe")
var count = E.of("1000000000000000000000000001")
var allocation = Q.allocate_whole({"a": count, "b": count, "c": count}, count)
check(Q.sum(allocation), str(count), "allocation conserves every unit")
check(allocation.a, "333333333333333333333333334", "deterministic remainder")
var clock = Clock.advance({"datetime": E.of("1e27")}, 0.000000001, 1)
check(E.subtract(clock.datetime, E.of("1e27")), "1/1000000000", "single tick at huge age")
var world = load("res://world/owniverse/owniverse.gd").new()
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
for id in world.catalog.entities:
world.state.amount[id] = 0
world.state.stats.total_amount[id] = 0
var group: String = world.catalog.entities[id].group_as
if group != "":
world.state.amount[group] = 0
world.state.stats.total_amount[group] = 0
world.production.produce_hs("H", 1)
check(world.state.energy, "999999999999999999999999999", "spend one energy")
var life = world.life.life_products(world.state, world.catalog.entities.EON5, count)
check(Q.sum(life), str(count), "life conservation at 1e27")
var precise = E.divide(count, E.of("100000000000000000000000000000000000003"))
var bounded = E.bounded(precise)
check(E.lt(E.absolute(E.subtract(precise, bounded)), E.of("1e-36")), "true", "bounded fractional error")
check(E.bounded(count), str(count), "integer counts never quantized")
world.state.amount.S = E.of("1e24")
world.state.amount.H = E.of("1e24")
world.state.amount.RD = 1234567
world.state.amount.YD = 765432
world.state.amount.STN = 654321
world.speed = 6
var started: int = Time.get_ticks_msec()
for i in 100:
world.state.amount.RD = E.add(world.state.amount.RD, i + 1)
world.evolute(0.1)
check(str(world.state.auto_star_deficits).length() < 500, "true", "fraction storage stays bounded")
print("100 changing-composition cycles: %d ms" % (Time.get_ticks_msec() - started))
var saves = root.get_node("SaveManager")
check(saves.validate_data({"format_version": 3, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), "", "long-running state saves")
for entity in world.catalog.entities.values(): entity.free()
world.free()
print("Exact math: %d checks, %d failures" % [checks, failures])
quit(0 if failures == 0 else 1)
+1
View File
@@ -0,0 +1 @@
uid://wxhre4pgx1lb
+20 -19
View File
@@ -1,4 +1,5 @@
extends SceneTree
const E = preload("res://world/owniverse/math/exact.gd")
const Clock = preload("res://world/owniverse/simulation_time.gd")
var checks: int = 0
var failures: int = 0
@@ -7,9 +8,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():
@@ -18,16 +19,16 @@ func fixture():
for id in global.OwniverseEntities:
var entity = load("res://world/owniverse/owniverse_entity.gd").new(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
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
world.catalog.entities["N"].product_per_year_amount = 10
world.catalog.entities["N"].auto = 0
world.state.amount.N = 1
return world
func dispose(world) -> void:
@@ -40,19 +41,19 @@ func run() -> void:
for id in world.catalog.entities:
var expected: Array = []
for key in world.catalog.entities:
if world.catalog.entities[key].group_as == id:
if E.eq(world.catalog.entities[key].group_as, id):
expected.append(key)
check(world.catalog.is_group(id) == (not expected.is_empty()), "Group classification: " + id)
check(world.catalog.members(id) == (expected if not expected.is_empty() else [id]), "Group members: " + id)
check(E.eq(world.catalog.is_group(id), ((not expected.is_empty()))), E.add("Group classification: ", id))
check(E.eq(world.catalog.members(id), ((expected if (not expected.is_empty()) else [id]))), E.add("Group members: ", id))
var members: Array = world.catalog.members("Star")
members.clear()
check(not world.catalog.members("Star").is_empty(), "Returned members are independent")
check((not world.catalog.members("Star").is_empty()), "Returned members are independent")
world.catalog.entities.N.group_as = "TestGroup"
world.catalog.rebuild_group_index()
check(world.catalog.members("TestGroup") == ["N"], "Explicit definition reload")
check(E.eq(world.catalog.members("TestGroup"), ["N"]), "Explicit definition reload")
world.catalog.entities.N.free()
world.catalog.entities.erase("N")
check(not world.catalog.is_group("TestGroup"), "Removed definition invalidates index")
print("Group index: %d checks, %d failures" % [checks, failures])
check((not world.catalog.is_group("TestGroup")), "Removed definition invalidates index")
print(("Group index: %d checks, %d failures" % [checks, failures]))
dispose(world)
quit(1 if failures else 0)
quit((1 if failures else 0))
+65 -64
View File
@@ -1,4 +1,5 @@
extends SceneTree
const E = preload("res://world/owniverse/math/exact.gd")
const Clock = preload("res://world/owniverse/simulation_time.gd")
var checks: int = 0
var failures: int = 0
@@ -7,9 +8,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():
@@ -18,16 +19,16 @@ func fixture():
for id in global.OwniverseEntities:
var entity = load("res://world/owniverse/owniverse_entity.gd").new(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
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
world.catalog.entities["N"].product_per_year_amount = 10
world.catalog.entities["N"].auto = 0
world.state.amount.N = 1
return world
func dispose(world) -> void:
@@ -37,87 +38,87 @@ func dispose(world) -> void:
func run() -> void:
var world = fixture()
world.state.amount.N = 0.0
world.state.amount.HP = 40000.0
world.state.amount.N = 0
world.state.amount.HP = 40000
world.evolution.initialize_life_batches()
check(world.state.batches.HP.size() == 1, "Old HP receives life cohort")
check(world.state.batches.HP[0].start_year == 5e6, "HP uses source span")
check(E.eq(world.state.batches.HP.size(), 1), "Old HP receives life cohort")
check(E.eq(world.state.batches.HP[0].start_year, 5000000), "HP uses source span")
var before: Dictionary = world.state.duplicate(true)
world.evolution.initialize_life_batches()
check(world.state == before, "Initialization is idempotent")
world.evolution.evolve_batches(world.state, 5e6 + 5e6 / 40.0)
check(world.state.amount.EON1 == 10.0, "HP abiogenesis follows source probability")
check(world.state.amount.RP == 100.0, "HP degradation produces RP")
check(world.state.amount.HP == 39890.0, "Unchanged planets retry, not disappear")
check(world.state.stats.total_amount.HP == 0.0, "Retry does not inflate creation statistics")
check(world.state.batches.HP.size() > 0, "Survivors get next attempt")
check(E.eq(world.state, before), "Initialization is idempotent")
world.evolution.evolve_batches(world.state, E.add(5000000, E.divide(5000000, 40)))
check(E.eq(world.state.amount.EON1, 10), "HP abiogenesis follows source probability")
check(E.eq(world.state.amount.RP, 100), "HP degradation produces RP")
check(E.eq(world.state.amount.HP, 39890), "Unchanged planets retry, not disappear")
check(E.eq(world.state.stats.total_amount.HP, 0), "Retry does not inflate creation statistics")
check(E.gt(world.state.batches.HP.size(), 0), "Survivors get next attempt")
var physical: Dictionary = world.ledger.physical_amounts(world.state)
check(physical.HP + physical.EON1 + physical.RP == 40000.0, "Life conserves planets")
check(root.get_node("SaveManager").validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Life save validates")
check(E.eq(E.add(E.add(physical.HP, physical.EON1), physical.RP), 40000), "Life conserves planets")
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 3, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Life save validates")
var restored = fixture()
restored.state = bytes_to_var(var_to_bytes(world.state))
world.evolution.evolve_batches(world.state, 11e6)
restored.evolution.evolve_batches(restored.state, 11e6)
check(world.state == restored.state, "Life continuation preserves cohorts and carries")
restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
world.evolution.evolve_batches(world.state, 11000000)
restored.evolution.evolve_batches(restored.state, 11000000)
check(E.eq(world.state, restored.state), "Life continuation preserves cohorts and carries")
dispose(restored)
dispose(world)
world = fixture()
world.state.amount.RD = 1.0
world.state.binds.RD = {"EON5": 40000.0}
world.state.amount.RD = 1
world.state.binds.RD = {"EON5": 40000}
world.evolution.initialize_life_batches()
world.evolution.evolve_batches(world.state, 5e5 + 5e5 / 40.0)
check(world.state.binds.RD["C0.0"] == 10.0, "Intelligence emerges at the same host star")
check(world.state.amount["C0.0"] == 0.0, "Life transition keeps planet bound")
world.evolution.evolve_batches(world.state, E.add(500000, E.divide(500000, 40)))
check(E.eq(world.state.binds.RD["C0.0"], 10), "Intelligence emerges at the same host star")
check(E.eq(world.state.amount["C0.0"], 0), "Life transition keeps planet bound")
check(world.state.batches.has("C0.0"), "New C0.0 starts civilization evolution")
dispose(world)
world = fixture()
var progress: float = 0.0
var degraded: float = 0.0
var progress: Variant = 0
var degraded: Variant = 0
var valid_outcomes: bool = true
for i in 4001:
var products: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1.0)
var total: float = 0.0
var products: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1)
var total: Variant = 0
for value in products.values():
total += value
valid_outcomes = valid_outcomes and value >= 0.0 and value == floorf(value)
valid_outcomes = valid_outcomes and total == 1.0
progress += products.get("EON1", 0.0)
degraded += products.get("RP", 0.0)
total = E.add(total, value)
valid_outcomes = ((valid_outcomes and E.ge(value, 0)) and E.eq(value, E.floor_value(value)))
valid_outcomes = (valid_outcomes and E.eq(total, 1))
progress = E.add(progress, products.get("EON1", 0))
degraded = E.add(degraded, products.get("RP", 0))
check(valid_outcomes, "Every single-planet attempt has whole outcomes and conserves population")
check(progress == 1.0 and degraded == 10.0, "Rare outcomes accumulate deterministically")
check((E.eq(progress, 1) and E.eq(degraded, 10)), "Rare outcomes accumulate deterministically")
dispose(world)
world = fixture()
world.state.time = {"datetime": 123.4, "year": 123.4}
world.state.binds.RD = {"HP": 2.0}
world.state.time = {"datetime": E.of("123.4"), "year": E.of("123.4")}
world.state.binds.RD = {"HP": 2}
world.evolution.initialize_life_batches()
check(world.state.batches.HP[0].created_from == 123.4, "Old bound worlds begin life at load time")
world.stellar.destroy_planets(world.state, "RD", 1.0, 1)
world.evolution.evolve_batches(world.state, 1e7)
check(world.ledger.physical_amounts(world.state).HP == 0.0 and world.state.binds.RD.RP == 2.0, "Destroyed life does not resurrect from cohort")
check(E.eq(world.state.batches.HP[0].created_from, E.of("123.4")), "Old bound worlds begin life at load time")
world.stellar.destroy_planets(world.state, "RD", 1, 1)
world.evolution.evolve_batches(world.state, 10000000)
check((E.eq(world.ledger.physical_amounts(world.state).HP, 0) and E.eq(world.state.binds.RD.RP, 2)), "Destroyed life does not resurrect from cohort")
dispose(world)
world = fixture()
world.state.time = {"datetime": 123.4, "year": 123.4}
world.state.amount["C0.0"] = 40000.0
world.state.time = {"datetime": E.of("123.4"), "year": E.of("123.4")}
world.state.amount["C0.0"] = 40000
world.evolution.initialize_life_batches()
check(world.state.batches["C0.0"][0].created_from == 123.4, "Civilization cohorts begin at the current time")
check(E.eq(world.state.batches["C0.0"][0].created_from, E.of("123.4")), "Civilization cohorts begin at the current time")
before = world.state.duplicate(true)
world.evolution.initialize_life_batches()
check(world.state == before, "Life batch initialization is idempotent")
world.evolution.evolve_batches(world.state, 2173.4)
check(world.state.amount["C0.1"] == 700.0 and world.state.amount.EON5 == 1000.0, "C0.0 progresses and degrades by Unity rates")
check(world.state.amount["C0.0"] == 38300.0, "C0.0 remainder stays alive")
check(E.eq(world.state, before), "Life batch initialization is idempotent")
world.evolution.evolve_batches(world.state, E.of("2173.4"))
check((E.eq(world.state.amount["C0.1"], 700) and E.eq(world.state.amount.EON5, 1000)), "C0.0 progresses and degrades by Unity rates")
check(E.eq(world.state.amount["C0.0"], 38300), "C0.0 remainder stays alive")
dispose(world)
world = fixture()
world.state.amount.RD = 1.0
world.state.binds.RD = {"C0.1": 40000.0}
world.state.amount.RD = 1
world.state.binds.RD = {"C0.1": 40000}
world.evolution.initialize_life_batches()
world.evolution.evolve_batches(world.state, 2050.0)
check(world.state.binds.RD["C0.2"] == 700.0, "C0.1 reaches ethical civilization without losing host")
check(world.state.binds.RD["C0.0"] == 1000.0, "C0.1 degradation follows Unity")
check(root.get_node("SaveManager").validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Extended life save validates")
world.evolution.evolve_batches(world.state, 2050)
check(E.eq(world.state.binds.RD["C0.2"], 700), "C0.1 reaches ethical civilization without losing host")
check(E.eq(world.state.binds.RD["C0.0"], 1000), "C0.1 degradation follows Unity")
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 3, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Extended life save validates")
dispose(world)
print("Life: %d checks, %d failures" % [checks, failures])
quit(0 if failures == 0 else 1)
print(("Life: %d checks, %d failures" % [checks, failures]))
quit((0 if E.eq(failures, 0) else 1))
+28 -27
View File
@@ -1,4 +1,5 @@
extends SceneTree
const E = preload("res://world/owniverse/math/exact.gd")
var checks: int = 0
var failures: int = 0
@@ -7,9 +8,9 @@ func _initialize() -> void:
call_deferred("run")
func check(value: bool, message: String) -> void:
checks += 1
if not value:
failures += 1
checks = E.add(checks, 1)
if (not value):
failures = E.add(failures, 1)
push_error(message)
func run() -> void:
@@ -21,48 +22,48 @@ func run() -> void:
await process_frame
await process_frame
var world = stage.owniverse
world.state.binds.RD = {"EON1": 10.0, "EON2": 4.0, "C0.0": 3.0}
world.state.amount.EON1 = 2.0
world.state.stats.total_amount.EON1 = 12.0
world.state.stats.total_amount.EON2 = 4.0
world.state.stats.total_amount["C0.0"] = 3.0
world.state.binds.RD = {"EON1": 10, "EON2": 4, "C0.0": 3}
world.state.amount.EON1 = 2
world.state.stats.total_amount.EON1 = 12
world.state.stats.total_amount.EON2 = 4
world.state.stats.total_amount["C0.0"] = 3
var before: Dictionary = world.state.duplicate(true)
stage._refresh_universe_ui()
var counts: Dictionary = world.presentation.amounts(world.state, world.catalog)
check(counts.EON1 == 12.0 and counts.EON2 == 4.0 and counts.LP == 16.0, "Individual eons and their total include bound planets")
check(counts["C0.0"] == 3.0 and counts.C0 == 3.0 and counts.Planet == 19.0, "Civilization detail does not double the planetary total")
check(world.presentation.amounts(world.state, world.catalog, true).EON1 == 12.0 and world.presentation.amounts(world.state, world.catalog, true).LP == 16.0, "Historical counts retain individual and aggregate values")
check(world.state == before, "Presentation does not modify simulation state")
check(((E.eq(counts.EON1, 12) and E.eq(counts.EON2, 4)) and E.eq(counts.LP, 16)), "Individual eons and their total include bound planets")
check(((E.eq(counts["C0.0"], 3) and E.eq(counts.C0, 3)) and E.eq(counts.Planet, 19)), "Civilization detail does not double the planetary total")
check((E.eq(world.presentation.amounts(world.state, world.catalog, true).EON1, 12) and E.eq(world.presentation.amounts(world.state, world.catalog, true).LP, 16)), "Historical counts retain individual and aggregate values")
check(E.eq(world.state, before), "Presentation does not modify simulation state")
var pending: Array = ["EON1", "EON2", "EON3", "C0.0"]
var pending_before: Array = pending.duplicate()
var decision: Dictionary = world.presentation.unlocks(world.state, world.catalog, pending)
check(decision.remaining == ["EON3"], "Only unmet requirements remain pending")
check(E.eq(decision.remaining, ["EON3"]), "Only unmet requirements remain pending")
for id in ["EON1", "EON2", "LP", "C0.0", "C0"]:
check(decision.visible.has(id), "Unlock projection includes detail and alias: " + id)
check(pending == pending_before and world.state == before, "Unlock evaluation leaves inputs untouched")
check(decision.visible.has(id), E.add("Unlock projection includes detail and alias: ", id))
check((E.eq(pending, pending_before) and E.eq(world.state, before)), "Unlock evaluation leaves inputs untouched")
var snapshot: Dictionary = world.presentation.pending_amounts.duplicate()
world.presentation.deferred_updates = true
world.state.amount.EON1 += 1.0
world.state.amount.EON1 = E.add(world.state.amount.EON1, 1)
world.presentation.refresh(world.state, world.catalog)
check(world.presentation.dirty and world.presentation.pending_amounts == snapshot, "Deferred refresh retains pending snapshot")
check((world.presentation.dirty and E.eq(world.presentation.pending_amounts, snapshot)), "Deferred refresh retains pending snapshot")
world.presentation.refresh(world.state, world.catalog, true)
check(not world.presentation.dirty and world.presentation.pending_amounts.EON1 == 13.0, "Forced refresh publishes latest amounts")
world.state.amount.EON1 -= 1.0
check(((not world.presentation.dirty) and E.eq(world.presentation.pending_amounts.EON1, 13)), "Forced refresh publishes latest amounts")
world.state.amount.EON1 = E.subtract(world.state.amount.EON1, 1)
world.presentation.deferred_updates = false
world.presentation.refresh(world.state, world.catalog)
for id in ["EON1", "EON2", "LP", "C0.0", "C0"]:
check(stage.owniverse_items[id].visible, "Unlock both levels: " + id)
check(stage.owniverse_items[id].description.text != "", "Populate the icon counter: " + id)
check(not stage.owniverse_items.EON3.visible, "Unreached eon remains locked")
check(stage.owniverse_items[id].visible, E.add("Unlock both levels: ", id))
check(E.ne(stage.owniverse_items[id].description.text, ""), E.add("Populate the icon counter: ", id))
check((not stage.owniverse_items.EON3.visible), "Unreached eon remains locked")
stage.item_activated(stage.owniverse_items.LP)
check(stage.owniverse_items.EON1.is_visible_in_tree(), "Selecting LP shows its unlocked eons")
stage.item_activated(stage.owniverse_items.C0)
check(stage.owniverse_items["C0.0"].is_visible_in_tree() and not stage.owniverse_items.EON1.is_visible_in_tree(), "Selecting C0 switches the detail panel")
world.state.binds.RD.EON2 = 0.0
check((stage.owniverse_items["C0.0"].is_visible_in_tree() and (not stage.owniverse_items.EON1.is_visible_in_tree())), "Selecting C0 switches the detail panel")
world.state.binds.RD.EON2 = 0
stage._refresh_universe_ui()
check(stage.owniverse_items.EON2.visible and stage.owniverse_items.EON2.description.text == "", "Previously reached stage stays unlocked after extinction with no stale count")
check((stage.owniverse_items.EON2.visible and E.eq(stage.owniverse_items.EON2.description.text, "")), "Previously reached stage stays unlocked after extinction with no stale count")
for entity in world.catalog.entities.values(): entity.free()
world.free()
scene.free()
print("Life icons: %d checks, %d failures" % [checks, failures])
quit(0 if failures == 0 else 1)
print(("Life icons: %d checks, %d failures" % [checks, failures]))
quit((0 if E.eq(failures, 0) else 1))
+15 -14
View File
@@ -1,4 +1,5 @@
extends SceneTree
const E = preload("res://world/owniverse/math/exact.gd")
var checks: int = 0
var failures: int = 0
@@ -7,9 +8,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 run() -> void:
@@ -19,20 +20,20 @@ func run() -> void:
var names: Array = ["batches", "ledger", "production", "automatic", "stellar", "life", "environment", "evolution"]
var references: Array = []
for name in names:
check(first.get(name) != second.get(name), "World has its own " + name)
check(first.get(name).world == first, "Module refers to its owner: " + name)
check(E.ne(first.get(name), second.get(name)), E.add("World has its own ", name))
check(E.eq(first.get(name).world, first), E.add("Module refers to its owner: ", name))
references.append(weakref(first.get(name)))
first.state.amount.H = 123.0
first.presentation.pending_amounts.H = 123.0
check(not second.state.amount.has("H") and second.presentation.pending_amounts.is_empty(), "State and pending presentation are isolated")
first.state.amount.H = 123
first.presentation.pending_amounts.H = 123
check(((not second.state.amount.has("H")) and second.presentation.pending_amounts.is_empty()), "State and pending presentation are isolated")
first.free()
for reference in references:
check(reference.get_ref() == null, "Module is released with its world")
check(second.evolution.world == second, "Other world remains usable")
check(E.eq(reference.get_ref(), null), "Module is released with its world")
check(E.eq(second.evolution.world, second), "Other world remains usable")
var snapshot: Dictionary = second.state.duplicate(true)
second.evolute(NAN)
second.evolute(-1.0)
check(second.state == snapshot, "Entry point rejects invalid time deltas")
second.evolute(E.neg(1))
check(E.eq(second.state, snapshot), "Entry point rejects invalid time deltas")
second.free()
print("Module lifecycle: %d checks, %d failures" % [checks, failures])
quit(1 if failures else 0)
print(("Module lifecycle: %d checks, %d failures" % [checks, failures]))
quit((1 if failures else 0))
+27 -26
View File
@@ -1,4 +1,5 @@
extends SceneTree
const E = preload("res://world/owniverse/math/exact.gd")
const Clock = preload("res://world/owniverse/simulation_time.gd")
var checks: int = 0
var failures: int = 0
@@ -7,9 +8,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():
@@ -18,16 +19,16 @@ func fixture():
for id in global.OwniverseEntities:
var entity = load("res://world/owniverse/owniverse_entity.gd").new(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
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
world.catalog.entities["N"].product_per_year_amount = 10
world.catalog.entities["N"].auto = 0
world.state.amount.N = 1
return world
func dispose(world) -> void:
@@ -38,21 +39,21 @@ func dispose(world) -> void:
func run() -> void:
var world = fixture()
world.evolution.initialize_life_batches()
world.catalog.entities.N.cost = {"H": 2.0}
world.catalog.entities.N.bind = {"S": 3.0}
world.state.amount.H = 10.0
world.state.amount.S = 0.0
world.state.auto_resources = {"H": 10.0}
world.state.batches.H = [{"amount": 10.0, "start_year": 1.0, "end_year": 2.0}]
world.catalog.entities.N.cost = {"H": 2}
world.catalog.entities.N.bind = {"S": 3}
world.state.amount.H = 10
world.state.amount.S = 0
world.state.auto_resources = {"H": 10}
world.state.batches.H = [{"amount": 10, "start_year": 1, "end_year": 2}]
var before: Dictionary = world.state.duplicate(true)
check(not world.production.try_production(world.state, world.catalog.entities.N, 1.0, true), "Binding failure rejects production")
check(world.state == before, "Failure preserves costs, batches and budget")
world.state.amount.S = 10.0
check((not world.production.try_production(world.state, world.catalog.entities.N, 1, true)), "Binding failure rejects production")
check(E.eq(world.state, before), "Failure preserves costs, batches and budget")
world.state.amount.S = 10
var old_batches: Array = world.state.batches.H
check(world.production.try_production(world.state, world.catalog.entities.N, 2.0, true), "Funded production succeeds")
check(world.state.amount.H == 6.0 and world.state.amount.S == 4.0, "Exact cost and binding")
check(world.state.auto_resources.H == 6.0, "Exact automatic budget")
check(old_batches[0].amount == 10.0 and world.state.batches.H[0].amount == 6.0, "Consumed batches isolated")
print("Transaction: %d checks, %d failures" % [checks, failures])
check(world.production.try_production(world.state, world.catalog.entities.N, 2, true), "Funded production succeeds")
check((E.eq(world.state.amount.H, 6) and E.eq(world.state.amount.S, 4)), "Exact cost and binding")
check(E.eq(world.state.auto_resources.H, 6), "Exact automatic budget")
check((E.eq(old_batches[0].amount, 10) and E.eq(world.state.batches.H[0].amount, 6)), "Consumed batches isolated")
print(("Transaction: %d checks, %d failures" % [checks, failures]))
dispose(world)
quit(1 if failures else 0)
quit((1 if failures else 0))
+33 -32
View File
@@ -1,4 +1,5 @@
extends SceneTree
const E = preload("res://world/owniverse/math/exact.gd")
const Math = preload("res://world/owniverse/quantity_math.gd")
var checks: int = 0
@@ -8,56 +9,56 @@ 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 run() -> void:
var Catalog = load("res://world/owniverse/entity_catalog.gd")
check(Math.allocate_whole({}, 10.0) == {}, "Empty allocation")
check(Math.allocate_whole({"b": 1.0, "a": 1.0}, 1.0) == {"a": 1.0, "b": 0.0}, "Stable lexical tie")
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")
for limit in range(30):
var requests: Dictionary = {"a": 3.9, "b": 9.1, "c": -2.0, "d": 7.0}
var requests: Dictionary = {"a": E.of("3.9"), "b": E.of("9.1"), "c": E.neg(2), "d": 7}
var result: Dictionary = Math.allocate_whole(requests, limit)
check(Math.sum(result) == minf(limit, 19.0), "Conserve bounded allocation")
check(E.eq(Math.sum(result), E.minimum(limit, 19)), "Conserve bounded allocation")
for id in result:
check(result[id] == floorf(result[id]) and result[id] >= 0.0 and result[id] <= floorf(maxf(0.0, requests[id])), "Whole bounded branch")
check(Math.normalize_distribution({"a": -1.0, "b": 0.0}) == {}, "Nonpositive weights")
check(Math.normalize_distribution({"a": 2.0, "b": 6.0}) == {"a": 0.25, "b": 0.75}, "Normalize weights")
check(Math.production_step([0.25, 0.1, 2.0]) == 20.0, "Common whole production step")
check(Math.production_step([]) == 1.0, "Free production step")
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")
var target: Dictionary = {}
var produced: float = 0.0
var produced: Variant = 0
for i in 10:
produced += Math.accumulate_whole(target, "remainders", "a", 0.1)
check(produced == 1.0, "Fractional production carries")
produced = E.add(produced, Math.accumulate_whole(target, "remainders", "a", E.of("0.1")))
check(E.eq(produced, 1), "Fractional production carries")
var remainders: Dictionary = {}
var totals: Dictionary = {"a": 0.0, "b": 0.0, "c": 0.0}
var totals: Dictionary = {"a": 0, "b": 0, "c": 0}
for i in 30:
var result: Dictionary = Math.split(remainders, "split", 1.0, {"a": 1.0, "b": 1.0, "c": 1.0})
check(Math.sum(result) == 1.0, "Split conserves count")
for id in result: totals[id] += result[id]
check(totals == {"a": 10.0, "b": 10.0, "c": 10.0}, "Rounding debt distributes over time")
produced = 0.0
for i in 10: produced += Math.accumulate_rate(remainders, "rate", 1.0, 0.1)
check(produced == 1.0, "Rate carries")
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")
var catalog = Catalog.new()
var rows: Dictionary = root.get_node("Global").OwniverseEntities
catalog.load_definitions(rows)
check(catalog.entities.size() == rows.size(), "Catalog loads all definitions")
check(E.eq(catalog.entities.size(), rows.size()), "Catalog loads all definitions")
for id in catalog.entities:
var entity = catalog.entities[id]
check(catalog.automatic_producers.has(entity) == (entity.product_per_year_type != ""), "Producer index")
check(catalog.unlockable_entities.has(entity) == (not entity.unlock.is_empty()), "Unlock index")
if entity.group_as != "":
check(catalog.groups[entity.group_as].has(id) and catalog.members(entity.group_as).has(id), "Group index")
if entity.event_group != "":
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, ""):
check(catalog.event_groups[entity.event_group].has(id), "Event index")
var members: Array = catalog.members("Star")
members.clear()
check(not catalog.members("Star").is_empty(), "Group results are independent")
check((not catalog.members("Star").is_empty()), "Group results are independent")
check(Catalog.new().entities.is_empty(), "Catalog instances are independent")
for entity in catalog.entities.values(): entity.free()
print("Quantity and catalog: %d checks, %d failures" % [checks, failures])
quit(1 if failures else 0)
print(("Quantity and catalog: %d checks, %d failures" % [checks, failures]))
quit((1 if failures else 0))
+39 -38
View File
@@ -1,4 +1,5 @@
extends SceneTree
const E = preload("res://world/owniverse/math/exact.gd")
var checks: int = 0
var failures: int = 0
@@ -6,20 +7,20 @@ 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(legacy: bool):
var world = load("res://tests/fixtures/legacy_stellar_events.gd" if legacy else "res://world/owniverse/owniverse.gd").new()
var world = load(("res://tests/fixtures/legacy_stellar_events.gd" if legacy else "res://world/owniverse/owniverse.gd")).new()
for id in root.get_node("Global").OwniverseEntities:
world.catalog.entities[id] = load("res://world/owniverse/owniverse_entity.gd").new(root.get_node("Global").OwniverseEntities[id])
world.state.amount[id] = 0.0
world.state.stats.total_amount[id] = 0.0
if world.catalog.entities[id].group_as != "":
world.state.amount[world.catalog.entities[id].group_as] = 0.0
world.state.stats.total_amount[world.catalog.entities[id].group_as] = 0.0
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
return world
func dispose(world) -> void:
@@ -27,52 +28,52 @@ func dispose(world) -> void:
world.free()
func equivalent(a: Variant, b: Variant) -> bool:
if a is Dictionary and b is Dictionary:
if a.size() != b.size(): return false
if ((a is Dictionary) and (b is Dictionary)):
if E.ne(a.size(), b.size()): return false
for key in a:
if not b.has(key) or not equivalent(a[key], b[key]): return false
if ((not b.has(key)) or (not equivalent(a[key], b[key]))): return false
return true
if a is Array and b is Array:
if a.size() != b.size(): return false
if ((a is Array) and (b is Array)):
if E.ne(a.size(), b.size()): return false
for i in a.size():
if not equivalent(a[i], b[i]): return false
if (not equivalent(a[i], b[i])): return false
return true
if (a is float or a is int) and (b is float or b is int):
return absf(float(a) - float(b)) < 1e-6
return a == b
if ((((a is float) or (a is int))) and (((b is float) or (b is int)))):
return E.lt(E.absolute(E.subtract(E.of(a), E.of(b))), E.of("1e-6"))
return E.eq(a, b)
func run() -> void:
for parent in ["LRG", "HRG", "RSG", "RHG"]:
for year in [101.0, 101.5, 104.75, 1767.3, 3005.0]:
for year in [101, E.of("101.5"), E.of("104.75"), E.of("1767.3"), 3005]:
var fast = fixture(false)
var legacy = fixture(true)
fast.state.amount[parent] = 20000.0
fast.state.amount.RD = 100000.0
fast.state.amount.HP = 50.0
fast.state.amount.Met = 100.0
fast.state.binds.RD = {"HP": 2000.0, "C0.1": 100.0}
fast.state.binds[parent] = {"HP": 10.0}
fast.state.batches[parent] = [{"amount": 20000.0, "start_year": 100.3, "end_year": 3000.7}]
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")}]
# Existing short-lived populations test integration with new arrivals.
fast.state.amount.SN = 3.0
fast.batches.append_batch(fast.state, "SN", 3.0, 100.5, 100.5)
if year == 104.75: fast.state.energy = 7.0
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
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]), "%s at %s: %s matches materialized events" % [parent, year, field])
check(equivalent(fast.state[field], legacy.state[field]), ("%s at %s: %s matches materialized events" % [parent, year, field]))
for id in ["SN", "DSN", "BSN", "HN", "GRB", "SGRB", "LGRB", "HGRB"]:
check(fast.state.batches.get(id, []).size() <= 3, "Only surviving event windows remain")
check(E.le(fast.state.batches.get(id, []).size(), 3), "Only surviving event windows remain")
dispose(fast)
dispose(legacy)
var fast = fixture(false)
var pending: Dictionary = {}
fast.batches.append_batch(fast.state, "SN", 10000.0, 0.25, 1667.25, pending, 1667.25)
check(fast.state.batches.SN.size() <= 2 and pending.SN.size() == 1, "Completed windows aggregate without transient batch allocation")
var total: float = pending.SN[0].amount
for batch in fast.state.batches.SN: total += batch.amount
check(total == 10000.0, "Direct completions plus survivors conserve event count")
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")
dispose(fast)
print("Short-lived events: %d checks, %d failures" % [checks, failures])
quit(0 if failures == 0 else 1)
print(("Short-lived events: %d checks, %d failures" % [checks, failures]))
quit((0 if E.eq(failures, 0) else 1))
+155 -154
View File
@@ -1,4 +1,5 @@
extends SceneTree
const E = preload("res://world/owniverse/math/exact.gd")
var checks: int = 0
var failures: int = 0
@@ -6,9 +7,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():
@@ -17,16 +18,16 @@ func fixture():
for id in global.OwniverseEntities:
var entity = load("res://world/owniverse/owniverse_entity.gd").new(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
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
world.catalog.entities["N"].product_per_year_amount = 10
world.catalog.entities["N"].auto = 0
world.state.amount.N = 1
return world
func dispose(world) -> void:
@@ -37,226 +38,226 @@ func dispose(world) -> void:
func run() -> void:
var world = fixture()
for id in ["DSN", "SN", "BSN", "HN", "SGRB", "GRB", "LGRB", "HGRB"]:
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
check(E.eq(world.stellar.evolution_products(world.state, world.catalog.entities[id], 3).blast, E.multiply(3, world.catalog.entities[id].blast)), E.add("Entity blast determines power: ", id))
world.catalog.entities.SN.output_and.blast = 999
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")
check(E.eq(world.stellar.evolution_products(world.state, world.catalog.entities.SN, 2).blast, 8), "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")
check((not world.stellar.evolution_products(world.state, world.catalog.entities.SN, 2).has("blast")), "Zero entity blast disables external event")
world.catalog.entities.SN.output_or = {"positive": "blast:999", "negative": "blast:999", "chance": 50}
check((not world.stellar.evolution_products(world.state, world.catalog.entities.SN, 2).has("blast")), "Alternative output cannot restore disabled blast")
dispose(world)
world = fixture()
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(E.eq(rd.planets.HP.chance, E.of("0.01")), "Preserve fractional planet probability")
check((E.eq(rd.planets.GG.amount, 2) and E.eq(rd.planets.GG.chance, E.of("0.08"))), "Read count and chance separately")
check(E.eq(rd._parse_planet_string("HP:1:0.01"), {"HP": {"amount": 1, "chance": E.of("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.catalog.entities[id].blastability, "Blast vulnerable: " + id)
check(world.catalog.entities[id].blastability, E.add("Blast vulnerable: ", id))
for id in ["RP", "GG", "AB", "C1.3", "C1.4", "C1.5"]:
check(not world.catalog.entities[id].blastability, "Blast protected: " + id)
check((not world.catalog.entities[id].blastability), E.add("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.stellar.evolution_products(world.state, world.catalog.entities.HRG, 1.0)
var products: Dictionary = world.stellar.evolution_products(world.state, world.catalog.entities.HRG, 1)
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")
totals[id] = E.add(totals.get(id, 0), products[id])
check((E.eq(totals.get("SN"), 99) and E.eq(totals.get("GRB"), 1)), "GI rare outcomes accumulate deterministically")
var restored = fixture()
restored.state = bytes_to_var(var_to_bytes(world.state))
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")
restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
check(E.eq(world.stellar.evolution_products(world.state, world.catalog.entities.HRG, 37), restored.stellar.evolution_products(restored.state, restored.catalog.entities.HRG, 37)), "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.batches.append_batch(world.state, "RD", 4.0, 0.0, 0.0)
var lifespan: float = rd.lifespan
var width: float = rd.lifespan_delta
world.state.amount.N = 0
world.state.amount.RD = 4
world.batches.append_batch(world.state, "RD", 4, 0, 0)
var lifespan: Variant = rd.lifespan
var width: Variant = rd.lifespan_delta
world.evolution.evolve_batches(world.state, lifespan)
check(world.state.amount.RD == 4.0, "Window start does not kill the whole batch")
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.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.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(E.eq(world.state.amount.RD, 4), "Window start does not kill the whole batch")
world.evolution.evolve_batches(world.state, E.add(lifespan, E.divide(width, 4)))
check((E.eq(world.state.amount.RD, 3) and E.eq(world.state.amount.TWD, 1)), "One quarter of a batch evolves")
world.evolution.evolve_batches(world.state, E.add(lifespan, E.divide(width, 2)))
check((E.eq(world.state.amount.RD, 2) and E.eq(world.state.amount.TWD, 2)), "Half the batch evolves")
world.evolution.evolve_batches(world.state, E.add(lifespan, width))
check((E.eq(world.state.amount.RD, 0) and E.eq(world.state.amount.TWD, 4)), "Window end completes evolution")
check(world.state.batches.RD.is_empty(), "Completed batch removed")
dispose(world)
# Creation accumulates independently for each star/planet pair.
world = fixture()
world.state.amount.RD = 100.0
world.state.amount.RD = 100
for i in 100:
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")
check(world.state.amount.HP == 0.0 and world.state.stats.total_amount.HP == 1.0, "Created planets are bound, counted once")
world.stellar.create_planets(world.state, "RD", 1, 0, 0, 1)
check(E.eq(world.state.binds.RD.HP, 1), "100 RD at metallicity 1 produce one HP")
check(E.eq(world.state.binds.RD.RP, 21), "Rocky planet probability retains fractions")
check((E.eq(world.state.binds.RD.GG, 16) and E.eq(world.state.binds.RD.AB, 120)), "Planet multiplicity applied")
check((E.eq(world.state.amount.HP, 0) and E.eq(world.state.stats.total_amount.HP, 1)), "Created planets are bound, counted once")
var saves: Node = root.get_node("SaveManager")
check(saves.validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Planet carry save validates")
check(E.eq(saves.validate_data({"format_version": 3, "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.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")
restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
world.stellar.create_planets(world.state, "RD", 37, 0, 0, E.of("0.7"))
restored.stellar.create_planets(restored.state, "RD", 37, 0, 0, E.of("0.7"))
check(E.eq(world.state, restored.state), "Planet carry continues after save")
dispose(restored)
dispose(world)
# A partially dying dwarf sterilizes HP; survivors and RP move to its heir.
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.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.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")
world.state.amount.RD = 4
world.state.binds.RD = {"HP": 8, "RP": 4, "C1.2": 4, "C1.3": 4, "GG": 4}
world.batches.append_batch(world.state, "RD", 4, 0, 0)
world.evolution.evolve_batches(world.state, E.add(lifespan, E.divide(width, 2)))
check((E.eq(world.state.amount.RD, 2) and E.eq(world.state.amount.TWD, 2)), "Partial star transition with planets")
check(E.eq(E.add(world.state.binds.RD.HP, world.state.binds.TWD.HP), 4), "Half of HP sterilized")
check((E.eq(world.state.binds.RD["C1.3"], 2) and E.eq(world.state.binds.TWD["C1.3"], 2)), "Protected civilization splits between parent and heir")
check(E.eq(E.add(world.state.binds.RD.RP, world.state.binds.TWD.RP), 10), "Sterilized planets become RP without loss")
check((E.eq(world.state.amount.RP, 0) and E.eq(world.state.amount["C1.3"], 0)), "Inherited planets never become free")
world.evolution.evolve_batches(world.state, E.add(lifespan, width))
check((E.eq(world.state.binds.RD["C1.3"], 0) and E.eq(world.state.binds.TWD["C1.3"], 4)), "No planets left at extinct parent type")
check(E.eq(saves.validate_data({"format_version": 3, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Inherited state validates")
dispose(world)
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.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)
world.state.amount.WS = 2
world.state.binds.WS = {"HP": 4, "C1.3": 4, "RP": 4, "GG": 4}
world.batches.append_batch(world.state, "WS", 2, 0, 0)
world.evolution.evolve_batches(world.state, E.add(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")
check((((E.eq(physical.HP, 0) and E.eq(physical["C1.3"], 0)) and E.eq(physical.RP, 0)) and E.eq(physical.GG, 0)), "Level 2 removes all planets, including protected civilization")
check(E.eq(world.state.amount.LRG, 2), "Giant evolves after local destruction")
dispose(world)
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.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.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")
world.state.amount.RD = 10
world.state.binds.RD = {"HP": 10, "C1.2": 10, "C1.3": 10, "RP": 0}
world.stellar.apply_blast(world.state, 5, {"blast": 1, "density": 1})
check((E.eq(world.state.binds.RD.HP, 5) and E.eq(world.state.binds.RD["C1.2"], 5)), "Blast damages vulnerable worlds proportionally")
check((E.eq(world.state.binds.RD["C1.3"], 10) and E.eq(world.state.binds.RD.RP, 10)), "Blast protects C1.3 and creates RP")
world.stellar.apply_blast(world.state, 1000000, {"blast": 1, "density": 1})
check((E.eq(world.state.binds.RD.HP, 0) and E.eq(world.state.binds.RD.RP, 20)), "Overwhelming blast is capped at available planets")
check((not world.state.amount.has("blast")), "Blast is not a resource")
dispose(world)
world = fixture()
world.state.amount.RD = 1.0
world.state.binds.RD = {"HP": 1.0}
world.state.amount.RD = 1
world.state.binds.RD = {"HP": 1}
for i in 10:
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")
world.stellar.apply_blast(world.state, 1, {"blast": 1, "density": E.of("0.1")})
check((E.eq(world.state.binds.RD.HP, 0) and E.eq(world.state.binds.RD.RP, 1)), "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.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")
check(saves.validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Blast state validates")
world.state.amount.SN = 1
world.batches.append_batch(world.state, "SN", 1, 0, 0)
world.evolution.evolve_batches(world.state, 2)
check((E.eq(world.state.amount.SN, 0) and E.eq(world.state.amount.LNS, 1)), "Supernova produces a remnant")
check(((E.eq(world.state.amount.H, 8) and E.eq(world.state.amount.He, 50)) and E.eq(world.state.amount.Met, 11)), "Supernova returns table resources")
check((not world.state.amount.has("blast")), "Supernova dispatches blast output")
check(E.eq(saves.validate_data({"format_version": 3, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Blast state validates")
dispose(world)
world = fixture()
world.state.amount.H = 1000.0
world.state.amount.Met = 100.0
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")
world.state.amount.H = 1000
world.state.amount.Met = 100
check(E.eq(world.production.produce("RD", 100), 100), "Manual star creation succeeds")
check(E.ge(world.state.binds.RD.HP, 1), "Manual creation dispatches planet formation")
check(E.eq(world.state.amount.H, 900), "Planet creation does not duplicate or spend stellar fuel")
dispose(world)
world = fixture()
world.state.amount.N = 0.0
world.state.amount.RD = 1.0
world.state.amount.H = 100.0
world.state.amount.Met = 100.0
world.state.auto_resources.H = 100.0
world.evolute(0.1)
check(world.state.amount.RD == 101.0 and world.state.amount.H == 0.0, "Automatic formation uses its resource budget")
check(world.state.binds.RD.HP >= 1.0, "Automatic formation also creates planets")
world.state.amount.N = 0
world.state.amount.RD = 1
world.state.amount.H = 100
world.state.amount.Met = 100
world.state.auto_resources.H = 100
world.evolute(E.of("0.1"))
check((E.eq(world.state.amount.RD, 101) and E.eq(world.state.amount.H, 0)), "Automatic formation uses its resource budget")
check(E.ge(world.state.binds.RD.HP, 1), "Automatic formation also creates planets")
dispose(world)
world = fixture()
world.catalog.entities.N.auto = 1.0
world.catalog.entities.N.auto = 1
# 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.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")
world.evolute(100)
check(E.eq(world.state.amount.HBD, 10), "Brown dwarfs form without existing ordinary stars")
check((E.eq(world.state.amount.H, 999) and E.eq(world.state.auto_resources.H, 999)), "Brown dwarfs consume exactly their reserved hydrogen")
check((E.eq(world.presentation.amounts(world.state, world.catalog).BD, 10) and E.gt(world.state.batches.HBD.size(), 0)), "Brown dwarfs appear in counter and evolution batches")
world.catalog.entities.N.product_per_year_amount = 0
world.evolute(100)
check(E.eq(world.state.amount.HBD, 10), "Stored automatic budget is not taxed repeatedly")
dispose(world)
world = fixture()
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")
world.catalog.entities.N.auto = 1
world.state.amount.RD = 1
world.evolute(50)
check((E.eq(world.state.amount.HBD, 5) and E.near(world.state.auto_star_deficits.HBD_fuel, E.of("0.5"))), "Individual dwarfs retain prepaid fractional fuel")
check(E.eq(world.state.amount.H, 0), "Prepaid fuel is not available to ordinary stars")
var resumed = fixture()
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)
check(world.state.amount.HBD == 10.0, "Accumulated quota eventually creates a whole batch")
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.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")
resumed.catalog.entities.N.auto = 1
resumed.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
world.evolute(50)
resumed.evolute(50)
check(E.eq(world.state.amount.HBD, 10), "Accumulated quota eventually creates a whole batch")
check(E.eq(world.state, resumed.state), "Brown dwarf quota survives serialization deterministically")
check((E.eq(world.state.amount.RD, 1000) and E.eq(world.state.amount.H, 0)), "Shared automatic hydrogen is spent once")
world.state.batches.HBD = [{"amount": 10, "start_year": 101, "end_year": 101}]
world.evolution.evolve_batches(world.state, 101)
check(((E.eq(world.state.amount.HBD, 0) and E.eq(world.state.amount.CBD, 10)) and E.eq(world.presentation.amounts(world.state, world.catalog).BD, 10)), "Cooling preserves aggregate brown dwarf count")
dispose(resumed)
dispose(world)
world = fixture()
var saw_individual: bool = false
for i in range(250):
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.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
world.state.amount.H = E.add(world.state.amount.H, 1)
world.state.auto_resources.H = E.add(world.state.auto_resources.get("H", 0), 1)
var before: Variant = world.state.amount.HBD
world.automatic.produce_brown_dwarfs(world.state, 1, E.of(i), E.of(E.add(i, 1)))
var born: Variant = E.subtract(world.state.amount.HBD, before)
check((E.eq(born, 0) or E.eq(born, 1)), "Small fuel increments produce individual dwarfs")
saw_individual = (saw_individual or E.eq(born, 1))
check(saw_individual, "Random rounding does not wait for ten dwarfs")
resumed = fixture()
resumed.state = bytes_to_var(var_to_bytes(world.state))
resumed.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
for i in range(250, 500):
for simulation in [world, resumed]:
simulation.state.amount.H += 1.0
simulation.state.auto_resources.H += 1.0
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.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")
simulation.state.amount.H = E.add(simulation.state.amount.H, 1)
simulation.state.auto_resources.H = E.add(simulation.state.auto_resources.H, 1)
simulation.automatic.produce_brown_dwarfs(simulation.state, 1, E.of(i), E.of(E.add(i, 1)))
check(E.eq(world.state, resumed.state), "Random sequence and prepaid fuel survive saving")
check(E.near(E.add(E.add(world.state.amount.H, E.multiply(world.state.amount.HBD, E.of("0.1"))), world.state.auto_star_deficits.HBD_fuel), 500), "Individual production conserves fuel including prepaid reserve")
check(E.near(E.add(world.state.amount.HBD, world.state.auto_star_deficits.HBD), 5), "Signed remainder preserves the 0.1 percent allocation")
var paused_count: Variant = world.state.amount.HBD
for i in 10: world.automatic.produce_brown_dwarfs(world.state, 0, 500, 501)
check(E.eq(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.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.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")
world.state.amount.HRG = 100
world.state.binds.HRG = {"C1.3": 100}
world.state.batches.HRG = [{"start_year": E.of("0.1"), "end_year": E.of("0.1"), "amount": 100}]
world.evolution.evolve_batches(world.state, E.of("0.1"))
check((E.eq(world.state.binds.SN["C1.3"], 99) and E.eq(world.state.binds.GRB["C1.3"], 1)), "Planets split among actual alternative successors")
check(E.eq(world.state.binds.HRG["C1.3"], 0), "Alternative evolution leaves no orphan bindings")
dispose(world)
world = fixture()
world.state.amount.N = 0.0
world.state.amount.HRG = 1.0
world.state.batches.HRG = [{"start_year": 0.1, "end_year": 0.1, "amount": 1.0}]
world.evolute(3.0)
check(world.state.amount.LNS == 1.0 and world.state.amount.SN == 0.0, "Long step follows multiple stellar generations")
check(world.state.amount.S >= 99.0, "Short-lived newborn supernova emits S before disappearing")
world.state.amount.N = 0
world.state.amount.HRG = 1
world.state.batches.HRG = [{"start_year": E.of("0.1"), "end_year": E.of("0.1"), "amount": 1}]
world.evolute(3)
check((E.eq(world.state.amount.LNS, 1) and E.eq(world.state.amount.SN, 0)), "Long step follows multiple stellar generations")
check(E.ge(world.state.amount.S, 99), "Short-lived newborn supernova emits S before disappearing")
dispose(world)
print("Stellar rules: %d checks, %d failures" % [checks, failures])
quit(0 if failures == 0 else 1)
print(("Stellar rules: %d checks, %d failures" % [checks, failures]))
quit((0 if E.eq(failures, 0) else 1))
+42 -41
View File
@@ -1,4 +1,5 @@
extends SceneTree
const E = preload("res://world/owniverse/math/exact.gd")
const Clock = preload("res://world/owniverse/simulation_time.gd")
var checks: int = 0
var failures: int = 0
@@ -7,9 +8,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():
@@ -18,16 +19,16 @@ func fixture():
for id in global.OwniverseEntities:
var entity = load("res://world/owniverse/owniverse_entity.gd").new(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
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
world.catalog.entities["N"].product_per_year_amount = 10
world.catalog.entities["N"].auto = 0
world.state.amount.N = 1
return world
func dispose(world) -> void:
@@ -38,55 +39,55 @@ func dispose(world) -> void:
func run() -> void:
var saves: Node = root.get_node("SaveManager")
var world = fixture()
world.evolute(0.09)
check(world.state.time.year == 0.0, "Do not process before a tenth boundary")
check(world.state.amount.H == 0.0, "No premature production")
world.evolute(0.01)
check(world.state.time.year == 0.1, "Process the first tenth")
check(world.state.amount.H == 1.0, "Production runs at 0.1 year")
world.evolute(0.2)
check(world.state.time.year == 0.3 and world.state.amount.H == 3.0, "Do not reprocess previous tenths")
world.evolute(E.of("0.09"))
check(E.eq(world.state.time.year, 0), "Do not process before a tenth boundary")
check(E.eq(world.state.amount.H, 0), "No premature production")
world.evolute(E.of("0.01"))
check(E.eq(world.state.time.year, E.of("0.1")), "Process the first tenth")
check(E.eq(world.state.amount.H, 1), "Production runs at 0.1 year")
world.evolute(E.of("0.2"))
check((E.eq(world.state.time.year, E.of("0.3")) and E.eq(world.state.amount.H, 3)), "Do not reprocess previous tenths")
var before: Dictionary = world.state.duplicate(true)
for invalid in [0.0, -1.0, NAN, INF, 1e308]:
for invalid in [0, E.neg(1), NAN, INF, E.of("1e308")]:
world.evolute(invalid)
check(world.state == before, "Invalid/overflow interval rejected")
check(saves.validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Fractional save is valid")
check(E.eq(world.state, before), "Invalid/overflow interval rejected")
check(E.eq(saves.validate_data({"format_version": 3, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Fractional save is valid")
var invalid_save: Dictionary = world.state.duplicate(true)
invalid_save.time.year = 0.25
check(saves.validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": invalid_save}) != "", "Reject inconsistent fractional clock")
invalid_save.time.year = E.of("0.25")
check(E.ne(saves.validate_data({"format_version": 3, "app_version": "", "ui": {"current_page": 0}, "state": invalid_save}), ""), "Reject inconsistent fractional clock")
dispose(world)
for fps in [30, 60, 144]:
world = fixture()
for frame in fps:
world.evolute(1.0 / fps)
check(world.state.time.year == 1.0, "One second at %d fps" % fps)
check(world.state.amount.H == 10.0, "Production across frames at %d fps" % fps)
world.evolute(E.divide(1, fps))
check(E.eq(world.state.time.year, 1), ("One second at %d fps" % fps))
check(E.eq(world.state.amount.H, 10), ("Production across frames at %d fps" % fps))
dispose(world)
world = fixture()
world.evolute(0.05)
world.evolute(E.of("0.05"))
world.speed = 2
world.evolute(0.005)
check(world.state.time.year == 0.1, "Speed change preserves accumulated fraction")
world.evolute(E.of("0.005"))
check(E.eq(world.state.time.year, E.of("0.1")), "Speed change preserves accumulated fraction")
world.speed = 100
var speed_limit: int = root.get_node("Global").SPEED_LIMIT
check(world.speed == speed_limit and world.state.speed_in_year == pow(10.0, speed_limit - 1), "Configured upper speed limit")
world.speed = -10
check(world.speed == 1 and world.state.speed_in_year == 1.0, "Lower speed limit unchanged")
check((E.eq(world.speed, speed_limit) and E.eq(world.state.speed_in_year, E.power(10, E.subtract(speed_limit, 1)))), "Configured upper speed limit")
world.speed = E.neg(10)
check((E.eq(world.speed, 1) and E.eq(world.state.speed_in_year, 1)), "Lower speed limit unchanged")
dispose(world)
world = fixture()
dispose(world)
world = fixture()
world.catalog.entities.N.output_and = {"He": 1.0}
world.catalog.entities.N.output_and = {"He": 1}
world.catalog.entities.N.output_or = {}
world.state.batches.N = [{"start_year": 0.1, "end_year": 0.1, "amount": 1.0}]
world.evolute(0.1)
check(world.state.amount.N == 0.0 and world.state.amount.He == 1.0, "Existing evolution executes at fractional boundary")
world.state.batches.N = [{"start_year": E.of("0.1"), "end_year": E.of("0.1"), "amount": 1}]
world.evolute(E.of("0.1"))
check((E.eq(world.state.amount.N, 0) and E.eq(world.state.amount.He, 1)), "Existing evolution executes at fractional boundary")
dispose(world)
check(Clock.processed_year(0.099) == 0.0, "Do not round a genuine incomplete tenth forward")
check(Clock.processed_year(1e9 + 0.45) == 1e9 + 0.4, "Large ages preserve fractional precision")
print("Time state: %d checks, %d failures" % [checks, failures])
quit(0 if failures == 0 else 1)
check(E.eq(Clock.processed_year(E.of("0.099")), 0), "Do not round a genuine incomplete tenth forward")
check(E.eq(Clock.processed_year(E.add(E.of("1e9"), E.of("0.45"))), E.add(E.of("1e9"), E.of("0.4"))), "Large ages preserve fractional precision")
print(("Time state: %d checks, %d failures" % [checks, failures]))
quit((0 if E.eq(failures, 0) else 1))
+20 -19
View File
@@ -1,4 +1,5 @@
extends SceneTree
const E = preload("res://world/owniverse/math/exact.gd")
var checks: int = 0
var failures: int = 0
@@ -7,9 +8,9 @@ func _initialize() -> void:
call_deferred("run")
func check(value: bool, message: String) -> void:
checks += 1
if not value:
failures += 1
checks = E.add(checks, 1)
if (not value):
failures = E.add(failures, 1)
push_error(message)
func run() -> void:
@@ -22,39 +23,39 @@ func run() -> void:
await process_frame
stage.stage_page_scroller.scroll_horizontal = stage.PAGE_WIDTH
await process_frame
stage.owniverse.state.amount.H = 1000.0
stage.owniverse.state.amount.RD = 2.0
stage.owniverse.state.amount.H = 1000
stage.owniverse.state.amount.RD = 2
stage.owniverse.presentation.refresh(stage.owniverse.state, stage.owniverse.catalog)
stage.set_owniverse_item_value()
var hydrogen = stage.owniverse_items.H
var dwarf = stage.owniverse_items.RD
check(hydrogen.description.text == "1 000", "Pending quantities reach controls")
check(dwarf.description.text == "2", "Other page receives its current quantity")
check(E.eq(hydrogen.description.text, "1 000"), "Pending quantities reach controls")
check(E.eq(dwarf.description.text, "2"), "Other page receives its current quantity")
check(stage.owniverse.presentation.pending_amounts.is_empty(), "Published queue is consumed by Stage")
stage.owniverse.presentation.deferred_updates = true
stage.owniverse.state.amount.H = 2000.0
stage.owniverse.state.amount.H = 2000
stage.owniverse.presentation.refresh(stage.owniverse.state, stage.owniverse.catalog)
check(stage.owniverse.presentation.dirty and stage.owniverse.presentation.pending_amounts.is_empty(), "Deferred refresh marks dirty without republishing")
check((stage.owniverse.presentation.dirty and stage.owniverse.presentation.pending_amounts.is_empty()), "Deferred refresh marks dirty without republishing")
stage.set_owniverse_item_value()
check(hydrogen.description.text == "1 000", "Labels retain last published quantity")
check(E.eq(hydrogen.description.text, "1 000"), "Labels retain last published quantity")
stage.owniverse.presentation.refresh(stage.owniverse.state, stage.owniverse.catalog, true)
stage.set_owniverse_item_value()
check(hydrogen.description.text == "2 000" and not stage.owniverse.presentation.dirty, "Forced refresh publishes latest state")
check((E.eq(hydrogen.description.text, "2 000") and (not stage.owniverse.presentation.dirty)), "Forced refresh publishes latest state")
stage.owniverse.presentation.deferred_updates = false
stage.item_activated(hydrogen)
stage.set_owniverse_item_value()
check(hydrogen.description.text == root.get_node("Global").format_number(stage.owniverse.state.amount.H), "Player production publishes the new quantity")
check(stage.owniverse.state.amount.H > 2000.0, "Production action actually executed")
check(E.eq(hydrogen.description.text, root.get_node("Global").format_number(stage.owniverse.state.amount.H)), "Player production publishes the new quantity")
check(E.gt(stage.owniverse.state.amount.H, 2000), "Production action actually executed")
dwarf.visible = true
stage.stage_page_scroller.scroll_horizontal = 2 * stage.PAGE_WIDTH
stage.stage_page_scroller.scroll_horizontal = E.multiply(2, stage.PAGE_WIDTH)
await process_frame
stage._refresh_universe_ui()
check(dwarf.description.text == "2", "Newly visible page receives cached latest values")
stage.owniverse.state.time.year = 1234.5
check(E.eq(dwarf.description.text, "2"), "Newly visible page receives cached latest values")
stage.owniverse.state.time.year = E.of("1234.5")
stage._refresh_universe_ui()
check(stage.date_time.text == "1 234.5", "Time displays tenths with thousands separator")
check(E.eq(stage.date_time.text, "1 234.5"), "Time displays tenths with thousands separator")
for entity in stage.owniverse.catalog.entities.values(): entity.free()
stage.owniverse.free()
scene.free()
print("UI updates: %d checks, %d failures" % [checks, failures])
quit(0 if failures == 0 else 1)
print(("UI updates: %d checks, %d failures" % [checks, failures]))
quit((0 if E.eq(failures, 0) else 1))
+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))