extends SceneTree var failures: Array[String] = [] var forgotten_pairs: Array[String] = [] func _initialize() -> void: call_deferred("_run") func _run() -> void: var manager := _create_manager() var contributor: SimNPC = manager.npcs[0] var witness: SimNPC = manager.npcs[1] var lasting_listener: SimNPC = manager.npcs[2] var recent_listener: SimNPC = manager.npcs[3] for npc in manager.npcs: npc.position = Vector3(40.0 + npc.id * 2.0, 0.0, 0.0) contributor.position = Vector3.ZERO witness.position = Vector3(4.0, 0.0, 0.0) lasting_listener.position = Vector3(20.0, 0.0, 0.0) recent_listener.position = Vector3(22.0, 0.0, 0.0) witness.hunger = 20.0 lasting_listener.hunger = 85.0 recent_listener.hunger = 20.0 contributor.add_inventory(SimulationIds.RESOURCE_FOOD, 2.0) _check( is_equal_approx( manager.economy.deposit_inventory(contributor, SimulationIds.RESOURCE_FOOD), 2.0 ), "Retention should begin from a real completed food deposit" ) var deposit_event := _find_deposit_event(manager, contributor.id) _check(deposit_event != null, "The objective deposit fact should exist") if deposit_event == null: manager.free() _finish() return var event_id := int(deposit_event.data["event_id"]) _prepare_worker(witness, Vector3(20.0, 0.0, 0.0), true) _prepare_worker(lasting_listener, Vector3(21.0, 0.0, 0.0), false) manager.notify_npc_arrived(lasting_listener.id) _prepare_worker(recent_listener, Vector3(22.0, 0.0, 0.0), false) manager.notify_npc_arrived(recent_listener.id) _check( ( manager.npc_knows_event(lasting_listener.id, event_id) and manager.npc_knows_event(recent_listener.id, event_id) ), "The direct witness should communicate the same fact to both listeners" ) var lasting_record: KnownEventStateRecord = manager.get_known_event_record( lasting_listener.id, event_id ) var recent_record: KnownEventStateRecord = manager.get_known_event_record( recent_listener.id, event_id ) _check( ( lasting_record != null and recent_record != null and lasting_record.get_acquired_tick() == manager.tick_count and recent_record.get_acquired_tick() == manager.tick_count and ( lasting_record.get_source_acquisition_method() == SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED ) ), "Communicated memories should retain acquisition time and direct-source proof" ) _check( manager.is_known_event_lasting(lasting_listener.id, event_id), "A fact that changed trust should become a lasting relationship cause" ) _check( not manager.is_known_event_lasting(recent_listener.id, event_id), "A fact without a social consequence should remain recent" ) _check( ( manager.get_memory_summary(lasting_listener.id) == {"lasting": 1, "recent": 0} and manager.get_memory_summary(recent_listener.id) == {"lasting": 0, "recent": 1} ), "Memory summaries should distinguish lasting and recent knowledge" ) var objective_event_count: int = manager.economic_events.size() var objective_next_event_id: int = manager.next_event_id for npc in manager.npcs: npc.set_task(SimulationIds.ACTION_REST, 1000.0) npc.target_id = &"retention_hold" npc.start_working() var review_interval: int = manager.get_knowledge_review_interval() for _step in range(review_interval - 1): manager.simulate_tick() _check( manager.npc_knows_event(recent_listener.id, event_id), "Recent knowledge should survive until the exact daily review boundary" ) var saved_json: String = manager.serialize_state() var boundary_data = JSON.parse_string(saved_json) boundary_data["simulation"]["tick_count"] = review_interval var boundary_restored := _create_manager(998) _check( boundary_restored.restore_state_from_json(JSON.stringify(boundary_data)), "A save loaded on a review boundary should restore successfully" ) _check( ( boundary_restored.npc_knows_event(lasting_listener.id, event_id) and not boundary_restored.npc_knows_event(recent_listener.id, event_id) and not boundary_restored.npc_knows_event(witness.id, event_id) and not boundary_restored.npc_knows_event(contributor.id, event_id) ), "Loading exactly on a review boundary should immediately expire stale recent facts" ) var restored := _create_manager(999) _check( restored.restore_state_from_json(saved_json), "Retention metadata should restore immediately before review" ) manager.event_knowledge_forgotten.connect(_on_event_knowledge_forgotten) manager.simulate_tick() restored.simulate_tick() _check( restored.get_state_checksum() == manager.get_state_checksum(), "Original and restored simulations should forget the same facts at the same tick" ) _check( ( manager.npc_knows_event(lasting_listener.id, event_id) and not manager.npc_knows_event(recent_listener.id, event_id) and not manager.npc_knows_event(witness.id, event_id) and not manager.npc_knows_event(contributor.id, event_id) ), "Daily review should keep only the lasting causal memory" ) _check( forgotten_pairs == ["0:%d" % event_id, "1:%d" % event_id, "3:%d" % event_id], "Forgetting signals should be emitted once in stable knower/event order" ) _check( ( manager.economic_events.size() == objective_event_count and manager.next_event_id == objective_next_event_id ), "Forgetting knowledge must not prune or replay objective event history" ) var lasting_relationship: RelationshipStateRecord = ( manager.relationship_system.get_relationship(lasting_listener.id, contributor.id) ) _check( ( lasting_relationship != null and is_equal_approx(lasting_relationship.get_trust(), 0.65) and lasting_relationship.get_last_trust_cause_event_id() == event_id ), "Forgetting routine facts must preserve trust and its explainable current cause" ) var reload_probe := _create_manager(1001) _check( reload_probe.restore_state_from_json(manager.serialize_state()), "A communicated memory should remain valid after its direct speaker forgets" ) _prepare_worker(witness, Vector3(20.0, 0.0, 0.0), true) _prepare_worker(recent_listener, Vector3(21.0, 0.0, 0.0), true) var before_stale_share: String = manager.get_state_checksum() _check( not manager.try_communicate_at_shared_activity(witness.id, recent_listener.id), "A direct witness who forgot a fact should no longer be able to share it" ) _check( manager.get_state_checksum() == before_stale_share, "Rejected stale communication should not mutate state" ) _prepare_choice_divergence(manager, contributor, lasting_listener, recent_listener) var lasting_choice: ActionSelectionResult = manager.action_selector.select_action( lasting_listener, manager.village, 0.5, manager.npcs ) var recent_choice: ActionSelectionResult = manager.action_selector.select_action( recent_listener, manager.village, 0.5, manager.npcs ) _check( ( lasting_choice.action_id == SimulationIds.ACTION_GATHER_FOOD and contributor.npc_name in lasting_choice.reason ), "The lasting causal memory should preserve the informed helping choice" ) _check( recent_choice.action_id == SimulationIds.ACTION_PATROL, "The villager whose routine fact faded should continue ordinary guard work" ) manager.free() boundary_restored.free() restored.free() reload_probe.free() _finish() func _prepare_worker(npc: SimNPC, position: Vector3, start_working: bool) -> void: npc.set_task(SimulationIds.ACTION_PATROL) npc.target_id = &"guard_post" npc.position = position if start_working: npc.start_working() func _prepare_choice_divergence( manager: Node, contributor: SimNPC, informed: SimNPC, control: SimNPC ) -> void: var pantry: StorageStateRecord = manager.get_pantry() pantry.withdraw( SimulationIds.RESOURCE_FOOD, maxf( ( pantry.get_amount(SimulationIds.RESOURCE_FOOD) - (ActionSelectionSystem.RELATIONSHIP_AID_PANTRY_THRESHOLD - 5.0) ), 0.0 ) ) manager.economy.sync_resource(SimulationIds.RESOURCE_FOOD) manager.economy.deposit_resource(SimulationIds.RESOURCE_WOOD, 10.0) manager.village.safety = 50.0 manager.village.knowledge = 20.0 manager.village.update_priorities() contributor.hunger = 95.0 contributor.is_starving = true contributor.is_dead = false for observer in [informed, control]: observer.profession = SimulationIds.PROFESSION_GUARD observer.hunger = 20.0 observer.energy = 80.0 observer.is_starving = false observer.starvation_ticks = 0 observer.inventory.clear() observer.last_task = &"" func _find_deposit_event(manager: Node, actor_id: int) -> EconomicEventRecord: for event in manager.get_npc_events(actor_id, 4): if StringName(event.data["event_type"]) == SimulationIds.EVENT_STORAGE_DEPOSITED: return event return null func _create_manager(seed_value: int = 831) -> Node: var manager: Node = load("res://simulation/SimulationManager.gd").new() manager.simulation_seed = seed_value manager.debug_logs = false var home_positions: Array[Vector3] = [ Vector3(0.0, 0.0, 0.0), Vector3(1.0, 0.0, 0.0), Vector3(2.0, 0.0, 0.0), Vector3(3.0, 0.0, 0.0), Vector3(30.0, 0.0, 30.0), Vector3(40.0, 0.0, 40.0), ] manager.home_positions = home_positions root.add_child(manager) manager.set_process(false) return manager func _on_event_knowledge_forgotten(knower_id: int, event: EconomicEventRecord) -> void: forgotten_pairs.append("%d:%d" % [knower_id, int(event.data["event_id"])]) func _finish() -> void: if failures.is_empty(): print("[TEST] Knowledge retention passed: recent fades, lasting cause remains") quit(0) return for failure in failures: push_error("[TEST] " + failure) quit(1) func _check(condition: bool, message: String) -> void: if not condition: failures.append(message)