feat: let opportunity helpers act autonomously
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
```text
|
```text
|
||||||
SimulationManager tick
|
SimulationManager tick
|
||||||
-> ActionExecutionSystem advances needs and working progress
|
-> ActionExecutionSystem advances needs and working progress
|
||||||
|
-> VillageOpportunitySystem re-derives the current capable helper
|
||||||
-> ActionSelectionSystem chooses an action when the NPC is idle
|
-> ActionSelectionSystem chooses an action when the NPC is idle
|
||||||
-> SimulationManager stores the selected action
|
-> SimulationManager stores the selected action
|
||||||
-> npc_target_requested
|
-> npc_target_requested
|
||||||
@@ -24,6 +25,9 @@ SimulationManager tick
|
|||||||
- queries `RelationshipSystem` after personal survival/schedule overrides, so a
|
- queries `RelationshipSystem` after personal survival/schedule overrides, so a
|
||||||
trusted starving acquaintance can redirect ordinary work toward food
|
trusted starving acquaintance can redirect ordinary work toward food
|
||||||
gathering while the pantry is low;
|
gathering while the pantry is low;
|
||||||
|
- accepts the current ephemeral `OpportunityHelperResult` and, only for its
|
||||||
|
matching NPC, selects the reported ordinary gather/deposit action after
|
||||||
|
urgent self-care and sleep/meal schedule branches;
|
||||||
- evaluates current utility scores;
|
- evaluates current utility scores;
|
||||||
- consumes the NPC's deterministic decision RNG;
|
- consumes the NPC's deterministic decision RNG;
|
||||||
- returns an action ID, optional urgent-duration override, branch reason,
|
- returns an action ID, optional urgent-duration override, branch reason,
|
||||||
@@ -68,9 +72,10 @@ SimulationManager remains the orchestrator and event boundary. It owns
|
|||||||
simulation records, invokes the focused systems, stores selected actions and
|
simulation records, invokes the focused systems, stores selected actions and
|
||||||
targets, and translates presentation callbacks into simulation transitions.
|
targets, and translates presentation callbacks into simulation transitions.
|
||||||
It also publishes the latest `ActionSelectionResult` for presentation; the UI
|
It also publishes the latest `ActionSelectionResult` for presentation; the UI
|
||||||
does not recompute decisions. At completion it atomically pays any
|
does not recompute decisions. Each idle selection re-derives the capable helper
|
||||||
definition-backed stored-resource cost before applying the action effect. A
|
instead of consulting persisted assignment state. At completion it atomically
|
||||||
late shortfall suppresses the effect and records a `task_blocked` fact. The
|
pays any definition-backed stored-resource cost before applying the action
|
||||||
|
effect. A late shortfall suppresses the effect and records a `task_blocked` fact. The
|
||||||
manager also captures actor/nearby knowledge before forwarding newly recorded
|
manager also captures actor/nearby knowledge before forwarding newly recorded
|
||||||
events into `RelationshipSystem`. When an NPC arrives beside a worker at the
|
events into `RelationshipSystem`. When an NPC arrives beside a worker at the
|
||||||
same non-storage activity site, the manager may transfer one direct known fact
|
same non-storage activity site, the manager may transfer one direct known fact
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ results.
|
|||||||
SimulationClock
|
SimulationClock
|
||||||
-> SimulationManager orchestrates one deterministic tick
|
-> SimulationManager orchestrates one deterministic tick
|
||||||
-> ActionExecutionSystem advances needs and work
|
-> ActionExecutionSystem advances needs and work
|
||||||
|
-> VillageOpportunitySystem re-derives the current capable helper
|
||||||
-> ActionSelectionSystem chooses an action
|
-> ActionSelectionSystem chooses an action
|
||||||
-> ActionTargetResolver resolves a stable target ID
|
-> ActionTargetResolver resolves a stable target ID
|
||||||
-> VillageEconomy performs inventory/storage transactions
|
-> VillageEconomy performs inventory/storage transactions
|
||||||
@@ -45,7 +46,8 @@ would otherwise obscure that lifecycle:
|
|||||||
open/resolved/invalidated lifecycle for the proven pantry-food and blocked-
|
open/resolved/invalidated lifecycle for the proven pantry-food and blocked-
|
||||||
work wood consumers. It also derives one ephemeral `OpportunityHelperResult`
|
work wood consumers. It also derives one ephemeral `OpportunityHelperResult`
|
||||||
from knowledge, directed relationships, inventory, action definitions, and
|
from knowledge, directed relationships, inventory, action definitions, and
|
||||||
finite-resource state without changing resources or assigning tasks;
|
finite-resource state without changing resources or assigning tasks. The
|
||||||
|
ordinary action selector consumes that result only for the matching idle NPC;
|
||||||
- `simulation/persistence/` owns save-slot file safety;
|
- `simulation/persistence/` owns save-slot file safety;
|
||||||
- `simulation/state/` owns versioned serialized record contracts;
|
- `simulation/state/` owns versioned serialized record contracts;
|
||||||
- `simulation/definitions/` owns stable IDs and immutable action/profession
|
- `simulation/definitions/` owns stable IDs and immutable action/profession
|
||||||
@@ -103,7 +105,9 @@ improving ownership.
|
|||||||
event IDs rather than object references or prose.
|
event IDs rather than object references or prose.
|
||||||
- Opportunity records reference stable NPC, storage, resource, trigger-event,
|
- Opportunity records reference stable NPC, storage, resource, trigger-event,
|
||||||
and resolution-event IDs. Their generator may observe authoritative state
|
and resolution-event IDs. Their generator may observe authoritative state
|
||||||
and history, but it does not mutate the economy or command NPC behavior.
|
and history, but it does not mutate the economy or command NPC behavior. The
|
||||||
|
manager re-queries capable helpers at idle selection boundaries; any selected
|
||||||
|
supply task then follows the ordinary persisted NPC-task and target contracts.
|
||||||
- Prefer one tested vertical behavior over a generic framework with no proven
|
- Prefer one tested vertical behavior over a generic framework with no proven
|
||||||
consumers.
|
consumers.
|
||||||
|
|
||||||
|
|||||||
@@ -736,13 +736,19 @@ Completed:
|
|||||||
deposit action and real inventory/resource route that makes them capable.
|
deposit action and real inventory/resource route that makes them capable.
|
||||||
The result is re-derived after restore and does not assign work or alter the
|
The result is re-derived after restore and does not assign work or alter the
|
||||||
simulation.
|
simulation.
|
||||||
|
29. Autonomous capable helper: the matching idle villager now consumes that
|
||||||
|
ephemeral result through ordinary action selection after urgent self-care
|
||||||
|
and sleep/meal precedence. Existing target resolution chooses and reserves
|
||||||
|
the finite source, while no quest acceptance, reward, or helper assignment
|
||||||
|
is introduced.
|
||||||
|
|
||||||
Next:
|
Next:
|
||||||
|
|
||||||
1. Let the currently derived helper consume the query through ordinary
|
1. Prove one direct information-to-help chain: transfer the exact open-need
|
||||||
autonomous action selection, after urgent self-care and schedule precedence.
|
trigger at an existing shared activity, then let the newly informed trusted
|
||||||
Re-query instead of persisting assignment, and keep acceptance/rewards out
|
villager be re-derived and respond through the same autonomous selector.
|
||||||
of scope, as sequenced in `LEARNING_ROADMAP.md`.
|
Keep generic dialogue and multi-hop rumours out of scope, as sequenced in
|
||||||
|
`LEARNING_ROADMAP.md`.
|
||||||
|
|
||||||
Do not start with GIS data, a full city, a large asset pack, or more NPC
|
Do not start with GIS data, a full city, a large asset pack, or more NPC
|
||||||
mechanics. The next proof is a beautiful stage for the systems that already
|
mechanics. The next proof is a beautiful stage for the systems that already
|
||||||
|
|||||||
+13
-3
@@ -112,6 +112,15 @@ does not select an exact gather target; that remains with `ActiveWorldAdapter`
|
|||||||
and `ActionTargetResolver`. No event, reservation, RNG draw, task mutation, or
|
and `ActionTargetResolver`. No event, reservation, RNG draw, task mutation, or
|
||||||
serialized assignment is created.
|
serialized assignment is created.
|
||||||
|
|
||||||
|
At an idle NPC decision boundary, `SimulationManager` performs that query again
|
||||||
|
and gives the ephemeral result to `ActionSelectionSystem`. Personal survival,
|
||||||
|
low-energy rest, mourning, and sleep/meal schedule branches retain precedence;
|
||||||
|
otherwise only the named helper selects the reported ordinary gather/deposit
|
||||||
|
action. From there, existing target resolution chooses and reserves an exact
|
||||||
|
finite resource or storage target. The current NPC task may serialize normally
|
||||||
|
while in progress, but no opportunity-to-helper assignment or acceptance state
|
||||||
|
exists in the save.
|
||||||
|
|
||||||
The food-loop regression verifies this chain:
|
The food-loop regression verifies this chain:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -151,6 +160,7 @@ hearing, personalized reinforcement/decay, multi-hop rumours, secrecy, false
|
|||||||
beliefs, and multi-event causal graphs belong in later event/history slices.
|
beliefs, and multi-event causal graphs belong in later event/history slices.
|
||||||
They should extend this record family without making prose authoritative or
|
They should extend this record family without making prose authoritative or
|
||||||
recomputing old evidence from current positions.
|
recomputing old evidence from current positions.
|
||||||
The current opportunity family is likewise a bounded two-consumer projection
|
The current opportunity family is likewise a bounded two-type projection with
|
||||||
with one read-only helper query, not a generic quest, reward, acceptance,
|
one read-only helper query and one ordinary action-selection consumer, not a
|
||||||
dialogue, helper assignment, or capable-helper framework.
|
generic quest, reward, acceptance, dialogue, helper assignment, or capable-
|
||||||
|
helper framework.
|
||||||
|
|||||||
@@ -792,11 +792,28 @@ The first bounded capable-helper query is complete:
|
|||||||
- food and wood scenarios re-derive the same result after restore, and the
|
- food and wood scenarios re-derive the same result after restore, and the
|
||||||
village summary explains the current helper route from real simulation facts.
|
village summary explains the current helper route from real simulation facts.
|
||||||
|
|
||||||
Milestone 7 is not complete. The immediate next slice is one bounded autonomous
|
The first bounded autonomous helper consumer is complete:
|
||||||
consumer of this query: let the currently derived helper choose the reported
|
|
||||||
gather/deposit action through ordinary selection while preserving urgent
|
- when an NPC becomes idle, `SimulationManager` re-derives the current helper
|
||||||
self-care and schedule precedence. Re-query rather than persisting assignment,
|
and passes that ephemeral result into ordinary action selection;
|
||||||
and do not add quest acceptance or reward state.
|
- only the matching helper can select the reported gather/deposit action, and
|
||||||
|
starvation, critical hunger, mourning, low energy, sleep, and meal behavior
|
||||||
|
keep precedence;
|
||||||
|
- the resulting task uses the existing definition, target resolver, finite-node
|
||||||
|
reservation, travel, extraction, inventory, and storage transaction paths;
|
||||||
|
- no helper assignment, acceptance, reward, opportunity-specific event, or RNG
|
||||||
|
draw is added, while an in-progress ordinary task continues through the
|
||||||
|
existing NPC save fields and task-start history;
|
||||||
|
- focused headless and Jajce runtime regressions prove autonomous selection,
|
||||||
|
precedence, finite-source targeting, reservation, and restore-time re-query.
|
||||||
|
|
||||||
|
Milestone 7 is not complete. The immediate next slice should connect the
|
||||||
|
existing one-hop fact transfer to this path: prove that a direct witness or
|
||||||
|
performer can communicate an open need's exact trigger at a real shared
|
||||||
|
activity, after which the newly informed trusted villager is re-derived as the
|
||||||
|
helper and responds through the same autonomous selection contract. Keep this
|
||||||
|
as a truthful event-reference transfer, not generic dialogue or multi-hop
|
||||||
|
rumour infrastructure.
|
||||||
|
|
||||||
Recently completed:
|
Recently completed:
|
||||||
|
|
||||||
|
|||||||
+18
-6
@@ -619,8 +619,10 @@ These are expected prototype constraints, not necessarily isolated bugs:
|
|||||||
`supply_missing_wood` need from exact known evidence. Both have exact NPC and
|
`supply_missing_wood` need from exact known evidence. Both have exact NPC and
|
||||||
player supply resolutions; the wood need also closes deterministically on
|
player supply resolutions; the wood need also closes deterministically on
|
||||||
interested-party death or one-day staleness. One read-only query derives and
|
interested-party death or one-day staleness. One read-only query derives and
|
||||||
explains a capable helper, but there is still no helper assignment,
|
explains a capable helper, and the matching idle NPC can consume it through
|
||||||
acceptance, rewards, free-form dialogue, or quest log.
|
ordinary action selection after urgent self-care and schedule precedence.
|
||||||
|
There is still no helper assignment, acceptance, rewards, free-form dialogue,
|
||||||
|
or quest log.
|
||||||
- The reason inspector exposes current decisions, utility rejections, one exact
|
- The reason inspector exposes current decisions, utility rejections, one exact
|
||||||
relationship cause, and a compact person-history view that distinguishes
|
relationship cause, and a compact person-history view that distinguishes
|
||||||
importance-ranked retained memories from objective personal actions.
|
importance-ranked retained memories from objective personal actions.
|
||||||
@@ -911,10 +913,20 @@ produce one deterministic result. The village summary explains that result;
|
|||||||
save/load re-derives it, and querying changes no task, reservation, RNG stream,
|
save/load re-derives it, and querying changes no task, reservation, RNG stream,
|
||||||
resource, event, or checksum.
|
resource, event, or checksum.
|
||||||
|
|
||||||
Milestone 7 remains in progress. The immediate next slice is one bounded
|
The first autonomous helper consumer is complete. At each idle decision
|
||||||
autonomous consumer: allow the currently derived helper to choose its reported
|
boundary the manager re-derives capability, and only the matching helper may
|
||||||
ordinary supply action after urgent self-care and schedule rules, without
|
select the reported ordinary gather/deposit action. Urgent self-care and
|
||||||
persisted assignment, quest acceptance, or reward state.
|
sleep/meal behavior still win; accepted supply work uses the existing target
|
||||||
|
resolver, finite-node reservation, travel, inventory, and transaction paths.
|
||||||
|
The ordinary in-progress NPC task can save normally, but no helper assignment,
|
||||||
|
quest acceptance, reward, or opportunity-specific event is persisted; ordinary
|
||||||
|
task-start history remains unchanged.
|
||||||
|
|
||||||
|
Milestone 7 remains in progress. The immediate next slice should prove the
|
||||||
|
combined information-to-help path: communicate an open need's exact trigger
|
||||||
|
once at a real shared activity, then re-derive the newly informed trusted NPC as
|
||||||
|
the autonomous helper. Keep the transfer direct and source-backed rather than
|
||||||
|
introducing generic dialogue or multi-hop rumours.
|
||||||
|
|
||||||
The remaining simulation-garden target still aims for:
|
The remaining simulation-garden target still aims for:
|
||||||
|
|
||||||
|
|||||||
@@ -202,6 +202,11 @@ relationship, inventory, action-definition, storage-capacity, and finite-
|
|||||||
resource records. Save/restore regressions require the same helper/action/route
|
resource records. Save/restore regressions require the same helper/action/route
|
||||||
result and an unchanged checksum before and after querying.
|
result and an unchanged checksum before and after querying.
|
||||||
|
|
||||||
|
When an idle derived helper chooses the reported supply action, only the
|
||||||
|
existing NPC task, task-state, target, travel, and reservation fields serialize.
|
||||||
|
The helper result remains absent: restore retains any in-progress ordinary task
|
||||||
|
and independently re-derives current capability from restored facts.
|
||||||
|
|
||||||
## Resource authority
|
## Resource authority
|
||||||
|
|
||||||
`SimulationManager` owns `ResourceStateRecord` instances independently of the
|
`SimulationManager` owns `ResourceStateRecord` instances independently of the
|
||||||
|
|||||||
@@ -156,7 +156,6 @@ func simulate_tick() -> void:
|
|||||||
tick_count += 1
|
tick_count += 1
|
||||||
if debug_logs:
|
if debug_logs:
|
||||||
print("--- Tick ", tick_count, " ---")
|
print("--- Tick ", tick_count, " ---")
|
||||||
|
|
||||||
var village_was_changed := false
|
var village_was_changed := false
|
||||||
for npc in npcs:
|
for npc in npcs:
|
||||||
village_was_changed = _simulate_npc_tick(npc) or village_was_changed
|
village_was_changed = _simulate_npc_tick(npc) or village_was_changed
|
||||||
@@ -207,7 +206,8 @@ func _select_action_if_idle(npc: SimNPC, previous_state: StringName) -> void:
|
|||||||
return
|
return
|
||||||
if npc.task_state not in [SimNPC.TASK_STATE_IDLE, SimNPC.TASK_STATE_COMPLETE]:
|
if npc.task_state not in [SimNPC.TASK_STATE_IDLE, SimNPC.TASK_STATE_COMPLETE]:
|
||||||
return
|
return
|
||||||
var selection := action_selector.select_action(npc, village, clock.time_of_day(), npcs)
|
var helper := get_active_opportunity_helper()
|
||||||
|
var selection := action_selector.select_action(npc, village, clock.time_of_day(), npcs, helper)
|
||||||
if selection == null:
|
if selection == null:
|
||||||
return
|
return
|
||||||
latest_decisions[npc.id] = selection
|
latest_decisions[npc.id] = selection
|
||||||
|
|||||||
@@ -18,7 +18,11 @@ var relationship_system: RefCounted
|
|||||||
|
|
||||||
|
|
||||||
func select_action(
|
func select_action(
|
||||||
npc: SimNPC, village: SimVillage, time_of_day: float = 0.5, all_npcs: Array = []
|
npc: SimNPC,
|
||||||
|
village: SimVillage,
|
||||||
|
time_of_day: float = 0.5,
|
||||||
|
all_npcs: Array = [],
|
||||||
|
opportunity_helper: OpportunityHelperResult = null
|
||||||
) -> ActionSelectionResult:
|
) -> ActionSelectionResult:
|
||||||
if npc.is_dead:
|
if npc.is_dead:
|
||||||
return null
|
return null
|
||||||
@@ -98,6 +102,17 @@ func select_action(
|
|||||||
return ActionSelectionResult.new(
|
return ActionSelectionResult.new(
|
||||||
SimulationIds.ACTION_GATHER_FOOD, -1.0, "Hungry; pantry is empty"
|
SimulationIds.ACTION_GATHER_FOOD, -1.0, "Hungry; pantry is empty"
|
||||||
)
|
)
|
||||||
|
var is_opportunity_helper := _is_current_opportunity_helper(npc, opportunity_helper)
|
||||||
|
if is_opportunity_helper and npc.energy < 25.0:
|
||||||
|
return ActionSelectionResult.new(
|
||||||
|
SimulationIds.ACTION_REST, -1.0, "Energy is low; resting before helping"
|
||||||
|
)
|
||||||
|
if is_opportunity_helper:
|
||||||
|
return ActionSelectionResult.new(
|
||||||
|
opportunity_helper.action_id,
|
||||||
|
-1.0,
|
||||||
|
"Responding to village need: %s" % opportunity_helper.reason
|
||||||
|
)
|
||||||
if npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) > 0.0:
|
if npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) > 0.0:
|
||||||
return ActionSelectionResult.new(
|
return ActionSelectionResult.new(
|
||||||
SimulationIds.ACTION_DEPOSIT_FOOD, -1.0, "Carrying food for storage"
|
SimulationIds.ACTION_DEPOSIT_FOOD, -1.0, "Carrying food for storage"
|
||||||
@@ -131,6 +146,24 @@ func select_action(
|
|||||||
return _choose_best_work_action(npc, village)
|
return _choose_best_work_action(npc, village)
|
||||||
|
|
||||||
|
|
||||||
|
func _is_current_opportunity_helper(
|
||||||
|
npc: SimNPC, opportunity_helper: OpportunityHelperResult
|
||||||
|
) -> bool:
|
||||||
|
return (
|
||||||
|
opportunity_helper != null
|
||||||
|
and opportunity_helper.helper_npc_id == npc.id
|
||||||
|
and (
|
||||||
|
opportunity_helper.action_id
|
||||||
|
in [
|
||||||
|
SimulationIds.ACTION_GATHER_FOOD,
|
||||||
|
SimulationIds.ACTION_GATHER_WOOD,
|
||||||
|
SimulationIds.ACTION_DEPOSIT_FOOD,
|
||||||
|
SimulationIds.ACTION_DEPOSIT_WOOD,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _choose_best_work_action(npc: SimNPC, village: SimVillage) -> ActionSelectionResult:
|
func _choose_best_work_action(npc: SimNPC, village: SimVillage) -> ActionSelectionResult:
|
||||||
var scores := {
|
var scores := {
|
||||||
SimulationIds.ACTION_GATHER_FOOD:
|
SimulationIds.ACTION_GATHER_FOOD:
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ func _initialize() -> void:
|
|||||||
|
|
||||||
func _run() -> void:
|
func _run() -> void:
|
||||||
_test_selection_and_execution_are_separate()
|
_test_selection_and_execution_are_separate()
|
||||||
|
_test_opportunity_helper_selection_preserves_precedence()
|
||||||
_test_target_resolution_and_travel_are_separate()
|
_test_target_resolution_and_travel_are_separate()
|
||||||
|
|
||||||
if failures.is_empty():
|
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:
|
func _test_target_resolution_and_travel_are_separate() -> void:
|
||||||
var adapter := FakeActiveWorld.new()
|
var adapter := FakeActiveWorld.new()
|
||||||
adapter.resource_candidates = [
|
adapter.resource_candidates = [
|
||||||
@@ -185,6 +237,28 @@ func _on_travel_requested(_npc: SimNPC, target_position: Vector3) -> void:
|
|||||||
travel_requests.append(target_position)
|
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:
|
func _check(condition: bool, message: String) -> void:
|
||||||
if not condition:
|
if not condition:
|
||||||
failures.append(message)
|
failures.append(message)
|
||||||
|
|||||||
@@ -317,6 +317,30 @@ func _run() -> void:
|
|||||||
),
|
),
|
||||||
"A real missing-wood fact should surface through wood-specific UI without a food cue"
|
"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)
|
contributor.add_inventory(SimulationIds.RESOURCE_WOOD, 1.0)
|
||||||
_check(
|
_check(
|
||||||
is_equal_approx(
|
is_equal_approx(
|
||||||
|
|||||||
@@ -109,13 +109,16 @@ func _run() -> void:
|
|||||||
)
|
)
|
||||||
|
|
||||||
manager.simulate_tick()
|
manager.simulate_tick()
|
||||||
|
var deposit_decision: ActionSelectionResult = manager.get_latest_decision(helper.id)
|
||||||
_check(
|
_check(
|
||||||
(
|
(
|
||||||
helper.current_task == SimulationIds.ACTION_DEPOSIT_FOOD
|
helper.current_task == SimulationIds.ACTION_DEPOSIT_FOOD
|
||||||
and helper.target_id == SimulationIds.STORAGE_VILLAGE_PANTRY
|
and helper.target_id == SimulationIds.STORAGE_VILLAGE_PANTRY
|
||||||
and helper.has_travel_target
|
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
|
helper_visual.global_position = helper.travel_target_position
|
||||||
manager.synchronize_npc_position(helper.id, helper_visual.global_position)
|
manager.synchronize_npc_position(helper.id, helper_visual.global_position)
|
||||||
|
|||||||
@@ -107,6 +107,26 @@ func _run() -> void:
|
|||||||
manager.get_state_checksum() == checksum_before_helper_query,
|
manager.get_state_checksum() == checksum_before_helper_query,
|
||||||
"Capable-helper discovery should not mutate authoritative simulation state"
|
"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_json: String = manager.serialize_state()
|
||||||
var active_restored := _create_manager(1202)
|
var active_restored := _create_manager(1202)
|
||||||
@@ -127,8 +147,12 @@ func _run() -> void:
|
|||||||
"Active restore should preserve opportunity identity and protected evidence"
|
"Active restore should preserve opportunity identity and protected evidence"
|
||||||
)
|
)
|
||||||
_check(
|
_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()
|
active_restored.free()
|
||||||
|
|
||||||
|
|||||||
@@ -222,11 +222,17 @@ func begin_pantry_restock() -> bool:
|
|||||||
var interested := _get_npc(_interested_villager_id)
|
var interested := _get_npc(_interested_villager_id)
|
||||||
if helper == null or interested == null or simulation_manager.get_active_opportunity() == null:
|
if helper == null or interested == null or simulation_manager.get_active_opportunity() == null:
|
||||||
return false
|
return false
|
||||||
|
var opportunity_helper: OpportunityHelperResult = (
|
||||||
|
simulation_manager.get_active_opportunity_helper()
|
||||||
|
)
|
||||||
|
if opportunity_helper == null or opportunity_helper.helper_npc_id != helper.id:
|
||||||
|
return false
|
||||||
var selection: ActionSelectionResult = simulation_manager.action_selector.select_action(
|
var selection: ActionSelectionResult = simulation_manager.action_selector.select_action(
|
||||||
helper,
|
helper,
|
||||||
simulation_manager.village,
|
simulation_manager.village,
|
||||||
simulation_manager.clock.time_of_day(),
|
simulation_manager.clock.time_of_day(),
|
||||||
simulation_manager.npcs
|
simulation_manager.npcs,
|
||||||
|
opportunity_helper
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
selection == null
|
selection == null
|
||||||
|
|||||||
Reference in New Issue
Block a user