Files
gamedev-the-steward/tests/deterministic_simulation_test.gd
T
admin 37610242bf 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
2026-07-05 11:23:12 +02:00

53 lines
1.6 KiB
GDScript

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)