feat: add deterministic simulation foundation
Replace implicit randomness with seeded per-NPC RandomNumberGenerator streams so the same seed always produces the same scenario outcome. - Add SimulationClock (RefCounted): explicit fixed-step clock that converts frame delta into simulation ticks while preserving sub-tick remainder; replaces raw _process tick_timer accumulation - Add simulation_seed export (int=1337) to SimulationManager; drive per-NPC RNG streams via seed + npc_id + stream_id derivation (stream 0=init, stream 1=wander) - Add get_wander_offset(npc_id) to SimulationManager so wander targets are deterministic per seed; WorldViewManager calls it instead of local randf_range() - Add get_state_snapshot() and get_state_checksum() to SimulationManager for headless verification and future save/load - Convert SimNPC/SimVillage const DEBUG_LOGS to instance var debug_logs so headless tests can silence output without recompilation - Pass RandomNumberGenerator through SimNPC._init() instead of using global randf_range() for hunger/energy/position/scoring noise - Remove obsolete farm_zone and forest_zone exports from WorldViewManager and their marker/tree scene children from main.tscn; NPC gather tasks use ResourceNode exclusively - Rename TaskZone container to ActivityMarkers in main.tscn and update all scene paths (player, WVM, tests) to match - Add headless deterministic_simulation_test.gd: verifies clock accuracy, same-seed equality, different-seed divergence, and 24-tick scenario checksum without loading main.tscn
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
extends SceneTree
|
||||
|
||||
var failures: Array[String] = []
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
func _run() -> void:
|
||||
var clock := SimulationClock.new(1.0)
|
||||
_check(clock.advance(0.4) == 0, "Clock should not tick before its interval")
|
||||
_check(clock.advance(0.6) == 1, "Clock should emit one tick at its interval")
|
||||
_check(clock.advance(2.4) == 2, "Clock should preserve remainder across multiple ticks")
|
||||
_check(clock.elapsed_ticks == 3, "Clock should track elapsed simulation ticks")
|
||||
|
||||
var first := _run_scenario(424242)
|
||||
var second := _run_scenario(424242)
|
||||
var different := _run_scenario(424243)
|
||||
|
||||
_check(first == second, "The same seed and commands should produce the same checksum")
|
||||
_check(first != different, "A different seed should change the scenario checksum")
|
||||
|
||||
if failures.is_empty():
|
||||
print("[TEST] Deterministic simulation passed | checksum=", first)
|
||||
quit(0)
|
||||
return
|
||||
|
||||
for failure in failures:
|
||||
push_error("[TEST] " + failure)
|
||||
quit(1)
|
||||
|
||||
func _run_scenario(seed_value: int) -> String:
|
||||
var manager: Node = load("res://simulation/SimulationManager.gd").new()
|
||||
manager.simulation_seed = seed_value
|
||||
manager.debug_logs = false
|
||||
root.add_child(manager)
|
||||
manager.set_process(false)
|
||||
|
||||
for step in 24:
|
||||
manager.simulate_tick()
|
||||
for npc in manager.npcs:
|
||||
if npc.task_state == SimNPC.TASK_STATE_TRAVELING:
|
||||
manager.notify_npc_arrived(npc.id)
|
||||
|
||||
var snapshot: Dictionary = manager.get_state_snapshot()
|
||||
snapshot.erase("seed")
|
||||
var checksum := JSON.stringify(snapshot).sha256_text()
|
||||
manager.free()
|
||||
return checksum
|
||||
|
||||
func _check(condition: bool, message: String) -> void:
|
||||
if not condition:
|
||||
failures.append(message)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bexyksljoddd0
|
||||
@@ -28,10 +28,10 @@ func _run() -> void:
|
||||
]
|
||||
var destinations: Array[Vector3] = []
|
||||
for marker_path in [
|
||||
"TaskZone/FoodZone",
|
||||
"TaskZone/GuardZone",
|
||||
"TaskZone/StudyZone",
|
||||
"TaskZone/RestZone"
|
||||
"ActivityMarkers/FoodZone",
|
||||
"ActivityMarkers/GuardZone",
|
||||
"ActivityMarkers/StudyZone",
|
||||
"ActivityMarkers/RestZone"
|
||||
]:
|
||||
destinations.append(main_scene.get_node(marker_path).global_position)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user