extends SceneTree var failures: Array[String] = [] var transfers: Array[Dictionary] = [] func _initialize() -> void: call_deferred("_run") func _run() -> void: var manager := _create_manager(1301) manager.event_knowledge_transferred.connect(_on_event_knowledge_transferred) var interested: SimNPC = manager.npcs[0] var speaker: SimNPC = manager.npcs[1] var helper: SimNPC = manager.npcs[2] interested.position = Vector3.ZERO speaker.position = Vector3(4.0, 0.0, 0.0) helper.position = Vector3(30.0, 0.0, 0.0) for npc in manager.npcs: npc.hunger = 20.0 npc.energy = 80.0 _set_wood_amount(manager, 0.5) interested.set_task(SimulationIds.ACTION_STUDY, 1.0) interested.start_working() manager.simulate_tick() var opportunity: OpportunityStateRecord = manager.get_active_opportunity() var trigger := _latest_event_of_type(manager, SimulationIds.EVENT_TASK_BLOCKED, interested.id) _check(opportunity != null and trigger != null, "Blocked work should open a real wood need") if opportunity == null or trigger == null: manager.free() _finish() return var trigger_id := int(trigger.data["event_id"]) _check( ( manager.npc_knows_event(interested.id, trigger_id) and manager.npc_knows_event(speaker.id, trigger_id) and not manager.npc_knows_event(helper.id, trigger_id) and ( manager.get_known_event_record(speaker.id, trigger_id).get_acquisition_method() == SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED ) ), "The future speaker should directly witness the need while the helper remains uninformed" ) helper.profession = SimulationIds.PROFESSION_WOODCUTTER _add_trusted_relationship(manager, helper.id, interested.id) var tree := _create_helper_tree() root.add_child(tree) await process_frame _check(manager.register_resource_node(tree), "The helper route should use a finite tree") _check( manager.get_active_opportunity_helper() == null, "Trust and supply should not make an uninformed villager a helper" ) interested.add_inventory(SimulationIds.RESOURCE_FOOD, 1.0) _check( manager.economy.deposit_inventory(interested, SimulationIds.RESOURCE_FOOD) > 0.0, "Setup should create one newer directly witnessed distractor fact" ) var distractor := _latest_event_of_type( manager, SimulationIds.EVENT_STORAGE_DEPOSITED, interested.id ) _check(distractor != null, "The distractor deposit should enter objective history") if distractor == null: tree.queue_free() manager.free() _finish() return var distractor_id := int(distractor.data["event_id"]) var no_lasting_events: Array[int] = [] var default_order: Array[int] = manager.event_knowledge_system.get_communicable_event_ids( speaker.id, no_lasting_events ) _check( not default_order.is_empty() and default_order[0] == distractor_id, "Without active-need priority, the newer direct fact would be discussed first" ) _prepare_shared_activity(speaker, helper) var event_count_before: int = manager.economic_events.size() var next_event_id_before: int = manager.next_event_id manager.notify_npc_arrived(helper.id) var communicated: KnownEventStateRecord = manager.get_known_event_record(helper.id, trigger_id) _check( ( communicated != null and ( communicated.get_acquisition_method() == SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED ) and communicated.get_source_npc_id() == speaker.id and ( communicated.get_source_acquisition_method() == SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED ) and not manager.npc_knows_event(helper.id, distractor_id) and ( transfers == [{"speaker_id": speaker.id, "listener_id": helper.id, "event_id": trigger_id}] ) ), "Shared work should communicate the exact active trigger before newer unrelated facts" ) _check( ( manager.economic_events.size() == event_count_before and manager.next_event_id == next_event_id_before ), "Reporting the need should reference existing history without adding an event" ) var helper_result: OpportunityHelperResult = manager.get_active_opportunity_helper() _check( ( helper_result != null and helper_result.helper_npc_id == helper.id and helper_result.trigger_event_id == trigger_id and helper_result.action_id == SimulationIds.ACTION_GATHER_WOOD ), "The newly informed trusted woodcutter should be re-derived as the capable helper" ) helper.current_task = SimulationIds.ACTION_IDLE helper.task_state = SimNPC.TASK_STATE_IDLE helper.task_complete = true helper.target_id = &"" helper.has_travel_target = false manager.simulate_tick() var decision: ActionSelectionResult = manager.get_latest_decision(helper.id) _check( ( decision != null and decision.action_id == SimulationIds.ACTION_GATHER_WOOD and "Responding to village need" in decision.reason and helper.current_task == SimulationIds.ACTION_GATHER_WOOD ), "The communicated fact should flow into ordinary autonomous helper selection" ) var saved_json: String = manager.serialize_state() var restored := _create_manager(1302) _check( restored.restore_state_from_json(saved_json), "The communicated helper path should restore" ) var restored_record: KnownEventStateRecord = restored.get_known_event_record( helper.id, trigger_id ) var restored_helper: OpportunityHelperResult = restored.get_active_opportunity_helper() _check( ( restored.get_state_checksum() == manager.get_state_checksum() and restored_record != null and restored_record.get_source_npc_id() == speaker.id and restored_helper != null and restored_helper.helper_npc_id == helper.id ), "Restore should preserve provenance and re-derive capability without assignment state" ) restored.free() manager.free() tree.queue_free() await process_frame _finish() func _prepare_shared_activity(speaker: SimNPC, listener: SimNPC) -> void: speaker.set_task(SimulationIds.ACTION_PATROL) speaker.target_id = &"opportunity_guard_post" speaker.position = Vector3(20.0, 0.0, 0.0) speaker.start_working() listener.set_task(SimulationIds.ACTION_PATROL) listener.target_id = speaker.target_id listener.position = speaker.position + Vector3(1.0, 0.0, 0.0) func _add_trusted_relationship(manager: Node, observer_id: int, subject_id: int) -> void: var records: Array[RelationshipStateRecord] = manager.relationship_system.get_all_sorted() for index in records.size(): var relationship := records[index] if ( relationship.get_observer_id() == observer_id and relationship.get_subject_id() == subject_id ): records[index] = RelationshipStateRecord.create(observer_id, subject_id, 0.7, 0.7) manager.relationship_system.restore(records) return records.append(RelationshipStateRecord.create(observer_id, subject_id, 0.7, 0.7)) manager.relationship_system.restore(records) func _set_wood_amount(manager: Node, amount: float) -> void: var woodpile: StorageStateRecord = manager.get_woodpile() woodpile.withdraw(SimulationIds.RESOURCE_WOOD, woodpile.get_amount(SimulationIds.RESOURCE_WOOD)) woodpile.deposit(SimulationIds.RESOURCE_WOOD, amount) manager.economy.sync_resource(SimulationIds.RESOURCE_WOOD) func _latest_event_of_type( manager: Node, event_type: StringName, actor_id: int ) -> EconomicEventRecord: var events: Array = manager.get_npc_events(actor_id, 8) for index in range(events.size() - 1, -1, -1): var event := events[index] as EconomicEventRecord if StringName(event.data["event_type"]) == event_type: return event return null func _create_helper_tree() -> ResourceNode: var tree := ResourceNode.new() tree.name = "CommunicatedHelperTree" tree.node_id = &"communicated_helper_tree" tree.action_id = SimulationIds.ACTION_GATHER_WOOD tree.resource_id = SimulationIds.RESOURCE_WOOD tree.initial_amount = 2.0 tree.yield_per_action = 1.0 tree.can_npcs_use = true tree.can_player_use = false tree.debug_label_enabled = false var interaction_point := Marker3D.new() interaction_point.name = "InteractionPoint" tree.add_child(interaction_point) return tree 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 var home_positions: Array[Vector3] = [ Vector3(0.0, 0.0, 0.0), Vector3(2.0, 0.0, 0.0), Vector3(20.0, 0.0, 0.0), Vector3(30.0, 0.0, 0.0), Vector3(40.0, 0.0, 0.0), Vector3(50.0, 0.0, 0.0), ] manager.home_positions = home_positions root.add_child(manager) manager.set_process(false) for npc in manager.npcs: npc.set_task(SimulationIds.ACTION_WANDER, 1000.0) npc.start_working() return manager func _on_event_knowledge_transferred( speaker_id: int, listener_id: int, event: EconomicEventRecord ) -> void: transfers.append( { "speaker_id": speaker_id, "listener_id": listener_id, "event_id": int(event.data["event_id"]) } ) func _check(condition: bool, message: String) -> void: if not condition: failures.append(message) func _finish() -> void: if failures.is_empty(): print("[TEST] Opportunity communication helper passed: report -> response") quit(0) return for failure in failures: push_error("[TEST] " + failure) quit(1)