feat: add knowledge-gated pantry opportunities
This commit is contained in:
@@ -0,0 +1,369 @@
|
||||
extends SceneTree
|
||||
|
||||
var failures: Array[String] = []
|
||||
var opened_ids: Array[int] = []
|
||||
var resolved_ids: Array[int] = []
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var manager := _create_manager()
|
||||
manager.opportunity_opened.connect(_on_opportunity_opened)
|
||||
manager.opportunity_resolved.connect(_on_opportunity_resolved)
|
||||
var actor: SimNPC = manager.npcs[0]
|
||||
var witness: SimNPC = manager.npcs[1]
|
||||
var interested: SimNPC = manager.npcs[2]
|
||||
var supplier: SimNPC = manager.npcs[3]
|
||||
var pantry: StorageStateRecord = manager.get_pantry()
|
||||
_set_pantry_amount(manager, 1.0)
|
||||
actor.position = Vector3.ZERO
|
||||
witness.position = Vector3(4.0, 0.0, 0.0)
|
||||
interested.position = Vector3(20.0, 0.0, 0.0)
|
||||
supplier.position = Vector3(40.0, 0.0, 0.0)
|
||||
actor.hunger = 60.0
|
||||
witness.hunger = 20.0
|
||||
interested.hunger = 85.0
|
||||
|
||||
_check(
|
||||
manager.get_active_opportunity() == null,
|
||||
"An empty-or-low pantry alone must not invent an opportunity"
|
||||
)
|
||||
_check(
|
||||
is_equal_approx(
|
||||
manager.economy.withdraw_to_inventory(actor, SimulationIds.RESOURCE_FOOD, 1.0), 1.0
|
||||
),
|
||||
"The opportunity trigger should be a real last-food withdrawal"
|
||||
)
|
||||
var trigger_event := _latest_event_of_type(
|
||||
manager, SimulationIds.EVENT_STORAGE_WITHDRAWN, actor.id
|
||||
)
|
||||
_check(trigger_event != null, "The last-food withdrawal should exist in objective history")
|
||||
if trigger_event == null:
|
||||
manager.free()
|
||||
_finish()
|
||||
return
|
||||
var trigger_event_id := int(trigger_event.data["event_id"])
|
||||
_check(
|
||||
(
|
||||
is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), 0.0)
|
||||
and manager.npc_knows_event(actor.id, trigger_event_id)
|
||||
and manager.npc_knows_event(witness.id, trigger_event_id)
|
||||
and not manager.npc_knows_event(interested.id, trigger_event_id)
|
||||
and manager.get_active_opportunity() == null
|
||||
),
|
||||
"Knowledge without critical hunger and critical hunger without knowledge should not open"
|
||||
)
|
||||
|
||||
_prepare_shared_patrol(actor, interested, Vector3(20.0, 0.0, 0.0))
|
||||
var event_count_before_open: int = manager.economic_events.size()
|
||||
_check(
|
||||
manager.try_communicate_at_shared_activity(actor.id, interested.id),
|
||||
"A direct knower should be able to report the empty pantry at shared work"
|
||||
)
|
||||
var opportunity: OpportunityStateRecord = manager.get_active_opportunity()
|
||||
_check(opportunity != null, "A critically hungry knower should open the village need")
|
||||
if opportunity == null:
|
||||
manager.free()
|
||||
_finish()
|
||||
return
|
||||
_check(
|
||||
(
|
||||
opportunity.get_opportunity_id() == 0
|
||||
and opportunity.get_trigger_event_id() == trigger_event_id
|
||||
and opportunity.get_interested_npc_id() == interested.id
|
||||
and opportunity.get_target_id() == SimulationIds.STORAGE_VILLAGE_PANTRY
|
||||
and opportunity.get_resource_id() == SimulationIds.RESOURCE_FOOD
|
||||
and is_equal_approx(opportunity.get_target_amount(), 1.0)
|
||||
and opportunity.get_status() == OpportunityStateRecord.STATUS_OPEN
|
||||
),
|
||||
"The open opportunity should reference the real knower, trigger, storage, and goal"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
opened_ids == [opportunity.get_opportunity_id()]
|
||||
and manager.economic_events.size() == event_count_before_open
|
||||
and manager.opportunity_system.next_opportunity_id == 1
|
||||
),
|
||||
"Opening should emit once without adding quest-only objective events"
|
||||
)
|
||||
_check(
|
||||
manager.is_known_event_lasting(interested.id, trigger_event_id),
|
||||
"An unresolved village need should protect its interested villager's causal memory"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
not manager.try_communicate_at_shared_activity(actor.id, interested.id)
|
||||
and manager.opportunity_system.get_all_sorted().size() == 1
|
||||
),
|
||||
"Repeated reports should neither replace provenance nor duplicate an open need"
|
||||
)
|
||||
|
||||
var active_json: String = manager.serialize_state()
|
||||
var active_restored := _create_manager(902)
|
||||
_check(
|
||||
active_restored.restore_state_from_json(active_json),
|
||||
"An active opportunity should survive schema-v8 save and restore"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
active_restored.get_state_checksum() == manager.get_state_checksum()
|
||||
and active_restored.get_active_opportunity() != null
|
||||
and active_restored.is_known_event_lasting(interested.id, trigger_event_id)
|
||||
),
|
||||
"Active restore should preserve checksum, stable identity, and protected evidence"
|
||||
)
|
||||
active_restored.tick_count = active_restored.get_knowledge_review_interval()
|
||||
active_restored.call("_maintain_event_knowledge", true)
|
||||
_check(
|
||||
active_restored.npc_knows_event(interested.id, trigger_event_id),
|
||||
"Daily memory review must retain the evidence for an unresolved opportunity"
|
||||
)
|
||||
var restored_supplier: SimNPC = active_restored.npcs[3]
|
||||
restored_supplier.add_inventory(SimulationIds.RESOURCE_FOOD, 1.0)
|
||||
_check(
|
||||
is_equal_approx(
|
||||
active_restored.economy.deposit_inventory(
|
||||
restored_supplier, SimulationIds.RESOURCE_FOOD
|
||||
),
|
||||
1.0
|
||||
),
|
||||
"A restored opportunity should still accept a real pantry supply event"
|
||||
)
|
||||
var restored_resolution: OpportunityStateRecord = (
|
||||
active_restored.get_latest_opportunity_for_npc(interested.id)
|
||||
)
|
||||
_check(
|
||||
(
|
||||
restored_resolution.get_status() == OpportunityStateRecord.STATUS_RESOLVED
|
||||
and active_restored.get_active_opportunity() == null
|
||||
and (
|
||||
(
|
||||
active_restored
|
||||
. get_opportunity_resolution_event(restored_resolution)
|
||||
. data["event_id"]
|
||||
)
|
||||
== restored_resolution.get_resolution_event_id()
|
||||
)
|
||||
),
|
||||
"Restored wiring should close exactly once against the new persisted event ID"
|
||||
)
|
||||
active_restored.free()
|
||||
|
||||
interested.position = Vector3(21.0, 0.0, 0.0)
|
||||
supplier.position = Vector3(22.0, 0.0, 0.0)
|
||||
supplier.add_inventory(SimulationIds.RESOURCE_FOOD, 1.0)
|
||||
var event_count_before_resolution: int = manager.economic_events.size()
|
||||
_check(
|
||||
is_equal_approx(
|
||||
manager.economy.deposit_inventory(supplier, SimulationIds.RESOURCE_FOOD), 1.0
|
||||
),
|
||||
"An NPC should resolve the need through the real inventory-to-pantry transaction"
|
||||
)
|
||||
var resolved: OpportunityStateRecord = manager.get_latest_opportunity_for_npc(interested.id)
|
||||
var resolution_event: EconomicEventRecord = manager.get_opportunity_resolution_event(resolved)
|
||||
_check(
|
||||
(
|
||||
resolved != null
|
||||
and resolved.get_status() == OpportunityStateRecord.STATUS_RESOLVED
|
||||
and manager.get_active_opportunity() == null
|
||||
and resolution_event != null
|
||||
and (
|
||||
StringName(resolution_event.data["event_type"])
|
||||
== SimulationIds.EVENT_STORAGE_DEPOSITED
|
||||
)
|
||||
and resolved.get_resolution_event_id() == int(resolution_event.data["event_id"])
|
||||
and is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), 1.0)
|
||||
),
|
||||
"The opportunity should close on the exact later NPC supply event"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
resolved_ids == [opportunity.get_opportunity_id()]
|
||||
and manager.economic_events.size() == event_count_before_resolution + 1
|
||||
),
|
||||
"Resolution should emit once and reuse the supply event instead of creating quest history"
|
||||
)
|
||||
_check(
|
||||
not manager.is_known_event_lasting(interested.id, trigger_event_id),
|
||||
"Resolved needs should release their trigger evidence back to normal memory retention"
|
||||
)
|
||||
var supplier_relationship: RelationshipStateRecord = (
|
||||
manager.relationship_system.get_relationship(interested.id, supplier.id)
|
||||
)
|
||||
_check(
|
||||
(
|
||||
supplier_relationship != null
|
||||
and is_equal_approx(supplier_relationship.get_trust(), 0.65)
|
||||
and (
|
||||
supplier_relationship.get_last_trust_cause_event_id()
|
||||
== resolved.get_resolution_event_id()
|
||||
)
|
||||
),
|
||||
"The real resolving deposit should retain its existing witnessed trust consequence"
|
||||
)
|
||||
|
||||
var resolved_json: String = manager.serialize_state()
|
||||
var resolved_restored := _create_manager(903)
|
||||
_check(
|
||||
resolved_restored.restore_state_from_json(resolved_json),
|
||||
"Resolved opportunity history should restore"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
resolved_restored.get_state_checksum() == manager.get_state_checksum()
|
||||
and resolved_restored.get_active_opportunity() == null
|
||||
and (
|
||||
resolved_restored.get_latest_opportunity_for_npc(interested.id).get_status()
|
||||
== OpportunityStateRecord.STATUS_RESOLVED
|
||||
)
|
||||
),
|
||||
"Resolved restore should preserve checksum without reopening the old shortage"
|
||||
)
|
||||
resolved_restored.free()
|
||||
manager.free()
|
||||
|
||||
opened_ids.clear()
|
||||
resolved_ids.clear()
|
||||
var player_manager := _create_manager(904)
|
||||
var player_setup := _open_via_communication(player_manager)
|
||||
var player_interested: SimNPC = player_setup["interested"]
|
||||
var berry := ResourceNode.new()
|
||||
berry.name = "OpportunityBerryBush"
|
||||
berry.node_id = &"opportunity_berry_bush"
|
||||
berry.action_id = SimulationIds.ACTION_GATHER_FOOD
|
||||
berry.resource_id = SimulationIds.RESOURCE_FOOD
|
||||
berry.initial_amount = 1.0
|
||||
berry.yield_per_action = 1.0
|
||||
berry.debug_label_enabled = false
|
||||
var interaction_point := Marker3D.new()
|
||||
interaction_point.name = "InteractionPoint"
|
||||
berry.add_child(interaction_point)
|
||||
root.add_child(berry)
|
||||
await process_frame
|
||||
_check(
|
||||
player_manager.register_resource_node(berry),
|
||||
"The player resolution branch should bind a real finite ResourceNode"
|
||||
)
|
||||
_check(
|
||||
is_equal_approx(player_manager.harvest_resource_node(berry), 1.0),
|
||||
"Player harvesting should supply the real pantry through the existing command"
|
||||
)
|
||||
var player_resolved: OpportunityStateRecord = player_manager.get_latest_opportunity_for_npc(
|
||||
player_interested.id
|
||||
)
|
||||
var player_resolution: EconomicEventRecord = player_manager.get_opportunity_resolution_event(
|
||||
player_resolved
|
||||
)
|
||||
_check(
|
||||
(
|
||||
player_resolved != null
|
||||
and player_resolved.get_status() == OpportunityStateRecord.STATUS_RESOLVED
|
||||
and player_resolution != null
|
||||
and (
|
||||
StringName(player_resolution.data["event_type"])
|
||||
== SimulationIds.EVENT_RESOURCE_EXTRACTED
|
||||
)
|
||||
and int(player_resolution.data["actor_id"]) == -1
|
||||
and (
|
||||
StringName(player_resolution.data["destination_id"])
|
||||
== SimulationIds.STORAGE_VILLAGE_PANTRY
|
||||
)
|
||||
),
|
||||
"Player harvesting should be the second real resolution path with exact history"
|
||||
)
|
||||
|
||||
berry.queue_free()
|
||||
player_manager.free()
|
||||
_finish()
|
||||
|
||||
|
||||
func _open_via_communication(manager: Node) -> Dictionary:
|
||||
var actor: SimNPC = manager.npcs[0]
|
||||
var interested: SimNPC = manager.npcs[2]
|
||||
_set_pantry_amount(manager, 1.0)
|
||||
for npc in manager.npcs:
|
||||
npc.position = Vector3(40.0 + npc.id * 10.0, 0.0, 0.0)
|
||||
actor.position = Vector3.ZERO
|
||||
actor.hunger = 60.0
|
||||
interested.hunger = 90.0
|
||||
manager.economy.withdraw_to_inventory(actor, SimulationIds.RESOURCE_FOOD, 1.0)
|
||||
_prepare_shared_patrol(actor, interested, Vector3(20.0, 0.0, 0.0))
|
||||
_check(
|
||||
manager.try_communicate_at_shared_activity(actor.id, interested.id),
|
||||
"Player branch should open from the same one-hop known shortage"
|
||||
)
|
||||
_check(manager.get_active_opportunity() != null, "Player branch needs an active opportunity")
|
||||
return {"actor": actor, "interested": interested}
|
||||
|
||||
|
||||
func _prepare_shared_patrol(first: SimNPC, second: SimNPC, position: Vector3) -> void:
|
||||
for npc in [first, second]:
|
||||
npc.set_task(SimulationIds.ACTION_PATROL)
|
||||
npc.target_id = &"guard_post"
|
||||
npc.start_working()
|
||||
first.position = position
|
||||
second.position = position + Vector3(1.0, 0.0, 0.0)
|
||||
|
||||
|
||||
func _set_pantry_amount(manager: Node, amount: float) -> void:
|
||||
var pantry: StorageStateRecord = manager.get_pantry()
|
||||
pantry.withdraw(SimulationIds.RESOURCE_FOOD, pantry.get_amount(SimulationIds.RESOURCE_FOOD))
|
||||
pantry.deposit(SimulationIds.RESOURCE_FOOD, amount)
|
||||
manager.economy.sync_resource(SimulationIds.RESOURCE_FOOD)
|
||||
|
||||
|
||||
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_manager(seed_value: int = 901) -> 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 _on_opportunity_opened(opportunity: OpportunityStateRecord) -> void:
|
||||
opened_ids.append(opportunity.get_opportunity_id())
|
||||
|
||||
|
||||
func _on_opportunity_resolved(
|
||||
opportunity: OpportunityStateRecord, _cause_event: EconomicEventRecord
|
||||
) -> void:
|
||||
resolved_ids.append(opportunity.get_opportunity_id())
|
||||
|
||||
|
||||
func _finish() -> void:
|
||||
if failures.is_empty():
|
||||
print("[TEST] Food shortage opportunity passed: known need -> real supply")
|
||||
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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://c42mc37eiqjrw
|
||||
@@ -119,6 +119,105 @@ func _run() -> void:
|
||||
not listener_visual.get_node("TrustReactionRoot").visible,
|
||||
"Learning a fact without a relationship consequence should not show a trust blossom"
|
||||
)
|
||||
|
||||
var village_stats_label := (
|
||||
main_scene.get_node("UI/VillagePanel/MarginContainer/VillageStatsLabel") as Label
|
||||
)
|
||||
var pantry_state: StorageStateRecord = simulation_manager.get_pantry()
|
||||
contributor.hunger = 85.0
|
||||
var withdrawn_for_need: float = simulation_manager.economy.withdraw_to_inventory(
|
||||
contributor,
|
||||
SimulationIds.RESOURCE_FOOD,
|
||||
pantry_state.get_amount(SimulationIds.RESOURCE_FOOD)
|
||||
)
|
||||
_check(withdrawn_for_need > 0.0, "Runtime opportunity setup should empty the real pantry")
|
||||
var opportunity: OpportunityStateRecord = simulation_manager.get_active_opportunity()
|
||||
_check(opportunity != null, "Emptying the pantry should open one known shortage need")
|
||||
_check(
|
||||
(
|
||||
"Village need" in village_stats_label.text
|
||||
and "◆ Restock the empty pantry" in village_stats_label.text
|
||||
and (
|
||||
"%s is worried about the shortage" % contributor.npc_name
|
||||
in village_stats_label.text
|
||||
)
|
||||
and "Food 0 / 1" in village_stats_label.text
|
||||
),
|
||||
"An open opportunity should add one compact state-derived village need"
|
||||
)
|
||||
village_ui.selected_npc_index = contributor.id
|
||||
village_ui.call("_refresh_npc_inspector")
|
||||
_check(
|
||||
(
|
||||
"Open village need" in inspector_label.text
|
||||
and "◆ Restock the empty pantry" in inspector_label.text
|
||||
and "Food 0 / 1" in inspector_label.text
|
||||
),
|
||||
"The interested villager should expose the open pantry need in their inspector"
|
||||
)
|
||||
village_ui.selected_npc_index = listener.id
|
||||
village_ui.call("_refresh_npc_inspector")
|
||||
_check(
|
||||
"Open village need" not in inspector_label.text,
|
||||
"An unrelated selected villager should not inherit someone else's open need"
|
||||
)
|
||||
|
||||
_check(
|
||||
(
|
||||
simulation_manager.economy.deposit_inventory(contributor, SimulationIds.RESOURCE_FOOD)
|
||||
> 0.0
|
||||
),
|
||||
"Opportunity presentation should resolve from a real pantry deposit"
|
||||
)
|
||||
_check(
|
||||
"Village need" not in village_stats_label.text,
|
||||
"Resolved opportunities should leave the compact village summary"
|
||||
)
|
||||
village_ui.selected_npc_index = contributor.id
|
||||
village_ui.call("_refresh_npc_inspector")
|
||||
_check(
|
||||
(
|
||||
"Resolved village need" in inspector_label.text
|
||||
and "◆ %s restocked the pantry" % contributor.npc_name in inspector_label.text
|
||||
and "Food target 1" in inspector_label.text
|
||||
),
|
||||
"The interested villager should retain a resolved detail naming the real actor"
|
||||
)
|
||||
village_ui.selected_npc_index = listener.id
|
||||
village_ui.call("_refresh_npc_inspector")
|
||||
_check(
|
||||
"Resolved village need" not in inspector_label.text,
|
||||
"Resolved opportunity detail should remain scoped to its interested villager"
|
||||
)
|
||||
|
||||
var player_need_withdrawal: float = simulation_manager.economy.withdraw_to_inventory(
|
||||
contributor,
|
||||
SimulationIds.RESOURCE_FOOD,
|
||||
pantry_state.get_amount(SimulationIds.RESOURCE_FOOD)
|
||||
)
|
||||
_check(player_need_withdrawal > 0.0, "Player resolution setup should reopen a real need")
|
||||
var player_opportunity: OpportunityStateRecord = simulation_manager.get_active_opportunity()
|
||||
_check(player_opportunity != null, "A later shortage should open a new opportunity record")
|
||||
var player := main_scene.get_node("Player") as Node3D
|
||||
var opportunity_bush := (
|
||||
main_scene.get_node("JajceWorld/WorldObjects/ResourceNodes/BerryBush_01") as ResourceNode
|
||||
)
|
||||
var opportunity_bush_state: ResourceStateRecord = simulation_manager.get_resource_state(
|
||||
opportunity_bush.node_id
|
||||
)
|
||||
opportunity_bush_state.set_amount_remaining(1.0)
|
||||
player.global_position = opportunity_bush.interaction_point.global_position
|
||||
player.call("try_interact")
|
||||
village_ui.selected_npc_index = contributor.id
|
||||
village_ui.call("_refresh_npc_inspector")
|
||||
_check(
|
||||
(
|
||||
player_opportunity.get_status() == OpportunityStateRecord.STATUS_RESOLVED
|
||||
and "Resolved village need" in inspector_label.text
|
||||
and "◆ Player restocked the pantry" in inspector_label.text
|
||||
),
|
||||
"Player gathering should resolve the same need and be named from its real event"
|
||||
)
|
||||
_check(
|
||||
main_scene.has_node("JajceWorld/TerrainRoot/Terrain3D"),
|
||||
"Playable runtime should instance the Jajce Terrain3D world"
|
||||
|
||||
@@ -18,7 +18,9 @@ func _run() -> void:
|
||||
_test_previous_world_knowledge_migration()
|
||||
_test_previous_world_provenance_migration()
|
||||
_test_previous_world_retention_migration()
|
||||
_test_previous_world_opportunity_migration()
|
||||
_test_relationship_schema_rejection()
|
||||
_test_opportunity_schema_rejection()
|
||||
_test_schema_rejection()
|
||||
|
||||
if failures.is_empty():
|
||||
@@ -371,7 +373,7 @@ func _test_previous_world_provenance_migration() -> void:
|
||||
func _test_previous_world_retention_migration() -> void:
|
||||
var manager := _create_manager(61)
|
||||
var previous_data: Dictionary = manager.create_state_record().to_dictionary()
|
||||
previous_data["schema_version"] = SimulationStateRecord.PREVIOUS_SCHEMA_VERSION
|
||||
previous_data["schema_version"] = SimulationStateRecord.RETENTION_LEGACY_SCHEMA_VERSION
|
||||
previous_data["simulation"]["tick_count"] = 50
|
||||
var deposit_event := EconomicEventRecord.create(
|
||||
0,
|
||||
@@ -439,6 +441,25 @@ func _test_previous_world_retention_migration() -> void:
|
||||
manager.free()
|
||||
|
||||
|
||||
func _test_previous_world_opportunity_migration() -> void:
|
||||
var manager := _create_manager(62)
|
||||
var previous_data: Dictionary = manager.create_state_record().to_dictionary()
|
||||
previous_data["schema_version"] = SimulationStateRecord.PREVIOUS_SCHEMA_VERSION
|
||||
previous_data.erase("opportunities")
|
||||
previous_data["simulation"].erase("next_opportunity_id")
|
||||
var migrated := SimulationStateRecord.from_dictionary(previous_data)
|
||||
_check(migrated != null, "World schema v7 should add an empty opportunity stream")
|
||||
if migrated != null:
|
||||
_check(
|
||||
(
|
||||
migrated.opportunities.is_empty()
|
||||
and int(migrated.simulation["next_opportunity_id"]) == 0
|
||||
),
|
||||
"World v7 migration should initialize deterministic opportunity identity"
|
||||
)
|
||||
manager.free()
|
||||
|
||||
|
||||
func _test_relationship_schema_rejection() -> void:
|
||||
var manager := _create_manager(58)
|
||||
var missing_cause_data: Dictionary = manager.create_state_record().to_dictionary()
|
||||
@@ -685,6 +706,249 @@ func _test_relationship_schema_rejection() -> void:
|
||||
manager.free()
|
||||
|
||||
|
||||
func _test_opportunity_schema_rejection() -> void:
|
||||
var manager := _create_manager(63)
|
||||
var valid_open := _build_opportunity_world(manager, false)
|
||||
_check(
|
||||
SimulationStateRecord.from_dictionary(valid_open) != null,
|
||||
"A known empty-pantry trigger should accept one open restock opportunity"
|
||||
)
|
||||
|
||||
var missing_knowledge := valid_open.duplicate(true)
|
||||
missing_knowledge["event_knowledge"] = []
|
||||
_check(
|
||||
SimulationStateRecord.from_dictionary(missing_knowledge) == null,
|
||||
"An open opportunity should require its interested NPC to know the trigger"
|
||||
)
|
||||
var knowledge_acquired_too_late := valid_open.duplicate(true)
|
||||
knowledge_acquired_too_late["event_knowledge"][0]["acquisition_method"] = String(
|
||||
SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY
|
||||
)
|
||||
knowledge_acquired_too_late["event_knowledge"][0]["acquired_tick"] = 3
|
||||
_check(
|
||||
SimulationStateRecord.from_dictionary(knowledge_acquired_too_late) == null,
|
||||
"An open opportunity cannot predate the interested NPC's knowledge"
|
||||
)
|
||||
var already_supplied := valid_open.duplicate(true)
|
||||
for storage_data in already_supplied["storages"]:
|
||||
if StringName(storage_data["storage_id"]) == SimulationIds.STORAGE_VILLAGE_PANTRY:
|
||||
storage_data["amounts"][String(SimulationIds.RESOURCE_FOOD)] = 1.0
|
||||
_check(
|
||||
SimulationStateRecord.from_dictionary(already_supplied) == null,
|
||||
"An open pantry need should reject a save whose target is already supplied"
|
||||
)
|
||||
|
||||
var duplicate_id := valid_open.duplicate(true)
|
||||
var second_opportunity: Dictionary = duplicate_id["opportunities"][0].duplicate(true)
|
||||
second_opportunity["trigger_event_id"] = 1
|
||||
duplicate_id["economic_events"].append(_create_pantry_withdrawal_event(1, 3, 1).to_dictionary())
|
||||
duplicate_id["event_knowledge"].append(
|
||||
(
|
||||
KnownEventStateRecord
|
||||
. create(
|
||||
1,
|
||||
1,
|
||||
SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED,
|
||||
KnownEventStateRecord.NO_SOURCE_NPC_ID,
|
||||
3
|
||||
)
|
||||
. to_dictionary()
|
||||
)
|
||||
)
|
||||
duplicate_id["opportunities"].append(second_opportunity)
|
||||
duplicate_id["simulation"]["next_event_id"] = 2
|
||||
duplicate_id["simulation"]["next_opportunity_id"] = 2
|
||||
_check(
|
||||
SimulationStateRecord.from_dictionary(duplicate_id) == null,
|
||||
"Opportunity IDs should be unique"
|
||||
)
|
||||
var multiple_open := duplicate_id.duplicate(true)
|
||||
multiple_open["opportunities"][1]["opportunity_id"] = 1
|
||||
_check(
|
||||
SimulationStateRecord.from_dictionary(multiple_open) == null,
|
||||
"The bounded pantry slice should restore at most one open opportunity"
|
||||
)
|
||||
|
||||
var duplicate_trigger := valid_open.duplicate(true)
|
||||
var repeated_trigger: Dictionary = duplicate_trigger["opportunities"][0].duplicate(true)
|
||||
repeated_trigger["opportunity_id"] = 1
|
||||
duplicate_trigger["opportunities"].append(repeated_trigger)
|
||||
duplicate_trigger["simulation"]["next_opportunity_id"] = 2
|
||||
_check(
|
||||
SimulationStateRecord.from_dictionary(duplicate_trigger) == null,
|
||||
"One objective trigger should not create duplicate opportunity records"
|
||||
)
|
||||
|
||||
var colliding_next_id := valid_open.duplicate(true)
|
||||
colliding_next_id["simulation"]["next_opportunity_id"] = 0
|
||||
_check(
|
||||
SimulationStateRecord.from_dictionary(colliding_next_id) == null,
|
||||
"The next opportunity ID should remain above restored history"
|
||||
)
|
||||
|
||||
var unknown_interested_npc := valid_open.duplicate(true)
|
||||
unknown_interested_npc["opportunities"][0]["interested_npc_id"] = 999
|
||||
_check(
|
||||
SimulationStateRecord.from_dictionary(unknown_interested_npc) == null,
|
||||
"Opportunities should reference an existing interested NPC"
|
||||
)
|
||||
|
||||
var future_creation := valid_open.duplicate(true)
|
||||
future_creation["opportunities"][0]["created_tick"] = 11
|
||||
_check(
|
||||
SimulationStateRecord.from_dictionary(future_creation) == null,
|
||||
"Opportunity creation cannot occur after the saved simulation tick"
|
||||
)
|
||||
|
||||
var before_trigger := valid_open.duplicate(true)
|
||||
before_trigger["opportunities"][0]["created_tick"] = 1
|
||||
_check(
|
||||
SimulationStateRecord.from_dictionary(before_trigger) == null,
|
||||
"Opportunity creation cannot predate its trigger event"
|
||||
)
|
||||
|
||||
var invalid_trigger := valid_open.duplicate(true)
|
||||
invalid_trigger["economic_events"][0]["destination_id"] = "unrelated_inventory"
|
||||
_check(
|
||||
SimulationStateRecord.from_dictionary(invalid_trigger) == null,
|
||||
"A restock opportunity trigger should be a real NPC pantry withdrawal"
|
||||
)
|
||||
|
||||
var player_trigger := valid_open.duplicate(true)
|
||||
player_trigger["economic_events"][0]["actor_id"] = -1
|
||||
player_trigger["economic_events"][0]["destination_id"] = "player_inventory"
|
||||
_check(
|
||||
SimulationStateRecord.from_dictionary(player_trigger) == null,
|
||||
"Player extraction or consumption should not masquerade as an informed NPC trigger"
|
||||
)
|
||||
|
||||
var valid_resolved := _build_opportunity_world(manager, true)
|
||||
_check(
|
||||
SimulationStateRecord.from_dictionary(valid_resolved) != null,
|
||||
"A later NPC pantry deposit should resolve an opportunity without retained knowledge"
|
||||
)
|
||||
|
||||
var player_resolved := _build_opportunity_world(manager, true)
|
||||
player_resolved["economic_events"][1] = (
|
||||
EconomicEventRecord
|
||||
. create(
|
||||
1,
|
||||
SimulationIds.EVENT_RESOURCE_EXTRACTED,
|
||||
3,
|
||||
-1,
|
||||
&"schema_test_berry",
|
||||
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||
SimulationIds.RESOURCE_FOOD,
|
||||
1.0
|
||||
)
|
||||
. to_dictionary()
|
||||
)
|
||||
player_resolved["resources"].append(
|
||||
_create_player_food_resource_state(&"schema_test_berry").to_dictionary()
|
||||
)
|
||||
_check(
|
||||
SimulationStateRecord.from_dictionary(player_resolved) != null,
|
||||
"A later player resource extraction into the pantry should be a valid resolution"
|
||||
)
|
||||
var unknown_player_source := player_resolved.duplicate(true)
|
||||
unknown_player_source["economic_events"][1]["source_id"] = "missing_berry"
|
||||
_check(
|
||||
SimulationStateRecord.from_dictionary(unknown_player_source) == null,
|
||||
"A player resolution should reference a real player-usable food resource"
|
||||
)
|
||||
|
||||
var invalid_resolution := valid_resolved.duplicate(true)
|
||||
invalid_resolution["economic_events"][1]["destination_id"] = "somewhere_else"
|
||||
_check(
|
||||
SimulationStateRecord.from_dictionary(invalid_resolution) == null,
|
||||
"Resolution events should actually supply food to the opportunity target"
|
||||
)
|
||||
|
||||
var mismatched_resolution_tick := valid_resolved.duplicate(true)
|
||||
mismatched_resolution_tick["opportunities"][0]["resolved_tick"] = 4
|
||||
_check(
|
||||
SimulationStateRecord.from_dictionary(mismatched_resolution_tick) == null,
|
||||
"Resolved opportunity ticks should match the referenced resolution event"
|
||||
)
|
||||
manager.free()
|
||||
|
||||
|
||||
func _build_opportunity_world(manager: Node, resolved: bool) -> Dictionary:
|
||||
var world: Dictionary = manager.create_state_record().to_dictionary()
|
||||
world["simulation"]["tick_count"] = 10
|
||||
for storage_data in world["storages"]:
|
||||
if StringName(storage_data["storage_id"]) == SimulationIds.STORAGE_VILLAGE_PANTRY:
|
||||
storage_data["amounts"][String(SimulationIds.RESOURCE_FOOD)] = (
|
||||
1.0 if resolved else 0.0
|
||||
)
|
||||
world["economic_events"] = [_create_pantry_withdrawal_event(0, 2, 0).to_dictionary()]
|
||||
world["simulation"]["next_event_id"] = 1
|
||||
var opportunity := OpportunityStateRecord.create(0, 2, 0, 0)
|
||||
if resolved:
|
||||
world["economic_events"].append(
|
||||
(
|
||||
EconomicEventRecord
|
||||
. create(
|
||||
1,
|
||||
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
||||
3,
|
||||
1,
|
||||
SimulationIds.npc_inventory_id(1),
|
||||
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||
SimulationIds.RESOURCE_FOOD,
|
||||
1.0
|
||||
)
|
||||
. to_dictionary()
|
||||
)
|
||||
)
|
||||
world["simulation"]["next_event_id"] = 2
|
||||
opportunity.resolve(1, 3)
|
||||
world["event_knowledge"] = []
|
||||
else:
|
||||
world["event_knowledge"] = [
|
||||
(
|
||||
KnownEventStateRecord
|
||||
. create(
|
||||
0,
|
||||
0,
|
||||
SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED,
|
||||
KnownEventStateRecord.NO_SOURCE_NPC_ID,
|
||||
2
|
||||
)
|
||||
. to_dictionary()
|
||||
)
|
||||
]
|
||||
world["opportunities"] = [opportunity.to_dictionary()]
|
||||
world["simulation"]["next_opportunity_id"] = 1
|
||||
return world
|
||||
|
||||
|
||||
func _create_pantry_withdrawal_event(
|
||||
event_id: int, tick: int, actor_id: int
|
||||
) -> EconomicEventRecord:
|
||||
return EconomicEventRecord.create(
|
||||
event_id,
|
||||
SimulationIds.EVENT_STORAGE_WITHDRAWN,
|
||||
tick,
|
||||
actor_id,
|
||||
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||
SimulationIds.npc_inventory_id(actor_id),
|
||||
SimulationIds.RESOURCE_FOOD,
|
||||
1.0
|
||||
)
|
||||
|
||||
|
||||
func _create_player_food_resource_state(node_id: StringName) -> ResourceStateRecord:
|
||||
var resource_node := ResourceNode.new()
|
||||
resource_node.node_id = node_id
|
||||
resource_node.action_id = SimulationIds.ACTION_GATHER_FOOD
|
||||
resource_node.resource_id = SimulationIds.RESOURCE_FOOD
|
||||
resource_node.can_player_use = true
|
||||
var resource_state := ResourceStateRecord.create_from_node(resource_node)
|
||||
resource_node.free()
|
||||
return resource_state
|
||||
|
||||
|
||||
func _create_manager(seed_value: int) -> Node:
|
||||
var manager: Node = load("res://simulation/SimulationManager.gd").new()
|
||||
manager.simulation_seed = seed_value
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
extends GutTest
|
||||
|
||||
const FoodShortageOpportunitySystemScript := preload(
|
||||
"res://simulation/opportunities/FoodShortageOpportunitySystem.gd"
|
||||
)
|
||||
|
||||
|
||||
func test_opening_requires_current_knowledge_and_care() -> void:
|
||||
var system := FoodShortageOpportunitySystemScript.new()
|
||||
var pantry := _empty_pantry()
|
||||
var npc := _npc(0, 90.0)
|
||||
var npcs: Array[SimNPC] = [npc]
|
||||
var knowledge := EventKnowledgeSystem.new()
|
||||
var trigger := _withdrawal_event(4, 8, npc.id)
|
||||
|
||||
assert_null(system.consider_event(trigger, 8, pantry, npcs, knowledge, {}))
|
||||
_remember(knowledge, npc.id, 4, 8)
|
||||
npc.hunger = FoodShortageOpportunitySystem.CARE_HUNGER_THRESHOLD - 0.01
|
||||
assert_null(system.consider_event(trigger, 8, pantry, npcs, knowledge, {}))
|
||||
assert_true(system.get_all_sorted().is_empty())
|
||||
assert_eq(system.next_opportunity_id, 0)
|
||||
|
||||
|
||||
func test_highest_hunger_then_lowest_id_deterministically_owns_the_opportunity() -> void:
|
||||
var system := FoodShortageOpportunitySystemScript.new()
|
||||
var pantry := _empty_pantry()
|
||||
var lower_hunger := _npc(1, 85.0)
|
||||
var high_id := _npc(5, 92.0)
|
||||
var low_id := _npc(2, 92.0)
|
||||
var npcs: Array[SimNPC] = [high_id, lower_hunger, low_id]
|
||||
var knowledge := EventKnowledgeSystem.new()
|
||||
var trigger := _withdrawal_event(7, 10, lower_hunger.id)
|
||||
for npc in npcs:
|
||||
_remember(knowledge, npc.id, 7, 10)
|
||||
var pantry_before := pantry.to_dictionary()
|
||||
var tasks_before: Array[StringName] = []
|
||||
for npc in npcs:
|
||||
tasks_before.append(npc.current_task)
|
||||
|
||||
var opened: OpportunityStateRecord = system.consider_event(
|
||||
trigger, 10, pantry, npcs, knowledge, {}
|
||||
)
|
||||
|
||||
assert_not_null(opened)
|
||||
assert_eq(opened.get_opportunity_id(), 0)
|
||||
assert_eq(opened.get_interested_npc_id(), low_id.id)
|
||||
assert_eq(opened.get_trigger_event_id(), 7)
|
||||
assert_eq(opened.get_created_tick(), 10)
|
||||
assert_eq(opened.get_opportunity_type(), SimulationIds.OPPORTUNITY_RESTOCK_EMPTY_PANTRY)
|
||||
assert_eq(opened.get_status(), OpportunityStateRecord.STATUS_OPEN)
|
||||
assert_eq(opened.get_target_id(), SimulationIds.STORAGE_VILLAGE_PANTRY)
|
||||
assert_eq(opened.get_resource_id(), SimulationIds.RESOURCE_FOOD)
|
||||
assert_eq(opened.get_target_amount(), 1.0)
|
||||
assert_eq(pantry.to_dictionary(), pantry_before)
|
||||
for index in npcs.size():
|
||||
assert_eq(npcs[index].current_task, tasks_before[index])
|
||||
assert_eq(system.next_opportunity_id, 1)
|
||||
|
||||
|
||||
func test_near_equal_hunger_still_uses_the_exact_highest_value() -> void:
|
||||
var system := FoodShortageOpportunitySystemScript.new()
|
||||
var pantry := _empty_pantry()
|
||||
var lower_id := _npc(1, 92.0)
|
||||
var slightly_hungrier := _npc(9, 92.000001)
|
||||
var npcs: Array[SimNPC] = [lower_id, slightly_hungrier]
|
||||
var knowledge := EventKnowledgeSystem.new()
|
||||
var trigger := _withdrawal_event(8, 10, lower_id.id)
|
||||
for npc in npcs:
|
||||
_remember(knowledge, npc.id, 8, 10)
|
||||
|
||||
var opened: OpportunityStateRecord = system.consider_event(
|
||||
trigger, 10, pantry, npcs, knowledge, {}
|
||||
)
|
||||
|
||||
assert_not_null(opened)
|
||||
assert_eq(opened.get_interested_npc_id(), slightly_hungrier.id)
|
||||
|
||||
|
||||
func test_late_knowledge_can_open_old_event_and_open_opportunity_dedupes() -> void:
|
||||
var system := FoodShortageOpportunitySystemScript.new()
|
||||
var pantry := _empty_pantry()
|
||||
var npc := _npc(3, 88.0)
|
||||
var npcs: Array[SimNPC] = [npc]
|
||||
var knowledge := EventKnowledgeSystem.new()
|
||||
var original_trigger := _withdrawal_event(10, 4, npc.id)
|
||||
|
||||
assert_null(system.consider_event(original_trigger, 4, pantry, npcs, knowledge, {}))
|
||||
_remember(knowledge, npc.id, 10, 12)
|
||||
var opened: OpportunityStateRecord = system.consider_event(
|
||||
original_trigger, 12, pantry, npcs, knowledge, {}
|
||||
)
|
||||
assert_not_null(opened)
|
||||
assert_eq(opened.get_created_tick(), 12)
|
||||
|
||||
var duplicate_trigger := _withdrawal_event(11, 13, npc.id)
|
||||
_remember(knowledge, npc.id, 11, 13)
|
||||
assert_null(system.consider_event(duplicate_trigger, 13, pantry, npcs, knowledge, {}))
|
||||
assert_eq(system.get_all_sorted().size(), 1)
|
||||
assert_eq(system.next_opportunity_id, 1)
|
||||
|
||||
|
||||
func test_only_strict_later_npc_deposit_resolves_without_mutating_supply() -> void:
|
||||
var fixture := _open_fixture()
|
||||
var system: FoodShortageOpportunitySystem = fixture["system"]
|
||||
var pantry: StorageStateRecord = fixture["pantry"]
|
||||
var npcs: Array[SimNPC] = fixture["npcs"]
|
||||
var knowledge: EventKnowledgeSystem = fixture["knowledge"]
|
||||
pantry.deposit(SimulationIds.RESOURCE_FOOD, 1.0)
|
||||
var food_before := pantry.get_amount(SimulationIds.RESOURCE_FOOD)
|
||||
|
||||
var invalid_events: Array[EconomicEventRecord] = [
|
||||
_supply_event(20, 11, SimulationIds.EVENT_RESOURCE_EXTRACTED, 0),
|
||||
_supply_event(21, 11, SimulationIds.EVENT_STORAGE_DEPOSITED, -1),
|
||||
EconomicEventRecord.create(
|
||||
25,
|
||||
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
||||
11,
|
||||
0,
|
||||
&"not_the_actor_inventory",
|
||||
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||
SimulationIds.RESOURCE_FOOD,
|
||||
1.0
|
||||
),
|
||||
EconomicEventRecord.create(
|
||||
22,
|
||||
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
||||
11,
|
||||
0,
|
||||
SimulationIds.npc_inventory_id(0),
|
||||
SimulationIds.STORAGE_VILLAGE_WOODPILE,
|
||||
SimulationIds.RESOURCE_FOOD,
|
||||
1.0
|
||||
),
|
||||
EconomicEventRecord.create(
|
||||
23,
|
||||
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
||||
11,
|
||||
0,
|
||||
SimulationIds.npc_inventory_id(0),
|
||||
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||
SimulationIds.RESOURCE_FOOD,
|
||||
0.0
|
||||
),
|
||||
]
|
||||
for event in invalid_events:
|
||||
assert_null(system.consider_event(event, 11, pantry, npcs, knowledge, {}))
|
||||
assert_eq(pantry.get_amount(SimulationIds.RESOURCE_FOOD), food_before)
|
||||
assert_eq(system.get_open_opportunity().get_status(), OpportunityStateRecord.STATUS_OPEN)
|
||||
|
||||
var valid_supply := _supply_event(24, 12, SimulationIds.EVENT_STORAGE_DEPOSITED, 0)
|
||||
var resolved: OpportunityStateRecord = system.consider_event(
|
||||
valid_supply, 12, pantry, npcs, knowledge, {}
|
||||
)
|
||||
assert_not_null(resolved)
|
||||
assert_eq(resolved.get_status(), OpportunityStateRecord.STATUS_RESOLVED)
|
||||
assert_eq(resolved.get_resolution_event_id(), 24)
|
||||
assert_eq(resolved.get_resolved_tick(), 12)
|
||||
assert_null(system.get_open_opportunity())
|
||||
assert_eq(pantry.get_amount(SimulationIds.RESOURCE_FOOD), food_before)
|
||||
|
||||
|
||||
func test_player_extraction_into_pantry_is_the_other_valid_resolution() -> void:
|
||||
var fixture := _open_fixture()
|
||||
var system: FoodShortageOpportunitySystem = fixture["system"]
|
||||
var pantry: StorageStateRecord = fixture["pantry"]
|
||||
var npcs: Array[SimNPC] = fixture["npcs"]
|
||||
var knowledge: EventKnowledgeSystem = fixture["knowledge"]
|
||||
pantry.deposit(SimulationIds.RESOURCE_FOOD, 1.0)
|
||||
var player_supply := _supply_event(30, 14, SimulationIds.EVENT_RESOURCE_EXTRACTED, -1)
|
||||
assert_null(system.consider_event(player_supply, 14, pantry, npcs, knowledge, {}))
|
||||
assert_not_null(system.get_open_opportunity())
|
||||
|
||||
var resolved: OpportunityStateRecord = system.consider_event(
|
||||
player_supply, 14, pantry, npcs, knowledge, _player_resource_states()
|
||||
)
|
||||
|
||||
assert_not_null(resolved)
|
||||
assert_eq(resolved.get_resolution_event_id(), 30)
|
||||
assert_eq(resolved.get_status(), OpportunityStateRecord.STATUS_RESOLVED)
|
||||
|
||||
|
||||
func test_restore_preserves_ids_sorted_queries_and_resolves_only_oldest_open() -> void:
|
||||
var older := OpportunityStateRecord.create(2, 5, 4, 7)
|
||||
var newer := OpportunityStateRecord.create(5, 8, 7, 9)
|
||||
var restored_records: Array[OpportunityStateRecord] = [newer, older]
|
||||
var system := FoodShortageOpportunitySystemScript.new()
|
||||
system.restore(restored_records, 6)
|
||||
|
||||
assert_eq(system.next_opportunity_id, 6)
|
||||
assert_eq(system.get_all_sorted().map(_opportunity_id), [2, 5])
|
||||
assert_eq(system.get_open_opportunity(), older)
|
||||
assert_eq(system.get_latest_for_npc(9), newer)
|
||||
assert_null(system.get_latest_for_npc(100))
|
||||
|
||||
var pantry := _empty_pantry()
|
||||
pantry.deposit(SimulationIds.RESOURCE_FOOD, 1.0)
|
||||
var npcs: Array[SimNPC] = [_npc(0, 10.0)]
|
||||
var supply := _supply_event(40, 10, SimulationIds.EVENT_STORAGE_DEPOSITED, 0)
|
||||
var resolved: OpportunityStateRecord = system.consider_event(
|
||||
supply, 10, pantry, npcs, EventKnowledgeSystem.new(), {}
|
||||
)
|
||||
assert_eq(resolved, older)
|
||||
assert_eq(older.get_status(), OpportunityStateRecord.STATUS_RESOLVED)
|
||||
assert_eq(newer.get_status(), OpportunityStateRecord.STATUS_OPEN)
|
||||
assert_eq(system.get_open_opportunity(), newer)
|
||||
|
||||
|
||||
func test_record_parser_rejects_malformed_state_combinations() -> void:
|
||||
var valid := OpportunityStateRecord.create(3, 10, 8, 2)
|
||||
assert_not_null(OpportunityStateRecord.from_dictionary(valid.to_dictionary()))
|
||||
|
||||
var invalid_open := valid.to_dictionary()
|
||||
invalid_open["resolution_event_id"] = 9
|
||||
assert_null(OpportunityStateRecord.from_dictionary(invalid_open))
|
||||
var invalid_type := valid.to_dictionary()
|
||||
invalid_type["opportunity_type"] = "generic_quest"
|
||||
assert_null(OpportunityStateRecord.from_dictionary(invalid_type))
|
||||
var invalid_target := valid.to_dictionary()
|
||||
invalid_target["target_amount"] = 2.0
|
||||
assert_null(OpportunityStateRecord.from_dictionary(invalid_target))
|
||||
var invalid_resolved := valid.to_dictionary()
|
||||
invalid_resolved["status"] = String(OpportunityStateRecord.STATUS_RESOLVED)
|
||||
invalid_resolved["resolution_event_id"] = 12
|
||||
invalid_resolved["resolved_tick"] = 9
|
||||
assert_null(OpportunityStateRecord.from_dictionary(invalid_resolved))
|
||||
invalid_resolved["resolved_tick"] = 10
|
||||
invalid_resolved["resolution_event_id"] = valid.get_trigger_event_id()
|
||||
assert_null(OpportunityStateRecord.from_dictionary(invalid_resolved))
|
||||
|
||||
|
||||
func _open_fixture() -> Dictionary:
|
||||
var system := FoodShortageOpportunitySystemScript.new()
|
||||
var pantry := _empty_pantry()
|
||||
var npc := _npc(0, 90.0)
|
||||
var npcs: Array[SimNPC] = [npc]
|
||||
var knowledge := EventKnowledgeSystem.new()
|
||||
var trigger := _withdrawal_event(1, 10, npc.id)
|
||||
_remember(knowledge, npc.id, 1, 10)
|
||||
assert_not_null(system.consider_event(trigger, 10, pantry, npcs, knowledge, {}))
|
||||
return {"system": system, "pantry": pantry, "npcs": npcs, "knowledge": knowledge}
|
||||
|
||||
|
||||
func _empty_pantry() -> StorageStateRecord:
|
||||
return StorageStateRecord.create(
|
||||
SimulationIds.STORAGE_VILLAGE_PANTRY, {String(SimulationIds.RESOURCE_FOOD): 0.0}
|
||||
)
|
||||
|
||||
|
||||
func _npc(npc_id: int, hunger: float) -> SimNPC:
|
||||
var npc := SimNPC.new(npc_id, "NPC %d" % npc_id, SimulationIds.PROFESSION_FARMER, 5.0, 5.0)
|
||||
npc.hunger = hunger
|
||||
npc.is_dead = false
|
||||
return npc
|
||||
|
||||
|
||||
func _withdrawal_event(event_id: int, tick: int, actor_id: int) -> EconomicEventRecord:
|
||||
return EconomicEventRecord.create(
|
||||
event_id,
|
||||
SimulationIds.EVENT_STORAGE_WITHDRAWN,
|
||||
tick,
|
||||
actor_id,
|
||||
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||
SimulationIds.npc_inventory_id(actor_id),
|
||||
SimulationIds.RESOURCE_FOOD,
|
||||
1.0
|
||||
)
|
||||
|
||||
|
||||
func _supply_event(
|
||||
event_id: int, tick: int, event_type: StringName, actor_id: int
|
||||
) -> EconomicEventRecord:
|
||||
var source_id := &"player_resource"
|
||||
if event_type == SimulationIds.EVENT_STORAGE_DEPOSITED and actor_id >= 0:
|
||||
source_id = SimulationIds.npc_inventory_id(actor_id)
|
||||
return EconomicEventRecord.create(
|
||||
event_id,
|
||||
event_type,
|
||||
tick,
|
||||
actor_id,
|
||||
source_id,
|
||||
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||
SimulationIds.RESOURCE_FOOD,
|
||||
1.0
|
||||
)
|
||||
|
||||
|
||||
func _player_resource_states() -> Dictionary:
|
||||
var resource_node := ResourceNode.new()
|
||||
resource_node.node_id = &"player_resource"
|
||||
resource_node.action_id = SimulationIds.ACTION_GATHER_FOOD
|
||||
resource_node.resource_id = SimulationIds.RESOURCE_FOOD
|
||||
resource_node.can_player_use = true
|
||||
var resource_state := ResourceStateRecord.create_from_node(resource_node)
|
||||
var result := {resource_node.node_id: resource_state}
|
||||
resource_node.free()
|
||||
return result
|
||||
|
||||
|
||||
func _remember(
|
||||
knowledge: EventKnowledgeSystem, knower_id: int, event_id: int, acquired_tick: int
|
||||
) -> void:
|
||||
var records := knowledge.get_all_sorted()
|
||||
records.append(
|
||||
KnownEventStateRecord.create(
|
||||
knower_id,
|
||||
event_id,
|
||||
SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED,
|
||||
KnownEventStateRecord.NO_SOURCE_NPC_ID,
|
||||
acquired_tick
|
||||
)
|
||||
)
|
||||
knowledge.restore(records)
|
||||
|
||||
|
||||
func _opportunity_id(opportunity: OpportunityStateRecord) -> int:
|
||||
return opportunity.get_opportunity_id()
|
||||
@@ -0,0 +1 @@
|
||||
uid://cnodtf7lh6ona
|
||||
Reference in New Issue
Block a user