Implement short-lived stellar event handling and optimize batch processing; add tests for new functionality
This commit is contained in:
@@ -1,5 +1,13 @@
|
||||
# Проверки поэтапного переноса из Owniverse.U
|
||||
|
||||
## Прямой расчёт завершившихся сверхновых
|
||||
|
||||
```sh
|
||||
godot --headless --path Owniverse.GI --script res://tests/test_short_lived_events.gd
|
||||
```
|
||||
|
||||
Сравниваются ускоренный путь и прежний путь с созданием всех партий, отключаемый только в тестовой фикстуре. Проверяются LRG, HRG, RSG и RHG на границах коротких и длинных интервалов, существующие SN, нехватка энергии, ресурсы, бласты, привязки, остатки округления и живые партии. Дополнительно проверяется, что длинный интервал создаёт не более двух живых окон и одну запись завершений, сохраняя общее количество. Сохранение игрока этот тест не использует.
|
||||
|
||||
`test_stellar_rules.gd` также проверяет автоматическое создание HBD: долю 0,1% нового автоматического водорода, появление по одному со случайным округлением, сохранение оплаченного остатка топлива, отсутствие повторного начисления на старый запас, продолжение случайной последовательности после сериализации и переход HBD → CBD с сохранением счётчика BD. Формулы описаны в `../simulation_rules.md`.
|
||||
|
||||
## Фоновое сохранение и частота обновления интерфейса
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
extends "res://world/owniverse/owniverse.gd"
|
||||
|
||||
# Keep the old materialized-batch path as an independent comparison mode.
|
||||
func _uses_short_lived_fast_path(_id: String) -> bool:
|
||||
return false
|
||||
+1
@@ -0,0 +1 @@
|
||||
uid://cn6hhpjw1sjpb
|
||||
@@ -92,6 +92,22 @@ func run() -> void:
|
||||
check(reader.load_data("shutdown", "test-password").state.energy == 6.0, "Teardown drains final pending snapshot")
|
||||
reader.free()
|
||||
world.free()
|
||||
# Exercise the stage entry point too: manager tests alone do not catch a
|
||||
# regression back to the synchronous API in stage.gd.
|
||||
var shared_manager: Node = root.get_node("SaveManager")
|
||||
var original_directory: String = shared_manager.save_directory
|
||||
shared_manager.save_directory = directory
|
||||
var stage = load("res://stage/stage.gd").new()
|
||||
stage.pending_page = 2
|
||||
stage.owniverse.state.energy = 42.0
|
||||
stage.save_data()
|
||||
check(shared_manager.is_saving(), "Stage queues a background save instead of waiting for disk")
|
||||
stage.owniverse.state.energy = 43.0
|
||||
shared_manager.flush_saves()
|
||||
check(shared_manager.load_data("owniverse", "qwerty").state.energy == 42.0, "Stage save writes an independent snapshot")
|
||||
stage.owniverse.free()
|
||||
stage.free()
|
||||
shared_manager.save_directory = original_directory
|
||||
global.save_plaintext_copy = old_plaintext
|
||||
for filename in DirAccess.get_files_at(directory):
|
||||
DirAccess.remove_absolute(directory.path_join(filename))
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
extends SceneTree
|
||||
var checks: int = 0
|
||||
var failures: int = 0
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func check(condition: bool, message: String) -> void:
|
||||
checks += 1
|
||||
if not condition:
|
||||
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()
|
||||
for id in root.get_node("Global").OwniverseEntities:
|
||||
world.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.entities[id].group_as != "":
|
||||
world.state.amount[world.entities[id].group_as] = 0.0
|
||||
world.state.stats.total_amount[world.entities[id].group_as] = 0.0
|
||||
return world
|
||||
|
||||
func dispose(world) -> void:
|
||||
for entity in world.entities.values(): entity.free()
|
||||
world.free()
|
||||
|
||||
func equivalent(a: Variant, b: Variant) -> bool:
|
||||
if a is Dictionary and b is Dictionary:
|
||||
if a.size() != b.size(): return false
|
||||
for key in a:
|
||||
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
|
||||
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 absf(float(a) - float(b)) < 1e-6
|
||||
return 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]:
|
||||
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}]
|
||||
# Existing short-lived populations test integration with new arrivals.
|
||||
fast.state.amount.SN = 3.0
|
||||
fast._append_batch(fast.state, "SN", 3.0, 100.5, 100.5)
|
||||
if year == 104.75: fast.state.energy = 7.0
|
||||
legacy.state = fast.state.duplicate(true)
|
||||
fast._evolve_batches(fast.state, year)
|
||||
legacy._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])
|
||||
for id in ["SN", "DSN", "BSN", "HN", "GRB", "SGRB", "LGRB", "HGRB"]:
|
||||
check(fast.state.batches.get(id, []).size() <= 3, "Only surviving event windows remain")
|
||||
dispose(fast)
|
||||
dispose(legacy)
|
||||
var fast = fixture(false)
|
||||
var pending: Dictionary = {}
|
||||
fast._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")
|
||||
dispose(fast)
|
||||
print("Short-lived events: %d checks, %d failures" % [checks, failures])
|
||||
quit(0 if failures == 0 else 1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dp7a88n0mmyb5
|
||||
@@ -36,6 +36,17 @@ func dispose(world) -> void:
|
||||
|
||||
func run() -> void:
|
||||
var world = fixture()
|
||||
for id in ["DSN", "SN", "BSN", "HN", "SGRB", "GRB", "LGRB", "HGRB"]:
|
||||
check(world._evolution_products(world.state, world.entities[id], 3.0).blast == 3.0 * world.entities[id].blast, "Entity blast determines power: " + id)
|
||||
world.entities.SN.output_and.blast = 999.0
|
||||
world.entities.SN.blast = 4
|
||||
check(world._evolution_products(world.state, world.entities.SN, 2.0).blast == 8.0, "Imported output cannot override or double blast power")
|
||||
world.entities.SN.blast = 0
|
||||
check(not world._evolution_products(world.state, world.entities.SN, 2.0).has("blast"), "Zero entity blast disables external event")
|
||||
world.entities.SN.output_or = {"positive": "blast:999", "negative": "blast:999", "chance": 50.0}
|
||||
check(not world._evolution_products(world.state, world.entities.SN, 2.0).has("blast"), "Alternative output cannot restore disabled blast")
|
||||
dispose(world)
|
||||
world = fixture()
|
||||
var rd = world.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")
|
||||
|
||||
Reference in New Issue
Block a user