extends SceneTree var failures: 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 listener: SimNPC = manager.npcs[2] var control: SimNPC = manager.npcs[3] contributor.position = Vector3.ZERO witness.position = Vector3(4.0, 0.0, 0.0) listener.position = Vector3(20.0, 0.0, 0.0) control.position = Vector3(21.0, 0.0, 0.0) for observer in [witness, listener, control]: observer.hunger = 85.0 contributor.add_inventory(SimulationIds.RESOURCE_FOOD, 2.0) _check( is_equal_approx( manager.economy.deposit_inventory(contributor, SimulationIds.RESOURCE_FOOD), 2.0 ), "Communication should begin from a real completed food-deposit fact" ) var deposit_event := _find_deposit_event(manager, contributor.id) _check(deposit_event != null, "The original deposit must remain in objective event history") if deposit_event == null: manager.free() _finish() return var event_id := int(deposit_event.data["event_id"]) _check( ( manager.npc_knows_event(contributor.id, event_id) and manager.npc_knows_event(witness.id, event_id) and not manager.npc_knows_event(listener.id, event_id) and not manager.npc_knows_event(control.id, event_id) ), "Only the contributor and nearby witness should know the fact initially" ) _check( ( manager.get_known_event_record(witness.id, event_id).get_acquisition_method() == SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED ), "The teller should retain direct witness provenance" ) for npc in [witness, listener]: npc.set_task(SimulationIds.ACTION_EAT) npc.target_id = SimulationIds.STORAGE_VILLAGE_PANTRY npc.position = Vector3(20.0 + npc.id, 0.0, 0.0) npc.start_working() _check( not manager.try_communicate_at_shared_activity(witness.id, listener.id), "Passing at storage should not trigger the bounded shared-work conversation" ) _prepare_shared_patrol(witness, listener, Vector3(20.0, 0.0, 0.0), Vector3(30.0, 0.0, 0.0)) var before_failed_transfer: String = manager.get_state_checksum() _check( not manager.try_communicate_at_shared_activity(witness.id, listener.id), "Matching target IDs must not teleport knowledge between distant workers" ) _check( manager.get_state_checksum() == before_failed_transfer, "A rejected distant conversation must not mutate simulation state" ) listener.set_task(SimulationIds.ACTION_PATROL) listener.target_id = witness.target_id listener.position = witness.position + Vector3(1.0, 0.0, 0.0) var event_count_before: int = manager.economic_events.size() var next_event_id_before: int = manager.next_event_id manager.notify_npc_arrived(listener.id) _check( manager.npc_knows_event(listener.id, event_id), "Arriving beside the witness at shared work should transfer one fact" ) _check( ( manager.economic_events.size() == event_count_before and manager.next_event_id == next_event_id_before ), "Communication must reference the original event without replaying or duplicating it" ) var communicated_record: KnownEventStateRecord = manager.get_known_event_record( listener.id, event_id ) _check( ( communicated_record != null and ( communicated_record.get_acquisition_method() == SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED ) and communicated_record.get_source_npc_id() == witness.id ), "The listener should preserve who communicated the objective fact" ) var listener_relationship: RelationshipStateRecord = ( manager.relationship_system.get_relationship(listener.id, contributor.id) ) var control_relationship: RelationshipStateRecord = ( manager.relationship_system.get_relationship(control.id, contributor.id) ) _check( ( listener_relationship != null and is_equal_approx(listener_relationship.get_trust(), 0.65) and listener_relationship.get_last_trust_cause_event_id() == event_id ), "Learning the fact should apply its trust consequence only to the listener" ) _check( ( control_relationship != null and is_equal_approx( control_relationship.get_trust(), RelationshipStateRecord.NEUTRAL_TRUST ) and not manager.npc_knows_event(control.id, event_id) ), "A nearby uninvolved villager should not receive a directed conversation" ) var before_repeat: String = manager.get_state_checksum() _check( not manager.try_communicate_at_shared_activity(witness.id, listener.id), "Repeating the same conversation should not replace first-acquisition provenance" ) _check( manager.get_state_checksum() == before_repeat, "Repeated communication must not add knowledge or apply trust twice" ) control.set_task(SimulationIds.ACTION_PATROL) control.target_id = listener.target_id control.position = listener.position + Vector3(1.0, 0.0, 0.0) control.start_working() _check( not manager.try_communicate_at_shared_activity(listener.id, control.id), "Communicated facts should not relay again before the rumour phase" ) _prepare_choice_divergence(manager, contributor, listener, control) var informed_choice: ActionSelectionResult = manager.action_selector.select_action( listener, manager.village, 0.5, manager.npcs ) var control_choice: ActionSelectionResult = manager.action_selector.select_action( control, manager.village, 0.5, manager.npcs ) _check( ( informed_choice.action_id == SimulationIds.ACTION_GATHER_FOOD and contributor.npc_name in informed_choice.reason ), "The informed listener should act on communicated evidence" ) _check( control_choice.action_id == SimulationIds.ACTION_PATROL, "The uninformed control should continue ordinary guard work" ) var saved_json: String = manager.serialize_state() var restored := _create_manager(999) _check(restored.restore_state_from_json(saved_json), "Communicated provenance should restore") var restored_record: KnownEventStateRecord = restored.get_known_event_record( listener.id, event_id ) _check( ( restored_record != null and ( restored_record.get_acquisition_method() == SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED ) and restored_record.get_source_npc_id() == witness.id ), "Save/load should preserve the exact speaker provenance" ) _check( restored.get_state_checksum() == manager.get_state_checksum(), "Restored communication state should retain the deterministic checksum" ) var restored_listener: SimNPC = restored.npcs[listener.id] var restored_control: SimNPC = restored.npcs[control.id] var current_next_informed_choice: ActionSelectionResult = manager.action_selector.select_action( listener, manager.village, 0.5, manager.npcs ) var current_next_control_choice: ActionSelectionResult = manager.action_selector.select_action( control, manager.village, 0.5, manager.npcs ) var restored_informed_choice: ActionSelectionResult = restored.action_selector.select_action( restored_listener, restored.village, 0.5, restored.npcs ) var restored_control_choice: ActionSelectionResult = restored.action_selector.select_action( restored_control, restored.village, 0.5, restored.npcs ) _check( ( restored_informed_choice.action_id == current_next_informed_choice.action_id and restored_control_choice.action_id == current_next_control_choice.action_id ), "Restored knowledge should preserve the next informed and control choices" ) _check( restored.get_state_checksum() == manager.get_state_checksum(), "Equivalent future choices should advance restored RNG streams identically" ) manager.free() restored.free() _finish() func _prepare_shared_patrol( witness: SimNPC, listener: SimNPC, witness_position: Vector3, listener_position: Vector3 ) -> void: for npc in [witness, listener]: npc.set_task(SimulationIds.ACTION_PATROL) npc.target_id = &"guard_post" witness.position = witness_position listener.position = listener_position witness.start_working() listener.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 for observer in [informed, control]: observer.profession = SimulationIds.PROFESSION_GUARD observer.hunger = 20.0 observer.energy = 80.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 = 821) -> 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 _finish() -> void: if failures.is_empty(): print("[TEST] Communicated knowledge passed: witness -> coworker -> informed choice") 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)