124 lines
5.8 KiB
GDScript
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()
|