Add new tests and implement frame budget management
- Introduced `civilization_benchmark.gd` and `legacy_quantity_math.gd` for benchmarking and legacy quantity calculations. - Created `test_dynamic_math.gd`, `test_frame_evolution.gd`, and `test_sparse_batches.gd` to validate mathematical operations and frame evolution logic. - Implemented `frame_budget.gd` to manage cooperative work on the main thread, ensuring no partially calculated state is published. - Added UID files for all new scripts to maintain unique identifiers.
This commit is contained in:
@@ -155,3 +155,53 @@ godot --headless --path Owniverse.GI --script res://tests/test_civilization_evol
|
||||
```
|
||||
|
||||
Проверяются реальные коэффициенты C0.2, войны и побочный ущерб C2.0, замена планет колониями с сохранением хозяина, дипломатическое влияние, бонус периода от структур и деградация при их отсутствии. Ускоренная модель проходит все стадии C0.2 → C4.4 → Ct. Проверки контролируют сохранение населения, целочисленность этик, валидацию сохранения и детерминированное продолжение после сериализации.
|
||||
|
||||
## Профиль цивилизаций на шестой скорости
|
||||
|
||||
```sh
|
||||
godot --headless --path Owniverse.GI --script res://tests/benchmark_civilizations.gd -- frames=20 profile
|
||||
godot --path Owniverse.GI --script res://tests/benchmark_civilizations.gd -- ui scenario=mixed frames=10 profile
|
||||
```
|
||||
|
||||
Сценарии `mixed`, `large`, `fragmented` заполняют все стадии и этики, синхронизируя
|
||||
переходы; `large` использует по 6e20 цивилизаций, `fragmented` — десять партий.
|
||||
Это искусственные стрессовые сценарии. `scenario=stable` использует штатные
|
||||
сроки стадий. Параметр `snapshot=/tmp/civil` сохраняет состояние каждого сценария
|
||||
для сравнения до и после изменения. UI запускает настоящую сцену со страницей
|
||||
жизни и реальным delta, без загрузки и записи сохранения игрока.
|
||||
|
||||
`profile` выводит суммарные микросекунды и число вызовов по этапам. Вложенные
|
||||
этапы входят во время родительских; складывать их нельзя. В игре сбор выключен.
|
||||
Кэши физических количеств принадлежат отдельному миру и проверяют изменения
|
||||
количеств, вложенных привязок и индексов каталога. Кэш структуры правил не входит
|
||||
в сохранение: при изменении Claim/бонусов нужно перестроить индексы каталога,
|
||||
при изменении home в существующей таблице — invalidate_civilization_layout().
|
||||
|
||||
После оптимизации на M4 Pro, по 20 шагов 1/60 секунды, среднее время:
|
||||
`mixed` 295 → 235 мс, `large` 666 → 539 мс, `fragmented` 359 → 282 мс.
|
||||
Полные состояния всех трёх сценариев после 20 шагов совпали до и после изменения.
|
||||
Дополнительно тест распределений сравнивает новый путь с прежним на 300
|
||||
случайных наборах, включая 1e40 и накопленные долги округления.
|
||||
|
||||
## Расчёт между кадрами
|
||||
|
||||
`godot --headless --path Owniverse.GI --script res://tests/test_frame_evolution.gd`
|
||||
|
||||
Stage вызывает `evolute_frame`: транзакция получает бюджет 40 мс и уступает
|
||||
следующему кадру внутри создания партий и обработки стадий. Расчёт остаётся
|
||||
на главном потоке, состояние публикуется только после полного завершения.
|
||||
Длительность одной транзакции ограничена обычным шагом 1/60 секунды выбранной
|
||||
скорости. Запрошенное время хранится отдельно, поэтому отставание не превращается
|
||||
в один всё более тяжёлый шаг. При предельной нагрузке время симуляции может
|
||||
отставать от запрошенного; это не ограничение FPS интерфейса и не удаление событий.
|
||||
|
||||
Производство во время транзакции выполняется после её завершения. Запрошенное
|
||||
время, активный интервал, скорость и очередь производства входят в сохранение;
|
||||
при загрузке незавершённый интервал пересчитывается с опубликованного состояния.
|
||||
Сброс мира отменяет отложенный расчёт. Тест проверяет точное совпадение непрерывного
|
||||
и распределённого расчёта, очереди, переключение скорости, восстановление и отмену.
|
||||
|
||||
На M4 Pro, 180 кадров реального UI на шестой скорости: смешанный сценарий
|
||||
15.9 FPS (максимальный кадр 98 мс), большие количества 17.5 FPS (106 мс),
|
||||
много партий 16.3 FPS (97 мс).
|
||||
Это FPS отрисовки, а не число завершённых транзакций симуляции в секунду.
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
extends SceneTree
|
||||
const Fixture = preload("res://tests/fixtures/civilization_benchmark.gd")
|
||||
|
||||
func _initialize(): call_deferred("run")
|
||||
|
||||
func run():
|
||||
var ui: bool = "ui" in OS.get_cmdline_user_args()
|
||||
var scenarios: Array = ["mixed", "large", "fragmented"]
|
||||
var frames: int = 120
|
||||
var step_delta: float = 1.0 / 60.0
|
||||
for argument in OS.get_cmdline_user_args():
|
||||
if argument.begins_with("scenario="): scenarios = [argument.trim_prefix("scenario=")]
|
||||
if argument.begins_with("delta="): step_delta = float(argument.trim_prefix("delta="))
|
||||
if argument.begins_with("frames="): frames = maxi(2, int(argument.trim_prefix("frames=")))
|
||||
Engine.max_fps = 60
|
||||
if ui: DisplayServer.window_set_size(Vector2i(540, 960))
|
||||
for scenario in scenarios:
|
||||
var scene
|
||||
var stage
|
||||
var world
|
||||
if ui:
|
||||
scene = load("res://world/world.tscn").instantiate()
|
||||
stage = scene.get_node("Stage")
|
||||
stage.owniverse.free()
|
||||
stage.set_script(load("res://tests/fixtures/benchmark_ui_stage.gd"))
|
||||
root.add_child(scene)
|
||||
await process_frame
|
||||
await process_frame
|
||||
world = stage.owniverse
|
||||
else:
|
||||
world = load("res://world/owniverse/owniverse.gd").new()
|
||||
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
|
||||
Fixture.seed(world, root.get_node("Global"), scenario == "fragmented", scenario == "large", scenario == "stable")
|
||||
world.profile_enabled = "profile" in OS.get_cmdline_user_args()
|
||||
var samples: Array = []
|
||||
if ui:
|
||||
stage.stage_page_scroller.scroll_horizontal = 4 * stage.PAGE_WIDTH
|
||||
for item in stage.owniverse_items.values(): item.visible = true
|
||||
for page in stage.item_pages: page.visible = true
|
||||
stage._set_item_description(stage.owniverse_items["C2.0"])
|
||||
stage._refresh_universe_ui()
|
||||
for warmup in 10: await process_frame
|
||||
stage.measuring = true
|
||||
for frame in frames:
|
||||
await process_frame
|
||||
if (frame + 1) % 10 == 0:
|
||||
print("%s: measured %d UI frames" % [scenario, frame + 1])
|
||||
stage.measuring = false
|
||||
samples = stage.process_samples
|
||||
report(scenario + " UI frame", stage.frame_samples)
|
||||
else:
|
||||
for frame in frames:
|
||||
var started: int = Time.get_ticks_usec()
|
||||
world.evolute(step_delta)
|
||||
samples.append((Time.get_ticks_usec() - started) / 1000.0)
|
||||
report(scenario + (" UI handler" if ui else " CPU"), samples)
|
||||
if ui:
|
||||
print("Completed simulation transactions: ", world.frame_commits, "; published year: ", world.state.time.year, "; requested year: ", world.frame_clock.get("year", 0))
|
||||
print("Statistics ", world.state.civilization_stats)
|
||||
if world.profile_enabled: print("Profile microseconds/calls: ", world.profile)
|
||||
for argument in OS.get_cmdline_user_args():
|
||||
if argument.begins_with("snapshot="):
|
||||
var file = FileAccess.open(argument.trim_prefix("snapshot=") + "-" + scenario + ".json", FileAccess.WRITE)
|
||||
file.store_string(JSON.stringify(world.E.encode(world.state)))
|
||||
for entity in world.catalog.entities.values(): entity.free()
|
||||
world.free()
|
||||
if ui: scene.free()
|
||||
quit()
|
||||
|
||||
func report(label: String, samples: Array):
|
||||
var total: float = 0.0
|
||||
for sample in samples: total += sample
|
||||
samples.sort()
|
||||
print("%s: mean %.2f ms, p95 %.2f ms, max %.2f ms, %.1f per second" % [label, total / samples.size(), samples[ceili(samples.size() * 0.95) - 1], samples.back(), samples.size() * 1000 / total])
|
||||
@@ -0,0 +1 @@
|
||||
uid://b5w4sp6eepypa
|
||||
@@ -22,8 +22,8 @@ func run():
|
||||
quit(1)
|
||||
return
|
||||
var data = JSON.parse_string(FileAccess.get_file_as_string(args[0]))
|
||||
if not data is Dictionary or data.get("format_version") != 4:
|
||||
push_error("Expected diagnostic JSON format 4")
|
||||
if not data is Dictionary or data.get("format_version") != 5:
|
||||
push_error("Expected diagnostic JSON format 5")
|
||||
quit(1)
|
||||
return
|
||||
var initial: Dictionary = E.decode(restore_json_integers(data)).state
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
extends SceneTree
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
|
||||
func _initialize(): call_deferred("run")
|
||||
|
||||
func run():
|
||||
for scenario in ["ordinary", "hypergiants"]:
|
||||
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.speed = 6
|
||||
world.state.amount.S = E.of("1e24")
|
||||
world.state.amount.H = E.of("1e24")
|
||||
for entry in [["RD", 1234567], ["YD", 765432], ["STN", 654321]]:
|
||||
world.state.amount[entry[0]] = entry[1]
|
||||
world.batches.append_batch(world.state, entry[0], entry[1], 0, 0)
|
||||
if scenario == "hypergiants":
|
||||
world.state.amount.HG = E.of("1e20")
|
||||
world.batches.append_batch(world.state, "HG", E.of("1e20"), 0, 0)
|
||||
var start_year = E.subtract(world.catalog.entities.HG.lifespan, 1000)
|
||||
world.state.time = {"datetime": start_year, "year": start_year}
|
||||
var samples: Array = []
|
||||
var total: int = 0
|
||||
for frame in 600:
|
||||
var started: int = Time.get_ticks_usec()
|
||||
world.evolute(1.0 / 60.0)
|
||||
var duration: int = Time.get_ticks_usec() - started
|
||||
samples.append(duration)
|
||||
total += duration
|
||||
samples.sort()
|
||||
print("Speed 6 %s: 600 frames, mean %.3f ms, p95 %.3f ms, max %.3f ms, %.1f cycles/sec" % [scenario, total / 600000.0, samples[569] / 1000.0, samples.back() / 1000.0, 600000000.0 / total])
|
||||
for entity in world.catalog.entities.values(): entity.free()
|
||||
world.free()
|
||||
quit()
|
||||
@@ -0,0 +1 @@
|
||||
uid://bboo6op14es24
|
||||
@@ -0,0 +1,56 @@
|
||||
extends SceneTree
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
|
||||
func _initialize(): call_deferred("run")
|
||||
|
||||
func report(label: String, samples: Array):
|
||||
var total: float = 0.0
|
||||
var slow: int = 0
|
||||
for sample in samples:
|
||||
total += sample
|
||||
if sample > 33.333: slow += 1
|
||||
samples.sort()
|
||||
print("%s: %d samples, mean %.3f ms, p95 %.3f ms, max %.3f ms, >33ms %d" % [label, samples.size(), total / samples.size(), samples[int(samples.size() * 0.95) - 1], samples.back(), slow])
|
||||
|
||||
func run():
|
||||
Engine.max_fps = 60
|
||||
DisplayServer.window_set_size(Vector2i(540, 960))
|
||||
print("Renderer: ", RenderingServer.get_current_rendering_method(), " adapter: ", RenderingServer.get_video_adapter_name())
|
||||
for scenario in ["ordinary", "hypergiants"]:
|
||||
for page in ([2] if "stars" in OS.get_cmdline_user_args() else [1, 2]):
|
||||
var scene = load("res://world/world.tscn").instantiate()
|
||||
var stage = scene.get_node("Stage")
|
||||
stage.owniverse.free()
|
||||
stage.set_script(load("res://tests/fixtures/benchmark_ui_stage.gd"))
|
||||
root.add_child(scene)
|
||||
await process_frame
|
||||
await process_frame
|
||||
var world = stage.owniverse
|
||||
world.speed = 6
|
||||
world.state.amount.S = E.of("1e24")
|
||||
world.state.amount.H = E.of("1e24")
|
||||
for entry in [["RD", 1234567], ["YD", 765432], ["STN", 654321]]:
|
||||
world.state.amount[entry[0]] = entry[1]
|
||||
world.batches.append_batch(world.state, entry[0], entry[1], 0, 0)
|
||||
if scenario == "hypergiants":
|
||||
world.state.amount.HG = E.of("1e20")
|
||||
world.batches.append_batch(world.state, "HG", E.of("1e20"), 0, 0)
|
||||
var start_year = E.subtract(world.catalog.entities.HG.lifespan, 1000)
|
||||
world.state.time = {"datetime": start_year, "year": start_year}
|
||||
stage.stage_page_scroller.scroll_horizontal = page * stage.PAGE_WIDTH
|
||||
for item in stage.owniverse_items.values(): item.visible = true
|
||||
if page == 2: stage._control_page(stage.owniverse_items.HG)
|
||||
stage._set_item_description(stage.owniverse_items.HG if page == 2 else stage.owniverse_items.H)
|
||||
stage._refresh_universe_ui()
|
||||
for warmup in 30: await process_frame
|
||||
stage.measuring = true
|
||||
for frame in 600: await process_frame
|
||||
stage.measuring = false
|
||||
var label: String = "%s page %d" % [scenario, page]
|
||||
report(label + " Stage CPU", stage.process_samples)
|
||||
report(label + " frame wall", stage.frame_samples)
|
||||
print("UI counters: H=", stage.owniverse_items.H.description.text, " HG=", stage.owniverse_items.HG.description.text, " date=", stage.date_time.text)
|
||||
for entity in world.catalog.entities.values(): entity.free()
|
||||
world.free()
|
||||
scene.free()
|
||||
quit()
|
||||
@@ -0,0 +1 @@
|
||||
uid://b3gmlo2vq6kk4
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
extends "res://stage/stage.gd"
|
||||
|
||||
var measuring: bool = false
|
||||
var process_samples: Array = []
|
||||
var frame_samples: Array = []
|
||||
var previous_frame: int = 0
|
||||
|
||||
# Exercise the production Stage, with isolated state and no player-save I/O.
|
||||
func load_data() -> bool: return false
|
||||
func save_data() -> void: pass
|
||||
func _notification(_what: int) -> void: pass
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if not measuring: return
|
||||
var started: int = Time.get_ticks_usec()
|
||||
if previous_frame: frame_samples.append((started - previous_frame) / 1000.0)
|
||||
previous_frame = started
|
||||
super._process(delta)
|
||||
process_samples.append((Time.get_ticks_usec() - started) / 1000.0)
|
||||
+1
@@ -0,0 +1 @@
|
||||
uid://d1wm7nav71y3o
|
||||
+1
File diff suppressed because one or more lines are too long
+30
@@ -0,0 +1,30 @@
|
||||
extends RefCounted
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
|
||||
static func seed(world, global, fragmented: bool = false, large: bool = false, stable: bool = false):
|
||||
for id in world.catalog.entities:
|
||||
world.state.amount[id] = 0
|
||||
world.state.stats.total_amount[id] = 0
|
||||
var entity = world.catalog.entities[id]
|
||||
if entity.group_as != "":
|
||||
world.state.amount[entity.group_as] = 0
|
||||
world.state.stats.total_amount[entity.group_as] = 0
|
||||
for id in ["RD", "BG", "TWD", "SNS", "MGT", "BH25", "SMBH", "SCD", "GEM", "LGG", "LSC", "LGF", "LGW", "LUB"]:
|
||||
world.state.amount[id] = E.of("1e48")
|
||||
world.state.amount.S = E.of("1e50")
|
||||
world.state.amount.H = E.of("1e48")
|
||||
world.state.binds.RD = {"HP": E.of("1e30"), "RP": E.of("1e30"), "GG": E.of("1e30"), "AB": E.of("1e30")}
|
||||
var count = E.of("6e20") if large else 6000
|
||||
for id in global.CivilizationRules:
|
||||
if E.le(global.CivilizationRules[id].span, 0): continue
|
||||
world.state.amount[id] = count
|
||||
world.state.ethics[id] = {}
|
||||
for ethic in ["m", "l", "w", "d", "c", "b"]:
|
||||
world.state.ethics[id][ethic] = E.divide(count, 6)
|
||||
world.state.batches[id] = []
|
||||
var parties: int = 10 if fragmented else 1
|
||||
for i in parties:
|
||||
var lifespan = E.of(global.CivilizationRules[id].span) if stable else 1
|
||||
world.state.batches[id].append({"amount": E.divide(count, parties), "created_from": i * 100, "created_to": 1000 + i * 100, "start_year": E.add(lifespan, i * 100), "end_year": E.add(lifespan, 1000 + i * 100)})
|
||||
world.ledger.refresh_groups(world.state)
|
||||
world.speed = 6
|
||||
@@ -0,0 +1 @@
|
||||
uid://bjloakx4qdpsj
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
extends RefCounted
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
# Frozen apportionment before the civilization performance refactor.
|
||||
|
||||
static func allocate_whole(requests: Dictionary, limit: Variant) -> Dictionary:
|
||||
var total: Variant = 0
|
||||
for amount in requests.values():
|
||||
total = E.add(total, E.floor_value(E.maximum(0, amount)))
|
||||
var available: Variant = E.floor_value(E.minimum(E.maximum(0, limit), total))
|
||||
var result: Dictionary = {}
|
||||
var order: Array = []
|
||||
var assigned: Variant = 0
|
||||
for id in requests:
|
||||
var expected: Variant = (E.mul_div(E.floor_value(E.maximum(0, requests[id])), available, total) if E.gt(total, 0) else 0)
|
||||
result[id] = E.floor_value(expected)
|
||||
assigned = E.add(assigned, result[id])
|
||||
order.append({"id": id, "rest": E.subtract(expected, result[id])})
|
||||
order.sort_custom(func ( a : Dictionary , b : Dictionary ) -> bool : return (E.gt(a.rest, b.rest) if E.ne(a.rest, b.rest) else E.lt(str(a.id), str(b.id))))
|
||||
for item : Dictionary in order:
|
||||
if E.ge(assigned, available):
|
||||
break
|
||||
if E.lt(result[item.id], E.floor_value(requests[item.id])):
|
||||
result[item.id] = E.add(result[item.id], 1)
|
||||
assigned = E.add(assigned, 1)
|
||||
return result
|
||||
|
||||
|
||||
static func sum(pop: Dictionary) -> Variant:
|
||||
var result: Variant = 0
|
||||
for n in pop.values(): result = E.add(result, n)
|
||||
return result
|
||||
|
||||
|
||||
static func split(remainders: Dictionary, key: String, count: Variant, weights: Dictionary) -> Dictionary:
|
||||
var result: Dictionary = {}
|
||||
var sum_weights: Variant = sum(weights)
|
||||
if (E.le(count, 0) or E.le(sum_weights, 0)): return result
|
||||
var expected: Dictionary = {}
|
||||
var positive_sum: Variant = 0
|
||||
for id in weights:
|
||||
expected[id] = E.add(E.mul_div(count, weights[id], sum_weights), remainders.get(E.add(E.add(key, ":"), id), 0))
|
||||
positive_sum = E.add(positive_sum, E.maximum(0, expected[id]))
|
||||
var order: Array = []
|
||||
var assigned: Variant = 0
|
||||
for id in weights:
|
||||
var normalized: Variant = (E.mul_div(E.maximum(0, expected[id]), count, positive_sum) if E.gt(positive_sum, 0) else 0)
|
||||
result[id] = E.floor_value(normalized)
|
||||
assigned = E.add(assigned, result[id])
|
||||
order.append({"id": id, "rest": E.subtract(normalized, result[id])})
|
||||
order.sort_custom(func ( a , b ) : return (E.gt(a.rest, b.rest) if E.ne(a.rest, b.rest) else E.lt(str(a.id), str(b.id))))
|
||||
for entry in order:
|
||||
if E.ge(assigned, count): break
|
||||
result[entry.id] = E.add(result[entry.id], 1)
|
||||
assigned = E.add(assigned, 1)
|
||||
for id in weights:
|
||||
remainders[E.add(E.add(key, ":"), id)] = E.clamp_value(E.subtract(expected[id], result[id]), E.neg(1), 1)
|
||||
return result
|
||||
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
uid://dxsly3qpyqrjy
|
||||
@@ -58,6 +58,19 @@ func run() -> void:
|
||||
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 pending_clock: Dictionary = preload("res://world/owniverse/simulation_time.gd").advance(world.state.time, 0.01, 1)
|
||||
data.simulation = {"requested_clock": pending_clock, "active_clock": pending_clock, "speed": 1, "productions": [{"id": "H", "amount": 7, "resource": true}]}
|
||||
manager.request_save("checkpoint", "test-password", data)
|
||||
manager.flush_saves()
|
||||
check(E.eq(manager.load_data("checkpoint", "test-password").simulation, data.simulation), "Encrypted save preserves requested time and queued production")
|
||||
var malformed: Dictionary = manager._snapshot("probe", "test-password", data).payload
|
||||
malformed.simulation.productions[0].id = "missing-entity"
|
||||
check(manager.validate_data(malformed).contains("queued production"), "Unknown queued production is rejected before resume")
|
||||
malformed = manager._snapshot("probe", "test-password", data).payload
|
||||
malformed.simulation.requested_clock.year = -1
|
||||
check(manager.validate_data(malformed).contains("simulation clock"), "Invalid requested clock is rejected before resume")
|
||||
data.erase("simulation")
|
||||
|
||||
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": E.neg(1), "state": {"unknown": true}}, false)
|
||||
|
||||
@@ -53,7 +53,7 @@ func invariants(world) -> void:
|
||||
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 (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": 4, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Civilization state validates")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Civilization state validates")
|
||||
|
||||
func run() -> void:
|
||||
var global: Node = root.get_node("Global")
|
||||
|
||||
@@ -59,7 +59,7 @@ func run() -> void:
|
||||
world.state.amount["C0.2"] = 3
|
||||
world.evolution.initialize_life_batches()
|
||||
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": 4, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Ethics state validates")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Ethics state validates")
|
||||
var restored = fixture()
|
||||
restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
|
||||
restored.evolution.initialize_life_batches()
|
||||
@@ -117,7 +117,7 @@ func run() -> void:
|
||||
world.evolution.initialize_life_batches()
|
||||
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": 4, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Capacity-limited state validates")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Capacity-limited state validates")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
|
||||
@@ -47,7 +47,7 @@ func run() -> void:
|
||||
var text: String = description.render(world, global, "RD")
|
||||
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))
|
||||
check(text.contains("40 000 000 000"), 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"))), E.add("Helium metadata and mixture: ", locale))
|
||||
text = description.render(world, global, "Shovelin", "Force")
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
extends SceneTree
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
const B = preload("res://world/owniverse/math/big_integer.gd")
|
||||
var checks: int = 0
|
||||
var failures: int = 0
|
||||
|
||||
func verify(condition: bool, label: String):
|
||||
checks += 1
|
||||
if not condition:
|
||||
failures += 1
|
||||
push_error(label)
|
||||
|
||||
func _initialize():
|
||||
call_deferred("run")
|
||||
|
||||
func run():
|
||||
var rng = RandomNumberGenerator.new()
|
||||
rng.seed = 619
|
||||
for trial in 300:
|
||||
var n: int = rng.randi_range(0, 100)
|
||||
var m: int = rng.randi_range(1, 100)
|
||||
var a: int = rng.randi_range(0, 1000)
|
||||
var b: int = rng.randi_range(0, 1000)
|
||||
var expected: int = 0
|
||||
for i in n: expected += (a * i + b) / m
|
||||
verify(E.eq(E.floor_sum(n, a, b, m), expected), "floor sum matches explicit windows")
|
||||
var huge = E.of("1e40")
|
||||
verify(E.lt(E.of("1e1000"), INF), "finite huge number is below positive infinity")
|
||||
verify(E.gt(E.of("-1e1000"), -INF), "finite huge negative number is above negative infinity")
|
||||
verify(E.gt(INF, E.of("1e1000")), "positive infinity is above finite huge number")
|
||||
verify(not E.eq(E.of("1e1000"), INF), "huge number is not infinity")
|
||||
verify(E.eq(E.floor_sum(huge, 2, 1, 2), E.divide(E.multiply(huge, E.subtract(huge, 1)), 2)), "floor sum supports huge window counts")
|
||||
var rows = JSON.parse_string(FileAccess.get_file_as_string("res://tests/fixtures/big_integer_oracle.json"))
|
||||
for row in rows:
|
||||
var a: Array = B.parse(row[0])
|
||||
var b: Array = B.parse(row[1])
|
||||
var qr: Array = B.divmod(a, b)
|
||||
verify(B.decimal(B.add(a, b)) == row[2], "independent big sum")
|
||||
verify(B.decimal(B.multiply(a, b)) == row[3], "independent big product")
|
||||
verify(B.decimal(qr[0]) == row[4], "independent big quotient")
|
||||
verify(B.decimal(qr[1]) == row[5], "independent big remainder")
|
||||
verify(E.fixed(E.multiply(E.of(row[0]), E.of(row[1]))) == row[3], "dynamic product")
|
||||
verify(E.eq(E.mul_div(E.of(row[0]), E.of(row[1]), E.of(row[1])), E.of(row[0])), "fused conservation")
|
||||
for text in ["9223372036854775807", "-9223372036854775808", "9223372036854775808", "-9223372036854775809", "1e300", "1e1000"]:
|
||||
var n = E.of(text)
|
||||
verify(E.eq(E.subtract(E.add(n, 1), n), 1), "unit preserved at " + text)
|
||||
verify(E.eq(E.decode(JSON.parse_string(JSON.stringify(E.encode(n)))), n), "round trip " + text)
|
||||
for text in ["1.25", "-1.25", "123.5"]:
|
||||
verify(E.approx(E.of(text)) == float(text), "approx fraction " + text)
|
||||
verify(not E.valid_encoded({"$exact": [[], "0000000000000000", 1]}), "reject empty blocks")
|
||||
verify(not E.valid_encoded({"$exact": [[1, 0], "0000000000000000", 1]}), "reject noncanonical blocks")
|
||||
verify(E.gt(E.power(E.of("1e400"), -1), 0) == false, "float fraction underflows beyond its range")
|
||||
verify(absf(E.approx(E.logarithm(E.of("1e1000"))) - 1000 * log(10.0)) < 1e-8, "logarithm beyond float range")
|
||||
verify(E.eq(E.square_root(E.of("1e1000")), E.of("1e500")), "square root beyond float range")
|
||||
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.state.amount.H = E.of("1e1000")
|
||||
world.state.amount.S = E.of("1e1000")
|
||||
var environment: Dictionary = world.environment.stellar_environment(world.state)
|
||||
verify(E.finite(environment.density) and E.gt(environment.density, 0), "huge density remains finite")
|
||||
verify(E.near(environment.blast, 1), "huge geometric ratio stays valid")
|
||||
for entity in world.catalog.entities.values(): entity.free()
|
||||
world.free()
|
||||
print("Dynamic math: %d checks, %d failures" % [checks, failures])
|
||||
quit(1 if failures else 0)
|
||||
@@ -0,0 +1 @@
|
||||
uid://8vkudj0tlnng
|
||||
@@ -94,7 +94,7 @@ func run():
|
||||
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": 4, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), "", "long-running state saves")
|
||||
check(saves.validate_data({"format_version": 5, "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])
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
extends SceneTree
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
||||
const Fixture = preload("res://tests/fixtures/civilization_benchmark.gd")
|
||||
var checks: int = 0
|
||||
var failures: int = 0
|
||||
|
||||
func _initialize(): call_deferred("run")
|
||||
func check(condition: bool, message: String):
|
||||
checks += 1
|
||||
if not condition:
|
||||
failures += 1
|
||||
push_error(message)
|
||||
func fixture():
|
||||
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
|
||||
Fixture.seed(world, root.get_node("Global"))
|
||||
return world
|
||||
func dispose(world):
|
||||
for entity in world.catalog.entities.values(): entity.free()
|
||||
world.free()
|
||||
|
||||
func run():
|
||||
Engine.max_fps = 0
|
||||
var direct = fixture()
|
||||
var sliced = fixture()
|
||||
for delta in [1.0 / 60.0, 0.133333333333]:
|
||||
direct.evolute(delta)
|
||||
var clock: Dictionary = Clock.advance(sliced.state.time, delta, sliced.speed)
|
||||
var budget = load("res://world/owniverse/frame_budget.gd").new(self, 1)
|
||||
await sliced.evolution.evolute_to_datetime(clock.datetime, clock, budget)
|
||||
check(E.eq(direct.state, sliced.state), "Sliced transaction exactly matches uninterrupted evolution")
|
||||
dispose(direct)
|
||||
dispose(sliced)
|
||||
direct = fixture()
|
||||
sliced = fixture()
|
||||
var initial_h: Variant = sliced.state.amount.H
|
||||
sliced.evolute_frame(1.0 / 60.0, self)
|
||||
check(sliced.frame_busy, "Heavy transaction yields before publishing")
|
||||
check(E.eq(sliced.state.time.year, 0), "Published clock stays at completed transaction")
|
||||
sliced.request_production("H", 1, true)
|
||||
check(E.eq(sliced.state.amount.H, initial_h), "Player action waits for transaction commit")
|
||||
sliced.evolute_frame(0.01, self)
|
||||
var requested: Dictionary = sliced.frame_clock.duplicate()
|
||||
sliced.speed = 3
|
||||
check(sliced.speed == 3 and sliced.state.speed == 6, "Speed change is visible and cannot mutate an unfinished transaction")
|
||||
while sliced.frame_busy: await process_frame
|
||||
direct.evolute(1.0 / 60.0)
|
||||
direct.production.produce_hs("H", 1)
|
||||
direct.speed = 3
|
||||
check(E.eq(direct.state, sliced.state), "Queued action and speed change survive publication")
|
||||
sliced.evolute_frame(0.001, self)
|
||||
var second_clock: Dictionary = Clock.advance(requested, 0.001, 3)
|
||||
var active: Dictionary = sliced.frame_active_clock.duplicate()
|
||||
while sliced.frame_busy: await process_frame
|
||||
direct.evolution.evolute_to_datetime(active.datetime, active)
|
||||
check(E.eq(direct.state, sliced.state), "Bounded frame transaction matches its exact target clock")
|
||||
check(E.eq(sliced.frame_clock, second_clock), "All elapsed time during a busy frame remains in the requested clock")
|
||||
check(sliced.frame_commits == 2, "Only complete simulation states are committed")
|
||||
dispose(direct)
|
||||
dispose(sliced)
|
||||
var original = fixture()
|
||||
original.evolute_frame(1.0 / 60.0, self)
|
||||
original.request_production("H", 123, true)
|
||||
original.evolute_frame(0.01, self)
|
||||
original.speed = 3
|
||||
var checkpoint: Dictionary = E.decode(E.encode(original.frame_checkpoint()))
|
||||
var published: Dictionary = E.decode(E.encode(original.state))
|
||||
var expected_clock: Dictionary = original.frame_active_clock.duplicate()
|
||||
dispose(original)
|
||||
for frame in 3: await process_frame
|
||||
var resumed = fixture()
|
||||
var expected = fixture()
|
||||
resumed.state = published.duplicate(true)
|
||||
expected.state = published.duplicate(true)
|
||||
resumed.restore_frame_checkpoint(checkpoint)
|
||||
resumed.evolute_frame(0.0, self)
|
||||
while resumed.frame_busy: await process_frame
|
||||
expected.evolution.evolute_to_datetime(expected_clock.datetime, expected_clock)
|
||||
expected.production.produce_hs("H", 123)
|
||||
expected.speed = 3
|
||||
check(E.eq(expected.state, resumed.state), "Checkpoint recomputes interrupted transaction and preserves queued production")
|
||||
check(E.eq(resumed.frame_clock, checkpoint.requested_clock), "Checkpoint preserves all requested elapsed time")
|
||||
dispose(resumed)
|
||||
dispose(expected)
|
||||
var cancelled = fixture()
|
||||
cancelled.evolute_frame(0.133333333333, self)
|
||||
check(cancelled.frame_busy, "Cancellation fixture has suspended work")
|
||||
var budget_ref: RefCounted = cancelled.frame_budget
|
||||
dispose(cancelled)
|
||||
for frame in 3: await process_frame
|
||||
check(budget_ref.cancelled, "Resetting the world cancels suspended work without publishing")
|
||||
var invalid = fixture()
|
||||
var before: Dictionary = invalid.state.duplicate(true)
|
||||
invalid.evolute_frame(1e308, self)
|
||||
invalid.evolute(1e308)
|
||||
check(not invalid.frame_busy and invalid.frame_clock.is_empty() and E.eq(before, invalid.state), "Overflowing elapsed float cannot enter either clock API")
|
||||
dispose(invalid)
|
||||
print("Frame evolution: %d checks, %d failures" % [checks, failures])
|
||||
quit(1 if failures else 0)
|
||||
@@ -0,0 +1 @@
|
||||
uid://te15r63sqhxs
|
||||
+2
-2
@@ -54,7 +54,7 @@ func run() -> void:
|
||||
check(E.gt(world.state.batches.HP.size(), 0), "Survivors get next attempt")
|
||||
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
||||
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": 4, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Life save validates")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Life save validates")
|
||||
var restored = fixture()
|
||||
restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
|
||||
world.evolution.evolve_batches(world.state, 11000000)
|
||||
@@ -118,7 +118,7 @@ func run() -> void:
|
||||
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": 4, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Extended life save validates")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 5, "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 E.eq(failures, 0) else 1))
|
||||
|
||||
@@ -2,6 +2,7 @@ extends SceneTree
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
|
||||
const Math = preload("res://world/owniverse/quantity_math.gd")
|
||||
const Legacy = preload("res://tests/fixtures/legacy_quantity_math.gd")
|
||||
var checks: int = 0
|
||||
var failures: int = 0
|
||||
|
||||
@@ -15,6 +16,24 @@ func check(condition: bool, message: String) -> void:
|
||||
push_error(message)
|
||||
|
||||
func run() -> void:
|
||||
var rng = RandomNumberGenerator.new()
|
||||
rng.seed = 65432
|
||||
var old_remainders: Dictionary = {}
|
||||
var new_remainders: Dictionary = {}
|
||||
for sample in 300:
|
||||
var requests: Dictionary = {}
|
||||
var weights: Dictionary = {}
|
||||
for id in ["w", "m", "b", "c", "d", "l"]:
|
||||
requests[id] = E.of(str(rng.randi_range(-10, 100)) + "." + str(rng.randi_range(0, 9)))
|
||||
weights[id] = rng.randi_range(0, 100)
|
||||
if sample % 3 == 0:
|
||||
for id in requests: requests[id] = E.multiply(requests[id], E.of("1e40"))
|
||||
var limit: Variant = E.multiply(rng.randi_range(0, 600), E.of("1e40")) if sample % 3 == 0 else rng.randi_range(0, 600)
|
||||
check(E.eq(Math.allocate_whole(requests, limit), Legacy.allocate_whole(requests, limit)), "Allocation matches legacy with limits, ties and big integers")
|
||||
var count: Variant = limit
|
||||
check(E.eq(Math.split(new_remainders, "population", count, weights), Legacy.split(old_remainders, "population", count, weights)), "Split matches legacy rounding")
|
||||
check(E.eq(new_remainders, old_remainders), "Signed remainder debt matches legacy")
|
||||
check_ledger_cache()
|
||||
var Catalog = load("res://world/owniverse/entity_catalog.gd")
|
||||
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")
|
||||
@@ -62,3 +81,32 @@ func run() -> void:
|
||||
for entity in catalog.entities.values(): entity.free()
|
||||
print(("Quantity and catalog: %d checks, %d failures" % [checks, failures]))
|
||||
quit((1 if failures else 0))
|
||||
|
||||
func check_ledger_cache() -> void:
|
||||
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
|
||||
world.state.amount.RD = E.of("1e40")
|
||||
world.state.binds.GEM = {"RD": 7}
|
||||
check(E.eq(world.ledger.physical_amounts(world.state).RD, E.add(E.of("1e40"), 7)), "Initial cached physical amount")
|
||||
var returned: Dictionary = world.ledger.physical_amounts(world.state)
|
||||
returned.RD = 0
|
||||
check(E.gt(world.ledger.physical_amounts(world.state).RD, 0), "Caller cannot mutate cached physical totals")
|
||||
world.state.binds.GEM.RD = 11
|
||||
check(E.eq(world.ledger.physical_amounts(world.state).RD, E.add(E.of("1e40"), 11)), "Nested binding change invalidates physical cache")
|
||||
world.state.amount.RD = 2
|
||||
check(E.eq(world.ledger.physical_amounts(world.state).RD, 13), "Direct count change invalidates physical cache")
|
||||
var other: Dictionary = world.state.duplicate(true)
|
||||
other.binds.clear()
|
||||
check(E.eq(world.ledger.physical_amounts(other).RD, 2), "Alternate state has independent physical totals")
|
||||
check(E.eq(world.ledger.physical_amounts(world.state).RD, 13), "Returning to original state refreshes cache")
|
||||
var revision: int = world.catalog.index_revision
|
||||
world.catalog.rebuild_group_index()
|
||||
check(world.catalog.index_revision > revision, "Catalog rebuild invalidates cached membership")
|
||||
check(E.eq(world.ledger.physical_amounts(world.state).RD, 13), "Rebuild retains correct physical totals")
|
||||
var rules: Dictionary = root.get_node("Global").CivilizationRules.duplicate(true)
|
||||
var original_stages: int = world.catalog.civilization_layout(rules).stages.size()
|
||||
rules["C2.0"].home = ""
|
||||
world.catalog.invalidate_civilization_layout()
|
||||
check(world.catalog.civilization_layout(rules).stages.size() == original_stages - 1, "Explicit rules change rebuilds civilization layout")
|
||||
for entity in world.catalog.entities.values(): entity.free()
|
||||
world.free()
|
||||
|
||||
@@ -38,8 +38,11 @@ func equivalent(a: Variant, b: Variant) -> bool:
|
||||
for i in a.size():
|
||||
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 E.lt(E.absolute(E.subtract(E.of(a), E.of(b))), E.of("1e-6"))
|
||||
if E.numeric(a) and E.numeric(b):
|
||||
# Counts must match exactly. Fractional float remainders can differ
|
||||
# when the analytic integral replaces thousands of subtractions.
|
||||
if not E.eq(E.floor_value(a), E.floor_value(b)): return false
|
||||
return E.le(E.absolute(E.subtract(a, b)), E.of("1e-8"))
|
||||
return E.eq(a, b)
|
||||
|
||||
func run() -> void:
|
||||
@@ -75,6 +78,19 @@ func run() -> void:
|
||||
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)
|
||||
# Dense completed events must not iterate a million elapsed windows.
|
||||
# An aligned, fully completed cohort produces exactly count * lifespan.
|
||||
fast = fixture(false)
|
||||
pending = {}
|
||||
var lifespan: Variant = fast.catalog.entities.SN.lifespan
|
||||
var window: Variant = E.divide(lifespan, root.get_node("Global").batch_count) if E.gt(lifespan, 10) else lifespan
|
||||
var creation_end: Variant = E.multiply(window, 1000000)
|
||||
var dense_count: Variant = E.of("1e20")
|
||||
var integral: Variant = fast.batches.append_batch(fast.state, "SN", dense_count, 0, creation_end, pending, E.add(creation_end, lifespan))
|
||||
check(E.eq(pending.SN[0].amount, dense_count), "Million-window aggregation conserves every event")
|
||||
check(E.eq(integral, E.multiply(dense_count, lifespan)), "Million-window aggregation preserves lifetime integral")
|
||||
check(fast.state.batches.SN.is_empty(), "Completed million-window cohort leaves no transient batches")
|
||||
dispose(fast)
|
||||
# Sparse explosions: thousands of time windows but very few actual events.
|
||||
# Exact interval edges and partial final windows exercise the jump boundary.
|
||||
for count in [1, 3, 17]:
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
extends SceneTree
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
var checks: int = 0
|
||||
var failures: int = 0
|
||||
func _initialize(): call_deferred("run")
|
||||
func check(value: bool, message: String):
|
||||
checks += 1
|
||||
if not value:
|
||||
failures += 1
|
||||
push_error(message)
|
||||
func run():
|
||||
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
|
||||
world.catalog.entities.N.lifespan = 24
|
||||
var width: Variant = E.divide(24, root.get_node("Global").batch_count)
|
||||
for count in [1, 2, 7, 11, 100, E.of("1e40")]:
|
||||
for interval in [[E.of("0.25"), E.of("101.75")], [0, 120], [3, 3]]:
|
||||
world.state.batches = {}
|
||||
world.state.batch_origins = {"N": E.of("0.125")}
|
||||
var expected: Array = []
|
||||
var origin: Variant = world.state.batch_origins.N
|
||||
var index: Variant = E.floor_value(E.divide(E.subtract(interval[0], origin), width))
|
||||
var assigned: Variant = 0
|
||||
var cursor: Variant = interval[0]
|
||||
while true:
|
||||
var start: Variant = E.add(origin, E.multiply(index, width))
|
||||
var end: Variant = E.add(origin, E.multiply(E.add(index, 1), width))
|
||||
if E.le(end, cursor):
|
||||
index = E.add(index, 1)
|
||||
continue
|
||||
var until: Variant = E.minimum(interval[1], end)
|
||||
var amount: Variant = E.subtract(count, assigned)
|
||||
if E.gt(interval[1], interval[0]) and E.lt(until, interval[1]):
|
||||
amount = E.subtract(E.floor_value(E.mul_div(count, E.subtract(until, interval[0]), E.subtract(interval[1], interval[0]))), assigned)
|
||||
if E.gt(amount, 0):
|
||||
expected.append({"created_from": start, "created_to": end, "start_year": E.add(start, 24), "end_year": E.add(end, 24), "amount": amount})
|
||||
assigned = E.add(assigned, amount)
|
||||
if E.ge(until, interval[1]): break
|
||||
cursor = end
|
||||
index = E.add(index, 1)
|
||||
world.batches.append_batch(world.state, "N", count, interval[0], interval[1])
|
||||
var actual: Array = []
|
||||
for batch in world.state.batches.N:
|
||||
if E.gt(batch.amount, 0): actual.append(batch)
|
||||
check(E.eq(actual, expected), "Sparse jump preserves every nonempty quota and boundary")
|
||||
var total: Variant = 0
|
||||
for batch in actual: total = E.add(total, batch.amount)
|
||||
check(E.eq(total, count), "Sparse jump preserves complete whole population")
|
||||
for entity in world.catalog.entities.values(): entity.free()
|
||||
world.free()
|
||||
print("Sparse batches: %d checks, %d failures" % [checks, failures])
|
||||
quit(1 if failures else 0)
|
||||
@@ -0,0 +1 @@
|
||||
uid://gtvie7p6j62x
|
||||
@@ -96,7 +96,7 @@ func run() -> void:
|
||||
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(E.eq(saves.validate_data({"format_version": 4, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Planet carry save validates")
|
||||
check(E.eq(saves.validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Planet carry save validates")
|
||||
restored = fixture()
|
||||
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"))
|
||||
@@ -118,7 +118,7 @@ func run() -> void:
|
||||
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": 4, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Inherited state validates")
|
||||
check(E.eq(saves.validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Inherited state validates")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
@@ -158,7 +158,7 @@ func run() -> void:
|
||||
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": 4, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Blast state validates")
|
||||
check(E.eq(saves.validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Blast state validates")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
|
||||
+2
-2
@@ -51,10 +51,10 @@ func run() -> void:
|
||||
for invalid in [0, E.neg(1), NAN, INF, 1e308]:
|
||||
world.evolute(invalid)
|
||||
check(E.eq(world.state, before), "Invalid/overflow interval rejected")
|
||||
check(E.eq(saves.validate_data({"format_version": 4, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Fractional save is valid")
|
||||
check(E.eq(saves.validate_data({"format_version": 5, "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 = E.of("0.25")
|
||||
check(E.ne(saves.validate_data({"format_version": 4, "app_version": "", "ui": {"current_page": 0}, "state": invalid_save}), ""), "Reject inconsistent fractional clock")
|
||||
check(E.ne(saves.validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": invalid_save}), ""), "Reject inconsistent fractional clock")
|
||||
dispose(world)
|
||||
|
||||
for fps in [30, 60, 144]:
|
||||
|
||||
@@ -133,7 +133,7 @@ func run() -> void:
|
||||
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": 4, "app_version": "test", "ui": {"current_page": 1}, "state": world.state}
|
||||
var payload: Dictionary = {"format_version": 5, "app_version": "test", "ui": {"current_page": 1}, "state": world.state}
|
||||
var manager: Node = root.get_node("SaveManager")
|
||||
check(E.eq(manager.validate_data(payload), ""), "New parameters and trends pass save validation")
|
||||
var old: Dictionary = payload.duplicate(true)
|
||||
|
||||
Reference in New Issue
Block a user