extends SceneTree var failures: Array[String] = [] func _initialize() -> void: call_deferred("_run") func _run() -> void: _test_feed_weak_villager() _test_repair_home_roof() _finish() func _test_feed_weak_villager() -> void: var manager := _create_manager(951) var weak: SimNPC = manager.npcs[0] var supplier: SimNPC = manager.npcs[1] _empty_pantry(manager) for npc in manager.npcs: npc.position = Vector3(40.0 + npc.id * 10.0, 0.0, 0.0) weak.position = Vector3.ZERO supplier.position = Vector3(2.0, 0.0, 0.0) weak.hunger = 95.0 weak.is_starving = true weak.energy = 10.0 manager.economy.withdraw_to_inventory(weak, SimulationIds.RESOURCE_FOOD, 1.0) weak.task_state = SimNPC.TASK_STATE_IDLE weak.task_complete = true manager.call("_select_action_if_idle", weak, SimNPC.TASK_STATE_IDLE) var opportunity: OpportunityStateRecord = manager.get_active_opportunity() _check( ( opportunity != null and opportunity.get_opportunity_type() == SimulationIds.OPPORTUNITY_FEED_WEAK_VILLAGER and opportunity.get_interested_npc_id() == weak.id and opportunity.get_resource_id() == SimulationIds.RESOURCE_FOOD ), "A starving weak villager with an empty pantry should open a feed-weak-villager need" ) if opportunity == null: manager.free() return supplier.position = Vector3(2.0, 0.0, 0.0) var supplier_relationship: RelationshipStateRecord = ( manager.relationship_system.get_relationship(supplier.id, weak.id) ) _check( ( supplier_relationship != null and ( supplier_relationship.increase_trust(0.15, RelationshipStateRecord.NO_CAUSE_EVENT) > 0.0 ) ), "Feed-weak setup should make the supplier a trusted known helper" ) supplier.add_inventory(SimulationIds.RESOURCE_FOOD, 1.0) var helper: OpportunityHelperResult = manager.get_active_opportunity_helper() _check( ( helper != null and helper.helper_npc_id == supplier.id and helper.action_id == SimulationIds.ACTION_DEPOSIT_FOOD ), "A trusted informed supplier should be derived as the capable helper" ) _check( is_equal_approx( manager.economy.deposit_inventory(supplier, SimulationIds.RESOURCE_FOOD), 1.0 ), "A real supplier should restock the pantry for the weak villager" ) _check( manager.get_active_opportunity() != null, "Stocking the pantry alone must not close a feed-weak-villager need" ) weak.add_inventory(SimulationIds.RESOURCE_FOOD, 1.0) _check(manager.economy.consume_npc_food(weak), "The weak villager should eat the supplied food") var resolved: OpportunityStateRecord = manager.get_latest_opportunity_for_npc(weak.id) var resolution: EconomicEventRecord = manager.get_opportunity_resolution_event(resolved) _check( ( resolved != null and resolved.get_status() == OpportunityStateRecord.STATUS_RESOLVED and resolution != null and (StringName(resolution.data["event_type"]) == SimulationIds.EVENT_ITEM_CONSUMED) and int(resolution.data["actor_id"]) == weak.id ), "A weak-villager need should resolve only when the interested villager actually eats" ) var saved_json: String = manager.serialize_state() var restored := _create_manager(952) _check( restored.restore_state_from_json(saved_json), "A feed-weak-villager need should survive save and restore" ) _check( ( restored.get_state_checksum() == manager.get_state_checksum() and ( restored.get_latest_opportunity_for_npc(weak.id).get_status() == OpportunityStateRecord.STATUS_RESOLVED ) ), "Restore should preserve the resolved feed-weak history and checksum" ) restored.free() _empty_pantry(manager) weak.hunger = 95.0 weak.is_starving = true weak.energy = 5.0 weak.task_state = SimNPC.TASK_STATE_IDLE weak.task_complete = true manager.call("_select_action_if_idle", weak, SimNPC.TASK_STATE_IDLE) var reopened: OpportunityStateRecord = manager.get_active_opportunity() _check( ( reopened != null and reopened.get_opportunity_type() == SimulationIds.OPPORTUNITY_FEED_WEAK_VILLAGER ), "A renewed weak condition should open a new feed-weak-villager need" ) var created_tick: int = reopened.get_created_tick() var invalidated: OpportunityStateRecord = manager.opportunity_system.maintain_open_opportunity( created_tick + manager.get_knowledge_review_interval(), manager.get_knowledge_review_interval(), manager.npcs, manager.get_pantry(), manager.get_woodpile() ) _check( ( invalidated != null and invalidated.get_status() == OpportunityStateRecord.STATUS_INVALIDATED and ( invalidated.get_invalidation_reason() == SimulationIds.OPPORTUNITY_INVALIDATED_EVIDENCE_STALE ) ), "An unresolved weak-villager need should close as stale after one simulated day" ) manager.free() func _test_repair_home_roof() -> void: var manager := _create_manager(953) var homeowner: SimNPC = manager.npcs[0] var supplier: SimNPC = manager.npcs[1] var woodpile: StorageStateRecord = manager.get_woodpile() woodpile.withdraw(SimulationIds.RESOURCE_WOOD, woodpile.get_amount(SimulationIds.RESOURCE_WOOD)) manager.economy.sync_resource(SimulationIds.RESOURCE_WOOD) manager.village.safety = 15.0 homeowner.position = Vector3.ZERO var definition := SimulationDefinitions.get_action(SimulationIds.ACTION_SLEEP) manager.call("_apply_action_completion", homeowner, SimulationIds.ACTION_SLEEP, definition) var opportunity: OpportunityStateRecord = manager.get_active_opportunity() _check( ( opportunity != null and opportunity.get_opportunity_type() == SimulationIds.OPPORTUNITY_REPAIR_HOME_ROOF and opportunity.get_interested_npc_id() == homeowner.id and opportunity.get_resource_id() == SimulationIds.RESOURCE_WOOD ), "A sleepless night in a low-safety village should open a damaged-roof wood need" ) if opportunity == null: manager.free() return supplier.add_inventory(SimulationIds.RESOURCE_WOOD, 1.0) _check( is_equal_approx( manager.economy.deposit_inventory(supplier, SimulationIds.RESOURCE_WOOD), 1.0 ), "A real supplier should deposit wood for the roof repair" ) var resolved: OpportunityStateRecord = manager.get_latest_opportunity_for_npc(homeowner.id) _check( ( resolved != null and resolved.get_status() == OpportunityStateRecord.STATUS_RESOLVED and manager.get_active_opportunity() == null ), "A real wood deposit should resolve the damaged-roof need" ) manager.free() func _empty_pantry(manager: Node) -> void: var pantry: StorageStateRecord = manager.get_pantry() pantry.withdraw(SimulationIds.RESOURCE_FOOD, pantry.get_amount(SimulationIds.RESOURCE_FOOD)) manager.economy.sync_resource(SimulationIds.RESOURCE_FOOD) func _create_manager(seed_value: int = 951) -> 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(10.0, 0.0, 0.0), Vector3(12.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] Village care opportunities passed: weak villager + damaged roof") 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)