121 lines
5.0 KiB
GDScript
121 lines
5.0 KiB
GDScript
extends SceneTree
|
|
|
|
var failures: Array[String] = []
|
|
|
|
|
|
func _init() -> void:
|
|
_run.call_deferred()
|
|
|
|
|
|
func _run() -> void:
|
|
await process_frame
|
|
_check_frequency_profile_lookup()
|
|
var first_run := await _run_scheduling_scenario()
|
|
var second_run := await _run_scheduling_scenario()
|
|
_expect_equal(str(first_run), str(second_run), "identical fact sequences should produce identical anchor layouts")
|
|
_finish()
|
|
|
|
|
|
func _check_frequency_profile_lookup() -> void:
|
|
var profile: Resource = load("res://resources/time_anchor_frequency_default.tres")
|
|
if profile == null:
|
|
failures.append("default frequency profile should load")
|
|
return
|
|
var expectations := {0: 1, 7: 1, 8: 2, 15: 2, 16: 4, 120: 4}
|
|
for streak: int in expectations.keys():
|
|
_expect_int(int(profile.call("anchors_per_window_for_streak", streak)), expectations[streak], "anchors per 4-beat window for streak %d" % streak)
|
|
_expect_equal(str(profile.call("anchor_offsets_for_streak", 0)), str([4]), "base tier should place one anchor at the end of the 4-beat window")
|
|
_expect_equal(str(profile.call("anchor_offsets_for_streak", 8)), str([2, 4]), "middle tier should place two anchors in the 4-beat window")
|
|
_expect_equal(str(profile.call("anchor_offsets_for_streak", 16)), str([1, 2, 3, 4]), "top tier should upgrade every beat in the 4-beat window")
|
|
|
|
|
|
func _run_scheduling_scenario() -> Array:
|
|
var bus: Node = load("res://autoload/event_bus.gd").new()
|
|
bus.name = "EventBus"
|
|
root.add_child(bus)
|
|
var manager: Node = load("res://autoload/time_phase_manager.gd").new()
|
|
manager.name = "TimePhaseManager"
|
|
root.add_child(manager)
|
|
var system: Node = load("res://autoload/time_anchor_system.gd").new()
|
|
system.name = "TimeAnchorSystem"
|
|
root.add_child(system)
|
|
await process_frame
|
|
|
|
var scheduled_beats: Array = []
|
|
bus.connect("time_anchor_scheduled", func(anchor: Dictionary) -> void:
|
|
scheduled_beats.append([int(anchor.get("beat", -1)), StringName(str(anchor.get("source", &"-")))])
|
|
)
|
|
|
|
# Base tier (streak 0): one anchor per 4-beat window, candidate 4 locks when the lead window opens.
|
|
system.call("update_periodic_scheduling", 0.0)
|
|
_expect_int(scheduled_beats.size(), 0, "candidate outside the lead window must not lock")
|
|
system.call("update_periodic_scheduling", 2.1)
|
|
_expect_int(scheduled_beats.size(), 1, "candidate entering the lead window should lock")
|
|
if not scheduled_beats.is_empty():
|
|
_expect_int(scheduled_beats[0][0], 4, "first periodic anchor should land on beat 4")
|
|
_expect_equal(scheduled_beats[0][1], &"periodic", "periodic anchor should carry the periodic source")
|
|
|
|
# Locked anchors do not retro-change when the streak tier jumps.
|
|
bus.emit_signal("streak_changed", 16)
|
|
system.call("update_periodic_scheduling", 2.2)
|
|
_expect_int(int(system.call("anchored_beat_count")), 1, "tier change must not retro-modify locked anchors")
|
|
|
|
# Top tier: every beat in the next 4-beat window can become an anchor.
|
|
system.call("update_periodic_scheduling", 3.5)
|
|
_expect_bool(beats_only_of(scheduled_beats).has(5), true, "top tier should lock beat 5 in the next window")
|
|
system.call("update_periodic_scheduling", 4.5)
|
|
_expect_bool(beats_only_of(scheduled_beats).has(6), true, "top tier should lock beat 6 in the next window")
|
|
|
|
# Explicit chart anchor restarts the next 4-beat window from its beat.
|
|
bus.emit_signal("streak_changed", 0)
|
|
system.call("schedule_time_anchor", 10, &"chart")
|
|
system.call("update_periodic_scheduling", 8.5)
|
|
var beats_only: Array = scheduled_beats.map(func(entry: Array) -> int: return entry[0])
|
|
_expect_bool(beats_only.count(10) == 1, true, "explicit anchor beat should appear exactly once")
|
|
system.call("update_periodic_scheduling", 12.1)
|
|
_expect_bool(beats_only_of(scheduled_beats).has(14), true, "periodic count should restart from the explicit anchor's 4-beat window (10 + 4 = 14)")
|
|
|
|
# chart_reset clears the schedule and the streak.
|
|
bus.emit_signal("chart_reset", &"scenario")
|
|
_expect_int(int(system.call("anchored_beat_count")), 0, "chart_reset should clear scheduled anchors")
|
|
_expect_int(int(system.get("current_streak")), 0, "chart_reset should clear the cached streak")
|
|
|
|
var result := beats_only_of(scheduled_beats)
|
|
root.remove_child(system)
|
|
system.free()
|
|
root.remove_child(manager)
|
|
manager.free()
|
|
root.remove_child(bus)
|
|
bus.free()
|
|
await process_frame
|
|
return result
|
|
|
|
|
|
func beats_only_of(entries: Array) -> Array:
|
|
return entries.map(func(entry: Array) -> int: return entry[0])
|
|
|
|
|
|
func _expect_equal(actual: Variant, expected: Variant, label: String) -> void:
|
|
if actual != expected:
|
|
failures.append("%s: expected %s, got %s" % [label, expected, actual])
|
|
|
|
|
|
func _expect_int(actual: int, expected: int, label: String) -> void:
|
|
if actual != expected:
|
|
failures.append("%s: expected %d, got %d" % [label, expected, actual])
|
|
|
|
|
|
func _expect_bool(actual: bool, expected: bool, label: String) -> void:
|
|
if actual != expected:
|
|
failures.append("%s: expected %s, got %s" % [label, expected, actual])
|
|
|
|
|
|
func _finish() -> void:
|
|
if failures.is_empty():
|
|
print("PASS time anchor scheduling")
|
|
quit(0)
|
|
else:
|
|
for failure: String in failures:
|
|
push_error(failure)
|
|
quit(1)
|