132 lines
4.8 KiB
GDScript
132 lines
4.8 KiB
GDScript
extends SceneTree
|
|
|
|
const DeterministicRngScript = preload("res://scripts/core/deterministic_rng.gd")
|
|
const SimulationDigestScript = preload("res://scripts/core/simulation_digest.gd")
|
|
|
|
var _checks := 0
|
|
var _failures := 0
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
print("S0: deterministic RNG and simulation digest")
|
|
_test_stream_reproducibility()
|
|
_test_stream_isolation()
|
|
_test_snapshot_restore()
|
|
_test_unbiased_range_contract()
|
|
_test_digest_normalization()
|
|
_test_digest_sequence_sensitivity()
|
|
_test_digest_float_normalization()
|
|
|
|
if _failures == 0:
|
|
print("PASS: %d checks" % _checks)
|
|
else:
|
|
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
|
|
quit(_failures)
|
|
|
|
|
|
func _test_stream_reproducibility() -> void:
|
|
var first = DeterministicRngScript.from_root_seed(20260827, &"guest_ai")
|
|
var second = DeterministicRngScript.from_root_seed(20260827, &"guest_ai")
|
|
var first_values: Array[int] = []
|
|
var second_values: Array[int] = []
|
|
for draw_index in range(64):
|
|
first_values.append(first.next_u32())
|
|
second_values.append(second.next_u32())
|
|
_expect(first_values == second_values, "same root seed and stream ID reproduce the sequence")
|
|
_expect(first.get_counter() == 64, "raw draw counter advances exactly once per sample")
|
|
|
|
|
|
func _test_stream_isolation() -> void:
|
|
var guest = DeterministicRngScript.from_root_seed(20260827, &"guest_ai")
|
|
var events = DeterministicRngScript.from_root_seed(20260827, &"event_service")
|
|
var guest_values: Array[int] = []
|
|
var event_values: Array[int] = []
|
|
for draw_index in range(16):
|
|
guest_values.append(guest.next_u32())
|
|
event_values.append(events.next_u32())
|
|
_expect(guest_values != event_values, "different subsystem stream IDs derive isolated sequences")
|
|
|
|
|
|
func _test_snapshot_restore() -> void:
|
|
var original = DeterministicRngScript.from_root_seed(-17, &"authority_service")
|
|
for draw_index in range(11):
|
|
original.next_u32()
|
|
var saved_state: Dictionary = original.snapshot()
|
|
var expected_tail: Array[int] = []
|
|
for draw_index in range(20):
|
|
expected_tail.append(original.next_u32())
|
|
|
|
var restored = DeterministicRngScript.from_root_seed(0, &"temporary")
|
|
_expect(restored.restore(saved_state), "valid RNG snapshot restores")
|
|
var actual_tail: Array[int] = []
|
|
for draw_index in range(20):
|
|
actual_tail.append(restored.next_u32())
|
|
_expect(actual_tail == expected_tail, "restored state continues at the exact next sample")
|
|
_expect(restored.get_counter() == 31, "snapshot restores and continues the draw counter")
|
|
|
|
|
|
func _test_unbiased_range_contract() -> void:
|
|
var rng = DeterministicRngScript.from_root_seed(99, &"range_test")
|
|
var all_samples_inside := true
|
|
for draw_index in range(4096):
|
|
var sample: int = rng.randi_range(-7, 13)
|
|
all_samples_inside = all_samples_inside and sample >= -7 and sample <= 13
|
|
_expect(all_samples_inside, "inclusive integer range never produces an out-of-range value")
|
|
var counter_before_constant: int = rng.get_counter()
|
|
_expect(rng.randi_range(5, 5) == 5, "single-value range returns that value")
|
|
_expect(rng.get_counter() == counter_before_constant, "single-value range consumes no random sample")
|
|
|
|
|
|
func _test_digest_normalization() -> void:
|
|
var first := {}
|
|
first[&"resources"] = {&"gold": 4500, &"mana": 100}
|
|
first[&"minute"] = 721
|
|
first[&"positions"] = [Vector2i(3, 4), Vector2i(-2, 8)]
|
|
|
|
var second := {}
|
|
second[&"positions"] = [Vector2i(3, 4), Vector2i(-2, 8)]
|
|
second[&"minute"] = 721
|
|
var reversed_resources := {}
|
|
reversed_resources[&"mana"] = 100
|
|
reversed_resources[&"gold"] = 4500
|
|
second[&"resources"] = reversed_resources
|
|
|
|
var first_digest: String = SimulationDigestScript.compute(first)
|
|
var second_digest: String = SimulationDigestScript.compute(second)
|
|
_expect(first_digest.length() == 64, "simulation digest is a 256-bit lowercase hexadecimal value")
|
|
_expect(first_digest == second_digest, "dictionary insertion order does not affect the digest")
|
|
|
|
|
|
func _test_digest_sequence_sensitivity() -> void:
|
|
var forward: String = SimulationDigestScript.compute([1, 2, 3])
|
|
var reversed: String = SimulationDigestScript.compute([3, 2, 1])
|
|
_expect(
|
|
forward == "4e32bb2921e1b879d5b8778789d3bc7e46559d4448e95f9c2fed427d7e5c7b93",
|
|
"canonical digest format matches its versioned reference vector"
|
|
)
|
|
_expect(forward != reversed, "array order remains significant")
|
|
_expect(
|
|
SimulationDigestScript.compute({"value": 1}) != SimulationDigestScript.compute({"value": "1"}),
|
|
"value types remain significant"
|
|
)
|
|
|
|
|
|
func _test_digest_float_normalization() -> void:
|
|
_expect(
|
|
SimulationDigestScript.compute({"zero": 0.0}) == SimulationDigestScript.compute({"zero": -0.0}),
|
|
"positive and negative floating-point zero share one canonical representation"
|
|
)
|
|
|
|
|
|
func _expect(condition: bool, label: String) -> void:
|
|
_checks += 1
|
|
if condition:
|
|
print(" ✓ %s" % label)
|
|
return
|
|
_failures += 1
|
|
push_error(" ✗ %s" % label)
|