extends SceneTree const GuestDefinitionType = preload("res://scripts/guest/guest_definition.gd") const GuestRuntimeType = preload("res://scripts/guest/guest_runtime.gd") const GuestServiceType = preload("res://scripts/guest/guest_service.gd") var _checks := 0 var _failures := 0 func _initialize() -> void: call_deferred("_run") func _run() -> void: print("S3: deterministic Guest AI") _test_definition_and_runtime_contract() _test_need_updates_and_single_clamp() _test_need_context_and_adult_gate() _test_shared_need_context_fast_paths() _test_batched_need_updates_match_individual_api() _test_candidate_filtering() _test_integer_utility_and_stable_tie_break() _test_primary_need_priority() _test_state_machine_and_loop_guard() _test_atomic_queue_abandonment() _test_service_interruption_is_idempotent() _test_service_need_effect_schema() _test_path_failure_safe_recovery() _test_exit_persistence_boundary() _test_overdue_departure_snapshot() _test_snapshot_round_trip_and_atomic_rejection() _test_v1_need_migration_and_schema_rejection() _test_deterministic_decision_snapshot() if _failures == 0: print("PASS: %d checks" % _checks) else: push_error("FAIL: %d of %d checks failed" % [_failures, _checks]) quit(_failures) func _test_definition_and_runtime_contract() -> void: var definition := GuestDefinitionType.new().configure(&"adventurer", { &"tags": [&"adventurer", &"general", &"general"], &"wealth_min": 100, &"wealth_max": 500, &"base_patience": 45, &"risk_tolerance": 80, &"preference_weights": {&"meal": 8, &"quiet": -2}, &"group_size_weights": {1: 80, 2: 20}, &"legal_sensitivity": 60, &"quality_sensitivity": 70, &"adult_verified": false, &"need_rates": {&"hunger": 3, &"sleep": -2, &"leisure": 1}, &"credit_limit_gold": 100, &"minimum_quality": 40, }) _expect(definition.is_valid(), "complete guest definition validates") _expect(definition.tags == [&"adventurer", &"general"], "definition tags are unique and stable-sorted") _expect(definition.get_need_rate(&"intimacy") == 0, "omitted canonical Need rates normalize to zero") _expect(definition.need_rates.size() == 6, "definition always materializes the six canonical Need IDs") var round_trip := GuestDefinitionType.from_snapshot(definition.create_snapshot()) _expect(round_trip != null, "definition reconstructs from its canonical snapshot") _expect(round_trip.create_snapshot() == definition.create_snapshot(), "definition snapshot round trip is exact") var legacy_definition := GuestDefinitionType.new().configure(&"legacy_alias", { &"need_rates": {&"rest": 1}, }) _expect(not legacy_definition.is_valid(), "legacy Definition Need aliases are invalid outside v1 migration") var legacy_runtime := GuestRuntimeType.new().configure(&"legacy_guest", &"adventurer", { &"needs": {&"social": 10}, }) _expect(not legacy_runtime.is_valid(), "legacy Runtime Need aliases are invalid outside v1 migration") var coerced_adult := GuestDefinitionType.new().configure(&"coerced_adult", { &"adult_verified": "true", }) _expect(not coerced_adult.is_valid(), "adult verification cannot be supplied through truthy string coercion") var service := GuestServiceType.new() _expect(service.register_definition(definition), "valid definition registers") _expect(not service.register_definition(definition), "duplicate definition ID is rejected") var guest := service.spawn_guest(&"guest_contract", &"adventurer", 10, { &"planned_exit_at": 200, &"current_cell": Vector3i(2, 3, 0), }) _expect(guest != null and guest.is_valid(), "guest runtime spawns with required authoritative fields") _expect(guest.wallet_gold == 100 and guest.credit_limit_gold == 100, "definition supplies deterministic wallet floor and credit limit") _expect(guest.patience_remaining == 45, "definition supplies base queue patience") _expect(service.validate_invariants()[&"valid"], "fresh GuestService invariants hold") func _test_need_updates_and_single_clamp() -> void: var service := _service_with_definitions() var guest := service.spawn_guest(&"guest_needs", &"adventurer", 0, { &"state": GuestRuntimeType.State.MOVING, &"needs": { &"hunger": 99, &"sleep": 1, &"hygiene": 99, &"medical": 99, &"leisure": 99, &"intimacy": 99, }, &"planned_exit_at": 500, }) var report: Dictionary = service.update_needs(guest.guest_id, { &"dirt": 2, &"injury": 2, &"illness": 2, &"need_effects": { &"hunger": 5, &"sleep": -5, &"hygiene": 3, &"medical": 3, &"leisure": 3, &"intimacy": 3, }, }) _expect(report[&"ok"], "Need update succeeds for a live guest") _expect(report[&"deltas"]["hunger"] == 10, "base, movement and external hunger effects add before clamp") _expect(report[&"deltas"]["sleep"] == -5, "positive and negative sleep effects combine before clamp") _expect(guest.needs[&"hunger"] == 100 and guest.needs[&"sleep"] == 0, "Need upper and lower bounds clamp once") _expect(guest.needs[&"hygiene"] == 100 and guest.needs[&"medical"] == 100, "dirt, injury and illness update their distinct Needs") _expect(guest.needs[&"leisure"] == 100 and guest.needs[&"intimacy"] == 100, "leisure and intimacy effects use canonical IDs") _expect(service.advance_one_minute(1)[&"ok"], "one-minute GuestService cadence advances") _expect(not service.advance_one_minute(1)[&"ok"], "the same minute cannot apply Need drift twice") func _test_need_context_and_adult_gate() -> void: var service := _service_with_definitions() var verified := service.spawn_guest(&"verified_context", &"verified_noble", 0, { &"needs": {&"hunger": 0, &"sleep": 0, &"hygiene": 10, &"medical": 20, &"leisure": 0, &"intimacy": 30}, &"planned_exit_at": 500, }) var unverified := service.spawn_guest(&"unverified_context", &"adventurer", 0, { &"needs": {&"hunger": 0, &"sleep": 0, &"hygiene": 10, &"medical": 20, &"leisure": 0, &"intimacy": 30}, &"planned_exit_at": 500, }) var day := service.update_needs(verified.guest_id, { &"unstimulated": false, &"dirt": 5, &"injury": 4, &"illness": 3, &"is_night": false, &"intimacy_day": 3, &"intimacy_night": 7, }) _expect(day[&"deltas"]["hygiene"] == 5, "integer dirt context raises Hygiene only") _expect(day[&"deltas"]["medical"] == 7, "injury and illness combine into Medical") _expect(day[&"deltas"]["intimacy"] == 3, "day context uses the adult-verified Intimacy day rate") var night := service.update_needs(verified.guest_id, { &"unstimulated": false, &"is_night": true, &"intimacy_day": 3, &"intimacy_night": 7, }) _expect(night[&"deltas"]["intimacy"] == 7, "night context selects the separate Intimacy night rate") var blocked := service.update_needs(unverified.guest_id, { &"unstimulated": false, &"is_night": true, &"intimacy_night": 7, }) _expect(blocked[&"deltas"]["intimacy"] == 0, "unverified guests cannot receive contextual adult Intimacy drift") var before_invalid := service.create_snapshot() var wrong_type := service.update_needs(verified.guest_id, {&"injury": "4"}) _expect(not wrong_type[&"ok"], "stringified Need context integers are rejected") _expect(service.create_snapshot() == before_invalid, "invalid context leaves live Need state untouched") var legacy_alias := service.update_needs(verified.guest_id, { &"need_effects": {&"rest": -5}, }) _expect(not legacy_alias[&"ok"], "legacy Need aliases are rejected outside snapshot migration") _expect(service.create_snapshot() == before_invalid, "legacy context rejection is atomic") func _test_batched_need_updates_match_individual_api() -> void: var batched := _service_with_definitions() batched.spawn_guest(&"z_guest", &"adventurer", 0, { &"state": GuestRuntimeType.State.MOVING, &"group_id": &"party", &"needs": {&"hunger": 10, &"sleep": 20, &"hygiene": 30, &"medical": 40, &"leisure": 50, &"intimacy": 60}, &"planned_exit_at": 500, }) batched.spawn_guest(&"a_guest", &"verified_noble", 0, { &"needs": {&"hunger": 5, &"sleep": 15, &"hygiene": 25, &"medical": 35, &"leisure": 45, &"intimacy": 55}, &"planned_exit_at": 500, }) var individual := GuestServiceType.new() individual.restore_snapshot(batched.create_snapshot()) var default_context := { &"unstimulated": false, &"dirt": 1, &"need_effects": {&"sleep": 3}, } var z_context := { &"solo_guest": true, &"injury": 2, &"need_effects": {&"hunger": -1, &"leisure": 4}, } var expected_reports: Array[Dictionary] = [ individual.update_needs(&"a_guest", default_context), individual.update_needs(&"z_guest", z_context), ] individual.current_minute = 1 var batch_report := batched.advance_one_minute(1, { &"default": default_context, "z_guest": z_context, }) _expect( batch_report[&"guest_reports"] == expected_reports, "batched Need cadence preserves individual report values and stable guest-id order" ) _expect( batched.create_snapshot() == individual.create_snapshot(), "optimized batched Need mutation is snapshot-equivalent to individual updates" ) func _test_shared_need_context_fast_paths() -> void: var source := _service_with_definitions() for guest_id: StringName in [&"z_guest", &"a_guest"]: source.spawn_guest(guest_id, &"adventurer", 0, { &"state": GuestRuntimeType.State.MOVING, &"needs": { &"hunger": 10, &"sleep": 20, &"hygiene": 30, &"medical": 40, &"leisure": 50, &"intimacy": 60, }, &"planned_exit_at": 500, }) var start := source.create_snapshot() var empty_fast := GuestServiceType.new() var empty_reference := GuestServiceType.new() empty_fast.restore_snapshot(start) empty_reference.restore_snapshot(start) var empty_report := empty_fast.advance_one_minute(1) var empty_reference_report := empty_reference.advance_one_minute(1, { &"default": {}, &"a_guest": {}, &"z_guest": {}, }) _expect( empty_report == empty_reference_report, "empty shared Need context preserves the preflight-path report exactly" ) _expect( empty_fast.create_snapshot() == empty_reference.create_snapshot(), "empty shared Need context preserves the preflight-path snapshot exactly" ) var common_context := { &"unstimulated": false, &"dirt": 2, &"injury": 1, &"need_effects": {&"sleep": 3, &"leisure": -2}, } var default_fast := GuestServiceType.new() var default_reference := GuestServiceType.new() default_fast.restore_snapshot(start) default_reference.restore_snapshot(start) var default_report := default_fast.advance_one_minute(1, { &"default": common_context, }) var default_reference_report := default_reference.advance_one_minute(1, { &"default": common_context, &"a_guest": common_context, &"z_guest": common_context, }) _expect( default_report == default_reference_report, "default-only shared Need context preserves the override-path report exactly" ) _expect( default_fast.create_snapshot() == default_reference.create_snapshot(), "default-only shared Need context preserves the override-path snapshot exactly" ) var invalid_default := GuestServiceType.new() invalid_default.restore_snapshot(start) var before_invalid := invalid_default.create_snapshot() var invalid_report := invalid_default.advance_one_minute(1, { &"default": {&"injury": "1"}, }) _expect(not invalid_report[&"ok"], "invalid default-only Need context is rejected") _expect( invalid_default.create_snapshot() == before_invalid, "invalid default-only Need context is atomic across the population and minute" ) var malformed_default := GuestServiceType.new() var malformed_reference := GuestServiceType.new() malformed_default.restore_snapshot(start) malformed_reference.restore_snapshot(start) var malformed_report := malformed_default.advance_one_minute(1, { &"default": "legacy malformed default", }) var malformed_reference_report := malformed_reference.advance_one_minute(1) _expect( malformed_report == malformed_reference_report, "non-Dictionary default retains the legacy empty-context meaning" ) _expect( malformed_default.create_snapshot() == malformed_reference.create_snapshot(), "malformed-default compatibility remains snapshot-exact" ) func _test_candidate_filtering() -> void: var service := _service_with_definitions() var guest := service.spawn_guest(&"guest_filter", &"adventurer", 0, { &"wallet_gold": 500, &"credit_limit_gold": 100, &"planned_exit_at": 1_000, }) var candidates: Array = [ _candidate(&"cannot_pay", {&"price_gold": 501}), _candidate(&"adult_room", {&"adult_only": true}), _candidate(&"unreachable", {&"reachable": false}), _candidate(&"closed", {&"is_open": false}), _candidate(&"too_slow", {&"wait_minutes": 120}), _candidate(&"too_risky", {&"risk_level": 81}), _candidate(&"too_illegal", {&"illegal_level": 41}), _candidate(&"too_poor", {&"expected_quality": 39}), _candidate(&"legacy_relief", {&"need_relief": {&"fun": 40}}), _candidate(&"legacy_effects", {&"need_effects": {&"safety": -20}}), _candidate(&"canonical_effects", {&"need_effects": {&"medical": -20}}), _candidate(&"credit_ok", {&"price_gold": 550, &"credit_allowed": true}), _candidate(&"normal_ok"), ] var filtered := service.extract_candidates(guest.guest_id, candidates, { &"absolute_minute": 10, &"adult_content_enabled": true, &"wait_hard_limit": 120, }) _expect(_candidate_ids(filtered) == [&"canonical_effects", &"credit_ok", &"normal_ok"], "candidate extraction applies every hard filter before Utility") var reasons := _rejection_map(service.get_last_candidate_rejections(guest.guest_id)) _expect(reasons[&"cannot_pay"] == &"CANNOT_PAY", "SYS-GST-001 rejects an unaffordable facility") _expect(reasons[&"adult_room"] == &"ADULT_NOT_VERIFIED", "SYS-GST-002 rejects restricted content before Utility") _expect(reasons[&"unreachable"] == &"UNREACHABLE", "unreachable facility is removed") _expect(reasons[&"too_slow"] == &"WAIT_HARD_LIMIT", "wait equal to the hard limit is removed") _expect(reasons[&"too_risky"] == &"RISK_REFUSED", "risk above guest tolerance is removed") _expect(reasons[&"too_illegal"] == &"ILLEGALITY_REFUSED", "illegal level above sensitivity threshold is removed") _expect(reasons[&"too_poor"] == &"QUALITY_TOO_LOW", "quality below guest minimum is removed") _expect(reasons[&"legacy_relief"] == &"INVALID_NEED_MAP", "candidate Need maps cannot use legacy aliases") _expect(reasons[&"legacy_effects"] == &"INVALID_NEED_MAP", "candidate need_effects also reject migration-only aliases") guest.wallet_gold = 100 var reserve_filtered := service.extract_candidates(guest.guest_id, [ _candidate(&"meal_with_cash", {&"price_gold": 80}), _candidate(&"leisure_below_reserve", { &"price_gold": 80, &"service_category": &"entertainment", }), ], {&"absolute_minute": 10}) _expect(_candidate_ids(reserve_filtered) == [&"meal_with_cash"], "discretionary spending preserves the personality-based survival reserve") guest.wallet_gold = 500 var verified := service.spawn_guest(&"guest_verified", &"verified_noble", 0, { &"wallet_gold": 1_000, &"planned_exit_at": 1_000, }) var adult_candidates := service.extract_candidates(verified.guest_id, [ _candidate(&"adult_room", {&"adult_only": true}), ], {&"adult_content_enabled": true, &"absolute_minute": 1}) _expect(adult_candidates.size() == 1, "verified adult guest can retain a permitted restricted candidate") var content_blocked := service.extract_candidates(verified.guest_id, [ _candidate(&"adult_room", {&"adult_only": true}), ], {&"adult_content_enabled": false, &"absolute_minute": 1}) _expect(content_blocked.is_empty(), "Content Filter still overrides verified adult status") var callback_order: Array[StringName] = [] service.extract_candidates(guest.guest_id, [ _candidate(&"z_path"), _candidate(&"a_path"), ], { &"absolute_minute": 1, &"reachability_callback": func(_runtime: GuestRuntime, facility: Dictionary, _context: Dictionary) -> bool: callback_order.append(facility[&"facility_id"]) return true, }) _expect(callback_order == [&"a_path", &"z_path"], "reachability callbacks run in stable facility-ID order") func _test_integer_utility_and_stable_tie_break() -> void: var service := _service_with_definitions() var guest := service.spawn_guest(&"guest_utility", &"adventurer", 0, { &"wallet_gold": 1_000, &"planned_exit_at": 1_000, &"state": GuestRuntimeType.State.DECIDING, &"needs": {&"hunger": 90}, }) var shared := { &"need_relief": 40, &"preference": 2, &"expected_quality": 3, &"novelty": 0, &"group_fit": 4, &"price_burden": 5, &"travel_minutes": 6, &"wait_minutes": 7, &"risk": 8, &"risk_sensitivity": 2, &"repetition_penalty": 9, &"urgency": 90, &"minimum_quality": 0, } var candidate_z := shared.duplicate(true) candidate_z[&"facility_id"] = &"z_room" var candidate_a := shared.duplicate(true) candidate_a[&"facility_id"] = &"a_room" var evaluated := service.evaluate_candidate(guest.guest_id, candidate_z) _expect(evaluated[&"utility"] is int, "Utility result is an integer") _expect(evaluated[&"utility"] == 264, "Utility follows the documented integer term equation exactly") var ranked := service.rank_candidates(guest.guest_id, [candidate_z, candidate_a]) _expect(ranked[0][&"facility_id"] == &"a_room", "SYS-GST-003 resolves a full Utility tie by facility_id") var decision := service.decide(guest.guest_id, [candidate_z, candidate_a], 1, { &"exit_threshold": 15, }) _expect(decision[&"ok"] and decision[&"candidate"][&"facility_id"] == &"a_room", "decision reserves the stable winning candidate") _expect(guest.state == GuestRuntimeType.State.MOVING and guest.target_facility_id == &"a_room", "successful decision enters MOVING with one target") func _test_state_machine_and_loop_guard() -> void: var service := _service_with_definitions() var guest := service.spawn_guest(&"guest_states", &"adventurer", 0, { &"planned_exit_at": 500, }) _expect(service.transition_guest(guest.guest_id, GuestRuntimeType.State.ENTERING, 10)[&"ok"], "SPAWNING enters ENTERING") _expect(service.transition_guest(guest.guest_id, GuestRuntimeType.State.DECIDING, 10)[&"ok"], "ENTERING enters DECIDING") _expect(service.transition_guest(guest.guest_id, GuestRuntimeType.State.MOVING, 10)[&"ok"], "DECIDING enters MOVING") _expect(service.transition_guest(guest.guest_id, GuestRuntimeType.State.REPLANNING, 10)[&"ok"], "MOVING enters REPLANNING") var guarded := service.transition_guest(guest.guest_id, GuestRuntimeType.State.DECIDING, 10) _expect(not guarded[&"ok"] and guarded[&"loop_guard"], "fifth transition in one Tick activates AI loop guard") _expect(guest.state == GuestRuntimeType.State.DECIDING, "AI loop guard stops the guest in DECIDING") _expect(service.get_diagnostics()[-1][&"code"] == &"AI_LOOP_GUARD", "loop guard leaves a diagnostic record") var invalid := service.transition_guest(guest.guest_id, GuestRuntimeType.State.DESPAWNING, 11) _expect(not invalid[&"ok"], "invalid state-machine edge is rejected") func _test_atomic_queue_abandonment() -> void: var service := _service_with_definitions() var guest := _moving_guest(service, &"guest_queue") var enqueue_calls := [0] var entered := service.enter_queue( guest.guest_id, &"meal_room", &"request_queue", 4, 1, func(_guest_id: StringName, _facility_id: StringName, _request_id: StringName) -> bool: enqueue_calls[0] += 1 return true ) _expect(entered[&"ok"] and enqueue_calls[0] == 1, "queue entry crosses the Callable boundary once") _expect(guest.state == GuestRuntimeType.State.QUEUING and guest.patience_remaining == 1, "queue entry records facility, request and initial patience") var failed_calls := [0] var failed_leave := service.tick_queue_patience( guest.guest_id, 5, 0, func(_guest_id: StringName, _facility_id: StringName, _request_id: StringName) -> bool: failed_calls[0] += 1 return false ) _expect(not failed_leave[&"ok"] and failed_calls[0] == 1, "external queue removal failure is surfaced") _expect(guest.state == GuestRuntimeType.State.QUEUING and guest.patience_remaining == 1, "failed queue removal leaves guest state completely unchanged") _expect(guest.target_facility_id == &"meal_room" and guest.queue_request_id == &"request_queue", "failed queue removal retains both cross-service references") var successful_calls := [0] var abandoned := service.tick_queue_patience( guest.guest_id, 5, 0, func(callback_guest_id: StringName, facility_id: StringName, request_id: StringName) -> Dictionary: successful_calls[0] += 1 return {&"ok": callback_guest_id == guest.guest_id and facility_id == &"meal_room" and request_id == &"request_queue"} ) _expect(abandoned[&"ok"] and abandoned[&"abandoned"] and successful_calls[0] == 1, "SYS-GST-004 removes an exhausted guest through one atomic callback") _expect(guest.state == GuestRuntimeType.State.ABANDONING and guest.patience_remaining == 0, "successful patience exhaustion enters ABANDONING") _expect(guest.target_facility_id.is_empty() and guest.queue_request_id.is_empty(), "atomic abandonment clears facility and queue ownership together") func _test_service_interruption_is_idempotent() -> void: var service := _service_with_definitions() var guest := _moving_guest(service, &"guest_interrupt") _expect(service.begin_service(guest.guest_id, &"bath", &"service_1", 4)[&"ok"], "moving guest begins a facility service") var callback_calls := [0] var first := service.interrupt_service( guest.guest_id, &"service_1", 5, func(_guest_id: StringName, _service_id: StringName) -> bool: callback_calls[0] += 1 return true ) var replay := service.interrupt_service( guest.guest_id, &"service_1", 5, func(_guest_id: StringName, _service_id: StringName) -> bool: callback_calls[0] += 1 return true ) _expect(first[&"ok"] and not first[&"replayed"], "first facility stop interrupts the active service") _expect(replay[&"ok"] and replay[&"replayed"], "duplicate facility stop is an idempotent replay") _expect(callback_calls[0] == 1, "SYS-GST-005 invokes interruption settlement only once") _expect(guest.state == GuestRuntimeType.State.INTERRUPTED and guest.current_service_id.is_empty(), "interruption frees guest service ownership") func _test_service_need_effect_schema() -> void: var service := _service_with_definitions() var guest := _moving_guest(service, &"guest_effect_schema") guest.needs[&"sleep"] = 95 guest.needs[&"hygiene"] = 5 _expect(service.begin_service(guest.guest_id, &"bath", &"service_effects", 4)[&"ok"], "canonical Need-effect fixture begins service") var before_invalid := service.create_snapshot() var invalid := service.complete_service( guest.guest_id, &"service_effects", 5, 80, {&"rest": -50} ) _expect(not invalid[&"ok"], "service completion rejects a legacy Need-effect alias") _expect(service.create_snapshot() == before_invalid, "invalid service effects do not partially complete service") var completed := service.complete_service( guest.guest_id, &"service_effects", 5, 120, {&"sleep": 20, &"hygiene": -20} ) _expect(completed[&"ok"] and completed[&"satisfaction_sample"] == 100, "canonical service effects complete with satisfaction clamp") _expect(guest.needs[&"sleep"] == 100 and guest.needs[&"hygiene"] == 0, "service Need effects clamp each canonical axis once") func _test_path_failure_safe_recovery() -> void: var service := _service_with_definitions() var guest := _moving_guest(service, &"guest_path") var recovery_targets: Array = [ {&"kind": &"emergency_exit", &"cell": Vector3i(9, 0, 0)}, {&"kind": &"entrance", &"cell": Vector3i(5, 0, 0)}, {&"kind": &"guidance", &"cell": Vector3i(2, 0, 0)}, ] var reachable := func(_from: Vector3i, target: Vector3i, _guest_id: StringName, _data: Dictionary) -> bool: return target != Vector3i(2, 0, 0) var first := service.record_path_failure(guest.guest_id, 4, recovery_targets, reachable) var second := service.record_path_failure(guest.guest_id, 5, recovery_targets, reachable) _expect(first[&"failure_count"] == 1 and not first[&"recovery_started"], "first path failure enters REPLANNING") _expect(second[&"failure_count"] == 2 and not second[&"recovery_started"], "second path failure remains recoverable") var third := service.record_path_failure(guest.guest_id, 6, recovery_targets, reachable) _expect(third[&"ok"] and third[&"recovery_started"], "SYS-GST-007 starts safe recovery on the third path failure") _expect(guest.state == GuestRuntimeType.State.LEAVING, "third failure cannot leave the guest stuck in REPLANNING") _expect(guest.recovery_kind == &"entrance" and guest.recovery_target_cell == Vector3i(5, 0, 0), "recovery tries guidance, entrance, then emergency exit in order") _expect(service.get_diagnostics()[-1][&"code"] == &"GUEST_PATH_RECOVERY", "safe path recovery emits a diagnostic record") var fallback_service := _service_with_definitions() var fallback_guest := _moving_guest(fallback_service, &"guest_fallback") fallback_service.record_path_failure(fallback_guest.guest_id, 4) fallback_service.record_path_failure(fallback_guest.guest_id, 5) var fallback := fallback_service.record_path_failure(fallback_guest.guest_id, 6, [], Callable()) _expect(fallback[&"recovery_kind"] == &"emergency_despawn", "no-route case uses a virtual emergency egress rather than deadlocking") func _test_exit_persistence_boundary() -> void: var service := _service_with_definitions() var guest := service.spawn_guest(&"guest_exit", &"adventurer", 0, { &"state": GuestRuntimeType.State.LEAVING, &"wallet_gold": 777, &"debt_gold": 90, &"satisfaction_total": 180, &"satisfaction_samples": 2, &"unresolved_incident_ids": [&"incident_b", &"incident_a"], &"outstanding_reservation_ids": [&"reservation_1"], &"planned_exit_at": 500, }) var result := service.despawn_guest(guest.guest_id, 1) _expect(result[&"ok"] and not service.has_guest(guest.guest_id), "leaving guest runtime is released after exit") var record := service.get_persistent_record(guest.guest_id) _expect(record[&"debt_gold"] == 90, "SYS-GST-006 preserves unresolved debt") _expect(record[&"unresolved_incident_ids"] == ["incident_a", "incident_b"], "SYS-GST-006 preserves unresolved incidents") _expect(record[&"outstanding_reservation_ids"] == ["reservation_1"], "SYS-GST-006 preserves outstanding reservations") _expect(not record.has(&"wallet_gold") and not record.has(&"needs") and not record.has(&"satisfaction_total"), "ordinary transient guest state does not survive despawn") func _test_overdue_departure_snapshot() -> void: for replan: bool in [false, true]: var service := _service_with_definitions() var guest := service.spawn_guest(&"overdue_deadline", &"adventurer", 0, { &"state": GuestRuntimeType.State.DECIDING, &"planned_exit_at": 4, }) _expect(guest != null, "overdue deadline fixture spawns through public GuestService") if guest == null: continue if replan: _expect(bool(service.enter_queue(guest.guest_id, &"meal_room", &"overdue_request", 4, 1).get(&"ok", false)), "public queue entry before final patience") _expect(bool(service.tick_queue_patience(guest.guest_id, 5, 0, func(_guest: StringName, _facility: StringName, _request: StringName): return true).get(&"ok", false)), "ordinary patience exhaustion after original deadline") _expect(bool(service.transition_guest(guest.guest_id, GuestRuntimeType.State.DECIDING, 5, &"REPLAN_AFTER_INTERRUPTION").get(&"ok", false)), "public overdue replan retains deadline") else: _expect(bool(service.begin_leaving(guest.guest_id, 5, &"PLANNED_EXIT").get(&"ok", false)), "public leaving can occur after original deadline") _expect(guest.planned_exit_at == 4 and guest.state_entered_at == 5 and guest.is_valid(), "deadline preserved independently of actual later state timestamp") var snapshot := service.create_snapshot() var restored := GuestServiceType.new() _expect(restored.restore_snapshot(snapshot), "overdue ordinary state accepts complete restore") _expect(var_to_bytes(restored.create_snapshot()) == var_to_bytes(snapshot), "overdue entire typed state roundtrip preserves original deadline") for field: StringName in [&"planned_exit_at", &"state_entered_at"]: var corrupt := snapshot.duplicate(true) corrupt[&"guests"][0][field] = -1 _expect(not restored.restore_snapshot(corrupt), "negative departure/state timestamp remains rejected " + String(field)) _expect(var_to_bytes(restored.create_snapshot()) == var_to_bytes(snapshot), "negative timestamp rejection remains atomic " + String(field)) func _test_snapshot_round_trip_and_atomic_rejection() -> void: var original := _service_with_definitions() var moving := _moving_guest(original, &"guest_saved_moving") var queued := _moving_guest(original, &"guest_saved_queue") original.enter_queue(queued.guest_id, &"cafeteria", &"request_saved", 4, 12) moving.path_failure_count = 1 moving.suspicion = 33 original.advance_one_minute(20) var snapshot := original.create_snapshot() var restored := GuestServiceType.new() _expect(restored.restore_snapshot(snapshot), "GuestService restores a valid snapshot") _expect(restored.create_snapshot() == snapshot, "GuestService snapshot round trip is byte-structure equivalent") _expect(restored.validate_invariants()[&"valid"], "restored guest state satisfies every invariant") var before_corrupt := restored.create_snapshot() var corrupt_need := snapshot.duplicate(true) corrupt_need[&"guests"][0][&"needs"]["hunger"] = 101 _expect(not restored.restore_snapshot(corrupt_need), "out-of-range Need rejects snapshot restore") _expect(restored.create_snapshot() == before_corrupt, "rejected Need snapshot leaves live state untouched") var duplicate_guest := snapshot.duplicate(true) duplicate_guest[&"guests"].append(duplicate_guest[&"guests"][0].duplicate(true)) _expect(not restored.restore_snapshot(duplicate_guest), "duplicate guest ID rejects snapshot restore") _expect(restored.create_snapshot() == before_corrupt, "duplicate rejection is also atomic") var orphan_type := snapshot.duplicate(true) orphan_type[&"guests"][0][&"guest_type_id"] = "missing_type" _expect(not restored.restore_snapshot(orphan_type), "unknown GuestDefinition reference rejects restore") _expect(restored.create_snapshot() == before_corrupt, "orphan-reference rejection preserves live state") func _test_v1_need_migration_and_schema_rejection() -> void: var source := _service_with_definitions() source.spawn_guest(&"guest_legacy", &"verified_noble", 0, { &"needs": {&"hunger": 11, &"sleep": 22, &"hygiene": 0, &"medical": 44, &"leisure": 55, &"intimacy": 66}, &"planned_exit_at": 500, }) var canonical := source.create_snapshot() var legacy := canonical.duplicate(true) legacy[&"version"] = 1 for definition: Dictionary in legacy[&"definitions"]: var rates: Dictionary = definition[&"need_rates"] definition[&"need_rates"] = { &"hunger": rates["hunger"], &"rest": rates["sleep"], &"fun": rates["leisure"], &"social": rates["intimacy"], &"safety": rates["medical"], } for guest_snapshot: Dictionary in legacy[&"guests"]: var needs: Dictionary = guest_snapshot[&"needs"] guest_snapshot[&"needs"] = { &"hunger": needs["hunger"], &"rest": needs["sleep"], &"fun": needs["leisure"], &"social": needs["intimacy"], &"safety": needs["medical"], } var migrated := GuestServiceType.new() _expect(migrated.restore_snapshot(legacy), "GuestService deterministically migrates an exact v1 five-Need snapshot") var migrated_snapshot := migrated.create_snapshot() var migrated_needs: Dictionary = migrated_snapshot[&"guests"][0][&"needs"] _expect(migrated_snapshot[&"version"] == 2, "v1 migration emits only the current GuestService schema") _expect(migrated_needs == { "hunger": 11, "hygiene": 0, "intimacy": 66, "leisure": 55, "medical": 44, "sleep": 22, }, "v1 rest/fun/social/safety map to sleep/leisure/intimacy/medical with Hygiene zero") _expect(migrated.validate_invariants()[&"valid"], "migrated six-Need graph satisfies current invariants") var stable_before := migrated.create_snapshot() var missing := canonical.duplicate(true) missing[&"guests"][0][&"needs"].erase("hygiene") _expect(not migrated.restore_snapshot(missing), "current snapshots reject a missing canonical Need") _expect(migrated.create_snapshot() == stable_before, "missing-Need rejection is atomic") var unknown := canonical.duplicate(true) unknown[&"guests"][0][&"needs"]["safety"] = 1 _expect(not migrated.restore_snapshot(unknown), "current snapshots reject unknown legacy Need keys") _expect(migrated.create_snapshot() == stable_before, "unknown-Need rejection is atomic") var wrong_type := canonical.duplicate(true) wrong_type[&"definitions"][0][&"need_rates"]["sleep"] = "1" _expect(not migrated.restore_snapshot(wrong_type), "current definition Need rates reject implicit integer coercion") _expect(migrated.create_snapshot() == stable_before, "definition type rejection is atomic") var mixed_v1 := legacy.duplicate(true) mixed_v1[&"guests"][0][&"needs"]["sleep"] = 22 _expect(not migrated.restore_snapshot(mixed_v1), "v1 migration rejects mixed legacy/current Need maps") _expect(migrated.create_snapshot() == stable_before, "mixed-schema rejection preserves live state") func _test_deterministic_decision_snapshot() -> void: var first := _deterministic_run() var second := _deterministic_run() _expect(first == second, "same guest definitions and candidates produce an identical authoritative snapshot") func _deterministic_run() -> Dictionary: var service := _service_with_definitions() var guest := service.spawn_guest(&"guest_determinism", &"adventurer", 0, { &"wallet_gold": 1_000, &"planned_exit_at": 1_000, &"state": GuestRuntimeType.State.DECIDING, &"needs": {&"hunger": 80, &"leisure": 50}, }) var candidates: Array = [ _candidate(&"z_meal", {&"need_relief": {&"hunger": 60}, &"facility_tags": [&"meal"], &"travel_minutes": 4}), _candidate(&"a_meal", {&"need_relief": {&"hunger": 60}, &"facility_tags": [&"meal"], &"travel_minutes": 4}), _candidate(&"theater", {&"need_relief": {&"leisure": 50}, &"facility_tags": [&"leisure"], &"travel_minutes": 3}), ] service.decide(guest.guest_id, candidates, 1, {&"absolute_minute": 1}) service.update_needs(guest.guest_id, {&"dirt": 2}) return service.create_snapshot() func _service_with_definitions() -> GuestService: var service := GuestServiceType.new() service.register_definition(GuestDefinitionType.new().configure(&"adventurer", { &"tags": [&"adventurer", &"general"], &"wealth_min": 100, &"wealth_max": 500, &"base_patience": 45, &"risk_tolerance": 80, &"preference_weights": {&"meal": 8, &"leisure": 3}, &"group_size_weights": {1: 100}, &"legal_sensitivity": 60, &"quality_sensitivity": 70, &"adult_verified": false, &"need_rates": {&"hunger": 3, &"sleep": -2, &"leisure": 1}, &"credit_limit_gold": 100, &"minimum_quality": 40, })) service.register_definition(GuestDefinitionType.new().configure(&"verified_noble", { &"tags": [&"wealthy", &"vip"], &"wealth_min": 1_000, &"wealth_max": 2_000, &"base_patience": 20, &"risk_tolerance": 40, &"legal_sensitivity": 20, &"adult_verified": true, &"minimum_quality": 40, })) return service func _moving_guest(service: GuestService, guest_id: StringName) -> GuestRuntime: var guest := service.spawn_guest(guest_id, &"adventurer", 0, { &"wallet_gold": 500, &"planned_exit_at": 1_000, }) service.transition_guest(guest_id, GuestRuntimeType.State.ENTERING, 1) service.transition_guest(guest_id, GuestRuntimeType.State.DECIDING, 2) service.transition_guest(guest_id, GuestRuntimeType.State.MOVING, 3) return guest func _candidate(facility_id: StringName, patch: Dictionary = {}) -> Dictionary: var candidate := { &"facility_id": facility_id, &"is_open": true, &"reachable": true, &"price_gold": 100, &"wait_minutes": 10, &"risk_level": 10, &"illegal_level": 10, &"expected_quality": 60, &"need_relief": 50, } for raw_key: Variant in patch: candidate[raw_key] = patch[raw_key] return candidate func _candidate_ids(candidates: Array[Dictionary]) -> Array[StringName]: var result: Array[StringName] = [] for candidate: Dictionary in candidates: result.append(StringName(candidate[&"facility_id"])) return result func _rejection_map(rejections: Array[Dictionary]) -> Dictionary: var result: Dictionary = {} for rejection: Dictionary in rejections: result[StringName(rejection[&"facility_id"])] = StringName(rejection[&"reason"]) return result func _expect(condition: bool, label: String) -> void: _checks += 1 if condition: print(" ✓ %s" % label) return _failures += 1 push_error(" ✗ %s" % label) func _test_primary_need_priority() -> void: var service := _service_with_definitions() var guest := service.spawn_guest(&"need_priority", &"adventurer", 0, { &"wallet_gold": 1000, &"planned_exit_at": 1000, &"state": GuestRuntime.State.DECIDING, &"needs": {&"hunger": 90, &"sleep": 70}, }) var food := _candidate(&"food", {&"need_relief": {&"hunger": 80}, &"expected_quality": 40}) var room := _candidate(&"room", {&"need_relief": {&"sleep": 90}, &"expected_quality": 100, &"preference": 100}) _expect(service.evaluate_candidate(guest.guest_id, room)[&"utility"] > service.evaluate_candidate(guest.guest_id, food)[&"utility"], "regression scenario has a tempting lower-need facility") _expect(service.select_best_candidate(guest.guest_id, [room,food])[&"facility_id"] == &"food", "highest need precedes quality and preference bonuses") var near_food := food.duplicate(true) near_food[&"facility_id"] = &"near_food" near_food[&"travel_minutes"] = 1 food[&"travel_minutes"] = 20 _expect(service.select_best_candidate(guest.guest_id, [food,near_food])[&"facility_id"] == &"near_food", "same need still compares distance") for field: StringName in [&"wait_minutes", &"price_burden"]: var costly := near_food.duplicate(true) costly[&"facility_id"] = &"costly" costly[field] = 100 _expect(service.select_best_candidate(guest.guest_id, [costly,near_food])[&"facility_id"] == &"near_food", "same need still compares queue or price") var unavailable := food.duplicate(true) unavailable[&"reachable"] = false var accepted := service.extract_candidates(guest.guest_id, [unavailable,room]) _expect(service.select_best_candidate(guest.guest_id, accepted)[&"facility_id"] == &"room", "unreachable highest-need facility does not block a usable alternative") unavailable[&"reachable"] = true unavailable[&"price_gold"] = 100000 accepted = service.extract_candidates(guest.guest_id, [unavailable,room]) _expect(service.select_best_candidate(guest.guest_id, accepted)[&"facility_id"] == &"room", "highest need does not bypass affordability") var unattractive := food.duplicate(true) unattractive[&"travel_minutes"] = 10000 _expect(service.select_best_candidate(guest.guest_id, [unattractive,room])[&"facility_id"] == &"room", "below-threshold high-need candidate does not stop fallback search") guest.needs[&"sleep"] = 90 _expect(service.select_best_candidate(guest.guest_id, [food,room])[&"facility_id"] == &"room", "equal highest needs use the existing utility comparison") guest.needs[&"sleep"] = 70 var before := service.create_snapshot() var observed := service.get_observation(guest.guest_id) observed[&"needs"][&"hunger"] = 0 _expect(guest.needs[&"hunger"] == 90 and service.create_snapshot() == before, "observation is read-only and owns its dictionaries") _expect(service.restore_snapshot(before), "pre-decision save restores") var replay := service.decide(guest.guest_id, [room,food], 1) _expect(replay[&"candidate"][&"facility_id"] == &"food", "restored runtime uses the same highest-need decision") var after := service.create_snapshot() _expect(service.restore_snapshot(before), "decision can be replayed from snapshot") service.decide(guest.guest_id, [food,room], 1) _expect(service.create_snapshot() == after, "candidate insertion order cannot change saved decision state") _expect(service.restore_snapshot(before), "reservation retry starts at the same state") var reserved := service.decide(guest.guest_id, [food,room], 1, {&"reserve_callback": func(_id: StringName, facility: StringName, _candidate: Dictionary) -> bool: return facility != &"food"}) _expect(reserved[&"candidate"][&"facility_id"] == &"room", "failed reservation tries the next usable candidate")