Files
rthythm_archor_game/tests/test_time_anchor_scheduling.gd
T
2026-07-06 22:59:47 -07:00

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)