Files
gamedev-the-steward/tests/opportunity_communication_helper_test.gd
T
2026-07-16 12:13:54 +02:00

277 lines
9.0 KiB
GDScript

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)