feat: deliver emergent Jajce commitments
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
extends SceneTree
|
||||
|
||||
var failures: Array[String] = []
|
||||
var manager: Node
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
manager = load("res://simulation/SimulationManager.gd").new()
|
||||
root.add_child(manager)
|
||||
var speaker: SimNPC = manager.npcs[0]
|
||||
speaker.position = Vector3.ZERO
|
||||
speaker.hunger = 85.0
|
||||
for npc in manager.npcs:
|
||||
if npc != speaker:
|
||||
npc.position = Vector3(100.0 + npc.id, 0.0, 0.0)
|
||||
_set_pantry_amount(1.0)
|
||||
_check(manager.get_quest_journal_entries().is_empty(), "Journal starts empty")
|
||||
_check(
|
||||
is_equal_approx(
|
||||
manager.economy.withdraw_to_inventory(speaker, SimulationIds.RESOURCE_FOOD, 1.0), 1.0
|
||||
),
|
||||
"A real last-food withdrawal creates the evidence"
|
||||
)
|
||||
var situations: Array[SituationStateRecord] = manager.get_active_situations()
|
||||
_check(situations.size() == 1, "The exact shortage evidence opens one world situation")
|
||||
if situations.is_empty():
|
||||
_finish()
|
||||
return
|
||||
var situation := situations[0]
|
||||
_check(
|
||||
manager.event_knowledge_system.knows_event(speaker.id, situation.get_trigger_event_id()),
|
||||
"The performing NPC knows the trigger fact"
|
||||
)
|
||||
_check(
|
||||
not manager.event_knowledge_system.knows_event(
|
||||
SimulationIds.PLAYER_ACTOR_ID, situation.get_trigger_event_id()
|
||||
),
|
||||
"The player does not know the situation before conversation"
|
||||
)
|
||||
_check(manager.get_quest_journal_entries().is_empty(), "Unknown situations stay out of journal")
|
||||
|
||||
manager.speed_index = 5
|
||||
var conversation_id: StringName = manager.begin_player_conversation(speaker.id)
|
||||
_check(not conversation_id.is_empty(), "The informed NPC can begin generated dialogue")
|
||||
_check(
|
||||
manager.speed_index == 2 and manager.is_dialogue_active(), "Dialogue runs simulation at 1x"
|
||||
)
|
||||
_check(
|
||||
manager.event_knowledge_system.knows_event(
|
||||
SimulationIds.PLAYER_ACTOR_ID, situation.get_trigger_event_id()
|
||||
),
|
||||
"Speaking communicates the exact trigger fact to the player"
|
||||
)
|
||||
_check(manager.get_quest_journal_entries().size() == 1, "Discovery projects into the journal")
|
||||
var turn: ConversationTurn = manager.conversation_service.get_turn(conversation_id)
|
||||
_check(turn != null and _has_intent(turn, ConversationIntentIds.OFFER_HELP), "Help is semantic")
|
||||
_check(_has_intent(turn, ConversationIntentIds.DECLINE), "Declining remains available")
|
||||
_check(
|
||||
_has_intent(turn, ConversationIntentIds.ASK_WHO_ELSE), "Alternative helpers can be asked"
|
||||
)
|
||||
var offer := _option_for_intent(turn, ConversationIntentIds.OFFER_HELP)
|
||||
var offered: ConversationSelectionResult = manager.select_player_conversation_option(
|
||||
conversation_id, offer.get_option_id(), turn.get_revision()
|
||||
)
|
||||
_check(offered.was_accepted(), "Offering help advances the deterministic conversation")
|
||||
_check(manager.get_active_commitments().size() == 1, "Offering help creates one commitment")
|
||||
turn = offered.get_turn()
|
||||
var accept := _option_for_intent(turn, ConversationIntentIds.ACCEPT)
|
||||
var accepted: ConversationSelectionResult = manager.select_player_conversation_option(
|
||||
conversation_id, accept.get_option_id(), turn.get_revision()
|
||||
)
|
||||
_check(accepted.was_accepted(), "Accepting the offer records a semantic act")
|
||||
_check(manager.get_active_commitments().size() == 1, "NPC acknowledgement cannot duplicate it")
|
||||
_check(
|
||||
(
|
||||
manager.get_quest_journal_entries()[0].get_selected_alternative_id()
|
||||
== SituationSystem.ALTERNATIVE_RESTOCK
|
||||
),
|
||||
"Acceptance selects a real resolution alternative without copying progress"
|
||||
)
|
||||
var saved: String = manager.serialize_state()
|
||||
var canonical_checksum: String = JSON.stringify(JSON.parse_string(saved)).sha256_text()
|
||||
manager.end_player_conversation(conversation_id)
|
||||
_check(manager.speed_index == 5, "Closing dialogue restores the prior speed")
|
||||
|
||||
var restored: Node = load("res://simulation/SimulationManager.gd").new()
|
||||
root.add_child(restored)
|
||||
_check(restored.restore_state_from_json(saved), "Mid-commitment save reloads")
|
||||
var restored_serialized: String = restored.serialize_state()
|
||||
var restored_checksum: String = (
|
||||
JSON.stringify(JSON.parse_string(restored_serialized)).sha256_text()
|
||||
)
|
||||
if restored_checksum != canonical_checksum:
|
||||
var saved_data: Dictionary = JSON.parse_string(saved)
|
||||
var restored_data: Dictionary = JSON.parse_string(restored_serialized)
|
||||
print("[TEST] checksum mismatch sections: ", _different_keys(saved_data, restored_data))
|
||||
_check(restored_checksum == canonical_checksum, "Mid-commitment continuation is deterministic")
|
||||
_check(restored.get_active_commitments().size() == 1, "The social commitment persists")
|
||||
restored.player_system.add_carried(SimulationIds.RESOURCE_FOOD, 1.0)
|
||||
_check(
|
||||
is_equal_approx(restored.deposit_player_inventory(SimulationIds.RESOURCE_FOOD), 1.0),
|
||||
"The player resolves the situation with an ordinary real deposit"
|
||||
)
|
||||
var restored_situation: SituationStateRecord = restored.situation_system.get_by_id(
|
||||
situation.get_situation_id()
|
||||
)
|
||||
var restored_commitment: CommitmentStateRecord = restored.commitment_system.get_all_sorted()[0]
|
||||
_check(
|
||||
restored_situation.get_status() == SituationStateRecord.STATUS_RESOLVED,
|
||||
"The exact deposit event resolves the world situation"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
restored_commitment.get_status() == CommitmentStateRecord.STATUS_FULFILLED
|
||||
and (
|
||||
restored_commitment.get_cause_event_id()
|
||||
== restored_situation.get_resolution_event_id()
|
||||
)
|
||||
),
|
||||
"That same event fulfills the commitment exactly once"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
restored.get_quest_journal_entries()[0].get_status()
|
||||
== QuestJournalEntryStateRecord.STATUS_COMPLETED
|
||||
),
|
||||
"The journal projects the closed world state"
|
||||
)
|
||||
_check(
|
||||
restored.conversation_history.size() >= 3,
|
||||
"Semantic conversation acts and topics persist, never rendered prose"
|
||||
)
|
||||
var fulfilled_fact: EconomicEventRecord = restored.commitment_lifecycle.get_fact(
|
||||
restored.event_log,
|
||||
CommitmentLifecycleService.EVENT_FULFILLED,
|
||||
restored_commitment.get_commitment_id()
|
||||
)
|
||||
_check(fulfilled_fact != null, "Fulfillment records a durable social outcome fact")
|
||||
var player_relationship: RelationshipStateRecord = (
|
||||
restored.relationship_system.get_relationship(speaker.id, SimulationIds.PLAYER_ACTOR_ID)
|
||||
)
|
||||
_check(
|
||||
(
|
||||
player_relationship != null
|
||||
and (
|
||||
player_relationship.get_last_trust_cause_event_id()
|
||||
== int(fulfilled_fact.data["event_id"])
|
||||
)
|
||||
),
|
||||
"The exact outcome fact causes the relationship consequence"
|
||||
)
|
||||
_check(
|
||||
SimulationStateRecord.from_json(restored.serialize_state()) != null,
|
||||
"Fulfilled commitment facts and relationship causes remain schema-valid"
|
||||
)
|
||||
var later_conversation: StringName = restored.begin_player_conversation(speaker.id)
|
||||
var later_turn: ConversationTurn = restored.conversation_service.get_turn(later_conversation)
|
||||
_check(
|
||||
later_turn != null and later_turn.get_act().get_intent_id() == ConversationIntentIds.THANK,
|
||||
"Later dialogue reflects the fulfilled commitment"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
StringName("outcome_event_%d" % restored_situation.get_resolution_event_id())
|
||||
in later_turn.get_reason_trace()
|
||||
),
|
||||
"Later dialogue traces the exact real deposit event"
|
||||
)
|
||||
restored.end_player_conversation(later_conversation)
|
||||
restored.free()
|
||||
manager.free()
|
||||
_finish()
|
||||
|
||||
|
||||
func _set_pantry_amount(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 _option_for_intent(turn: ConversationTurn, intent_id: StringName) -> ConversationOption:
|
||||
for option in turn.get_options():
|
||||
if option.get_intent_id() == intent_id:
|
||||
return option
|
||||
return null
|
||||
|
||||
|
||||
func _has_intent(turn: ConversationTurn, intent_id: StringName) -> bool:
|
||||
return _option_for_intent(turn, intent_id) != null
|
||||
|
||||
|
||||
func _check(condition: bool, message: String) -> void:
|
||||
if condition:
|
||||
return
|
||||
failures.append(message)
|
||||
push_error("[TEST] " + message)
|
||||
|
||||
|
||||
func _different_keys(left: Dictionary, right: Dictionary) -> Array[String]:
|
||||
var differences: Array[String] = []
|
||||
for key in left:
|
||||
if not right.has(key) or JSON.stringify(left[key]) != JSON.stringify(right[key]):
|
||||
differences.append(String(key))
|
||||
return differences
|
||||
|
||||
|
||||
func _finish() -> void:
|
||||
if failures.is_empty():
|
||||
print("[TEST] Emergent Jajce slice passed")
|
||||
quit(0)
|
||||
else:
|
||||
quit(1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dca87o6k2xfww
|
||||
@@ -38,16 +38,16 @@ func _run() -> void:
|
||||
_check(first["fixture_valid"], "The prepared benchmark state should pass schema validation")
|
||||
_check(
|
||||
(
|
||||
int(first["schema_version"]) == 2
|
||||
and SimulationScalingBenchmark.SCHEMA_VERSION == 2
|
||||
int(first["schema_version"]) == 3
|
||||
and SimulationScalingBenchmark.SCHEMA_VERSION == 3
|
||||
and (
|
||||
int(first["simulation_state_schema_version"])
|
||||
== SimulationStateRecord.SCHEMA_VERSION
|
||||
)
|
||||
and StringName(first["workload_id"]) == &"full_fidelity_combatant_headless_arrival_v2"
|
||||
and (StringName(first["workload_id"]) == &"full_fidelity_emergent_headless_arrival_v3")
|
||||
and (
|
||||
SimulationScalingBenchmark.WORKLOAD_ID
|
||||
== &"full_fidelity_combatant_headless_arrival_v2"
|
||||
== &"full_fidelity_emergent_headless_arrival_v3"
|
||||
)
|
||||
and int(first["population"]) == 12
|
||||
and int(first["npc_combatant_count"]) == 12
|
||||
@@ -86,6 +86,11 @@ func _run() -> void:
|
||||
"events_recorded",
|
||||
"start_known_reference_count",
|
||||
"end_known_reference_count",
|
||||
"start_situation_count",
|
||||
"end_situation_count",
|
||||
"start_commitment_count",
|
||||
"end_commitment_count",
|
||||
"conversation_act_count",
|
||||
"arrivals_processed",
|
||||
"warmup_arrivals",
|
||||
"tick_interval",
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
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"])))
|
||||
@@ -0,0 +1 @@
|
||||
uid://1walx8xflan0
|
||||
@@ -0,0 +1,86 @@
|
||||
extends GutTest
|
||||
|
||||
const MANAGER_SCRIPT := preload("res://simulation/SimulationManager.gd")
|
||||
|
||||
var manager: Node
|
||||
|
||||
|
||||
func before_each() -> void:
|
||||
manager = MANAGER_SCRIPT.new()
|
||||
add_child_autofree(manager)
|
||||
|
||||
|
||||
func test_world_v14_migrates_to_empty_emergent_collections() -> void:
|
||||
var previous: Dictionary = manager.create_state_record().to_dictionary()
|
||||
previous["schema_version"] = SimulationStateRecord.PRE_EMERGENT_SCHEMA_VERSION
|
||||
for field in ["situations", "quest_journal", "commitments", "conversation_history"]:
|
||||
previous.erase(field)
|
||||
for field in [
|
||||
"next_situation_id",
|
||||
"next_journal_entry_id",
|
||||
"next_commitment_id",
|
||||
"next_conversation_act_id",
|
||||
]:
|
||||
previous["simulation"].erase(field)
|
||||
var migrated: SimulationStateRecord = SimulationStateRecord.from_dictionary(previous)
|
||||
assert_not_null(migrated)
|
||||
assert_true(migrated.situations.is_empty())
|
||||
assert_true(migrated.quest_journal.is_empty())
|
||||
assert_true(migrated.commitments.is_empty())
|
||||
assert_true(migrated.conversation_history.is_empty())
|
||||
assert_eq(int(migrated.simulation["next_situation_id"]), 0)
|
||||
assert_eq(int(migrated.simulation["next_conversation_act_id"]), 0)
|
||||
|
||||
|
||||
func test_orphan_journal_and_situation_evidence_are_rejected() -> void:
|
||||
var orphan_journal: Dictionary = manager.create_state_record().to_dictionary()
|
||||
orphan_journal["quest_journal"] = [
|
||||
(
|
||||
QuestJournalEntryStateRecord
|
||||
. create(0, 42, SituationSystem.DEFINITION_PANTRY_SHORTAGE, 0)
|
||||
. to_dictionary()
|
||||
)
|
||||
]
|
||||
orphan_journal["simulation"]["next_journal_entry_id"] = 1
|
||||
assert_null(SimulationStateRecord.from_dictionary(orphan_journal))
|
||||
|
||||
var missing_evidence: Dictionary = manager.create_state_record().to_dictionary()
|
||||
missing_evidence["situations"] = [
|
||||
(
|
||||
SituationStateRecord
|
||||
. create(
|
||||
0,
|
||||
SituationSystem.DEFINITION_PANTRY_SHORTAGE,
|
||||
"missing-evidence",
|
||||
0,
|
||||
999,
|
||||
{"interested_npc_id": "0"}
|
||||
)
|
||||
. to_dictionary()
|
||||
)
|
||||
]
|
||||
missing_evidence["simulation"]["next_situation_id"] = 1
|
||||
assert_null(SimulationStateRecord.from_dictionary(missing_evidence))
|
||||
|
||||
|
||||
func test_conversation_history_requires_valid_actors_and_monotonic_id() -> void:
|
||||
var state: Dictionary = manager.create_state_record().to_dictionary()
|
||||
var npc_ref := WorldEntityRef.create(WorldEventRecord.ENTITY_TYPE_NPC, &"0")
|
||||
var player_ref := WorldEntityRef.create(WorldEventRecord.ENTITY_TYPE_PLAYER, &"-1")
|
||||
state["conversation_history"] = [
|
||||
(
|
||||
ConversationActStateRecord
|
||||
. create(
|
||||
0, 0, npc_ref, player_ref, ConversationIntentIds.GREET, [&"topic_pantry_shortage"]
|
||||
)
|
||||
. to_dictionary()
|
||||
)
|
||||
]
|
||||
assert_null(
|
||||
SimulationStateRecord.from_dictionary(state),
|
||||
"The next stable act ID cannot collide with retained history"
|
||||
)
|
||||
state["simulation"]["next_conversation_act_id"] = 1
|
||||
assert_not_null(SimulationStateRecord.from_dictionary(state))
|
||||
state["conversation_history"][0]["speaker"]["entity_id"] = "missing_npc"
|
||||
assert_null(SimulationStateRecord.from_dictionary(state))
|
||||
@@ -0,0 +1 @@
|
||||
uid://hr1yu4alsrsx
|
||||
@@ -181,7 +181,7 @@ func test_commitments_fulfill_break_release_and_supersede() -> void:
|
||||
)
|
||||
)
|
||||
var supplied_events := WorldEventStore.new()
|
||||
assert_true(supplied_events.append(_pantry_supply(9, 12)))
|
||||
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)
|
||||
@@ -267,13 +267,18 @@ func _pantry_facts(amount: float) -> Dictionary:
|
||||
}
|
||||
|
||||
|
||||
func _pantry_supply(event_id: int, tick: int) -> WorldEventRecord:
|
||||
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,
|
||||
3,
|
||||
&"npc_inventory_3",
|
||||
actor_id,
|
||||
source_id,
|
||||
&"village_pantry",
|
||||
&"food",
|
||||
1.0,
|
||||
|
||||
Reference in New Issue
Block a user