Preserve approved pixel tower prototype as development baseline
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
class_name DeterministicRng
|
||||
extends RefCounted
|
||||
|
||||
## A small deterministic random stream for simulation code.
|
||||
##
|
||||
## Each subsystem must create its own stream from the shared root seed and a
|
||||
## stable stream ID. Saving both state and counter allows an exact continuation
|
||||
## without coupling unrelated systems through one global random sequence.
|
||||
|
||||
const ALGORITHM_ID: StringName = &"xorshift32-sha256-v1"
|
||||
const SNAPSHOT_VERSION := 1
|
||||
const UINT32_MASK := 0xffffffff
|
||||
const UINT32_RANGE := 0x100000000
|
||||
const FALLBACK_NON_ZERO_STATE := 0x6d2b79f5
|
||||
|
||||
var _root_seed := 0
|
||||
var _stream_id: StringName = &"default"
|
||||
var _state := FALLBACK_NON_ZERO_STATE
|
||||
var _counter := 0
|
||||
|
||||
|
||||
static func from_root_seed(root_seed: int, stream_id: StringName) -> Variant:
|
||||
# Loading by resource path keeps this factory usable on a clean checkout,
|
||||
# before Godot has generated its global class-name cache.
|
||||
var result: Variant = load("res://scripts/core/deterministic_rng.gd").new()
|
||||
result.reset(root_seed, stream_id)
|
||||
return result
|
||||
|
||||
|
||||
func reset(root_seed: int, stream_id: StringName) -> void:
|
||||
_root_seed = root_seed
|
||||
_stream_id = stream_id
|
||||
_state = _derive_non_zero_state(root_seed, stream_id)
|
||||
_counter = 0
|
||||
|
||||
|
||||
func next_u32() -> int:
|
||||
# xorshift32 operates only on non-negative 32-bit values. Intermediates stay
|
||||
# below the signed 64-bit limit used by GDScript integers.
|
||||
var value := _state
|
||||
value ^= (value << 13) & UINT32_MASK
|
||||
value ^= value >> 17
|
||||
value ^= (value << 5) & UINT32_MASK
|
||||
_state = value & UINT32_MASK
|
||||
_counter += 1
|
||||
return _state
|
||||
|
||||
|
||||
func randi_range(minimum: int, maximum: int) -> int:
|
||||
if minimum > maximum:
|
||||
push_error("DeterministicRng range minimum must not exceed maximum")
|
||||
return minimum
|
||||
|
||||
# A crossing-zero subtraction can overflow only for ranges much larger than
|
||||
# this generator supports, so reject it before evaluating the subtraction.
|
||||
var width := 0
|
||||
if minimum < 0 and maximum > 0:
|
||||
if maximum > minimum + UINT32_MASK:
|
||||
push_error("DeterministicRng supports ranges of at most 2^32 values")
|
||||
return minimum
|
||||
width = maximum - minimum
|
||||
else:
|
||||
width = maximum - minimum
|
||||
if width > UINT32_MASK:
|
||||
push_error("DeterministicRng supports ranges of at most 2^32 values")
|
||||
return minimum
|
||||
|
||||
var span := width + 1
|
||||
if span == 1:
|
||||
return minimum
|
||||
|
||||
# Rejection sampling avoids modulo bias for spans which do not divide 2^32.
|
||||
var acceptance_limit := UINT32_RANGE - (UINT32_RANGE % span)
|
||||
while true:
|
||||
var sample := next_u32()
|
||||
if sample < acceptance_limit:
|
||||
return minimum + (sample % span)
|
||||
return minimum
|
||||
|
||||
|
||||
func snapshot() -> Dictionary:
|
||||
return {
|
||||
"version": SNAPSHOT_VERSION,
|
||||
"algorithm": String(ALGORITHM_ID),
|
||||
"root_seed": _root_seed,
|
||||
"stream_id": String(_stream_id),
|
||||
"state": _state,
|
||||
"counter": _counter,
|
||||
}
|
||||
|
||||
|
||||
func restore(saved_state: Dictionary) -> bool:
|
||||
if int(saved_state.get("version", -1)) != SNAPSHOT_VERSION:
|
||||
push_error("Unsupported DeterministicRng snapshot version")
|
||||
return false
|
||||
if StringName(saved_state.get("algorithm", "")) != ALGORITHM_ID:
|
||||
push_error("DeterministicRng snapshot algorithm does not match")
|
||||
return false
|
||||
|
||||
var restored_state := int(saved_state.get("state", 0))
|
||||
var restored_counter := int(saved_state.get("counter", -1))
|
||||
if restored_state <= 0 or restored_state > UINT32_MASK:
|
||||
push_error("DeterministicRng snapshot contains an invalid state")
|
||||
return false
|
||||
if restored_counter < 0:
|
||||
push_error("DeterministicRng snapshot contains an invalid counter")
|
||||
return false
|
||||
|
||||
_root_seed = int(saved_state.get("root_seed", 0))
|
||||
_stream_id = StringName(saved_state.get("stream_id", "default"))
|
||||
_state = restored_state
|
||||
_counter = restored_counter
|
||||
return true
|
||||
|
||||
|
||||
func get_root_seed() -> int:
|
||||
return _root_seed
|
||||
|
||||
|
||||
func get_stream_id() -> StringName:
|
||||
return _stream_id
|
||||
|
||||
|
||||
func get_state() -> int:
|
||||
return _state
|
||||
|
||||
|
||||
func get_counter() -> int:
|
||||
return _counter
|
||||
|
||||
|
||||
static func _derive_non_zero_state(root_seed: int, stream_id: StringName) -> int:
|
||||
var stream_bytes := String(stream_id).to_utf8_buffer()
|
||||
var context := HashingContext.new()
|
||||
if context.start(HashingContext.HASH_SHA256) != OK:
|
||||
push_error("Could not initialize the deterministic RNG seed hash")
|
||||
return FALLBACK_NON_ZERO_STATE
|
||||
context.update("the-tree:deterministic-rng:v1\n".to_utf8_buffer())
|
||||
context.update(("%d\n%d\n" % [root_seed, stream_bytes.size()]).to_utf8_buffer())
|
||||
context.update(stream_bytes)
|
||||
var digest := context.finish()
|
||||
|
||||
var derived_state := 0
|
||||
for byte_index in range(4):
|
||||
derived_state = (derived_state << 8) | int(digest[byte_index])
|
||||
if derived_state == 0:
|
||||
# The all-zero state is absorbing for xorshift32. A second digest word is
|
||||
# already independently distributed; retain a literal fallback as a final
|
||||
# guard for the astronomically unlikely double-zero case.
|
||||
for byte_index in range(4, 8):
|
||||
derived_state = (derived_state << 8) | int(digest[byte_index])
|
||||
if derived_state == 0:
|
||||
derived_state = FALLBACK_NON_ZERO_STATE
|
||||
return derived_state
|
||||
Reference in New Issue
Block a user