feat: let opportunity helpers act autonomously

This commit is contained in:
Rijad Zuzo
2026-07-16 10:58:48 +02:00
parent eaa15c076c
commit 0332c57a89
14 changed files with 253 additions and 30 deletions
+74
View File
@@ -28,6 +28,7 @@ func _initialize() -> void:
func _run() -> void:
_test_selection_and_execution_are_separate()
_test_opportunity_helper_selection_preserves_precedence()
_test_target_resolution_and_travel_are_separate()
if failures.is_empty():
@@ -91,6 +92,57 @@ func _test_selection_and_execution_are_separate() -> void:
)
func _test_opportunity_helper_selection_preserves_precedence() -> void:
var village := SimVillage.new()
village.food = 100.0
village.wood = 100.0
village.safety = 100.0
village.knowledge = 100.0
village.update_modifiers()
village.update_priorities()
var npc := SimNPC.new(702, "OpportunityHelper", SimulationIds.PROFESSION_WANDERER, 5.0, 5.0)
npc.hunger = 20.0
npc.energy = 80.0
var helper := _helper_result(npc.id, SimulationIds.ACTION_GATHER_WOOD)
var selector := ActionSelectionSystem.new()
var selection := selector.select_action(npc, village, 0.5, [npc], helper)
_check(
(
selection.action_id == SimulationIds.ACTION_GATHER_WOOD
and "Responding to village need" in selection.reason
and "Knows the need" in selection.reason
),
"The matching derived helper should choose its reported ordinary supply action"
)
npc.energy = 20.0
selection = selector.select_action(npc, village, 0.5, [npc], helper)
_check(
selection.action_id == SimulationIds.ACTION_REST,
"Urgent low energy should take precedence over an opportunity response"
)
npc.energy = 50.0
selection = selector.select_action(npc, village, 0.0, [npc], helper)
_check(
selection.action_id == SimulationIds.ACTION_SLEEP,
"The sleep schedule should take precedence over an opportunity response"
)
npc.energy = 80.0
npc.hunger = 90.0
selection = selector.select_action(npc, village, 0.5, [npc], helper)
_check(
selection.action_id == SimulationIds.ACTION_WITHDRAW_FOOD,
"Critical hunger should take precedence over an opportunity response"
)
npc.hunger = 20.0
helper = _helper_result(npc.id + 1, SimulationIds.ACTION_GATHER_WOOD)
selection = selector.select_action(npc, village, 0.5, [npc], helper)
_check(
"Responding to village need" not in selection.reason,
"A helper result must not redirect a different NPC"
)
func _test_target_resolution_and_travel_are_separate() -> void:
var adapter := FakeActiveWorld.new()
adapter.resource_candidates = [
@@ -185,6 +237,28 @@ func _on_travel_requested(_npc: SimNPC, target_position: Vector3) -> void:
travel_requests.append(target_position)
func _helper_result(helper_id: int, action_id: StringName) -> OpportunityHelperResult:
return (
OpportunityHelperResult
. new(
{
"opportunity_id": 9,
"helper_npc_id": helper_id,
"action_id": action_id,
"source_id": "",
"resource_id": SimulationIds.RESOURCE_WOOD,
"trigger_event_id": 14,
"trust": 0.7,
"familiarity": 0.8,
"uses_inventory": false,
"available_source_count": 1,
"profession_match": false,
"reason": "Knows the need; trust 0.70 toward Neighbour; has 1 finite source",
}
)
)
func _check(condition: bool, message: String) -> void:
if not condition:
failures.append(message)
+24
View File
@@ -317,6 +317,30 @@ func _run() -> void:
),
"A real missing-wood fact should surface through wood-specific UI without a food cue"
)
witness.current_task = SimulationIds.ACTION_IDLE
witness.task_state = SimNPC.TASK_STATE_IDLE
witness.task_complete = true
witness.target_id = &""
witness.has_travel_target = false
witness.hunger = 20.0
witness.energy = 80.0
witness.mourning_ticks = 0
simulation_manager.simulate_tick()
var runtime_decision: ActionSelectionResult = simulation_manager.get_latest_decision(witness.id)
var helper_resource: ResourceStateRecord = simulation_manager.get_resource_state(
witness.target_id
)
_check(
(
runtime_decision != null
and runtime_decision.action_id == SimulationIds.ACTION_GATHER_WOOD
and "Responding to village need" in runtime_decision.reason
and witness.task_state == SimNPC.TASK_STATE_TRAVELING
and helper_resource != null
and helper_resource.get_reserved_by() == witness.id
),
"The runtime helper should autonomously enter normal target resolution and reservation"
)
contributor.add_inventory(SimulationIds.RESOURCE_WOOD, 1.0)
_check(
is_equal_approx(
+4 -1
View File
@@ -109,13 +109,16 @@ func _run() -> void:
)
manager.simulate_tick()
var deposit_decision: ActionSelectionResult = manager.get_latest_decision(helper.id)
_check(
(
helper.current_task == SimulationIds.ACTION_DEPOSIT_FOOD
and helper.target_id == SimulationIds.STORAGE_VILLAGE_PANTRY
and helper.has_travel_target
and deposit_decision != null
and "Responding to village need" in deposit_decision.reason
),
"The helper should autonomously choose the real pantry deposit after gathering"
"The re-derived helper should autonomously choose the real pantry deposit"
)
helper_visual.global_position = helper.travel_target_position
manager.synchronize_npc_position(helper.id, helper_visual.global_position)
+26 -2
View File
@@ -107,6 +107,26 @@ func _run() -> void:
manager.get_state_checksum() == checksum_before_helper_query,
"Capable-helper discovery should not mutate authoritative simulation state"
)
witness.current_task = SimulationIds.ACTION_IDLE
witness.task_state = SimNPC.TASK_STATE_IDLE
witness.task_complete = true
witness.target_id = &""
witness.has_travel_target = false
witness.hunger = 20.0
witness.energy = 80.0
witness.mourning_ticks = 0
manager.simulate_tick()
var helper_decision: ActionSelectionResult = manager.get_latest_decision(witness.id)
_check(
(
helper_decision != null
and helper_decision.action_id == helper_result.action_id
and "Responding to village need" in helper_decision.reason
and witness.current_task == SimulationIds.ACTION_GATHER_WOOD
and witness.task_state == SimNPC.TASK_STATE_TRAVELING
),
"An idle derived helper should autonomously select the ordinary wood supply action"
)
var active_json: String = manager.serialize_state()
var active_restored := _create_manager(1202)
@@ -127,8 +147,12 @@ func _run() -> void:
"Active restore should preserve opportunity identity and protected evidence"
)
_check(
restored_helper != null and restored_helper.helper_npc_id == witness.id,
"Active restore should re-derive the same capable helper"
(
restored_helper != null
and restored_helper.helper_npc_id == witness.id
and active_restored.npcs[witness.id].current_task == SimulationIds.ACTION_GATHER_WOOD
),
"Active restore should keep the ordinary task and re-derive the helper without assignment state"
)
active_restored.free()