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:
+18
-9
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user