extends SceneTree var _checks := 0 var _failures := 0 var _clock: Variant var _state: Variant var _runtime: Variant func _initialize() -> void: call_deferred("_run") func _run() -> void: print("S4: runtime relationship and event integration") _test_threshold_foreground_acknowledgement_and_idempotency() _test_paid_choice_escrow_and_commit() _test_missing_actor_releases_reservation_and_lock() _test_waiting_choice_snapshot_exact_replay() _cleanup() if _failures == 0: print("PASS: %d checks" % _checks) else: push_error("FAIL: %d of %d checks failed" % [_failures, _checks]) quit(_failures) func _reset_runtime(seed: int) -> void: _cleanup() _clock = load("res://autoload/game_clock.gd").new() _state = load("res://autoload/game_state.gd").new() _runtime = load("res://autoload/simulation_runtime.gd").new() _clock.reset(1, 6, 0) _state.reset() var report: Dictionary = _runtime.initialize(_clock, _state, seed) _clock.set_paused(true) _expect(report[&"valid"], "runtime initializes S4 definitions") _expect(_runtime.event_service.get_definition(&"relationship.threshold") != null, "canonical relationship threshold Event is registered") func _cleanup() -> void: if _runtime != null: _runtime.free() if _clock != null: _clock.free() if _state != null: _state.free() _runtime = null _clock = null _state = null func _test_threshold_foreground_acknowledgement_and_idempotency() -> void: _reset_runtime(4_001) _expect(_spawn_guest(&"relation_guest", 150).is_success(), "relationship actor is created through Runtime Command") var created = _runtime.submit_command( &"create_relationship", { &"relationship_id": &"relationship.runtime.threshold", &"actor_a_id": &"player", &"actor_b_id": &"relation_guest", &"options": { &"trust": 35, &"respect": 40, &"affection": 45, &"fear": 5, &"dependence": 10, &"shield_max": 60, &"shield_current": 60, }, }, &"player", &"relation_guest", 80, -1, &"s4.relationship.create.threshold" ) _expect(created.is_success(), "Relationship is created through Runtime Command") var relationship = _runtime.relationship_service.get_relationship(&"relationship.runtime.threshold") _expect(relationship != null and relationship.actor_a_id == &"player" and relationship.actor_b_id == &"relation_guest", "Runtime preserves normalized relationship actors") var changed = _runtime.submit_command( &"change_relationship", { &"relationship_id": relationship.relationship_id, &"metric_id": &"affection", &"delta": 10, }, &"player", relationship.relationship_id, 80, -1, &"s4.relationship.affection.gate" ) _expect(changed.is_success() and changed.details[&"gated_threshold"] == 50, "Affection change reports the 50 threshold gate") _expect(relationship.get_metric(&"affection") == 49, "background calculation cannot cross 50 before Event acknowledgement") _expect(_runtime.relationship_service.get_threshold_candidates().size() == 1, "threshold crossing creates one deterministic relationship candidate") _expect(_runtime.event_service.get_queued_instances().size() == 1, "candidate is projected into EventService without immediate playback") _clock.set_paused(false) _clock.advance_one_minute() var foreground = _runtime.event_service.get_foreground_instance() _expect(foreground != null and foreground.source_event_id == &"relationship.threshold", "relationship candidate starts as the foreground Event") _expect(_clock.paused, "foreground Choice automatically pauses Simulation") _expect(_runtime.event_service.get_actor_locks().size() == 2, "foreground threshold Event locks both relationship actors") var instance_id: StringName = foreground.event_instance_id var choice_instance_id: StringName = foreground.choice_instance_id var acknowledged = _runtime.submit_command( &"submit_event_choice", { &"event_instance_id": instance_id, &"choice_instance_id": choice_instance_id, &"choice_id": &"acknowledge", }, &"player", instance_id, 100, -1, &"s4.threshold.acknowledge.first" ) _expect(acknowledged.is_success(), "foreground acknowledgement resolves through Runtime Command") _expect(relationship.get_metric(&"affection") == 50, "acknowledged threshold Event applies the gated Affection point") _expect(_runtime.relationship_service.get_threshold_candidates().is_empty(), "resolved Event consumes its relationship candidate") _expect(_runtime.event_service.get_actor_locks().is_empty() and _runtime.event_service.get_foreground_instance() == null, "resolved Event releases every Actor Lock") _expect(not _clock.paused, "Runtime resumes when no foreground Event remains") _clock.set_paused(true) var replayed = _runtime.submit_command( &"submit_event_choice", { &"event_instance_id": instance_id, &"choice_instance_id": choice_instance_id, &"choice_id": &"acknowledge", }, &"player", instance_id, 100, -1, &"s4.threshold.acknowledge.transport-retry" ) _expect(replayed.is_success() and replayed.is_replay, "same choice_instance_id returns EventService idempotent replay") _expect(relationship.get_metric(&"affection") == 50, "choice replay does not apply relationship Actions twice") _expect(_count_domain_events(&"EVENT_CHOICE_RESOLVED", instance_id) == 1, "choice replay emits no duplicate resolution Domain Event") _expect(_runtime.relationship_service.validate_invariants()[&"ok"], "threshold flow preserves Relationship invariants") _expect(_runtime.event_service.validate_invariants()[&"ok"], "threshold flow preserves Event and Actor Lock invariants") func _test_paid_choice_escrow_and_commit() -> void: _reset_runtime(4_002) var registered = _register_paid_event_definition(&"event.paid.commit", 125) _expect(registered.is_success(), "paid EventDefinition registers through Runtime Command") var enqueued = _runtime.submit_command( &"enqueue_event", { &"event_id": &"event.paid.commit", &"actor_ids": [&"player"], &"options": {&"event_instance_id": &"event_instance.paid.commit"}, }, &"player", &"event_instance.paid.commit", 60, -1, &"s4.event.paid.enqueue" ) _expect(enqueued.is_success(), "paid Event candidate enters Runtime queue") _start_next_foreground_minute() var foreground = _runtime.event_service.get_foreground_instance() _expect(foreground != null and foreground.event_instance_id == &"event_instance.paid.commit", "paid Event reaches foreground Choice") var cash_before: int = _runtime.economy_service.get_balance(&"PLAYER_CASH") var reserved = _runtime.submit_command( &"reserve_event_choice_cost", { &"event_instance_id": foreground.event_instance_id, &"choice_id": &"purchase", }, &"player", foreground.event_instance_id, 100, -1, &"s4.event.paid.reserve" ) var escrow_account := &"EVENT_ESCROW:event_instance.paid.commit" _expect(reserved.is_success(), "paid Choice reserves Cost through Runtime boundary") _expect(_runtime.economy_service.get_balance(&"PLAYER_CASH") == cash_before - 125, "Gold leaves PLAYER_CASH exactly once at reservation") _expect(_runtime.economy_service.get_balance(escrow_account) == 125, "reserved Gold is held in Event-specific escrow") _expect(_state.get_resource(&"gold") == cash_before - 125, "GameState Gold projection follows escrow transfer") var committed = _runtime.submit_command( &"submit_event_choice", { &"event_instance_id": foreground.event_instance_id, &"choice_instance_id": foreground.choice_instance_id, &"choice_id": &"purchase", }, &"player", foreground.event_instance_id, 100, -1, &"s4.event.paid.commit" ) _expect(committed.is_success(), "paid Choice commits after revalidation") _expect(_runtime.economy_service.get_balance(escrow_account) == 0, "committed Event leaves no Gold in escrow") _expect(_runtime.economy_service.get_balance(&"OPERATING_EXPENSE") == 125, "escrow Gold posts to the configured expense account") _expect(_runtime.economy_service.get_balance(&"PLAYER_CASH") == cash_before - 125, "Cost commit never charges PLAYER_CASH a second time") _expect(_runtime.event_service.has_flag(&"paid_choice_completed"), "paid Choice Action applies only after Cost reservation") _expect(_runtime.economy_service.validate_ledger()[&"ok"], "escrow reserve and commit remain double-entry balanced") _expect(_runtime.event_service.validate_invariants()[&"ok"], "paid Choice completion preserves Event invariants") func _test_missing_actor_releases_reservation_and_lock() -> void: _reset_runtime(4_003) var registered_staff = _runtime.submit_command( &"register_staff", { &"staff_id": &"temporary_actor", &"role_id": &"service", &"options": {}, }, &"player", &"temporary_actor", 80, -1, &"s4.actor.temporary.register" ) _expect(registered_staff.is_success(), "temporary foreground actor registers through Runtime") _expect(_register_paid_event_definition(&"event.actor.missing", 80).is_success(), "missing-actor fixture EventDefinition registers") var enqueued = _runtime.submit_command( &"enqueue_event", { &"event_id": &"event.actor.missing", &"actor_ids": [&"temporary_actor"], &"options": {&"event_instance_id": &"event_instance.actor.missing"}, }, &"player", &"event_instance.actor.missing", 60, -1, &"s4.actor.missing.enqueue" ) _expect(enqueued.is_success(), "missing-actor fixture enters Event queue") _start_next_foreground_minute() var foreground = _runtime.event_service.get_foreground_instance() _expect(foreground != null and _runtime.event_service.get_actor_locks().has(&"temporary_actor"), "foreground Event holds temporary actor lock") var cash_before: int = _runtime.economy_service.get_balance(&"PLAYER_CASH") var reserved = _runtime.submit_command( &"reserve_event_choice_cost", { &"event_instance_id": foreground.event_instance_id, &"choice_id": &"purchase", }, &"player", foreground.event_instance_id, 100, -1, &"s4.actor.missing.reserve" ) _expect(reserved.is_success() and _runtime.economy_service.get_balance(&"PLAYER_CASH") == cash_before - 80, "missing-actor fixture owns reserved Gold") _expect(_runtime.staff_service.remove_staff(&"temporary_actor", &"TEST_REMOVED", true, _clock.absolute_minute).is_success(), "actor disappears through StaffService lifecycle") _clock.set_paused(false) _clock.advance_one_minute() var instance = _runtime.event_service.get_instance(&"event_instance.actor.missing") _expect(instance != null and instance.state == EventInstance.State.CANCELLED, "Runtime audit cancels Event whose actor disappeared") _expect(_runtime.event_service.get_actor_locks().is_empty() and _runtime.event_service.get_foreground_instance() == null, "actor disappearance releases Lock and foreground ownership") _expect(_runtime.economy_service.get_balance(&"EVENT_ESCROW:event_instance.actor.missing") == 0, "actor disappearance empties Event escrow") _expect(_runtime.economy_service.get_balance(&"PLAYER_CASH") == cash_before and _state.get_resource(&"gold") == cash_before, "actor disappearance refunds reserved Gold exactly once") _expect(_runtime.economy_service.get_balance(&"OPERATING_EXPENSE") == 0, "cancelled missing-actor Event never commits Cost") _expect(_runtime.economy_service.validate_ledger()[&"ok"], "missing-actor refund preserves ledger invariants") _expect(_runtime.event_service.validate_invariants()[&"ok"], "missing-actor recovery preserves Event invariants") func _test_waiting_choice_snapshot_exact_replay() -> void: _reset_runtime(4_004) _expect(_spawn_guest(&"snapshot_relation_guest", 175).is_success(), "snapshot relationship actor is created") var relationship_id := &"relationship.runtime.snapshot" var created = _runtime.submit_command( &"create_relationship", { &"relationship_id": relationship_id, &"actor_a_id": &"player", &"actor_b_id": &"snapshot_relation_guest", &"options": {&"affection": 45, &"shield_max": 50, &"shield_current": 50}, }, &"player", relationship_id, 80, -1, &"s4.snapshot.relationship.create" ) _expect(created.is_success(), "snapshot Relationship registers") var gated = _runtime.submit_command( &"change_relationship", {&"relationship_id": relationship_id, &"metric_id": &"affection", &"delta": 10}, &"player", relationship_id, 80, -1, &"s4.snapshot.relationship.gate" ) _expect(gated.is_success(), "snapshot scenario creates threshold Event candidate") _start_next_foreground_minute() var foreground = _runtime.event_service.get_foreground_instance() _expect(foreground != null and _clock.paused, "snapshot is captured while foreground Choice is waiting") var instance_id: StringName = foreground.event_instance_id var choice_instance_id: StringName = foreground.choice_instance_id # Save capture occurs after tick completion, including the incremented Kernel # snapshot_version; this is the exact boundary replayed below. var waiting_snapshot: Dictionary = _runtime.create_snapshot() var waiting_digest: String = SimulationDigest.compute(waiting_snapshot) _expect(waiting_digest.length() == 64, "waiting-Choice snapshot produces a stable digest") var first_resolution = _runtime.submit_command( &"submit_event_choice", { &"event_instance_id": instance_id, &"choice_instance_id": choice_instance_id, &"choice_id": &"acknowledge", }, &"player", instance_id, 100, -1, &"s4.snapshot.choice.resolve" ) _expect(first_resolution.is_success(), "waiting Choice resolves before rewind") var final_digest: String = _runtime.latest_digest _expect(_runtime.relationship_service.get_relationship(relationship_id).get_metric(&"affection") == 50, "first timeline reaches acknowledged threshold") var restored_ok: bool = _runtime.restore_snapshot(waiting_snapshot) _expect(restored_ok, "Runtime restores a snapshot containing Choice token and Actor Locks") _expect(_runtime.latest_digest == waiting_digest, "waiting-Choice restore reproduces exact digest") var restored_foreground = _runtime.event_service.get_foreground_instance() _expect(restored_foreground != null and restored_foreground.event_instance_id == instance_id, "restored Runtime recovers the same foreground Event") _expect(restored_foreground.choice_instance_id == choice_instance_id and _runtime.event_service.get_actor_locks().size() == 2, "Choice token and Actor Locks restore exactly") _expect(_clock.paused and _runtime.relationship_service.get_relationship(relationship_id).get_metric(&"affection") == 49, "restore returns to pre-acknowledgement paused relationship state") var replay_resolution = _runtime.submit_command( &"submit_event_choice", { &"event_instance_id": instance_id, &"choice_instance_id": choice_instance_id, &"choice_id": &"acknowledge", }, &"player", instance_id, 100, -1, &"s4.snapshot.choice.resolve" ) _expect(replay_resolution.is_success(), "restored Choice accepts the same deterministic Command") _expect(_runtime.latest_digest == final_digest, "Choice replay after restore reproduces exact final digest") _expect(_runtime.relationship_service.get_relationship(relationship_id).get_metric(&"affection") == 50, "replayed timeline reaches threshold once") _expect(_runtime.relationship_service.validate_invariants()[&"ok"], "restored relationship state satisfies invariants") _expect(_runtime.event_service.validate_invariants()[&"ok"], "restored Event queue and Lock state satisfy invariants") _expect(_runtime.economy_service.validate_ledger()[&"ok"], "snapshot replay preserves economic invariants") func _spawn_guest(guest_id: StringName, gold: int) -> CommandResult: return _runtime.submit_command( &"spawn_guest", { &"guest_id": guest_id, &"guest_type_id": &"traveler", &"gold": gold, }, &"player", guest_id, 50, -1, StringName("s4.guest.spawn.%s" % guest_id) ) func _register_paid_event_definition(event_id: StringName, gold_cost: int) -> CommandResult: return _runtime.submit_command( &"register_event_definition", {&"definition": { &"event_id": event_id, &"category": &"RELATIONSHIP", &"priority": 70, &"foreground": true, &"transaction_policy": &"ROLLBACK_ALL", &"missing_actor_policy": &"CANCEL", &"content_rating": &"GENERAL", &"choices": [{ &"choice_id": &"purchase", &"text_key": StringName("event.%s.purchase" % event_id), &"cost": {&"gold": gold_cost}, &"actions": [{ &"action_id": &"paid_choice_flag", &"type": &"SET_FLAG", &"payload": {&"flag_id": &"paid_choice_completed"}, }], }], }}, &"player", event_id, 60, -1, StringName("s4.event.definition.%s" % event_id) ) func _start_next_foreground_minute() -> void: _clock.set_paused(false) _clock.advance_one_minute() func _count_domain_events(event_type: StringName, entity_id: StringName) -> int: var count := 0 for event in _runtime.domain_events.recent_events(): if event.event_type == event_type and ( entity_id in event.entity_ids or StringName(event.payload.get(&"event_instance_id", &"")) == entity_id ): count += 1 return count func _expect(condition: bool, label: String) -> void: _checks += 1 if condition: print(" ✓ %s" % label) return _failures += 1 push_error(" ✗ %s" % label)