extends GutTest const CommitmentLifecycleService := preload( "res://simulation/situations/CommitmentLifecycleService.gd" ) const SimulationEventLogScript := preload("res://simulation/events/SimulationEventLog.gd") const RelationshipSystemScript := preload("res://simulation/relationships/RelationshipSystem.gd") func test_debtor_fulfillment_records_exact_facts_and_idempotent_consequences() -> void: var context := _open_commitment(SimulationIds.PLAYER_ACTOR_ID, 4, 20) var lifecycle := CommitmentLifecycleService.new() var acceptance := lifecycle.record_acceptance( context.commitment, context.event_log, context.relationships ) assert_not_null(acceptance) assert_eq(StringName(acceptance.data["event_type"]), CommitmentLifecycleService.EVENT_ACCEPTED) assert_eq(int(acceptance.data["commitment_id"]), context.commitment.get_commitment_id()) var relationship: RelationshipStateRecord = context.relationships.get_relationship( 4, SimulationIds.PLAYER_ACTOR_ID ) assert_not_null(relationship) assert_almost_eq( relationship.get_obligation(), CommitmentLifecycleService.ACCEPTED_OBLIGATION, 0.0001 ) assert_eq(relationship.get_last_obligation_cause_event_id(), int(acceptance.data["event_id"])) var resolution := _record_supply(context.event_log, 12, SimulationIds.PLAYER_ACTOR_ID) var closed := lifecycle.maintain_and_record( 12, context.commitments, context.situations, context.event_log, _pantry_facts(1.0), context.relationships ) assert_eq(closed, [context.commitment]) assert_eq(context.commitment.get_status(), CommitmentStateRecord.STATUS_FULFILLED) assert_eq(context.commitment.get_cause_event_id(), int(resolution.data["event_id"])) var fulfilled := lifecycle.get_fact( context.event_log, CommitmentLifecycleService.EVENT_FULFILLED, context.commitment.get_commitment_id() ) assert_not_null(fulfilled) assert_eq(int(fulfilled.data["cause_event_id"]), int(resolution.data["event_id"])) assert_almost_eq(relationship.get_obligation(), 0.0, 0.0001) assert_almost_eq( relationship.get_trust(), RelationshipStateRecord.NEUTRAL_TRUST + CommitmentLifecycleService.FULFILLED_TRUST, 0.0001 ) assert_eq(relationship.get_last_trust_cause_event_id(), int(fulfilled.data["event_id"])) assert_eq(relationship.get_last_obligation_cause_event_id(), int(fulfilled.data["event_id"])) var event_count: int = context.event_log.events.size() var relationship_before := relationship.to_dictionary() assert_same( lifecycle.record_outcome(context.commitment, context.event_log, context.relationships), fulfilled ) assert_true( ( lifecycle . maintain_and_record( 13, context.commitments, context.situations, context.event_log, _pantry_facts(1.0), context.relationships ) . is_empty() ) ) assert_eq(context.event_log.events.size(), event_count) assert_eq(relationship.to_dictionary(), relationship_before) _assert_fact_round_trip(context.event_log, fulfilled) func test_deadline_breaks_at_due_tick_and_applies_one_exact_penalty() -> void: var context := _open_commitment(2, 3, 12) var lifecycle := CommitmentLifecycleService.new() lifecycle.record_acceptance(context.commitment, context.event_log, context.relationships) var relationship: RelationshipStateRecord = context.relationships.get_relationship(3, 2) var closed := lifecycle.maintain_and_record( 12, context.commitments, context.situations, context.event_log, _pantry_facts(0.0), context.relationships ) assert_eq(closed, [context.commitment]) assert_eq(context.commitment.get_status(), CommitmentStateRecord.STATUS_BROKEN) assert_eq(context.commitment.get_close_reason(), SocialCommitmentSystem.REASON_DEADLINE_MISSED) var broken := lifecycle.get_fact( context.event_log, CommitmentLifecycleService.EVENT_BROKEN, context.commitment.get_commitment_id() ) assert_not_null(broken) assert_almost_eq(relationship.get_obligation(), 0.0, 0.0001) assert_almost_eq( relationship.get_trust(), RelationshipStateRecord.NEUTRAL_TRUST + CommitmentLifecycleService.BROKEN_TRUST, 0.0001 ) assert_almost_eq( relationship.get_hostility(), CommitmentLifecycleService.BROKEN_HOSTILITY, 0.0001 ) for dimension in [ RelationshipStateRecord.DIMENSION_OBLIGATION, RelationshipStateRecord.DIMENSION_TRUST, RelationshipStateRecord.DIMENSION_HOSTILITY, ]: assert_eq(relationship.get_last_cause_event_id(dimension), int(broken.data["event_id"])) var before := relationship.to_dictionary() assert_same( lifecycle.record_outcome(context.commitment, context.event_log, context.relationships), broken ) assert_eq(relationship.to_dictionary(), before) func test_other_npc_resolution_supersedes_without_trust_or_hostility_penalty() -> void: var context := _open_commitment(2, 4, 20) var lifecycle := CommitmentLifecycleService.new() lifecycle.record_acceptance(context.commitment, context.event_log, context.relationships) var relationship: RelationshipStateRecord = context.relationships.get_relationship(4, 2) var resolution := _record_supply(context.event_log, 12, 3) var closed := lifecycle.maintain_and_record( 12, context.commitments, context.situations, context.event_log, _pantry_facts(1.0), context.relationships ) assert_eq(closed, [context.commitment]) assert_eq(context.commitment.get_status(), CommitmentStateRecord.STATUS_SUPERSEDED) assert_eq( context.commitment.get_close_reason(), SocialCommitmentSystem.REASON_FACT_RESOLVED_BY_OTHER ) assert_eq(context.commitment.get_cause_event_id(), int(resolution.data["event_id"])) var superseded := lifecycle.get_fact( context.event_log, CommitmentLifecycleService.EVENT_SUPERSEDED, context.commitment.get_commitment_id() ) assert_not_null(superseded) assert_almost_eq(relationship.get_obligation(), 0.0, 0.0001) assert_almost_eq(relationship.get_trust(), RelationshipStateRecord.NEUTRAL_TRUST, 0.0001) assert_almost_eq(relationship.get_hostility(), 0.0, 0.0001) assert_eq(relationship.get_last_obligation_cause_event_id(), int(superseded.data["event_id"])) assert_eq(relationship.get_last_trust_cause_event_id(), RelationshipStateRecord.NO_CAUSE_EVENT) assert_eq( relationship.get_last_hostility_cause_event_id(), RelationshipStateRecord.NO_CAUSE_EVENT ) func test_released_commitment_records_fact_and_clears_obligation_without_penalty() -> void: var context := _open_commitment(2, 4, 20) var lifecycle := CommitmentLifecycleService.new() var relationship: RelationshipStateRecord = context.relationships.get_or_create_relationship( 4, 2 ) assert_almost_eq(relationship.adjust_obligation(0.9, -1), 0.9, 0.0001) var acceptance := lifecycle.record_acceptance( context.commitment, context.event_log, context.relationships ) assert_almost_eq(float(acceptance.data["obligation_delta"]), 0.1, 0.0001) assert_almost_eq(relationship.get_obligation(), 1.0, 0.0001) assert_true(context.commitments.release(0, 11, &"mutual_release")) var released := lifecycle.record_outcome( context.commitment, context.event_log, context.relationships ) assert_not_null(released) assert_eq(StringName(released.data["event_type"]), CommitmentLifecycleService.EVENT_RELEASED) assert_almost_eq(relationship.get_obligation(), 0.9, 0.0001) assert_almost_eq(relationship.get_trust(), RelationshipStateRecord.NEUTRAL_TRUST, 0.0001) assert_almost_eq(relationship.get_hostility(), 0.0, 0.0001) func _open_commitment(debtor_id: int, creditor_id: int, deadline_tick: int) -> Dictionary: var event_log := SimulationEventLogScript.new() var trigger := event_log.record_economic( 10, &"storage_withdrawn", creditor_id, &"village_pantry", StringName("npc_inventory_%d" % creditor_id), &"food", 1.0 ) var situations := SituationSystem.create_default() situations.consider_event( event_log.get_world_event(int(trigger.data["event_id"])), 10, _pantry_facts(0.0) ) var situation := situations.get_by_id(0) var commitments := SocialCommitmentSystem.new() var debtor_type := ( WorldEventRecord.ENTITY_TYPE_PLAYER if debtor_id == SimulationIds.PLAYER_ACTOR_ID else WorldEventRecord.ENTITY_TYPE_NPC ) var commitment := commitments.create_for_alternative( situation, situations.get_definition(situation.get_definition_id()), SituationSystem.ALTERNATIVE_RESTOCK, WorldEntityRef.create(debtor_type, StringName(str(debtor_id))), WorldEntityRef.create(WorldEventRecord.ENTITY_TYPE_NPC, StringName(str(creditor_id))), 10, deadline_tick ) return { "commitment": commitment, "commitments": commitments, "event_log": event_log, "relationships": RelationshipSystemScript.new(), "situations": situations, } func _record_supply(event_log: RefCounted, tick: int, actor_id: int) -> EconomicEventRecord: var source_id := ( &"player_inventory" if actor_id == SimulationIds.PLAYER_ACTOR_ID else StringName("npc_inventory_%d" % actor_id) ) return event_log.record_economic( tick, &"storage_deposited", actor_id, source_id, &"village_pantry", &"food", 1.0 ) func _pantry_facts(amount: float) -> Dictionary: return {"pantry.food": amount, "target_id": "village_pantry"} func _assert_fact_round_trip(event_log: RefCounted, expected: EconomicEventRecord) -> void: var records: Array[EconomicEventRecord] = [] for event in event_log.events: records.append(EconomicEventRecord.from_dictionary(event.to_dictionary())) var restored := SimulationEventLogScript.new() restored.restore(records, event_log.next_event_id) var actual := restored.get_by_id(int(expected.data["event_id"])) assert_not_null(actual) assert_eq(actual.to_dictionary(), expected.to_dictionary()) assert_not_null(restored.get_world_event(int(expected.data["event_id"])))