extends SceneTree const DefenseServiceType = preload("res://scripts/defense/defense_service.gd") var _checks := 0 var _failures := 0 func _initialize() -> void: call_deferred("_run") func _run() -> void: print("S5: deterministic wave-by-wave Raid defense") _test_threat_summary() _test_definition_and_registration_validation() _test_scores_penalties_and_stable_order() _test_layered_assets_and_breach() _test_explicit_and_offscreen_attackers_share_one_formula() _test_active_skills_and_successful_boundaries() _test_skill_boundary_validation_rollback() _test_skill_boundary_apply_and_outcome_rollback() _test_rng_transaction_retry_stability() _test_retreat_policies_and_manual_retreat_replay() _test_cancel_transaction_and_reconciliation() _test_cancel_incomplete_preparation() _test_resolution_operation_namespace_and_legacy_v1() _test_multiple_waves_require_explicit_transition() _test_raid_duration_and_wave_round_limits() _test_round_resolution_replay() _test_round_clock_contract() _test_wave_entry_and_metadata_validation() _test_armor_pool_accounting() _test_snapshot_round_trip_and_corrupt_atomicity() _test_cross_time_snapshot_rejection() _test_input_order_determinism() if _failures == 0: print("PASS: %d checks" % _checks) else: push_error("FAIL: %d of %d checks failed" % [_failures, _checks]) quit(_failures) func _test_threat_summary() -> void: var service := DefenseServiceType.new() _expect(service.get_threat_summary()[&"total"] == 0, "empty defense has no HUD threat") _must(service.create_raid(&"preparing", 0, _definition()), "HUD preparing raid created") _prepare_basic(service, &"active") var before := service.create_snapshot() var summary := service.get_threat_summary() _expect(summary == {&"preparing": 1, &"active": 1, &"between_waves": 0, &"total": 2}, "HUD counts preparing and active raids separately") summary[&"total"] = 99 _expect(service.get_threat_summary()[&"total"] == 2 and service.create_snapshot() == before, "HUD projection is owned and read only") _must(service.cancel_raid(&"preparing", 0), "HUD incomplete preparation can be cancelled") _expect(service.get_threat_summary()[&"preparing"] == 0 and service.get_threat_summary()[&"total"] == 1, "cancelled history is not an ongoing threat") var restored := DefenseServiceType.new() _expect(restored.restore_snapshot(service.create_snapshot()) and restored.get_threat_summary() == service.get_threat_summary(), "HUD projection follows restored authoritative state") func _test_definition_and_registration_validation() -> void: var service := DefenseServiceType.new() _expect(not service.create_raid(&"", 0, _definition()).is_success(), "Raid ID is required") _expect(not service.create_raid(&"bad_minute", -1, _definition()).is_success(), "creation minute cannot be negative") _expect(not service.create_raid(&"no_entries", 0, _definition({&"entry_points": []})).is_success(), "at least one entry point is required") _expect(not service.create_raid(&"no_objective", 0, _definition({&"objective_ids": []})).is_success(), "at least one objective is required") _expect(not service.create_raid(&"no_waves", 0, _definition({&"waves": []})).is_success(), "at least one wave is required") _expect(not service.create_raid(&"bad_threat", 0, _definition({&"waves": [{&"wave_id": &"w", &"threat_strength": 0}]})).is_success(), "wave strength must be positive") _expect(not service.create_raid(&"duplicate_wave", 0, _definition({&"waves": [ {&"wave_id": &"same", &"threat_strength": 10}, {&"wave_id": &"same", &"threat_strength": 20}, ]})).is_success(), "wave IDs are unique") _expect(not service.create_raid(&"unknown_entry", 0, _definition({&"waves": [{ &"wave_id": &"w", &"threat_strength": 10, &"entry_points": [&"not_declared"], }]})).is_success(), "wave entry points must belong to the Raid") _expect(not service.create_raid(&"unknown_objective", 0, _definition({&"waves": [{ &"wave_id": &"w", &"threat_strength": 10, &"objective_ids": [&"not_declared"], }]})).is_success(), "wave objectives must belong to the Raid") _expect(not service.create_raid(&"bad_policy", 0, _definition({&"retreat_policy": &"PANIC"})).is_success(), "retreat policy is enumerated") _expect(not service.create_raid(&"bad_duration", 0, _definition({&"round_minutes": 0})).is_success(), "round duration must be positive") _expect(not service.create_raid(&"bad_threshold", 0, _definition({&"defender_retreat_threshold": 101})).is_success(), "defender retreat threshold is bounded") var created = service.create_raid(&"valid", 10, _definition({ &"entry_points": [&"west", &"east", &"west"], &"objective_ids": [&"vault", &"core", &"vault"], &"waves": [{ &"wave_id": &"wave_a", &"threat_strength": 40, &"entry_points": [&"west", &"east"], &"objective_ids": [&"vault", &"core"], }], })) _expect(created.is_success() and created.details[&"wave_count"] == 1, "valid Raid definition is accepted") var canonical := service.get_raid(&"valid") _expect(canonical[&"entry_points"] == [&"east", &"west"], "entry point IDs are canonicalized") _expect(canonical[&"objective_ids"] == [&"core", &"vault"], "objective IDs are canonicalized") _expect(not service.create_raid(&"valid", 10, _definition()).is_success(), "duplicate Raid ID is rejected") _expect(not service.register_combatant(&"valid", &"bad_side", &"NEUTRAL", 10, 10).is_success(), "combatant side is validated") _expect(not service.register_combatant(&"valid", &"bad_health", &"DEFENDER", 10, 0).is_success(), "combatant health is validated") _expect(not service.register_combatant(&"valid", &"bad_wave", &"ATTACKER", 10, 10, {&"wave_id": &"missing"}).is_success(), "attacker wave reference is validated") _expect(not service.register_combatant(&"valid", &"bad_defender_wave", &"DEFENDER", 10, 10, {&"wave_id": &"wave_a"}).is_success(), "defenders cannot be wave-bound") _expect(service.register_combatant(&"valid", &"guard", &"DEFENDER", 20, 100, {&"health": 60, &"entry_point_id": &"east"}).is_success(), "valid wounded defender is registered") _expect(service.get_raid(&"valid")[&"combatants"][&"guard"][&"initial_health"] == 100, "retreat baseline uses maximum health") _expect(not service.register_asset(&"valid", &"core", &"DOOR", 100, 10).is_success(), "declared objective cannot be registered as a door") _expect(not service.register_asset(&"valid", &"ordinary", &"OBJECTIVE", 100, 10).is_success(), "undeclared asset cannot be registered as an objective") _expect(not service.register_asset(&"valid", &"door_bad_entry", &"DOOR", 100, 10, {&"entry_points": [&"north"]}).is_success(), "asset entry coverage is validated") _expect(service.register_asset(&"valid", &"core", &"OBJECTIVE", 100, 5, {&"integrity": 40}).is_success(), "first objective asset is registered") _expect(service.get_raid(&"valid")[&"assets"][&"core"][&"initial_integrity"] == 100, "objective retreat baseline uses maximum integrity") _expect(not service.start_raid(&"valid", 10).is_success(), "Raid cannot start until every objective is registered") _expect(service.register_asset(&"valid", &"vault", &"OBJECTIVE", 80, 5).is_success(), "second objective asset is registered") _expect(service.start_raid(&"valid", 10).is_success(), "complete Raid can start") _expect(service.get_raid(&"valid")[&"waves"][0][&"state"] == &"ACTIVE", "starting marks the first wave ACTIVE") _expect(not service.register_asset(&"valid", &"late", &"DOOR", 10, 1).is_success(), "assets cannot be added after start") _expect(not service.register_combatant(&"valid", &"late", &"DEFENDER", 1, 1).is_success(), "combatants cannot be added after start") _expect(service.validate_invariants()[&"ok"], "validated registration state satisfies all invariants") func _test_scores_penalties_and_stable_order() -> void: var service := DefenseServiceType.new().configure_callbacks(_fixed_roll(50)) _must(service.create_raid(&"scores", 0, _definition({ &"entry_points": [&"west", &"east"], &"terrain_bonus": 3, &"command_bonus": 4, &"surprise_penalty": 7, &"multiple_entry_penalty": 5, &"waves": [{ &"wave_id": &"wave_1", &"threat_strength": 40, &"entry_points": [&"west", &"east"], &"objective_ids": [&"core"], &"retreat_threshold": 0, &"max_rounds": 10, }], })), "score fixture Raid is created") _must(service.register_combatant(&"scores", &"z_guard", &"DEFENDER", 20, 100, {&"fatigue": 25}), "fatigued defender is registered") _must(service.register_combatant(&"scores", &"a_guard", &"DEFENDER", 10, 100), "fresh defender is registered") _must(service.register_combatant(&"scores", &"raider", &"ATTACKER", 20, 100, {&"wave_id": &"wave_1"}), "explicit attacker is registered") _must(service.register_asset(&"scores", &"core", &"OBJECTIVE", 100, 5), "score objective is registered") _must(service.register_asset(&"scores", &"tower", &"DEFENSE_DEVICE", 100, 12, {&"terrain_bonus": 2}), "score device is registered") _must(service.register_asset(&"scores", &"gate", &"DOOR", 100, 8), "score door is registered") _must(service.start_raid(&"scores", 0), "score Raid starts") var first = service.resolve_next_round(&"scores", &"score_round_1", 5) _expect(first.is_success(), "first score round resolves") var components: Dictionary = first.details[&"score_components"] _expect(components[&"defender_combat"] == 30, "defender score sums effective combat") _expect(components[&"asset_defense"] == 25, "door, device and objective all contribute defense") _expect(components[&"terrain_bonus"] == 5 and components[&"command_bonus"] == 4, "terrain and command bonuses are explicit score components") _expect(components[&"fatigue_penalty"] == 5, "fatigue scales the affected defender's effective combat") _expect(components[&"multiple_entry_penalty"] == 5, "second entry point applies one deterministic penalty") _expect(components[&"surprise_penalty"] == 7, "surprise applies on the first wave round") _expect(components[&"attacker_combat"] == 20 and components[&"aggregate_attacker_combat"] == 40, "explicit and aggregate attackers are visible score components") _expect(first.details[&"defense_score"] == 47 and first.details[&"attack_score"] == 60, "integer score formula has the expected result") var raid_after_first := service.get_raid(&"scores") _expect(raid_after_first[&"combatants"][&"a_guard"][&"fatigue"] == 5, "fresh defender gains five fatigue per round") _expect(raid_after_first[&"combatants"][&"z_guard"][&"fatigue"] == 30, "existing fatigue increases by the same fixed amount") var second = service.resolve_next_round(&"scores", &"score_round_2", 10) _expect(second.is_success() and second.details[&"score_components"][&"surprise_penalty"] == 0, "surprise never repeats after round one") _expect(second.details[&"defense_roll"] == 50 and second.details[&"attack_roll"] == 50, "injected rolls are deterministic") _expect(service.validate_invariants()[&"ok"], "multi-entry and fatigue score state remains valid") func _test_layered_assets_and_breach() -> void: var outcomes := {&"count": 0, &"payload": {}} var service := DefenseServiceType.new().configure_callbacks( _fixed_roll(50), Callable(), func(_payload: Dictionary, phase: StringName, token: Variant) -> Dictionary: if phase == &"apply": outcomes[&"count"] = int(outcomes[&"count"]) + 1 outcomes[&"payload"] = _payload.duplicate(true) return {&"ok": true, &"token": token} ) _must(service.create_raid(&"layers", 0, _definition({ &"waves": [{&"wave_id": &"wave_1", &"threat_strength": 100, &"retreat_threshold": 0, &"max_rounds": 10}], })), "layered Raid is created") _must(service.register_asset(&"layers", &"core", &"OBJECTIVE", 3, 0), "fragile objective is registered") _must(service.register_asset(&"layers", &"z_door", &"DOOR", 3, 0), "door is registered") _must(service.register_asset(&"layers", &"a_device", &"DEFENSE_DEVICE", 3, 0), "device is registered") _must(service.start_raid(&"layers", 0), "layered Raid starts") var door_round = service.resolve_next_round(&"layers", &"layer_1", 5) _expect(door_round.is_success() and door_round.details[&"defense_damage"][&"target_id"] == &"z_door", "lowest target-order door is damaged first") _expect(door_round.details[&"defense_damage"][&"destroyed"], "door integrity reaches zero in its round") var device_round = service.resolve_next_round(&"layers", &"layer_2", 10) _expect(device_round.is_success() and device_round.details[&"defense_damage"][&"target_id"] == &"a_device", "device is targeted after destroyed doors") _expect(device_round.details[&"score_components"][&"asset_defense"] == 0, "destroyed door no longer contributes defense") var objective_round = service.resolve_next_round(&"layers", &"layer_3", 15) _expect(objective_round.is_success() and objective_round.details[&"defense_damage"][&"target_id"] == &"core", "objective is reached after devices") _expect(objective_round.details[&"state"] == &"BREACHED" and objective_round.details[&"reason"] == &"OBJECTIVE_DESTROYED", "destroyed objective breaches the Raid") var raid := service.get_raid(&"layers") _expect(raid[&"assets"][&"z_door"][&"integrity"] == 0 and raid[&"assets"][&"a_device"][&"integrity"] == 0 and raid[&"assets"][&"core"][&"integrity"] == 0, "all three defense layers retain their per-round integrity updates") _expect(raid[&"waves"][0][&"state"] == &"BREACHED" and raid[&"result"] == &"BREACHED", "wave and Raid terminal states agree") _expect(outcomes[&"count"] == 1, "terminal outcome boundary applies exactly once") var asset_results: Array = outcomes[&"payload"][&"asset_results"] _expect([asset_results[0][&"asset_id"], asset_results[1][&"asset_id"], asset_results[2][&"asset_id"]] == [&"a_device", &"core", &"z_door"], "terminal asset reconciliation payload uses stable ID order") _expect(asset_results.all(func(asset: Dictionary) -> bool: return bool(asset[&"destroyed"])), "terminal payload exposes final integrity for every destroyed defense layer") _expect(service.validate_invariants()[&"ok"], "breached layered Raid remains valid") func _test_explicit_and_offscreen_attackers_share_one_formula() -> void: var aggregate_only := _attack_mix_service(&"aggregate_only", false) var mixed := _attack_mix_service(&"mixed", true) var aggregate_round = aggregate_only.resolve_next_round(&"aggregate_only", &"round", 5) var mixed_round = mixed.resolve_next_round(&"mixed", &"round", 5) _expect(aggregate_round.is_success() and mixed_round.is_success(), "aggregate-only and mixed rounds resolve") _expect(aggregate_round.details[&"attack_score"] == mixed_round.details[&"attack_score"], "equal explicit/offscreen effective power produces the same attack score") _expect(aggregate_round.details[&"damage_to_threat"] == mixed_round.details[&"damage_to_threat"], "equal attack power uses the same damage formula") _expect(mixed_round.details[&"attacker_damage"].size() == 1, "mixed damage is allocated to the stable explicit attacker first") _expect(mixed_round.details[&"attacker_damage"][0][&"target_id"] == &"visible_raider" and mixed_round.details[&"attacker_damage"][0][&"damage"] == 2, "explicit attacker consumes only its actual remaining durability") _expect(mixed_round.details[&"aggregate_damage"] == mixed_round.details[&"damage_applied_to_threat"] - 2, "only leftover damage reaches the offscreen aggregate") _expect(mixed.get_raid(&"mixed")[&"waves"][0][&"remaining_threat"] == 10 - mixed_round.details[&"aggregate_damage"], "aggregate threat is reduced once, never in parallel with explicit damage") _expect(mixed.get_raid(&"mixed")[&"combatants"][&"visible_raider"][&"state"] == DefenseServiceType.CombatantState.INCAPACITATED, "explicit attacker state follows its health") _expect(mixed.validate_invariants()[&"ok"], "mixed on/offscreen attacker state remains valid") func _test_active_skills_and_successful_boundaries() -> void: var calls: Array[String] = [] var service := DefenseServiceType.new().configure_callbacks( _fixed_roll(50), func(payload: Dictionary, phase: StringName, token: Variant) -> Dictionary: calls.append("%s:%s" % [phase, payload[&"action_id"]]) return {&"ok": true, &"token": token if token != null else String(payload[&"action_id"])} ) _must(service.create_raid(&"skills", 0, _definition({ &"waves": [{&"wave_id": &"wave_1", &"threat_strength": 50, &"retreat_threshold": 0, &"max_rounds": 10}], })), "skill Raid is created") _must(service.register_combatant(&"skills", &"caster", &"DEFENDER", 20, 100, {&"health": 50}), "skill caster is registered") _must(service.register_asset(&"skills", &"core", &"OBJECTIVE", 100, 5), "skill objective is registered") _must(service.register_asset(&"skills", &"door", &"DOOR", 100, 5, {&"integrity": 40}), "fortify target is registered") _must(service.start_raid(&"skills", 0), "skill Raid starts") _must(service.queue_active_skill(&"skills", &"z_rally", &"caster", &"RALLY", {&"potency": 10}), "RALLY is queued") _must(service.queue_active_skill(&"skills", &"a_power", &"caster", &"POWER_STRIKE", {&"potency": 5}), "POWER_STRIKE is queued") _must(service.queue_active_skill(&"skills", &"m_heal", &"caster", &"HEAL", {&"potency": 7, &"target_id": &"caster"}), "HEAL is queued") _must(service.queue_active_skill(&"skills", &"n_fortify", &"caster", &"FORTIFY", {&"potency": 6, &"target_id": &"door"}), "FORTIFY is queued") var queued_replay = service.queue_active_skill(&"skills", &"z_rally", &"caster", &"RALLY", {&"potency": 99}) _expect(queued_replay.is_success() and queued_replay.is_replay, "duplicate queued action ID is an idempotent replay") var result = service.resolve_next_round(&"skills", &"skill_round", 5) _expect(result.is_success(), "round with all active skills resolves") _expect(result.details[&"skill_ids"] == [&"a_power", &"m_heal", &"n_fortify", &"z_rally"], "skills resolve in stable action-ID order") _expect(result.details[&"score_components"][&"skill_bonus"] == 10, "RALLY contributes its score bonus") _expect(result.details[&"damage_to_threat"] >= 6, "POWER_STRIKE adds direct threat damage") var raid := service.get_raid(&"skills") _expect(raid[&"combatants"][&"caster"][&"health"] == 57, "HEAL updates defender health before damage") var fortify_effect: Dictionary = result.details[&"skill_effects"][2] _expect(fortify_effect[&"action_id"] == &"n_fortify" and fortify_effect[&"repaired"] == 6, "FORTIFY repairs its target before the attack") _expect(raid[&"assets"][&"door"][&"integrity"] == 46 - int(result.details[&"defense_damage"][&"damage"]), "round damage is applied after the FORTIFY repair") _expect(raid[&"queued_skills"].is_empty() and raid[&"applied_skill_ids"].size() == 4, "applied skills move from queue to idempotency history") _expect(calls.slice(0, 4) == ["validate:a_power", "validate:m_heal", "validate:n_fortify", "validate:z_rally"], "all boundary validations use stable action-ID order") _expect(calls.slice(4, 8) == ["apply:a_power", "apply:m_heal", "apply:n_fortify", "apply:z_rally"], "all boundary applications follow the validated order") var applied_replay = service.queue_active_skill(&"skills", &"z_rally", &"caster", &"RALLY") _expect(applied_replay.is_success() and applied_replay.is_replay, "applied skill action ID also replays without recharging") _expect(service.validate_invariants()[&"ok"], "active skill state satisfies invariants") func _test_skill_boundary_validation_rollback() -> void: var calls: Array[String] = [] var service := DefenseServiceType.new().configure_callbacks( _fixed_roll(50), func(payload: Dictionary, phase: StringName, token: Variant) -> Dictionary: calls.append("%s:%s" % [phase, payload[&"action_id"]]) if phase == &"validate" and payload[&"action_id"] == &"b_reject": return {&"ok": false, &"reason": &"NO_RESOURCE"} return {&"ok": true, &"token": token if token != null else String(payload[&"action_id"])} ) _prepare_basic(service, &"validation_rollback") _must(service.queue_active_skill(&"validation_rollback", &"a_reserved", &"guard", &"RALLY", {&"potency": 3}), "first reserved skill is queued") _must(service.queue_active_skill(&"validation_rollback", &"b_reject", &"guard", &"RALLY", {&"potency": 3}), "rejecting skill is queued") var before := service.create_snapshot() var failed = service.resolve_next_round(&"validation_rollback", &"rejected_round", 5) _expect(not failed.is_success(), "skill validation rejection aborts the round") _expect(service.create_snapshot() == before, "validation rejection leaves all local Raid state unchanged") _expect(calls == ["validate:a_reserved", "validate:b_reject", "rollback:a_reserved"], "earlier validation reservations roll back in reverse order") _expect(service.validate_invariants()[&"ok"], "validation rollback preserves invariants") func _test_skill_boundary_apply_and_outcome_rollback() -> void: var apply_calls: Array[String] = [] var applying := DefenseServiceType.new().configure_callbacks( _fixed_roll(50), func(payload: Dictionary, phase: StringName, token: Variant) -> Dictionary: apply_calls.append("%s:%s" % [phase, payload[&"action_id"]]) if phase == &"apply" and payload[&"action_id"] == &"b_fail": return {&"ok": false, &"token": "failed_token"} return {&"ok": true, &"token": token if token != null else String(payload[&"action_id"])} ) _prepare_basic(applying, &"apply_rollback") for action_id: StringName in [&"a_apply", &"b_fail", &"c_validated"]: _must(applying.queue_active_skill(&"apply_rollback", action_id, &"guard", &"RALLY", {&"potency": 2}), "apply rollback skill queues: %s" % action_id) var apply_before := applying.create_snapshot() var apply_failed = applying.resolve_next_round(&"apply_rollback", &"apply_failed", 5) _expect(not apply_failed.is_success(), "skill application rejection aborts the round") _expect(applying.create_snapshot() == apply_before, "application rejection leaves local state atomic") _expect(apply_calls == [ "validate:a_apply", "validate:b_fail", "validate:c_validated", "apply:a_apply", "apply:b_fail", "rollback:c_validated", "rollback:b_fail", "rollback:a_apply", ], "applied, failed and not-yet-applied validation tokens all roll back once") var skill_calls: Array[String] = [] var outcome_calls: Array[String] = [] var skill_boundary := func(payload: Dictionary, phase: StringName, token: Variant) -> Dictionary: skill_calls.append("%s:%s" % [phase, payload[&"action_id"]]) return {&"ok": true, &"token": token if token != null else &"skill_token"} var outcome_boundary := func(_payload: Dictionary, phase: StringName, token: Variant) -> Dictionary: outcome_calls.append(String(phase)) if phase == &"apply": return {&"ok": false, &"token": &"outcome_token"} return {&"ok": true, &"token": token if token != null else &"outcome_token"} var terminal := DefenseServiceType.new().configure_callbacks( _fixed_roll(50), skill_boundary, outcome_boundary ) _must(terminal.create_raid(&"outcome_rollback", 0, _definition({ &"waves": [{&"wave_id": &"wave_1", &"threat_strength": 100, &"retreat_threshold": 0, &"max_rounds": 1}], })), "terminal rollback Raid is created") _must(terminal.register_combatant(&"outcome_rollback", &"guard", &"DEFENDER", 10, 100), "terminal rollback guard is registered") _must(terminal.register_asset(&"outcome_rollback", &"core", &"OBJECTIVE", 100, 0), "terminal rollback objective is registered") _must(terminal.start_raid(&"outcome_rollback", 0), "terminal rollback Raid starts") _must(terminal.queue_active_skill(&"outcome_rollback", &"skill", &"guard", &"RALLY", {&"potency": 10}), "terminal rollback skill is queued") var terminal_before := terminal.create_snapshot() var outcome_failed = terminal.resolve_next_round(&"outcome_rollback", &"outcome_failed", 5) _expect(not outcome_failed.is_success(), "rejected terminal outcome aborts the round") _expect(terminal.create_snapshot() == terminal_before, "outcome rejection restores round, skill and wave state atomically") _expect(skill_calls == ["validate:skill", "apply:skill", "rollback:skill"], "successful external skill application is rolled back after outcome failure") _expect(outcome_calls == ["validate", "apply", "rollback"], "failed outcome apply invokes its rollback boundary") _expect(terminal.validate_invariants()[&"ok"], "cross-boundary rollback leaves a valid Raid") func _test_rng_transaction_retry_stability() -> void: var rng_state := {&"cursor": 0, &"values": [17, 83, 41, 59], &"phases": []} var outcome_state := {&"apply_attempts": 0} var roll_callback := func(_key: StringName, _context: Dictionary) -> int: var cursor := int(rng_state[&"cursor"]) var value := int(rng_state[&"values"][cursor]) rng_state[&"cursor"] = cursor + 1 return value var rng_boundary := func(_context: Dictionary, phase: StringName, token: Variant) -> Dictionary: (rng_state[&"phases"] as Array).append(phase) if phase == &"snapshot": return {&"ok": true, &"token": int(rng_state[&"cursor"])} if phase == &"rollback": rng_state[&"cursor"] = int(token) return {&"ok": true, &"token": token} var outcome_boundary := func(_payload: Dictionary, phase: StringName, token: Variant) -> Dictionary: if phase == &"apply": outcome_state[&"apply_attempts"] = int(outcome_state[&"apply_attempts"]) + 1 if int(outcome_state[&"apply_attempts"]) == 1: return {&"ok": false, &"token": &"first_attempt"} return {&"ok": true, &"token": token} var service := DefenseServiceType.new().configure_callbacks( roll_callback, Callable(), outcome_boundary, rng_boundary ) _must(service.create_raid(&"rng_retry", 0, _definition({ &"waves": [{&"wave_id": &"wave_1", &"threat_strength": 100, &"retreat_threshold": 0, &"max_rounds": 1}], })), "RNG retry Raid is created") _must(service.register_asset(&"rng_retry", &"core", &"OBJECTIVE", 1_000, 0), "RNG retry objective is registered") _must(service.start_raid(&"rng_retry", 0), "RNG retry Raid starts") var before := service.create_snapshot() var failed = service.resolve_next_round(&"rng_retry", &"rng_resolution", 5) _expect(not failed.is_success(), "terminal outcome failure aborts a stateful-RNG round") _expect(service.create_snapshot() == before, "failed stateful-RNG round leaves Defense state atomic") _expect(rng_state[&"cursor"] == 0, "RNG rollback restores the cursor consumed by both rolls") _expect(rng_state[&"phases"] == [&"snapshot", &"rollback"], "failed round snapshots then rolls back RNG state") var retried = service.resolve_next_round(&"rng_retry", &"rng_resolution", 5) _expect(retried.is_success() and retried.details[&"defense_roll"] == 17 and retried.details[&"attack_roll"] == 83, "retry consumes the exact same RNG values") _expect(rng_state[&"cursor"] == 2, "successful retry commits exactly two RNG draws") _expect(rng_state[&"phases"] == [&"snapshot", &"rollback", &"snapshot", &"commit"], "successful retry commits its RNG transaction") var replay = service.resolve_next_round(&"rng_retry", &"rng_resolution", 999) _expect(replay.is_success() and replay.is_replay and replay.details == retried.details, "RNG-backed resolution replays its exact committed payload") _expect(rng_state[&"cursor"] == 2 and (rng_state[&"phases"] as Array).size() == 4, "resolution replay consumes no RNG or boundary phase") _expect(service.validate_invariants()[&"ok"], "RNG retry result satisfies invariants") func _test_retreat_policies_and_manual_retreat_replay() -> void: var threshold := DefenseServiceType.new().configure_callbacks(_fixed_roll(50)) _must(threshold.create_raid(&"threshold", 0, _definition({&"retreat_policy": &"THRESHOLD", &"defender_retreat_threshold": 50})), "threshold Raid is created") _must(threshold.register_combatant(&"threshold", &"guard", &"DEFENDER", 10, 100, {&"health": 40}), "wounded threshold guard is registered") _must(threshold.register_asset(&"threshold", &"core", &"OBJECTIVE", 100, 5), "threshold objective is registered") _must(threshold.start_raid(&"threshold", 0), "threshold Raid starts") var threshold_round = threshold.resolve_next_round(&"threshold", &"threshold_round", 5) _expect(threshold_round.is_success() and threshold_round.details[&"state"] == &"DEFENDERS_RETREATED", "THRESHOLD retreats at or below configured health percentage") _expect(threshold.get_raid(&"threshold")[&"combatants"][&"guard"][&"state"] == DefenseServiceType.CombatantState.RETREATED, "automatic retreat updates active defenders") var protect := DefenseServiceType.new().configure_callbacks(_fixed_roll(50)) _must(protect.create_raid(&"protect", 0, _definition({&"retreat_policy": &"PROTECT_AND_RETREAT", &"defender_retreat_threshold": 50})), "protect Raid is created") _must(protect.register_combatant(&"protect", &"guard", &"DEFENDER", 10, 100), "protect guard is registered") _must(protect.register_asset(&"protect", &"core", &"OBJECTIVE", 100, 5, {&"integrity": 40}), "pre-damaged protected objective is registered") _must(protect.start_raid(&"protect", 0), "protect Raid starts") var protect_round = protect.resolve_next_round(&"protect", &"protect_round", 5) _expect(protect_round.is_success() and protect_round.details[&"reason"] == &"RETREAT_POLICY_TRIGGERED", "PROTECT_AND_RETREAT observes objective integrity against full capacity") for policy: StringName in [&"HOLD", &"MANUAL"]: var holding := DefenseServiceType.new().configure_callbacks(_fixed_roll(50)) _must(holding.create_raid(policy, 0, _definition({&"retreat_policy": policy, &"defender_retreat_threshold": 90})), "%s Raid is created" % policy) _must(holding.register_combatant(policy, &"guard", &"DEFENDER", 10, 100, {&"health": 10}), "%s wounded guard is registered" % policy) _must(holding.register_asset(policy, &"core", &"OBJECTIVE", 100, 5), "%s objective is registered" % policy) _must(holding.start_raid(policy, 0), "%s Raid starts" % policy) var round = holding.resolve_next_round(policy, StringName("%s_round" % policy), 5) _expect(round.is_success() and round.details[&"state"] == &"ACTIVE", "%s never triggers automatic retreat" % policy) var outcome_count := {&"apply": 0} var manual := DefenseServiceType.new().configure_callbacks( _fixed_roll(50), Callable(), func(_payload: Dictionary, phase: StringName, token: Variant) -> Dictionary: if phase == &"apply": outcome_count[&"apply"] = int(outcome_count[&"apply"]) + 1 return {&"ok": true, &"token": token} ) _prepare_basic(manual, &"manual") _must(manual.queue_active_skill(&"manual", &"unused_skill", &"guard", &"RALLY", {&"potency": 2}), "manual retreat fixture queues an unused skill") var retreated = manual.retreat_defenders(&"manual", &"manual_resolution", 3) var replay = manual.retreat_defenders(&"manual", &"manual_resolution", 99) _expect(retreated.is_success() and retreated.details[&"result"] == &"DEFENDERS_RETREATED", "manual retreat reaches its terminal state") _expect(replay.is_success() and replay.is_replay and replay.details == retreated.details, "manual retreat resolution ID replays exact stored outcome") _expect(outcome_count[&"apply"] == 1, "manual retreat consequence applies once across replay") _expect(retreated.details[&"retreated_defender_ids"] == [&"guard"] and retreated.details[&"retreated_attacker_ids"].is_empty(), "outcome separates defender and attacker retreat IDs for reconciliation") _expect(manual.get_raid(&"manual")[&"waves"][0][&"state"] == &"RETREATED", "manual retreat updates current wave state") _expect(manual.get_raid(&"manual")[&"queued_skills"].is_empty(), "manual retreat discards skills that never crossed the apply boundary") _expect(manual.validate_invariants()[&"ok"], "all retreat policy outcomes preserve invariants") func _test_cancel_transaction_and_reconciliation() -> void: var boundary_state := {&"apply_attempts": 0, &"rollbacks": 0, &"last_payload": {}} var resolved_signals := {&"count": 0, &"payload": {}} var outcome_boundary := func(payload: Dictionary, phase: StringName, token: Variant) -> Dictionary: if phase == &"apply": boundary_state[&"apply_attempts"] = int(boundary_state[&"apply_attempts"]) + 1 boundary_state[&"last_payload"] = payload.duplicate(true) if int(boundary_state[&"apply_attempts"]) == 1: return {&"ok": false, &"token": &"cancel_attempt"} elif phase == &"rollback": boundary_state[&"rollbacks"] = int(boundary_state[&"rollbacks"]) + 1 return {&"ok": true, &"token": token} var service := DefenseServiceType.new().configure_callbacks( _fixed_roll(50), Callable(), outcome_boundary ) service.raid_resolved.connect(func(_raid_id: StringName, payload: Dictionary) -> void: resolved_signals[&"count"] = int(resolved_signals[&"count"]) + 1 resolved_signals[&"payload"] = payload.duplicate(true) ) _must(service.create_raid(&"cancel_tx", 0, _definition({ &"waves": [{&"wave_id": &"wave_1", &"threat_strength": 100, &"retreat_threshold": 0, &"max_rounds": 10}], })), "cancel transaction Raid is created") _must(service.register_combatant(&"cancel_tx", &"guard", &"DEFENDER", 10, 100), "cancel transaction guard is registered") _must(service.register_asset(&"cancel_tx", &"core", &"OBJECTIVE", 1_000, 0), "cancel transaction objective is registered") _must(service.register_asset(&"cancel_tx", &"door", &"DOOR", 30, 0), "cancel transaction door is registered") _must(service.start_raid(&"cancel_tx", 0), "cancel transaction Raid starts") _must(service.resolve_next_round(&"cancel_tx", &"damage_round", 5), "cancel transaction accumulates one round of damage") _must(service.queue_active_skill(&"cancel_tx", &"pending_on_cancel", &"guard", &"RALLY", { &"potency": 3, &"metadata": {&"source": "cancel-test"}, }), "cancel transaction retains a pending skill before cancellation") var before_cancel := service.create_snapshot() var rejected = service.cancel_raid(&"cancel_tx", 6, &"PLAYER_ABORT", &"cancel_resolution") _expect(not rejected.is_success(), "cancel outcome rejection aborts cancellation") _expect(service.create_snapshot() == before_cancel, "rejected cancellation preserves accumulated Raid state atomically") _expect(boundary_state[&"apply_attempts"] == 1 and boundary_state[&"rollbacks"] == 1, "failed cancel apply invokes outcome rollback") _expect(resolved_signals[&"count"] == 0, "rejected cancellation emits no raid_resolved signal") var cancelled = service.cancel_raid(&"cancel_tx", 6, &"PLAYER_ABORT", &"cancel_resolution") _expect(cancelled.is_success() and cancelled.details[&"result"] == &"CANCELLED", "accepted cancellation reaches CANCELLED through the outcome boundary") _expect(cancelled.details[&"operation_kind"] == &"CANCEL", "cancel result stores its operation namespace") _expect(cancelled.details[&"asset_results"] == boundary_state[&"last_payload"][&"asset_results"], "cancel boundary receives final accumulated asset integrity") _expect(cancelled.details[&"combatant_results"] == boundary_state[&"last_payload"][&"combatant_results"], "cancel boundary receives final accumulated combatant state") _expect(service.get_raid(&"cancel_tx")[&"queued_skills"].is_empty(), "accepted cancel discards skills that never crossed the apply boundary") _expect(resolved_signals[&"count"] == 1 and resolved_signals[&"payload"] == cancelled.details, "accepted cancel emits one exact raid_resolved payload") var replay = service.cancel_raid(&"cancel_tx", 999, &"DIFFERENT_REASON", &"cancel_resolution") _expect(replay.is_success() and replay.is_replay and replay.details == cancelled.details, "cancel resolution replays canonically after terminal state") _expect(boundary_state[&"apply_attempts"] == 2 and resolved_signals[&"count"] == 1, "cancel replay cannot reapply consequences or signals") _expect(service.validate_invariants()[&"ok"], "transactional cancellation satisfies invariants") func _test_resolution_operation_namespace_and_legacy_v1() -> void: var round_service := DefenseServiceType.new().configure_callbacks(_fixed_roll(50)) _prepare_basic(round_service, &"namespace") var round_result = round_service.resolve_next_round(&"namespace", &"shared_id", 5) _expect(round_result.is_success() and round_result.details[&"operation_kind"] == &"ROUND", "round history stores ROUND operation kind") var after_round := round_service.create_snapshot() var wrong_retreat = round_service.retreat_defenders(&"namespace", &"shared_id", 6) _expect(not wrong_retreat.is_success() and not wrong_retreat.is_replay, "round resolution ID cannot replay as manual retreat") _expect(round_service.create_snapshot() == after_round, "cross-operation retreat collision is atomic") var wrong_cancel = round_service.cancel_raid(&"namespace", 6, &"CANCELLED", &"shared_id") _expect(not wrong_cancel.is_success() and not wrong_cancel.is_replay, "round resolution ID cannot replay as cancellation") _expect(round_service.create_snapshot() == after_round, "cross-operation cancel collision is atomic") var retreat_service := DefenseServiceType.new() _prepare_basic(retreat_service, &"retreat_namespace") var retreat = retreat_service.retreat_defenders(&"retreat_namespace", &"terminal_id", 3) _expect(retreat.is_success() and retreat.details[&"operation_kind"] == &"RETREAT", "manual retreat history stores RETREAT operation kind") var wrong_round = retreat_service.resolve_next_round(&"retreat_namespace", &"terminal_id", 500) _expect(not wrong_round.is_success() and not wrong_round.is_replay, "retreat resolution ID cannot replay as a round") var legacy_round_snapshot := after_round.duplicate(true) legacy_round_snapshot[&"raids"][0][&"resolved_rounds"][0][&"payload"].erase("operation_kind") var legacy_round := DefenseServiceType.new() _expect(legacy_round.restore_snapshot(legacy_round_snapshot), "version-1 round snapshot without operation_kind restores") _expect(legacy_round.create_snapshot() == legacy_round_snapshot, "legacy round snapshot round-trips without forced schema rewrite") var legacy_round_replay = legacy_round.resolve_next_round(&"namespace", &"shared_id", 999) _expect(legacy_round_replay.is_success() and legacy_round_replay.is_replay, "legacy round operation kind is inferred for exact replay") _expect(not legacy_round.retreat_defenders(&"namespace", &"shared_id", 999).is_success(), "legacy inferred round still rejects cross-operation replay") var legacy_retreat_snapshot := retreat_service.create_snapshot() legacy_retreat_snapshot[&"raids"][0][&"resolved_rounds"][0][&"payload"].erase("operation_kind") var legacy_retreat := DefenseServiceType.new() _expect(legacy_retreat.restore_snapshot(legacy_retreat_snapshot), "version-1 retreat snapshot without operation_kind restores") var legacy_retreat_replay = legacy_retreat.retreat_defenders(&"retreat_namespace", &"terminal_id", 999) _expect(legacy_retreat_replay.is_success() and legacy_retreat_replay.is_replay, "legacy retreat operation kind is inferred for exact replay") var cancel_service := DefenseServiceType.new() _prepare_basic(cancel_service, &"cancel_namespace") _must(cancel_service.cancel_raid(&"cancel_namespace", 3, &"LEGACY_CANCEL", &"cancel_terminal_id"), "legacy cancel fixture reaches its terminal state") var legacy_cancel_snapshot := cancel_service.create_snapshot() legacy_cancel_snapshot[&"raids"][0][&"resolved_rounds"][0][&"payload"].erase("operation_kind") var legacy_cancel := DefenseServiceType.new() _expect(legacy_cancel.restore_snapshot(legacy_cancel_snapshot), "version-1 cancel snapshot without operation_kind restores") _expect(legacy_cancel.create_snapshot() == legacy_cancel_snapshot, "legacy cancel snapshot round-trips without forced schema rewrite") var legacy_cancel_replay = legacy_cancel.cancel_raid(&"cancel_namespace", 999, &"DIFFERENT", &"cancel_terminal_id") _expect(legacy_cancel_replay.is_success() and legacy_cancel_replay.is_replay, "legacy cancel operation kind is inferred for exact replay") _expect(not legacy_cancel.retreat_defenders(&"cancel_namespace", &"cancel_terminal_id", 999).is_success(), "legacy inferred cancel rejects cross-operation replay") _expect(round_service.validate_invariants()[&"ok"] and retreat_service.validate_invariants()[&"ok"] and cancel_service.validate_invariants()[&"ok"], "operation namespaces preserve valid aggregates") func _test_multiple_waves_require_explicit_transition() -> void: var service := DefenseServiceType.new().configure_callbacks(_fixed_roll(50)) _must(service.create_raid(&"multi", 0, _definition({ &"waves": [ {&"wave_id": &"wave_1", &"threat_strength": 100, &"retreat_threshold": 0, &"max_rounds": 1}, {&"wave_id": &"wave_2", &"threat_strength": 100, &"retreat_threshold": 0, &"max_rounds": 1}, ], })), "two-wave Raid is created") _must(service.register_asset(&"multi", &"core", &"OBJECTIVE", 1_000, 0), "durable multi-wave objective is registered") _must(service.start_raid(&"multi", 0), "multi-wave Raid starts") var wave_one = service.resolve_next_round(&"multi", &"wave_one_round", 5) _expect(wave_one.is_success() and wave_one.details[&"state"] == &"BETWEEN_WAVES", "first max-round wave ends BETWEEN_WAVES") _expect(service.get_threat_summary()[&"between_waves"] == 1 and service.get_threat_summary()[&"active"] == 0, "HUD distinguishes waiting for next wave from combat") var between_snapshot := service.create_snapshot() _expect(not service.resolve_next_round(&"multi", &"illegal_between_round", 6).is_success(), "round cannot resolve while waiting between waves") _expect(service.create_snapshot() == between_snapshot, "failed between-wave round is atomic") _expect(service.get_raid(&"multi")[&"current_wave_index"] == 0, "next wave never starts implicitly") var begun = service.begin_next_wave(&"multi", 7) _expect(begun.is_success() and begun.details[&"wave_id"] == &"wave_2" and begun.details[&"wave_index"] == 1, "explicit command starts exactly the next wave") _expect(service.get_raid(&"multi")[&"waves"][1][&"state"] == &"ACTIVE", "next wave state becomes ACTIVE") _expect(not service.begin_next_wave(&"multi", 7).is_success(), "active wave cannot be skipped") var wave_two = service.resolve_next_round(&"multi", &"wave_two_round", 12) _expect(wave_two.is_success() and wave_two.details[&"state"] == &"DEFENDED", "last wave completion defends the Raid") _expect(service.get_threat_summary()[&"total"] == 0, "defended raid is removed from ongoing HUD threats") var raid := service.get_raid(&"multi") _expect(raid[&"current_round"] == 2 and raid[&"elapsed_minutes"] == 10, "round and elapsed counters span all waves") _expect(raid[&"waves"][0][&"state"] == &"RESOLVED" and raid[&"waves"][1][&"state"] == &"RESOLVED", "each completed wave retains its own state") _expect(not service.begin_next_wave(&"multi", 13).is_success(), "terminal Raid has no phantom next wave") _expect(service.validate_invariants()[&"ok"], "multi-wave terminal state satisfies invariants") func _test_raid_duration_and_wave_round_limits() -> void: var duration := DefenseServiceType.new().configure_callbacks(_fixed_roll(50)) _must(duration.create_raid(&"duration", 0, _definition({ &"max_duration_minutes": 5, &"round_minutes": 5, &"waves": [ {&"wave_id": &"wave_1", &"threat_strength": 100, &"retreat_threshold": 0, &"max_rounds": 10}, {&"wave_id": &"wave_2", &"threat_strength": 100, &"retreat_threshold": 0, &"max_rounds": 10}, ], })), "duration-limited Raid is created") _must(duration.register_asset(&"duration", &"core", &"OBJECTIVE", 1_000, 0), "duration objective is registered") _must(duration.start_raid(&"duration", 0), "duration Raid starts") var duration_result = duration.resolve_next_round(&"duration", &"duration_round", 5) _expect(duration_result.is_success() and duration_result.details[&"state"] == &"DEFENDED", "Raid-wide duration is a hard terminal boundary") _expect(duration_result.details[&"reason"] == &"RAID_DURATION_EXHAUSTED", "duration terminal reason is explicit") _expect(duration.get_raid(&"duration")[&"current_wave_index"] == 0, "duration never opens a later wave") var max_round := DefenseServiceType.new().configure_callbacks(_fixed_roll(50)) _must(max_round.create_raid(&"max_round", 0, _definition({ &"max_duration_minutes": 100, &"waves": [{&"wave_id": &"wave_1", &"threat_strength": 100, &"retreat_threshold": 0, &"max_rounds": 1}], })), "max-round Raid is created") _must(max_round.register_asset(&"max_round", &"core", &"OBJECTIVE", 1_000, 0), "max-round objective is registered") _must(max_round.start_raid(&"max_round", 0), "max-round Raid starts") var round_result = max_round.resolve_next_round(&"max_round", &"max_round_resolution", 5) _expect(round_result.is_success() and round_result.details[&"reason"] == &"WAVE_DURATION_EXHAUSTED", "wave max-round limit has a stable terminal reason") _expect(max_round.get_raid(&"max_round")[&"result"] == &"DEFENDED", "surviving the final wave round limit defends the Raid") _expect(duration.validate_invariants()[&"ok"] and max_round.validate_invariants()[&"ok"], "both duration boundaries remain valid") func _test_round_resolution_replay() -> void: var roll_calls := {&"count": 0} var roll_keys: Array[StringName] = [] var service := DefenseServiceType.new().configure_callbacks( func(key: StringName, _context: Dictionary) -> int: roll_calls[&"count"] = int(roll_calls[&"count"]) + 1 roll_keys.append(key) return 50 ) _prepare_basic(service, &"replay") var first = service.resolve_next_round(&"replay", &"same_resolution", 5) var snapshot_after_first := service.create_snapshot() var replay = service.resolve_next_round(&"replay", &"same_resolution", 500) _expect(first.is_success() and replay.is_success() and replay.is_replay, "duplicate resolution ID returns an idempotent replay") _expect(replay.details == first.details, "resolution replay returns the exact canonical payload") _expect(service.create_snapshot() == snapshot_after_first, "resolution replay mutates no Raid or clock state") _expect(roll_calls[&"count"] == 2, "defense and attack rolls are consumed only on first resolution") _expect(roll_keys == [&"replay:wave_1:1:DEFENSE", &"replay:wave_1:1:ATTACK"], "round roll keys and call order are stable") _expect(service.get_raid(&"replay")[&"current_round"] == 1, "replay cannot advance the round twice") _expect(service.validate_invariants()[&"ok"], "replayed Raid remains valid") var default_a := DefenseServiceType.new() var default_b := DefenseServiceType.new() _prepare_basic(default_a, &"default_roll") _prepare_basic(default_b, &"default_roll") var default_result_a = default_a.resolve_next_round(&"default_roll", &"default_round", 5) var default_result_b = default_b.resolve_next_round(&"default_roll", &"default_round", 5) _expect(default_result_a.details[&"defense_roll"] == default_result_b.details[&"defense_roll"] and default_result_a.details[&"attack_roll"] == default_result_b.details[&"attack_roll"], "built-in fallback rolls are deterministic from stable keys") _expect(default_result_a.details == default_result_b.details and default_a.create_snapshot() == default_b.create_snapshot(), "built-in deterministic roll reproduces the full round") func _test_round_clock_contract() -> void: var service := DefenseServiceType.new().configure_callbacks(_fixed_roll(50)) _prepare_basic(service, &"clock_contract") var initial := service.create_snapshot() var too_early = service.resolve_next_round(&"clock_contract", &"too_early", 4) _expect(not too_early.is_success() and too_early.details[&"earliest_minute"] == 5, "first round waits one full configured interval after start") _expect(service.create_snapshot() == initial, "early first round rejection is atomic") _must(service.resolve_next_round(&"clock_contract", &"round_1", 5), "first round resolves at its exact boundary") var after_first := service.create_snapshot() _expect(not service.resolve_next_round(&"clock_contract", &"same_minute_other_id", 5).is_success(), "different resolution ID cannot create a second round in the same minute") _expect(not service.resolve_next_round(&"clock_contract", &"before_interval", 9).is_success(), "later round cannot violate previous-round interval") _expect(service.create_snapshot() == after_first, "same-minute and interval failures preserve round history") _must(service.resolve_next_round(&"clock_contract", &"round_2", 10), "second round resolves after the full interval") _must(service.advance_time(20), "Defense clock advances independently") var after_clock := service.create_snapshot() _expect(not service.resolve_next_round(&"clock_contract", &"past_global_clock", 19).is_success(), "round cannot be inserted behind the current Defense clock") _expect(service.create_snapshot() == after_clock, "past-global-clock rejection is atomic") _must(service.resolve_next_round(&"clock_contract", &"round_3", 20), "round at current clock succeeds when interval is satisfied") var historical := DefenseServiceType.new() _must(historical.advance_time(10), "historical fixture clock advances") _expect(not historical.create_raid(&"past_create", 9, _definition()).is_success(), "Raid cannot be created in the past of the Defense clock") _must(historical.create_raid(&"future_start", 10, _definition()), "Raid can be created at the current clock") _must(historical.register_asset(&"future_start", &"core", &"OBJECTIVE", 100, 0), "future-start objective is registered") _must(historical.advance_time(15), "clock advances before Raid start") _expect(not historical.start_raid(&"future_start", 14).is_success(), "Raid cannot start behind the current Defense clock") _must(historical.start_raid(&"future_start", 15), "Raid starts at the current Defense clock") _expect(not historical.resolve_next_round(&"future_start", &"early_after_start", 19).is_success(), "first round interval is measured from actual start time") _must(historical.resolve_next_round(&"future_start", &"on_time_after_start", 20), "first round resolves exactly one interval after late start") _expect(service.validate_invariants()[&"ok"] and historical.validate_invariants()[&"ok"], "clock-contract fixtures satisfy invariants") func _test_wave_entry_and_metadata_validation() -> void: var entry_service := DefenseServiceType.new() _must(entry_service.create_raid(&"entry_scope", 0, _definition({ &"entry_points": [&"east", &"west"], &"waves": [{ &"wave_id": &"east_wave", &"threat_strength": 20, &"entry_points": [&"east"], &"objective_ids": [&"core"], }], })), "entry-scope Raid is created") _expect(not entry_service.register_combatant(&"entry_scope", &"wrong_gate", &"ATTACKER", 10, 10, { &"wave_id": &"east_wave", &"entry_point_id": &"west", }).is_success(), "attacker cannot use a global entry omitted by its assigned wave") _expect(entry_service.register_combatant(&"entry_scope", &"east_raider", &"ATTACKER", 10, 10, { &"wave_id": &"east_wave", &"entry_point_id": &"east", }).is_success(), "attacker can use an entry included by its assigned wave") var metadata_service := DefenseServiceType.new() _prepare_basic(metadata_service, &"metadata") var metadata_before := metadata_service.create_snapshot() var invalid_metadata: Array = [ {&"label": "non-dictionary root", &"value": 7}, {&"label": "Object", &"value": {"bad": RefCounted.new()}}, {&"label": "Callable", &"value": {"bad": Callable(self, "_run")}}, {&"label": "RID", &"value": {"bad": RID()}}, {&"label": "NaN", &"value": {"bad": NAN}}, {&"label": "positive infinity", &"value": {"nested": [1, INF]}}, {&"label": "non-string dictionary key", &"value": {1: "bad"}}, ] for fixture: Dictionary in invalid_metadata: var result = metadata_service.queue_active_skill(&"metadata", &"invalid_meta", &"guard", &"RALLY", { &"potency": 1, &"metadata": fixture[&"value"], }) _expect(not result.is_success(), "skill metadata rejects %s" % fixture[&"label"]) _expect(metadata_service.create_snapshot() == metadata_before, "rejected %s metadata is atomic" % fixture[&"label"]) var metadata_a := {"zeta": [&"name", 2, true, null], "alpha": {"b": 2.5, "a": "first"}} var metadata_b := {"alpha": {"a": "first", "b": 2.5}, "zeta": [&"name", 2, true, null]} _must(metadata_service.queue_active_skill(&"metadata", &"canonical", &"guard", &"RALLY", { &"potency": 1, &"metadata": metadata_a, }), "nested saveable metadata is accepted") var stored_metadata: Dictionary = metadata_service.get_raid(&"metadata")[&"queued_skills"][&"canonical"][&"metadata"] _expect(stored_metadata.keys() == ["alpha", "zeta"], "metadata dictionary keys are stored in lexical order") _expect(stored_metadata["zeta"][0] == "name" and stored_metadata["alpha"].keys() == ["a", "b"], "metadata recursively canonicalizes StringName values and nested keys") var metadata_peer := DefenseServiceType.new() _prepare_basic(metadata_peer, &"metadata") _must(metadata_peer.queue_active_skill(&"metadata", &"canonical", &"guard", &"RALLY", { &"potency": 1, &"metadata": metadata_b, }), "reverse-order metadata fixture is accepted") _expect(metadata_service.create_snapshot() == metadata_peer.create_snapshot(), "metadata snapshot is independent of dictionary insertion order") var corrupt_metadata := metadata_service.create_snapshot() corrupt_metadata[&"raids"][0][&"queued_skills"][0][&"metadata"] = {"bad": NAN} var restored := DefenseServiceType.new() _prepare_basic(restored, &"restored_baseline") var restored_before := restored.create_snapshot() _expect(not restored.restore_snapshot(corrupt_metadata), "snapshot restore rejects non-finite queued metadata") _expect(restored.create_snapshot() == restored_before, "corrupt metadata snapshot rejection is atomic") _expect(metadata_service.validate_invariants()[&"ok"] and metadata_peer.validate_invariants()[&"ok"], "canonical metadata fixtures satisfy invariants") func _test_armor_pool_accounting() -> void: var blocked := DefenseServiceType.new().configure_callbacks(_fixed_roll(50)) _must(blocked.create_raid(&"armor_block", 0, _definition({ &"waves": [{&"wave_id": &"wave_1", &"threat_strength": 20, &"retreat_threshold": 0, &"max_rounds": 10}], })), "armor-block Raid is created") _must(blocked.register_combatant(&"armor_block", &"guard", &"DEFENDER", 100, 100), "armor-block guard is registered") _must(blocked.register_combatant(&"armor_block", &"armored_raider", &"ATTACKER", 1, 100, { &"wave_id": &"wave_1", &"armor": 100, }), "heavily armored attacker is registered") _must(blocked.register_asset(&"armor_block", &"core", &"OBJECTIVE", 1_000, 0), "armor-block objective is registered") _must(blocked.start_raid(&"armor_block", 0), "armor-block Raid starts") var blocked_round = blocked.resolve_next_round(&"armor_block", &"blocked_round", 5) _expect(blocked_round.is_success() and blocked_round.details[&"damage_to_threat"] > 0, "armored target receives a positive raw damage budget") _expect(blocked_round.details[&"damage_applied_to_threat"] == 0, "armor uses max(0, damage - armor) without forced chip damage") _expect(blocked_round.details[&"aggregate_damage"] == 0, "damage absorbed by explicit armor cannot leak into aggregate threat") _expect(blocked_round.details[&"damage_mitigated_by_attackers"] == blocked_round.details[&"damage_to_threat"], "mitigation accounts for the entire blocked damage pool") _expect(blocked.get_raid(&"armor_block")[&"waves"][0][&"remaining_threat"] == 20 and blocked.get_raid(&"armor_block")[&"combatants"][&"armored_raider"][&"health"] == 100, "full armor block changes neither explicit nor aggregate durability") var overflow := DefenseServiceType.new().configure_callbacks(_fixed_roll(50)) _must(overflow.create_raid(&"armor_overflow", 0, _definition({ &"waves": [{&"wave_id": &"wave_1", &"threat_strength": 10, &"retreat_threshold": 0, &"max_rounds": 10}], })), "armor-overflow Raid is created") _must(overflow.register_combatant(&"armor_overflow", &"guard", &"DEFENDER", 200, 100), "armor-overflow guard is registered") _must(overflow.register_combatant(&"armor_overflow", &"front_raider", &"ATTACKER", 2, 2, { &"wave_id": &"wave_1", &"armor": 5, }), "front armored attacker is registered") _must(overflow.register_asset(&"armor_overflow", &"core", &"OBJECTIVE", 1_000, 0), "armor-overflow objective is registered") _must(overflow.start_raid(&"armor_overflow", 0), "armor-overflow Raid starts") var overflow_round = overflow.resolve_next_round(&"armor_overflow", &"overflow_round", 5) _expect(overflow_round.details[&"damage_to_threat"] == 12, "raw damage is capped by combined explicit and aggregate durability") _expect(overflow_round.details[&"attacker_damage"][0][&"damage"] == 2 and overflow_round.details[&"attacker_damage"][0][&"mitigated"] == 5, "front attacker consumes armor plus remaining health from the pool") _expect(overflow_round.details[&"aggregate_damage"] == 5, "only true raw overflow reaches aggregate threat") _expect(overflow_round.details[&"damage_applied_to_threat"] == 7 and overflow_round.details[&"damage_mitigated_by_attackers"] == 5, "applied plus mitigated damage reconciles the complete pool") _expect(overflow.get_raid(&"armor_overflow")[&"waves"][0][&"remaining_threat"] == 5, "aggregate durability decreases by overflow exactly once") var defended := DefenseServiceType.new().configure_callbacks(_fixed_roll(50)) _must(defended.create_raid(&"defense_armor", 0, _definition({ &"waves": [{&"wave_id": &"wave_1", &"threat_strength": 100, &"retreat_threshold": 0, &"max_rounds": 10}], })), "defense-armor Raid is created") _must(defended.register_asset(&"defense_armor", &"core", &"OBJECTIVE", 1_000, 0), "defense-armor objective is registered") _must(defended.register_asset(&"defense_armor", &"door", &"DOOR", 100, 0, {&"armor": 100}), "fully armored defense door is registered") _must(defended.start_raid(&"defense_armor", 0), "defense-armor Raid starts") var defense_round = defended.resolve_next_round(&"defense_armor", &"defense_armor_round", 5) _expect(defense_round.details[&"damage_to_defense"] > 0 and defense_round.details[&"defense_damage"][&"damage"] == 0, "defense armor also applies max(0, damage - armor)") _expect(defense_round.details[&"defense_damage"][&"mitigated"] == defense_round.details[&"damage_to_defense"] and defended.get_raid(&"defense_armor")[&"assets"][&"door"][&"integrity"] == 100, "fully mitigated defense damage leaves integrity unchanged") _expect(blocked.validate_invariants()[&"ok"] and overflow.validate_invariants()[&"ok"] and defended.validate_invariants()[&"ok"], "armor pool fixtures satisfy invariants") func _test_snapshot_round_trip_and_corrupt_atomicity() -> void: var source := DefenseServiceType.new().configure_callbacks(_fixed_roll(50)) _must(source.create_raid(&"snapshot_raid", 0, _definition({ &"entry_points": [&"west", &"east"], &"waves": [ {&"wave_id": &"wave_1", &"threat_strength": 60, &"entry_points": [&"east", &"west"], &"max_rounds": 10}, {&"wave_id": &"wave_2", &"threat_strength": 30, &"entry_points": [&"west"], &"max_rounds": 10}, ], })), "snapshot Raid is created") _must(source.register_combatant(&"snapshot_raid", &"guard_b", &"DEFENDER", 20, 100), "snapshot defender B is registered") _must(source.register_combatant(&"snapshot_raid", &"guard_a", &"DEFENDER", 10, 80), "snapshot defender A is registered") _must(source.register_combatant(&"snapshot_raid", &"raider", &"ATTACKER", 15, 30, {&"wave_id": &"wave_1", &"entry_point_id": &"east"}), "snapshot attacker is registered") _must(source.register_asset(&"snapshot_raid", &"core", &"OBJECTIVE", 200, 20), "snapshot objective is registered") _must(source.register_asset(&"snapshot_raid", &"door", &"DOOR", 50, 10, {&"entry_points": [&"east"]}), "snapshot door is registered") _must(source.start_raid(&"snapshot_raid", 0), "snapshot Raid starts") _must(source.queue_active_skill(&"snapshot_raid", &"applied", &"guard_a", &"RALLY", {&"potency": 4, &"metadata": {&"source": "test"}}), "snapshot applied skill is queued") _must(source.resolve_next_round(&"snapshot_raid", &"snapshot_round", 5), "snapshot round is resolved") _must(source.queue_active_skill(&"snapshot_raid", &"queued", &"guard_b", &"POWER_STRIKE", {&"potency": 2, &"metadata": {&"source": "test"}}), "snapshot pending skill is queued") var snapshot := source.create_snapshot() var restored := DefenseServiceType.new() _expect(restored.restore_snapshot(snapshot), "valid Defense snapshot restores") _expect(restored.create_snapshot() == snapshot, "Defense snapshot has an exact round trip") _expect(restored.validate_invariants()[&"ok"], "restored Defense snapshot satisfies invariants") _expect(restored.get_raid_ids() == [&"snapshot_raid"] and restored.get_active_raid_ids() == [&"snapshot_raid"], "restored indexes expose canonical Raid IDs") var restored_replay = restored.resolve_next_round(&"snapshot_raid", &"snapshot_round", 999) _expect(restored_replay.is_success() and restored_replay.is_replay and restored.get_raid(&"snapshot_raid")[&"current_round"] == 1, "restored resolution history remains idempotent") _expect(JSON.stringify(restored_replay.details) == JSON.stringify(snapshot[&"raids"][0][&"resolved_rounds"][0][&"payload"]), "restored replay returns its exact canonical payload") var json_snapshot: Variant = JSON.parse_string(JSON.stringify(snapshot)) var json_restored := DefenseServiceType.new() _expect(json_snapshot is Dictionary and json_restored.restore_snapshot(json_snapshot), "JSON-serialized Defense snapshot restores") var expected_json := JSON.stringify(snapshot) var actual_json := JSON.stringify(json_restored.create_snapshot()) _expect(actual_json == expected_json, "JSON transport preserves the canonical Defense snapshot") # Each corrupt restore is attempted against a non-empty live aggregate. A # rejection must preserve the previously restored snapshot byte-for-value. var corruptions: Array[Dictionary] = [] var duplicate := snapshot.duplicate(true) duplicate[&"raids"].append(duplicate[&"raids"][0].duplicate(true)) corruptions.append({&"name": "duplicate Raid ID", &"snapshot": duplicate}) var bad_remaining := snapshot.duplicate(true) bad_remaining[&"raids"][0][&"waves"][0][&"remaining_threat"] = 9_999 corruptions.append({&"name": "remaining threat above maximum", &"snapshot": bad_remaining}) var bad_wave_state := snapshot.duplicate(true) bad_wave_state[&"raids"][0][&"waves"][0][&"state"] = "TELEPORTING" corruptions.append({&"name": "unknown wave state", &"snapshot": bad_wave_state}) var bad_clock := snapshot.duplicate(true) bad_clock[&"current_minute"] = -1 corruptions.append({&"name": "negative Defense clock", &"snapshot": bad_clock}) var future_clock := snapshot.duplicate(true) future_clock[&"current_minute"] = 0 future_clock[&"raids"][0][&"state_changed_at"] = 1 corruptions.append({&"name": "Raid timestamp beyond clock", &"snapshot": future_clock}) var bad_health := snapshot.duplicate(true) bad_health[&"raids"][0][&"combatants"][0][&"health"] = -1 corruptions.append({&"name": "negative combatant health", &"snapshot": bad_health}) var bad_asset_entry := snapshot.duplicate(true) bad_asset_entry[&"raids"][0][&"assets"][0][&"entry_points"] = ["not_declared"] corruptions.append({&"name": "unknown asset entry", &"snapshot": bad_asset_entry}) var bad_attacker_wave_entry := snapshot.duplicate(true) bad_attacker_wave_entry[&"raids"][0][&"waves"][0][&"entry_points"] = ["west"] corruptions.append({ &"name": "attacker entry omitted by assigned wave", &"snapshot": bad_attacker_wave_entry, }) var bad_skill := snapshot.duplicate(true) bad_skill[&"raids"][0][&"queued_skills"][0][&"potency"] = 101 corruptions.append({&"name": "invalid queued skill potency", &"snapshot": bad_skill}) for corruption: Dictionary in corruptions: var before := restored.create_snapshot() _expect(not restored.restore_snapshot(corruption[&"snapshot"]), "corrupt snapshot rejects %s" % corruption[&"name"]) _expect(restored.create_snapshot() == before, "rejected %s is atomic" % corruption[&"name"]) _expect(not restored.last_error.is_empty(), "corrupt restore exposes a diagnostic error") _expect(restored.validate_invariants()[&"ok"], "all corrupt snapshot attempts preserve live invariants") func _test_cross_time_snapshot_rejection() -> void: var source := DefenseServiceType.new().configure_callbacks(_fixed_roll(50)) _prepare_basic(source, &"time_snapshot") _must(source.resolve_next_round(&"time_snapshot", &"time_round_1", 5), "time snapshot first round resolves") _must(source.resolve_next_round(&"time_snapshot", &"time_round_2", 10), "time snapshot second round resolves") var valid := source.create_snapshot() var restored := DefenseServiceType.new() _expect(restored.restore_snapshot(valid), "valid cross-time snapshot restores") var corruptions: Array[Dictionary] = [] var start_after_state := valid.duplicate(true) start_after_state[&"raids"][0][&"started_at"] = 11 corruptions.append({&"name": "start after state change", &"snapshot": start_after_state}) var first_too_early := valid.duplicate(true) first_too_early[&"raids"][0][&"resolved_rounds"][0][&"payload"]["absolute_minute"] = 4 corruptions.append({&"name": "first round before start interval", &"snapshot": first_too_early}) var same_minute_rounds := valid.duplicate(true) same_minute_rounds[&"raids"][0][&"resolved_rounds"][1][&"payload"]["absolute_minute"] = 5 corruptions.append({&"name": "two rounds in one minute", &"snapshot": same_minute_rounds}) var resolution_after_state := valid.duplicate(true) resolution_after_state[&"raids"][0][&"resolved_rounds"][1][&"payload"]["absolute_minute"] = 11 corruptions.append({&"name": "resolution after state change", &"snapshot": resolution_after_state}) var missing_history := valid.duplicate(true) missing_history[&"raids"][0][&"resolved_rounds"].remove_at(0) corruptions.append({&"name": "missing round history", &"snapshot": missing_history}) var invalid_operation := valid.duplicate(true) invalid_operation[&"raids"][0][&"resolved_rounds"][0][&"payload"]["operation_kind"] = "DUEL" corruptions.append({&"name": "invalid explicit operation kind", &"snapshot": invalid_operation}) var clock_before_state := valid.duplicate(true) clock_before_state[&"current_minute"] = 9 corruptions.append({&"name": "clock before latest state", &"snapshot": clock_before_state}) var live_finished := valid.duplicate(true) live_finished[&"raids"][0][&"finished_at"] = 10 corruptions.append({&"name": "live Raid with finish time", &"snapshot": live_finished}) for corruption: Dictionary in corruptions: var before := restored.create_snapshot() _expect(not restored.restore_snapshot(corruption[&"snapshot"]), "cross-time snapshot rejects %s" % corruption[&"name"]) _expect(restored.create_snapshot() == before, "rejected %s remains atomic" % corruption[&"name"]) var terminal := DefenseServiceType.new() _prepare_basic(terminal, &"terminal_time") _must(terminal.cancel_raid(&"terminal_time", 2, &"TEST_CANCEL", &"terminal_cancel"), "terminal time fixture cancels") var bad_finish := terminal.create_snapshot() bad_finish[&"raids"][0][&"finished_at"] = 1 var before_terminal_restore := restored.create_snapshot() _expect(not restored.restore_snapshot(bad_finish), "terminal snapshot rejects finish time different from state change") _expect(restored.create_snapshot() == before_terminal_restore, "terminal finish-time rejection is atomic") var bad_terminal_resolution_time := terminal.create_snapshot() bad_terminal_resolution_time[&"raids"][0][&"resolved_rounds"][0][&"payload"]["absolute_minute"] = 1 _expect(not restored.restore_snapshot(bad_terminal_resolution_time), "terminal command resolution must occur at the Raid finish time") _expect(restored.create_snapshot() == before_terminal_restore, "terminal resolution-time rejection is atomic") var wrong_terminal_operation := terminal.create_snapshot() wrong_terminal_operation[&"raids"][0][&"resolved_rounds"][0][&"payload"]["operation_kind"] = "RETREAT" _expect(not restored.restore_snapshot(wrong_terminal_operation), "terminal operation kind must agree with its result and Raid state") _expect(restored.create_snapshot() == before_terminal_restore, "terminal operation mismatch rejection is atomic") _expect(restored.validate_invariants()[&"ok"], "cross-time corruptions preserve restored invariants") func _test_input_order_determinism() -> void: var forward := _ordered_fixture(false) var reverse := _ordered_fixture(true) _expect(forward.create_snapshot() == reverse.create_snapshot(), "canonical snapshot is independent of registration and collection input order") var forward_round = forward.resolve_next_round(&"ordered", &"ordered_round", 5) var reverse_round = reverse.resolve_next_round(&"ordered", &"ordered_round", 5) _expect(forward_round.is_success() and reverse_round.is_success(), "both input-order variants resolve") _expect(forward_round.details == reverse_round.details, "round payload is input-order deterministic") _expect(forward.create_snapshot() == reverse.create_snapshot(), "post-round aggregate is input-order deterministic") _expect(forward_round.details[&"defense_damage"][&"target_id"] == &"a_door", "equal-priority asset targeting uses lexical stable ID") _expect(forward_round.details[&"attacker_damage"][0][&"target_id"] == &"a_attacker", "explicit attacker damage uses lexical stable ID") _expect(forward.validate_invariants()[&"ok"] and reverse.validate_invariants()[&"ok"], "both deterministic orderings preserve invariants") func _definition(overrides: Dictionary = {}) -> Dictionary: var definition := { &"source_id": &"fixture", &"entry_points": [&"gate"], &"objective_ids": [&"core"], &"waves": [{ &"wave_id": &"wave_1", &"threat_strength": 30, &"retreat_threshold": 0, &"max_rounds": 12, }], &"max_duration_minutes": 120, &"round_minutes": 5, &"retreat_policy": &"HOLD", &"defender_retreat_threshold": 20, &"terrain_bonus": 0, &"command_bonus": 0, &"surprise_penalty": 0, &"multiple_entry_penalty": 5, } for key: Variant in overrides.keys(): definition[key] = overrides[key] return definition func _fixed_roll(value: int) -> Callable: return func(_key: StringName, _context: Dictionary) -> int: return value func _prepare_basic(service: DefenseService, raid_id: StringName) -> void: _must(service.create_raid(raid_id, 0, _definition({ &"waves": [{&"wave_id": &"wave_1", &"threat_strength": 100, &"retreat_threshold": 0, &"max_rounds": 10}], })), "basic Raid is created: %s" % raid_id) _must(service.register_combatant(raid_id, &"guard", &"DEFENDER", 20, 100), "basic guard is registered: %s" % raid_id) _must(service.register_asset(raid_id, &"core", &"OBJECTIVE", 1_000, 10), "basic objective is registered: %s" % raid_id) _must(service.start_raid(raid_id, 0), "basic Raid starts: %s" % raid_id) func _attack_mix_service(raid_id: StringName, include_explicit: bool) -> DefenseService: var service := DefenseServiceType.new().configure_callbacks(_fixed_roll(50)) var aggregate_strength := 10 if include_explicit else 12 _must(service.create_raid(raid_id, 0, _definition({ &"waves": [{&"wave_id": &"wave_1", &"threat_strength": aggregate_strength, &"retreat_threshold": 0, &"max_rounds": 10}], })), "attack mix Raid is created: %s" % raid_id) _must(service.register_combatant(raid_id, &"guard", &"DEFENDER", 100, 100), "attack mix guard is registered: %s" % raid_id) if include_explicit: _must(service.register_combatant(raid_id, &"visible_raider", &"ATTACKER", 2, 2, {&"wave_id": &"wave_1"}), "visible attacker is registered") _must(service.register_asset(raid_id, &"core", &"OBJECTIVE", 1_000, 0), "attack mix objective is registered: %s" % raid_id) _must(service.start_raid(raid_id, 0), "attack mix Raid starts: %s" % raid_id) return service func _ordered_fixture(reverse: bool) -> DefenseService: var service := DefenseServiceType.new().configure_callbacks(_fixed_roll(50)) var entries: Array[StringName] = [&"east", &"west"] if reverse: entries.reverse() _must(service.create_raid(&"ordered", 0, _definition({ &"entry_points": entries, &"waves": [{ &"wave_id": &"wave_1", &"threat_strength": 40, &"entry_points": entries.duplicate(), &"objective_ids": [&"core"], &"retreat_threshold": 0, &"max_rounds": 10, }], })), "ordered Raid definition is created") var combatants := [ [&"z_guard", &"DEFENDER", 10, 100, {}], [&"a_guard", &"DEFENDER", 20, 100, {}], [&"z_attacker", &"ATTACKER", 5, 20, {&"wave_id": &"wave_1"}], [&"a_attacker", &"ATTACKER", 5, 20, {&"wave_id": &"wave_1"}], ] var assets := [ [&"core", &"OBJECTIVE", 1_000, 10, {}], [&"z_door", &"DOOR", 50, 5, {&"target_order": 10}], [&"a_door", &"DOOR", 50, 5, {&"target_order": 10}], ] if reverse: combatants.reverse() assets.reverse() for row: Array in combatants: _must(service.register_combatant(&"ordered", row[0], row[1], row[2], row[3], row[4]), "ordered combatant registers: %s" % row[0]) for row: Array in assets: _must(service.register_asset(&"ordered", row[0], row[1], row[2], row[3], row[4]), "ordered asset registers: %s" % row[0]) _must(service.start_raid(&"ordered", 0), "ordered Raid starts") return service func _must(result: CommandResult, message: String) -> bool: return _expect(result != null and result.is_success(), message) func _expect(condition: bool, message: String) -> bool: _checks += 1 if condition: return true _failures += 1 push_error(" CHECK FAILED: %s" % message) return false func _test_cancel_incomplete_preparation() -> void: for partial: bool in [false, true]: var service := DefenseServiceType.new() _must(service.create_raid(&"unprepared", 0, _definition({&"objective_ids": [&"core", &"annex"]})), "create raid with incomplete objective registration") if partial: _must(service.register_asset(&"unprepared", &"core", &"OBJECTIVE", 1000, 0), "register only one of two objectives") var before := service.create_snapshot() _expect(not service.start_raid(&"unprepared", 0).is_success() and service.create_snapshot() == before, "incomplete preparation still cannot start combat") var cancelled = service.cancel_raid(&"unprepared", 1, &"PLAYER_ABORT", &"cancel_unprepared") _expect(cancelled.is_success(), "incomplete preparation can be cancelled") if not cancelled.is_success(): continue var raid := service.get_raid(&"unprepared") _expect(raid[&"state"] == DefenseServiceType.RaidState.CANCELLED and raid[&"started_at"] == -1 and raid[&"current_round"] == 0, "preparation cancellation does not invent combat") _expect(raid[&"assets"].size() == (1 if partial else 0), "cancellation does not invent unregistered assets") var saved := service.create_snapshot() var restored := DefenseServiceType.new() _expect(restored.restore_snapshot(JSON.parse_string(JSON.stringify(saved))), "cancelled preparation survives JSON save transport") var replay = restored.cancel_raid(&"unprepared", 99, &"DIFFERENT", &"cancel_unprepared") _expect(replay.is_success() and replay.is_replay and restored.create_snapshot() == saved, "cancel replay after restore preserves state") var corrupt := saved.duplicate(true) corrupt[&"raids"][0][&"started_at"] = 0 _expect(not restored.restore_snapshot(corrupt) and restored.create_snapshot() == saved, "started cancelled raid still requires all objective assets and rejects corruption atomically") _expect(restored.validate_invariants()[&"ok"], "cancelled incomplete preparation remains valid")