extends SceneTree const BattleService = preload("res://fg3/contract2_battle_service.gd") const ExecutionService = preload("res://fg3/contract2_execution_service.gd") const Fixture = preload("res://tests/fg3/contract1_fixture.gd") const SaveV10 = preload("res://fg3/save_v10_service.gd") var failures: Array[String] = [] func _initialize() -> void: call_deferred("run_test") func run_test() -> void: verify_twelve_round_execution() verify_permanent_casualty_and_recovery() verify_replay_and_operation_atomicity() verify_route_and_artifact_counterfactuals() if failures.is_empty(): print("ABG_FG3_CONTRACT2_EXECUTION_OK rounds=12 routes=36 responses=2 state_reversals=2 artifacts=7 nondominant_plans=4 rewards=exact xp=exact history=24 pulse=event_bound casualty=permanent recovery=same_board replay=true duplicate=true save_v10=recovery_roundtrip offline=zero") quit(0) else: for failure in failures: push_error(failure) quit(1) func verify_twelve_round_execution() -> void: var start := Fixture.new().contract2_market_1_snapshot() var service := ExecutionService.new(start) expect(service.valid, "real first-Contract handoff activates Contract 2") if not service.valid: return var expected_coin := int(start.current_run.coin) var contract2_route_ids: Array = [] for market_index in range(1, 13): expect(service.state.current_run.phase == "contract2_market", "Market %d is open" % market_index) expect(service.state.current_run.market_index == market_index, "Market %d index is exact" % market_index) var route: Dictionary = service.state.current_run.market_board.battle_offers[0] var board_hash := String(service.state.current_run.market_board.board_hash) var revision_before := int(service.state.revision) var operation_id := "fg3-contract2-round-%02d-prepare" % market_index var prepared := service.commit_route(operation_id, revision_before, String(route.offer_id), board_hash) expect(prepared.ok and service.state.current_run.phase == "contract2_prepared", "Market %d route prepares" % market_index) var duplicate_prepare := service.commit_route(operation_id, revision_before, String(route.offer_id), board_hash) expect(duplicate_prepare.ok and duplicate_prepare.duplicate, "Market %d prepare retry is inert" % market_index) if not prepared.ok: return var plan_id := String(BattleService.PRIMARY_PLAN[market_index]) var battle := BattleService.new(service.state.snapshot()).run_to_terminal(plan_id) expect(battle.ok and battle.snapshot.outcome == "victory", "Market %d primary response wins" % market_index) if not battle.ok: return expect(event_count(battle.snapshot.event_log, "pulse_linked") == 1, "Market %d emits one disclosed pulse" % market_index) expect(event_count(battle.snapshot.event_log, "plan_response") == 1, "Market %d emits one bound plan response" % market_index) var coin_before := int(service.state.current_run.coin) var settled := service.settle_route( "fg3-contract2-round-%02d-settle" % market_index, service.state.revision, battle.snapshot ) expect(settled.ok, "Market %d settlement applies" % market_index) if not settled.ok: return expected_coin += int(route.reward_coin) contract2_route_ids.append(String(route.route_id)) expect(service.state.current_run.coin == coin_before + int(route.reward_coin), "Market %d pays exact reward" % market_index) expect(settled.receipt.xp_award == int(battle.snapshot.xp_grant), "Market %d pays exact XP" % market_index) expect(service.state.current_run.cleared_rounds == 12 + market_index, "Market %d increments global clears once" % market_index) expect(service.state.current_run.phase == "contract2_boss_muster", "Round 12 opens the second Boss Muster") expect(service.state.current_run.cleared_rounds == 24 and service.state.current_run.route_history.size() == 24, "both Contracts retain 24 route records") expect(service.state.current_run.route_history.slice(12).map(func(row): return String(row.route_id)) == contract2_route_ids, "second Record input retains the twelve exact routes") expect(service.state.current_run.coin == expected_coin, "all ordinary rewards sum exactly") expect(service.state.current_run.boss_muster.boss_id == "BO-R2-02" and service.state.current_run.boss_muster.plan_ids.size() == 2, "Drowned Bell Muster exposes two plans") func verify_permanent_casualty_and_recovery() -> void: var start := Fixture.new().contract2_market_1_snapshot() var service := ExecutionService.new(start) if not service.valid: expect(false, "recovery fixture validates") return var board_before: Dictionary = service.state.current_run.market_board.duplicate(true) var company_before: Array = Array(service.state.current_run.company).duplicate() var coin_before := int(service.state.current_run.coin) var route: Dictionary = service.state.current_run.market_board.battle_offers[2] var primary := String(BattleService.PRIMARY_PLAN[1]) var secondary := String(BattleService.SECONDARY_PLAN[1]) var failing_plan := "interrupt_pulse" if failing_plan in [primary, secondary]: failing_plan = "reorder_line" var prepared := service.commit_route( "fg3-contract2-defeat-prepare", service.state.revision, String(route.offer_id), String(board_before.board_hash) ) expect(prepared.ok, "risk-two recovery route prepares") if not prepared.ok: return var battle := BattleService.new(service.state.snapshot()).run_to_terminal(failing_plan) expect(battle.ok and battle.snapshot.outcome == "defeat" and battle.snapshot.fallen_recruit_ids.size() == 1, "unanswered pulse causes one permanent casualty") if not battle.ok: return var fallen_id := String(battle.snapshot.fallen_recruit_ids[0]) var equipment_id := String(start.current_run.equipped_by_recruit.get(fallen_id, "")) var settled := service.settle_route("fg3-contract2-defeat-settle", service.state.revision, battle.snapshot) expect(settled.ok and service.state.current_run.phase == "contract2_recovery", "defeat enters explicit recovery") expect(fallen_id not in service.state.current_run.company and service.state.current_run.company.size() == company_before.size() - 1, "Fallen Recruit is removed permanently") expect(service.state.current_run.coin == coin_before and service.state.current_run.cleared_rounds == 12, "defeat pays no Coin and no clear") expect(service.state.current_run.market_board == board_before, "defeat preserves the exact disclosed board") if not equipment_id.is_empty(): expect(service.state.current_run.inventory[equipment_id].wearer_id == "", "Fallen Recruit equipment returns to inventory") var recovery_snapshot := service.state.snapshot() var encoded := SaveV10.new().encode_snapshot(recovery_snapshot, 120) var decoded := SaveV10.new().decode_text(String(encoded.text), 120) expect(encoded.ok and decoded.ok and decoded.snapshot == recovery_snapshot, "recovery state roundtrips byte-equivalent") expect(decoded.offline_delta == SaveV10.OFFLINE_ZERO, "recovery reload invents no offline value") var recovery_revision := int(service.state.revision) var recovered := service.recover_route( "fg3-contract2-defeat-recover", recovery_revision, String(battle.snapshot.snapshot_hash), int(route.recovery_coin) ) expect(recovered.ok and service.state.current_run.phase == "contract2_market", "recovery reopens the same Market") expect(service.state.current_run.market_board == board_before, "recovery never rerolls route or pulse facts") expect(fallen_id not in service.state.current_run.company and service.state.current_run.company.size() == company_before.size() - 1, "recovery never resurrects or substitutes") expect(service.state.current_run.coin == coin_before - int(route.recovery_coin), "recovery charges the exact disclosed cost") var after_recovery := service.state.snapshot() var duplicate_recovery := service.recover_route( "fg3-contract2-defeat-recover", recovery_revision, String(battle.snapshot.snapshot_hash), int(route.recovery_coin) ) expect(duplicate_recovery.ok and duplicate_recovery.duplicate and service.state.snapshot() == after_recovery, "recovery retry cannot charge twice") func verify_replay_and_operation_atomicity() -> void: var service := ExecutionService.new(Fixture.new().contract2_market_1_snapshot()) if not service.valid: expect(false, "atomicity fixture validates") return var route: Dictionary = service.state.current_run.market_board.battle_offers[0] var prepared := service.commit_route( "fg3-contract2-atomic-prepare", service.state.revision, String(route.offer_id), String(service.state.current_run.market_board.board_hash) ) expect(prepared.ok, "atomicity route prepares") if not prepared.ok: return var battle := BattleService.new(service.state.snapshot()).run_to_terminal(String(BattleService.PRIMARY_PLAN[1])) var prepared_snapshot := service.state.snapshot() var tampered: Dictionary = battle.snapshot.duplicate(true) tampered.score = int(tampered.score) + 1 var rejected := service.settle_route("fg3-contract2-tampered", service.state.revision, tampered) expect(not rejected.ok and rejected.code == "battle_snapshot_mismatch" and service.state.snapshot() == prepared_snapshot, "tampered replay rejects without mutation") var settle_revision := int(service.state.revision) var settled := service.settle_route("fg3-contract2-atomic-settle", settle_revision, battle.snapshot) expect(settled.ok, "untampered replay settles") var settled_snapshot := service.state.snapshot() var duplicate := service.settle_route("fg3-contract2-atomic-settle", settle_revision, battle.snapshot) expect(duplicate.ok and duplicate.duplicate and service.state.snapshot() == settled_snapshot, "settlement retry cannot duplicate reward or XP") func verify_route_and_artifact_counterfactuals() -> void: var support_artifact := { 1: "AR-011", 2: "AR-012", 3: "AR-010", 4: "AR-009", 5: "AR-011", 6: "AR-010", 7: "AR-012", 8: "AR-011", 9: "AR-012", 10: "AR-012", 11: "AR-009", 12: "AR-009", } var primary_counts := {} var secondary_counts := {} for plan_id in BattleService.PLAN_IDS: primary_counts[plan_id] = 0 secondary_counts[plan_id] = 0 for market_index in range(1, 13): var primary := String(BattleService.PRIMARY_PLAN[market_index]) var secondary := String(BattleService.SECONDARY_PLAN[market_index]) var artifact_id := String(support_artifact[market_index]) primary_counts[primary] += 1 secondary_counts[secondary] += 1 var wrong_plan := "" for plan_id in BattleService.PLAN_IDS: if plan_id not in [primary, secondary]: wrong_plan = String(plan_id) break for risk in range(3): var primary_score := BattleService.route_plan_score( market_index, risk, primary, artifact_id, 3, 1.0 ) var secondary_score := BattleService.route_plan_score( market_index, risk, secondary, artifact_id, 3, 1.0 ) expect(primary_score >= 15, "Market %d risk %d retains a viable primary response" % [market_index, risk]) expect(secondary_score >= 15, "Market %d risk %d gains a viable secondary response in a matching state" % [market_index, risk]) var wrong_score := BattleService.route_plan_score( market_index, 0, wrong_plan, artifact_id, 3, 1.0 ) expect(wrong_score < 15, "Market %d does not reduce all four plans to equivalent wins" % market_index) for plan_id in BattleService.PLAN_IDS: expect(primary_counts[plan_id] > 0 and secondary_counts[plan_id] > 0, "%s changes rank across Markets" % plan_id) for artifact_id in BattleService.ARTIFACT_PLAN_BONUS: var values: Array = [] for plan_id in BattleService.PLAN_IDS: values.append(int(BattleService.ARTIFACT_PLAN_BONUS[artifact_id].get(plan_id, 0))) expect(values.max() > values.min(), "%s changes at least one plan comparison" % artifact_id) var m3_primary := String(BattleService.PRIMARY_PLAN[3]) var m3_secondary := String(BattleService.SECONDARY_PLAN[3]) expect( BattleService.route_plan_score(3, 1, m3_primary, "AR-011", 0, 1.0) > BattleService.route_plan_score(3, 1, m3_secondary, "AR-011", 0, 1.0), "AR-011 favors fortifying the first triple link" ) expect( BattleService.route_plan_score(3, 1, m3_primary, "AR-014", 0, 1.0) < BattleService.route_plan_score(3, 1, m3_secondary, "AR-014", 0, 1.0), "AR-014 reverses the first triple-link plan toward reordering" ) var m11_primary := String(BattleService.PRIMARY_PLAN[11]) var m11_secondary := String(BattleService.SECONDARY_PLAN[11]) expect( BattleService.route_plan_score(11, 1, m11_primary, "AR-011", 0, 1.0) > BattleService.route_plan_score(11, 1, m11_secondary, "AR-011", 0, 1.0), "AR-011 favors durable links late in the Contract" ) expect( BattleService.route_plan_score(11, 1, m11_primary, "AR-009", 0, 1.0) < BattleService.route_plan_score(11, 1, m11_secondary, "AR-009", 0, 1.0), "AR-009 reverses the late link plan toward interruption" ) func event_count(events: Array, kind: String) -> int: var count := 0 for event in events: if String(event.get("kind", "")) == kind: count += 1 return count func expect(condition: bool, message: String) -> void: if not condition: failures.append("FG-3 Contract-2 execution: %s" % message)