Files
TheTree_mock/tests/s0/test_kernel_and_save.gd

124 lines
5.8 KiB
GDScript

extends RefCounted
static func run(expect: Callable) -> void:
_test_fixed_tick_order(expect)
_test_frame_budget_and_overload(expect)
_test_boundary_pause_stops_catch_up(expect)
_test_apply_gate_aborts_before_clock_increment(expect)
_test_snapshot_restore(expect)
_test_save_round_trip(expect)
static func _test_fixed_tick_order(expect: Callable) -> void:
var clock = load("res://autoload/game_clock.gd").new()
clock.reset(1, 6, 0)
var phases: Array[int] = []
clock.tick_phase_started.connect(func(phase: int, _minute: int) -> void: phases.append(phase))
var handler_order: Array[StringName] = []
clock.register_tick_handler(clock.TickPhase.MANA, &"z_last", func(_minute: int) -> void: handler_order.append(&"z_last"))
clock.register_tick_handler(clock.TickPhase.MANA, &"a_first", func(_minute: int) -> void: handler_order.append(&"a_first"))
clock.advance_one_minute()
expect.call(clock.absolute_minute == 361, "fixed tick advances exactly one game minute")
expect.call(phases == range(clock.TickPhase.size()), "tick phases run in the specified order")
expect.call(handler_order == [&"a_first", &"z_last"], "phase handlers use stable id order")
clock.free()
static func _test_frame_budget_and_overload(expect: Callable) -> void:
var clock = load("res://autoload/game_clock.gd").new()
clock.reset(1, 6, 0)
var processed: int = clock.process_real_delta(5.0)
expect.call(processed == clock.MAX_TICKS_PER_FRAME, "one frame runs at most eight ticks")
expect.call(clock.absolute_minute == 368, "excess ticks remain queued instead of being skipped")
clock.reset(1, 6, 0)
clock.process_real_delta(100.0)
expect.call(clock.paused and clock.get_backlog_minutes() > 120, "large backlog pauses the simulation")
clock.free()
static func _test_boundary_pause_stops_catch_up(expect: Callable) -> void:
var clock = load("res://autoload/game_clock.gd").new()
clock.reset(1, 6, 0)
clock.register_tick_handler(
clock.TickPhase.TIME_BOUNDARIES,
&"test.pause_boundary",
func(minute: int) -> void:
if minute == 361:
clock.set_paused(true)
)
var processed: int = clock.process_real_delta(5.0)
expect.call(processed == 1, "a boundary pause stops the remaining frame tick budget")
expect.call(clock.absolute_minute == 361, "catch-up cannot advance beyond the pausing boundary")
clock.reset(1, 6, 0)
clock.advance_minutes(8)
expect.call(clock.absolute_minute == 361, "an initially-running manual batch stops when a handler pauses")
clock.unregister_tick_handler(clock.TickPhase.TIME_BOUNDARIES, &"test.pause_boundary")
clock.reset(1, 6, 0)
clock.set_paused(true)
clock.advance_minutes(2)
expect.call(clock.absolute_minute == 362, "an already-paused clock retains explicit manual stepping")
clock.free()
static func _test_apply_gate_aborts_before_clock_increment(expect: Callable) -> void:
var clock = load("res://autoload/game_clock.gd").new()
clock.reset(1, 6, 0)
clock.register_tick_handler(
clock.TickPhase.APPLY_COMMANDS,
&"test.abort_tick",
func(_minute: int) -> void:
clock.set_paused(true)
clock.request_current_tick_abort()
)
var starting_version: int = clock.snapshot_version
var processed: int = clock.process_real_delta(5.0)
expect.call(processed == 0, "an APPLY gate abort consumes no catch-up tick")
expect.call(
clock.absolute_minute == 360 and clock.snapshot_version == starting_version,
"an APPLY gate abort leaves Clock minute and snapshot version exact"
)
clock.advance_minutes(1)
expect.call(
clock.absolute_minute == 360 and clock.snapshot_version == starting_version,
"manual stepping cannot bypass an APPLY gate abort"
)
clock.free()
static func _test_snapshot_restore(expect: Callable) -> void:
var clock = load("res://autoload/game_clock.gd").new()
var state = load("res://autoload/game_state.gd").new()
clock.reset(2, 5, 30)
state.reset()
var starting_revision: int = state.revision
expect.call(state.apply_resource_deltas({&"gold": -250, &"materials": -5}), "multi-resource transaction succeeds")
expect.call(state.revision == starting_revision + 1, "atomic resource transaction creates one revision")
var before_failed: Dictionary = state.create_snapshot()
expect.call(not state.apply_resource_deltas({&"gold": -999999, &"mana": 10}), "invalid transaction is rejected")
expect.call(state.create_snapshot() == before_failed, "rejected transaction changes no resource")
var snapshot := SimulationSnapshot.capture(clock, state)
clock.advance_minutes(10)
state.add_resource(&"gold", 999)
expect.call(SimulationSnapshot.restore(snapshot, clock, state), "valid snapshot restores atomically")
expect.call(clock.absolute_minute == int(snapshot[&"clock"][&"absolute_minute"]), "clock snapshot restores exact minute")
expect.call(state.create_snapshot() == snapshot[&"game_state"], "game state snapshot restores exactly")
var minute_before_skip: int = clock.absolute_minute
expect.call(clock.process_real_delta(1.0) == 0 and clock.absolute_minute == minute_before_skip, "first frame after load does not advance")
clock.free()
state.free()
static func _test_save_round_trip(expect: Callable) -> void:
var save_path := "user://saves/s0_round_trip"
SaveService.delete_save(save_path)
var clock = load("res://autoload/game_clock.gd").new()
var state = load("res://autoload/game_state.gd").new()
clock.reset(3, 18, 45)
state.reset()
state.apply_resource_deltas({&"gold": -700, &"mana": 25})
var snapshot := SimulationSnapshot.capture(clock, state, {&"next_entity_id": 42})
var save_result := SaveService.save_snapshot(save_path, snapshot, {&"display_name": "S0 Round Trip"})
expect.call(save_result[&"ok"], "snapshot is written atomically")
var load_result := SaveService.load_snapshot(save_path)
expect.call(load_result[&"ok"], "saved snapshot passes checksum and schema validation")
if load_result[&"ok"]:
expect.call(load_result[&"snapshot"] == snapshot, "save and load preserve the complete snapshot")
SaveService.delete_save(save_path)
clock.free()
state.free()