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:
2026-07-05 11:23:12 +02:00
parent b50fae671a
commit 37610242bf
10 changed files with 212 additions and 100 deletions
+18 -9
View File
@@ -1,8 +1,6 @@
class_name SimNPC
extends RefCounted
const DEBUG_LOGS := true
const TASK_STATE_IDLE := "idle"
const TASK_STATE_TRAVELING := "traveling"
const TASK_STATE_WORKING := "working"
@@ -29,22 +27,33 @@ var task_progress := 0.0
var task_complete := true
var target_id: StringName = &""
var last_task := ""
var random_source: RandomNumberGenerator
var debug_logs := true
func _init(
_id: int,
_npc_name: String,
_profession: String,
_strength: float,
_intelligence: float
_intelligence: float,
_random_source: RandomNumberGenerator = null
) -> void:
id = _id
npc_name = _npc_name
profession = _profession
strength = _strength
intelligence = _intelligence
hunger = randf_range(20.0, 80.0)
energy = randf_range(40.0, 100.0)
position = Vector3(randf_range(-8.0, 8.0), 0.0, randf_range(-8.0, 8.0))
random_source = _random_source
if random_source == null:
random_source = RandomNumberGenerator.new()
random_source.seed = id + 1
hunger = random_source.randf_range(20.0, 80.0)
energy = random_source.randf_range(40.0, 100.0)
position = Vector3(
random_source.randf_range(-8.0, 8.0),
0.0,
random_source.randf_range(-8.0, 8.0)
)
func simulate_tick(village: SimVillage) -> void:
if is_dead:
@@ -104,7 +113,7 @@ func die_from_starvation() -> void:
task_duration = 0.0
task_complete = true
if DEBUG_LOGS:
if debug_logs:
print("[SimNPC] ", npc_name, " died from starvation.")
func choose_new_task(village: SimVillage) -> void:
@@ -231,9 +240,9 @@ func calculate_task_score(
if task_name == last_task:
score -= 1.5
score += randf_range(0.0, 0.5)
score += random_source.randf_range(0.0, 0.5)
if DEBUG_LOGS:
if debug_logs:
print(
"[SimNPC] ",
npc_name,