Files
owniverse/tests/test_frame_evolution.gd
T

121 lines
6.2 KiB
GDScript
Raw Normal View History

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)
var adaptive = fixture()
adaptive.frame_clock = Clock.advance(adaptive.state.time, 10.0, 6)
var debt_clock: Dictionary = adaptive.frame_clock.duplicate(true)
check(adaptive.update_frame_step() == 0.2, "Growing debt increases step to 0.2 seconds")
check(adaptive.update_frame_step() == 0.5, "Large debt increases step to bounded 0.5 seconds")
check(adaptive.update_frame_step() == 0.5 and E.eq(adaptive.frame_clock, debt_clock), "Maximum step preserves requested time")
adaptive.frame_clock = Clock.advance(adaptive.state.time, 0.3, 6)
check(adaptive.update_frame_step() == 0.5, "Hysteresis keeps step between thresholds")
adaptive.frame_clock = Clock.advance(adaptive.state.time, 0.05, 6)
check(adaptive.update_frame_step() == 0.2, "Small debt reduces step")
check(adaptive.update_frame_step() == 0.1, "Caught-up simulation returns to base step")
adaptive.frame_clock = Clock.advance(adaptive.state.time, 0.15, 6)
adaptive._frame_cost_ratio = 1.1
check(adaptive.update_frame_step() == 0.2, "Measured slow transactions increase step before two-step debt")
adaptive.frame_clock = Clock.advance(adaptive.state.time, 0.05, 6)
adaptive.speed = 3
check(adaptive.update_frame_step() == 1.0 / 60.0, "Low speeds retain 60 Hz scheduling")
adaptive.speed = 6
check(adaptive.update_frame_step() == 0.1, "Speed switch resets adaptive step")
dispose(adaptive)
print("Frame evolution: %d checks, %d failures" % [checks, failures])
quit(1 if failures else 0)