extends GutTest func test_pantry_definition_exposes_typed_lifecycle_contract() -> void: var definition := SituationSystem.create_pantry_shortage_definition() var errors := ( definition . validate( [ SituationSystem.PREDICATE_EVENT_TYPE, SituationSystem.PREDICATE_EVENT_PAYLOAD_EQUALS, SituationSystem.PREDICATE_EVENT_PARTICIPANT_ID, SituationSystem.PREDICATE_EVENT_PANTRY_SUPPLIED, ], [ SituationSystem.PREDICATE_STATE_NUMBER_LTE, SituationSystem.PREDICATE_STATE_NUMBER_GTE, ], [SituationSystem.EXPIRY_AGE_REACHED] ) ) assert_true(errors.is_empty(), str(errors)) assert_eq(definition.situation_definition_id, SituationSystem.DEFINITION_PANTRY_SHORTAGE) assert_eq(definition.dedupe_key_fields, [&"world_id", &"source_id"]) assert_eq(definition.severity, 70) assert_eq(definition.priority, 80) assert_eq(definition.cooldown_ticks, 50) assert_eq(definition.expiry_ticks, 200) assert_eq(definition.dialogue_topic_ids, [SituationSystem.DIALOGUE_TOPIC_PANTRY_SHORTAGE]) var alternative := definition.get_alternative(SituationSystem.ALTERNATIVE_RESTOCK) assert_not_null(alternative) assert_eq(alternative.commitment_terms["target_id"], "village_pantry") assert_eq(alternative.commitment_terms["amount"], 1.0) func test_generation_exposes_reasons_dedupes_and_enforces_cooldown() -> void: var system := SituationSystem.create_default() var event := _pantry_withdrawal(4, 10) var opened_results := system.consider_event(event, 10, _pantry_facts(0.0)) assert_eq(opened_results.size(), 1) assert_true(opened_results[0].matched) assert_eq(opened_results[0].opened_situation_id, 0) assert_eq(opened_results[0].reason_traces.size(), 5) assert_string_contains(opened_results[0].reason_traces[-1], "opened situation") var situation := system.get_by_id(0) assert_not_null(situation) assert_eq(situation.get_context()["source_id"], "village_pantry") assert_eq(situation.get_context()["actor_id"], "2") assert_eq(situation.get_context()["interested_npc_id"], "2") var duplicate := system.consider_event(_pantry_withdrawal(5, 11), 11, _pantry_facts(0.0))[0] assert_true(duplicate.matched) assert_eq(duplicate.opened_situation_id, -1) assert_string_contains(duplicate.reason_traces[-1], "dedupe blocked") var world_events := WorldEventStore.new() assert_true(world_events.append(_pantry_supply(8, 12))) var closed := system.maintain(12, world_events, _pantry_facts(1.0)) assert_eq(closed, [situation]) assert_eq(situation.get_status(), SituationStateRecord.STATUS_RESOLVED) assert_eq(situation.get_resolution_event_id(), 8) var cooldown := system.consider_event(_pantry_withdrawal(6, 20), 20, _pantry_facts(0.0))[0] assert_string_contains(cooldown.reason_traces[-1], "cooldown active until tick 62") var reopened := system.consider_event(_pantry_withdrawal(7, 62), 62, _pantry_facts(0.0))[0] assert_eq(reopened.opened_situation_id, 1) func test_generator_caps_at_three_and_orders_by_priority_then_severity() -> void: var definitions: Array[SituationDefinition] = [ _definition(&"lowest", 10, 10), _definition(&"highest", 40, 10), _definition(&"middle_high", 30, 40), _definition(&"middle_low", 30, 20), ] var system := SituationSystem.new() assert_true(system.configure(definitions).is_empty()) var results := system.consider_event(_pantry_withdrawal(10, 5), 5, _pantry_facts(0.0)) assert_eq(results.size(), 4) assert_eq( results.map(func(result: SituationEvaluationResult) -> String: return result.definition_id), ["highest", "middle_high", "middle_low", "lowest"] ) assert_eq(system.get_active_sorted().size(), SituationSystem.MAX_CONCURRENT_SITUATIONS) assert_eq( _definition_ids(system.get_active_sorted()), [&"highest", &"middle_high", &"middle_low"] ) assert_string_contains(results[-1].reason_traces[-1], "concurrency cap 3 reached") func test_nonmatching_events_trace_each_failed_fact_without_opening() -> void: var system := SituationSystem.create_default() var wrong_item := EconomicEventRecord.create( 1, &"storage_withdrawn", 3, 2, &"village_woodpile", &"npc_inventory_2", &"wood", 1.0 ) var result := ( system . consider_event( WorldEventRecord.from_economic_event(wrong_item, &"jajce"), 3, _pantry_facts(2.0) )[0] ) assert_false(result.matched) assert_eq(result.opened_situation_id, -1) assert_true(system.get_active_sorted().is_empty()) assert_true( result.reason_traces.any(func(reason: String) -> bool: return "did not match" in reason) ) func test_progress_and_journal_are_derived_from_world_facts() -> void: var situation_system := SituationSystem.create_default() situation_system.consider_event(_pantry_withdrawal(1, 10), 10, _pantry_facts(0.0)) var situation := situation_system.get_by_id(0) var journal := QuestJournalSystem.new() var entry := journal.add_situation( situation, situation_system.get_definition(situation.get_definition_id()), SituationSystem.ALTERNATIVE_RESTOCK, 10 ) var entry_before := entry.to_dictionary() var empty_progress := journal.derive_progress( entry, situation_system, WorldEventStore.new(), _pantry_facts(0.0) ) var partial_progress := journal.derive_progress( entry, situation_system, WorldEventStore.new(), _pantry_facts(0.5) ) var supplied_events := WorldEventStore.new() assert_true(supplied_events.append(_pantry_supply(4, 12))) var complete_progress := journal.derive_progress( entry, situation_system, supplied_events, _pantry_facts(1.0) ) assert_false(empty_progress.resolved) assert_eq(empty_progress.ratio(), 0.0) assert_false(partial_progress.resolved) assert_eq(partial_progress.ratio(), 0.0) assert_true(complete_progress.resolved) assert_eq(complete_progress.ratio(), 1.0) assert_eq(complete_progress.matched_event_id, 4) assert_eq(entry.to_dictionary(), entry_before) assert_false(entry.to_dictionary().has("progress")) assert_eq(entry.get_discovered_tick(), 10) assert_eq(entry.get_tracking_preference(), QuestJournalEntryStateRecord.TRACKING_AUTO) assert_true(entry.set_tracking_preference(QuestJournalEntryStateRecord.TRACKING_TRACKED)) assert_eq(journal.get_for_situation(0), entry) situation_system.maintain(12, supplied_events, _pantry_facts(1.0)) assert_eq(journal.synchronize(situation, 12), entry) assert_eq(entry.get_status(), QuestJournalEntryStateRecord.STATUS_COMPLETED) assert_eq( QuestJournalEntryStateRecord.from_dictionary(entry.to_dictionary()).to_dictionary(), entry.to_dictionary() ) var archive_entry := QuestJournalEntryStateRecord.create(4, 9, &"archive_test", 10) assert_true(archive_entry.archive(11)) assert_true(archive_entry.is_archived()) func test_commitments_fulfill_break_release_and_supersede() -> void: var system := SituationSystem.create_default() system.consider_event(_pantry_withdrawal(1, 10), 10, _pantry_facts(0.0)) var situation := system.get_by_id(0) var definition := system.get_definition(situation.get_definition_id()) var debtor := WorldEntityRef.create(&"player", &"-1") var creditor := WorldEntityRef.create(&"npc", &"4") var commitments := SocialCommitmentSystem.new() var fulfilled := commitments.create_for_alternative( situation, definition, SituationSystem.ALTERNATIVE_RESTOCK, debtor, creditor, 10, 20 ) assert_not_null(fulfilled) assert_eq(fulfilled.get_terms()["item_id"], "food") assert_null( commitments.create_for_alternative( situation, definition, SituationSystem.ALTERNATIVE_RESTOCK, debtor, creditor, 10 ) ) var supplied_events := WorldEventStore.new() assert_true(supplied_events.append(_pantry_supply(9, 12, SimulationIds.PLAYER_ACTOR_ID))) assert_eq(commitments.maintain(12, system, supplied_events, _pantry_facts(1.0)), [fulfilled]) assert_eq(fulfilled.get_status(), CommitmentStateRecord.STATUS_FULFILLED) assert_eq(fulfilled.get_cause_event_id(), 9) var broken := CommitmentStateRecord.create(1, 2, debtor, creditor, {"amount": 1.0}, 10, 11) var released := CommitmentStateRecord.create(2, 3, debtor, creditor, {"amount": 1.0}, 10) var superseded := CommitmentStateRecord.create(3, 4, debtor, creditor, {"amount": 1.0}, 10) assert_true(broken.close(CommitmentStateRecord.STATUS_BROKEN, 12, &"deadline_missed")) assert_true(released.close(CommitmentStateRecord.STATUS_RELEASED, 12, &"mutual_release")) assert_true(superseded.close(CommitmentStateRecord.STATUS_SUPERSEDED, 12, &"new_promise")) assert_eq(broken.get_status(), CommitmentStateRecord.STATUS_BROKEN) assert_eq(released.get_status(), CommitmentStateRecord.STATUS_RELEASED) assert_eq(superseded.get_status(), CommitmentStateRecord.STATUS_SUPERSEDED) for record in [fulfilled, broken, released, superseded]: assert_eq( CommitmentStateRecord.from_dictionary(record.to_dictionary()).to_dictionary(), record.to_dictionary() ) func test_opportunity_bridge_maps_pantry_identity_and_preserves_lifecycle() -> void: var opportunity := OpportunityStateRecord.create(8, 10, 4, 2) assert_true(opportunity.resolve(12, 14)) var system := SituationSystem.create_default() assert_true(system.import_opportunities([opportunity]).is_empty()) var imported := system.get_by_id(8) assert_not_null(imported) assert_eq(imported.get_definition_id(), SituationSystem.DEFINITION_PANTRY_SHORTAGE) assert_eq(imported.get_status(), SituationStateRecord.STATUS_RESOLVED) assert_eq(imported.get_trigger_event_id(), 4) assert_eq(imported.get_resolution_event_id(), 12) assert_eq(imported.get_context()["interested_npc_id"], 2) assert_eq( SituationStateRecord.from_dictionary(imported.to_dictionary()).to_dictionary(), imported.to_dictionary() ) func test_record_parsers_reject_impossible_lifecycle_state() -> void: var situation := SituationStateRecord.create(1, &"definition", "definition|world=jajce", 10) var invalid_situation := situation.to_dictionary() invalid_situation["status"] = String(SituationStateRecord.STATUS_RESOLVED) invalid_situation["closed_tick"] = 9 invalid_situation["close_reason"] = "done" assert_null(SituationStateRecord.from_dictionary(invalid_situation)) var entry := QuestJournalEntryStateRecord.create(1, 1, &"definition", 10) var invalid_entry := entry.to_dictionary() invalid_entry["status"] = String(QuestJournalEntryStateRecord.STATUS_COMPLETED) assert_null(QuestJournalEntryStateRecord.from_dictionary(invalid_entry)) var entity := WorldEntityRef.create(&"npc", &"1") assert_null(CommitmentStateRecord.create(1, 1, entity, entity, {"amount": 1.0}, 10)) var valid_commitment := CommitmentStateRecord.create( 1, 1, entity, WorldEntityRef.create(&"npc", &"2"), {"amount": 1.0}, 10 ) var invalid_commitment := valid_commitment.to_dictionary() invalid_commitment["status"] = String(CommitmentStateRecord.STATUS_FULFILLED) invalid_commitment["close_reason"] = "done" assert_null(CommitmentStateRecord.from_dictionary(invalid_commitment)) func _pantry_withdrawal(event_id: int, tick: int) -> WorldEventRecord: var economic := EconomicEventRecord.create( event_id, &"storage_withdrawn", tick, 2, &"village_pantry", &"npc_inventory_2", &"food", 1.0, Vector3(2.0, 0.0, 3.0) ) return WorldEventRecord.from_economic_event(economic, &"jajce", &"village_pantry") func _pantry_facts(amount: float) -> Dictionary: return { "pantry.food": amount, "target_id": "village_pantry", } func _pantry_supply(event_id: int, tick: int, actor_id: int = 3) -> WorldEventRecord: var source_id := ( &"player_inventory" if actor_id == SimulationIds.PLAYER_ACTOR_ID else StringName("npc_inventory_%d" % actor_id) ) var economic := EconomicEventRecord.create( event_id, &"storage_deposited", tick, actor_id, source_id, &"village_pantry", &"food", 1.0, Vector3(2.0, 0.0, 3.0) ) return WorldEventRecord.from_economic_event(economic, &"jajce", &"village_pantry") func _definition(definition_id: StringName, priority: int, severity: int) -> SituationDefinition: var definition := SituationSystem.create_pantry_shortage_definition() definition.situation_definition_id = definition_id definition.priority = priority definition.severity = severity definition.cooldown_ticks = 0 return definition func _definition_ids(situations: Array[SituationStateRecord]) -> Array[StringName]: var result: Array[StringName] = [] for situation in situations: result.append(situation.get_definition_id()) return result