extends SceneTree var failures: Array[String] = [] func _initialize() -> void: call_deferred("_run") func _run() -> void: _test_sleep_period_at_night() _test_work_period_during_day() _test_meal_period_hunger() _test_sleep_restores_energy() _test_schedule_serialization() _test_sleep_targets_home() _test_household_familiarity() if failures.is_empty(): print("[TEST] NPC schedule passed") quit(0) return for failure in failures: push_error("[TEST] " + failure) quit(1) func _create_manager(seed_value: int) -> Node: 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) return manager func _test_sleep_period_at_night() -> void: var manager := _create_manager(100) manager.clock.cycle_duration_seconds = 240.0 manager.clock.elapsed_ticks = 0 var npc: SimNPC = manager.npcs[0] npc.hunger = 20.0 npc.energy = 50.0 npc.home_position = npc.position var selection := ActionSelectionSystem.new().select_action( npc, manager.village, manager.clock.time_of_day() ) _check(selection != null, "Night-time should produce an action selection") _check( selection.action_id == SimulationIds.ACTION_SLEEP, "Low-energy NPC should choose SLEEP during night period" ) _check( selection.reason.to_lower().contains("night"), "Sleep selection reason should reference night-time" ) manager.free() func _test_work_period_during_day() -> void: var manager := _create_manager(200) manager.clock.cycle_duration_seconds = 240.0 manager.clock.elapsed_ticks = 100 var time_of_day: float = manager.clock.time_of_day() _check( time_of_day > 0.28 and time_of_day < 0.85, "100 ticks should land in the work period" ) var npc: SimNPC = manager.npcs[0] npc.profession = SimulationIds.PROFESSION_WANDERER npc.hunger = 20.0 npc.energy = 80.0 npc.home_position = npc.position var selection := ActionSelectionSystem.new().select_action( npc, manager.village, time_of_day ) _check(selection != null, "Daytime should produce an action selection") _check( selection.action_id != SimulationIds.ACTION_SLEEP, "A well-rested NPC should not choose SLEEP during work period" ) manager.free() func _test_meal_period_hunger() -> void: var manager := _create_manager(300) manager.clock.cycle_duration_seconds = 240.0 manager.clock.elapsed_ticks = 50 var time_of_day: float = manager.clock.time_of_day() _check( time_of_day >= 0.25 and time_of_day <= 0.35, "50 ticks should land in the breakfast meal period" ) var npc: SimNPC = manager.npcs[0] npc.profession = SimulationIds.PROFESSION_WANDERER npc.hunger = 65.0 npc.energy = 80.0 manager.village.food = 5.0 npc.home_position = npc.position var selection := ActionSelectionSystem.new().select_action( npc, manager.village, time_of_day ) _check(selection != null, "Meal period should produce an action selection") _check( selection.action_id == SimulationIds.ACTION_WITHDRAW_FOOD, "Hungry NPC during meal period with pantry food should WITHDRAW_FOOD" ) manager.free() func _test_sleep_restores_energy() -> void: var manager := _create_manager(400) manager.clock.cycle_duration_seconds = 240.0 manager.clock.elapsed_ticks = 0 var npc: SimNPC = manager.npcs[0] npc.hunger = 20.0 npc.energy = 30.0 npc.home_position = npc.position npc.set_task(SimulationIds.ACTION_SLEEP, 4.0) npc.start_working() var energy_before := npc.energy for tick in 4: manager.simulate_tick() _check(npc.task_complete, "Sleep should complete after its duration") _check( npc.energy > energy_before, "Completing sleep should restore energy" ) manager.free() func _test_schedule_serialization() -> void: var manager := _create_manager(500) manager.clock.cycle_duration_seconds = 180.0 manager.clock.elapsed_ticks = 30 var npc: SimNPC = manager.npcs[0] npc.home_position = Vector3(3.0, 0.0, 4.0) npc.position = Vector3(1.0, 0.0, 2.0) var saved_json: String = manager.serialize_state() var restored := _create_manager(1) _check( restored.restore_state_from_json(saved_json), "Schedule state should survive serialization round-trip" ) _check( is_equal_approx(restored.clock.cycle_duration_seconds, 180.0), "Cycle duration should round-trip through save" ) var restored_npc: SimNPC = restored.npcs[0] _check( restored_npc.home_position.distance_to(Vector3(3.0, 0.0, 4.0)) < 0.01, "NPC home position should round-trip through save" ) manager.free() restored.free() func _test_sleep_targets_home() -> void: var manager := _create_manager(600) manager.clock.cycle_duration_seconds = 240.0 manager.clock.elapsed_ticks = 0 var npc: SimNPC = manager.npcs[0] npc.energy = 40.0 npc.home_position = Vector3(5.0, 0.0, 5.0) npc.position = Vector3(0.0, 0.0, 0.0) npc.set_task(SimulationIds.ACTION_SLEEP) _check(npc.task_state == SimNPC.TASK_STATE_TRAVELING, "Sleep should put NPC in traveling state") var resolved: bool = manager.resolve_npc_target(npc.id, npc.position) _check(resolved, "Sleep target should resolve without an active world adapter") _check( npc.travel_target_position.distance_to(Vector3(5.0, 0.0, 5.0)) < 0.01, "Sleep should target the NPC home position" ) manager.free() func _test_household_familiarity() -> void: var manager: Node = load("res://simulation/SimulationManager.gd").new() manager.simulation_seed = 700 manager.debug_logs = false manager.set_process(false) manager.home_positions = [ Vector3(-15.0, 2.0, 7.0), Vector3(-15.0, 2.0, 8.0), Vector3(-8.5, 2.0, 3.0), Vector3(-6.5, 2.0, 1.5), Vector3(20.0, 0.0, 20.0), Vector3(22.0, 0.0, 22.0) ] root.add_child(manager) var a: SimNPC = manager.npcs[0] var b: SimNPC = manager.npcs[1] var c: SimNPC = manager.npcs[2] var d: SimNPC = manager.npcs[3] var e: SimNPC = manager.npcs[4] _check( a.familiarity.has(b.id) and b.familiarity.has(a.id), "NPCs with nearby homes should be mutual household members" ) _check( is_equal_approx(float(a.familiarity[b.id]), 0.5), "Household familiarity should start at baseline 0.5" ) _check( c.familiarity.has(d.id) and d.familiarity.has(c.id), "Second household pair should also be familiar" ) _check( not a.familiarity.has(e.id), "Distant NPCs should not have household familiarity" ) var saved_json: String = manager.serialize_state() var restored: Node = load("res://simulation/SimulationManager.gd").new() restored.debug_logs = false root.add_child(restored) restored.set_process(false) _check( restored.restore_state_from_json(saved_json), "Familiarity should survive save/load round-trip" ) var ra: SimNPC = restored.npcs[0] _check( ra.familiarity.size() > 0, "Restored NPC should retain household familiarity" ) manager.free() restored.free() func _check(condition: bool, message: String) -> void: if not condition: failures.append(message)