extends SceneTree const Adapter = preload("res://campaign/fg3_adapter.gd") const Contract1Battle = preload("res://fg3/contract1_battle_service.gd") const Contract2Battle = preload("res://fg3/contract2_battle_service.gd") const Fixture = preload("res://tests/fg3/contract1_fixture.gd") const SaveV10 = preload("res://fg3/save_v10_service.gd") const PROFILES := [ { "id": "normal", "artifact": "AR-011", "contract1_risks": [0, 1, 0, 1, 0], "contract2_risks": [0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1], "boss1_plan": "follow_index_clear_eggs", "boss2_plan": "interrupt_pulse_family", "decision_seconds": 6.0, }, { "id": "active", "artifact": "AR-012", "contract1_risks": [1, 1, 2, 1, 2], "contract2_risks": [1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1], "boss1_plan": "shift_hatch_area", "boss2_plan": "durable_links_stagger", "decision_seconds": 3.0, }, { "id": "efficient", "artifact": "AR-015", "contract1_risks": [2, 2, 1, 2, 1], "contract2_risks": [2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1], "boss1_plan": "shift_hatch_area", "boss2_plan": "durable_links_stagger", "decision_seconds": 2.0, }, { "id": "adversarial", "artifact": "AR-011", "contract1_risks": [2, 0, 0, 0, 0], "contract2_risks": [2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "boss1_plan": "follow_index_clear_eggs", "boss2_plan": "interrupt_pulse_family", "decision_seconds": 8.0, }, { "id": "returning", "artifact": "AR-015", "contract1_risks": [0, 1, 0, 1, 0], "contract2_risks": [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0], "boss1_plan": "shift_hatch_area", "boss2_plan": "durable_links_stagger", "decision_seconds": 7.0, }, ] var failures: Array[String] = [] func _initialize() -> void: call_deferred("run_test") func run_test() -> void: var results: Array[Dictionary] = [] for profile in PROFILES: results.append(simulate_profile(profile)) verify_results(results) if failures.is_empty(): var total_decisions := 0 var longest_gap := 0.0 var recoveries := 0 var reloads := 0 for result in results: total_decisions += int(result.decision_count) longest_gap = maxf(longest_gap, float(result.max_gap)) recoveries += int(result.recoveries) reloads += int(result.reloads) print("ABG_FG3_FIVE_PROFILE_OK profiles=normal,active,efficient,adversarial,returning core_decisions=%d per_profile=39 longest_gap=%.1f routes=17 bosses=2 records=2 recoveries=%d reloads=%d policy_signatures=5 artifacts=3 route_diversity=17 boss_plan_diversity=2 offline_rewards=0 resurrection=0 terminal=RG-03" % [total_decisions, longest_gap, recoveries, reloads]) quit(0) else: for failure in failures: push_error(failure) quit(1) func simulate_profile(profile: Dictionary) -> Dictionary: var adapter := Adapter.new() var configured := adapter.configure(Fixture.new().fg3_market_8_snapshot(String(profile.artifact))) if not bool(configured.get("ok", false)): return failed_result(String(profile.id), "configure") var labels: Array[String] = [] var timestamps: Array[float] = [] var recovery_labels: Array[String] = [] var recovery_timestamps: Array[float] = [] var reload_timestamps: Array[float] = [] var route_risks: Array[int] = [] var route_ids: Array[String] = [] var plan_ids: Array[String] = [] var clock := 0.0 var automatic_seconds := 0.0 var recoveries := 0 var reloads := 0 var fallen_ids: Array[String] = [] var offline_zero := true var decision_seconds := float(profile.decision_seconds) for offset in range(5): var market_index := 8 + offset var risk := int(profile.contract1_risks[offset]) var route := route_for_risk(adapter.view_state().routes, risk) if route.is_empty(): return failed_result(String(profile.id), "contract1_route_%d" % market_index) clock = record_decision(labels, timestamps, clock, "c1_m%d_route_r%d" % [market_index, risk], decision_seconds) route_risks.append(risk) route_ids.append(String(route.route_id)) var prepared := adapter.prepare_route(String(route.offer_id)) if not bool(prepared.get("ok", false)): return failed_result(String(profile.id), "contract1_prepare_%d" % market_index) var plan_id := String(Contract1Battle.PREFERRED_PLAN[String(route.route_id)]) clock = record_decision(labels, timestamps, clock, "c1_m%d_plan_%s" % [market_index, plan_id], decision_seconds) plan_ids.append(plan_id) var terminal := run_victory(adapter, plan_id) if not bool(terminal.get("ok", false)): return failed_result(String(profile.id), "contract1_battle_%d" % market_index) var duration := battle_seconds(terminal) automatic_seconds += duration clock += duration + 1.0 if not bool(adapter.settle_terminal().get("ok", false)): return failed_result(String(profile.id), "contract1_settle_%d" % market_index) if String(profile.id) == "returning" and market_index == 10: var reloaded := reload_adapter(adapter, 210 + reloads) if not bool(reloaded.get("ok", false)): return failed_result(String(profile.id), "return_market10") adapter = reloaded.adapter offline_zero = offline_zero and bool(reloaded.offline_zero) reloads += 1 clock += 1.5 reload_timestamps.append(clock) clock = record_decision(labels, timestamps, clock, "boss1_plan_%s" % String(profile.boss1_plan), decision_seconds) var boss1 := run_victory(adapter, String(profile.boss1_plan)) if not bool(boss1.get("ok", false)): return failed_result(String(profile.id), "boss1") var boss1_seconds := battle_seconds(boss1) automatic_seconds += boss1_seconds clock += boss1_seconds + 1.0 var boss1_settled := adapter.settle_terminal() if not bool(boss1_settled.get("ok", false)) or String(boss1_settled.view.mode) != "contract_record": return failed_result(String(profile.id), "boss1_settle") clock = record_decision(labels, timestamps, clock, "record1_commit", decision_seconds) var first_record := adapter.continue_result() if not bool(first_record.get("ok", false)) or String(first_record.view.mode) != "contract_handoff": return failed_result(String(profile.id), "record1") clock = record_decision(labels, timestamps, clock, "contract2_handoff", decision_seconds) var handoff := adapter.continue_result() if not bool(handoff.get("ok", false)) or String(handoff.view.contract_id) != "CTR-R2-02": return failed_result(String(profile.id), "handoff") if String(profile.id) == "returning": var handoff_reload := reload_adapter(adapter, 210 + reloads) if not bool(handoff_reload.get("ok", false)): return failed_result(String(profile.id), "return_handoff") adapter = handoff_reload.adapter offline_zero = offline_zero and bool(handoff_reload.offline_zero) reloads += 1 clock += 1.5 reload_timestamps.append(clock) for offset in range(12): var market_index := offset + 1 var risk := int(profile.contract2_risks[offset]) var route := route_for_risk(adapter.view_state().routes, risk) if route.is_empty(): return failed_result(String(profile.id), "contract2_route_%d" % market_index) clock = record_decision(labels, timestamps, clock, "c2_m%d_route_r%d" % [market_index, risk], decision_seconds) route_risks.append(risk) route_ids.append(String(route.route_id)) var prepared := adapter.prepare_route(String(route.offer_id)) if not bool(prepared.get("ok", false)): return failed_result(String(profile.id), "contract2_prepare_%d" % market_index) if String(profile.id) == "adversarial" and market_index == 1: clock = record_recovery(recovery_labels, recovery_timestamps, clock, "wrong_plan_interrupt_pulse", decision_seconds) var wrong_started := adapter.start_plan("interrupt_pulse") if not bool(wrong_started.get("ok", false)): return failed_result(String(profile.id), "adversarial_wrong_start") var wrong_terminal := adapter.run_battle_to_terminal() if not bool(wrong_terminal.get("ok", false)) or String(wrong_terminal.view.battle.outcome) != "defeat": return failed_result(String(profile.id), "adversarial_defeat") var wrong_seconds := battle_seconds(wrong_terminal) automatic_seconds += wrong_seconds clock += wrong_seconds + 1.0 fallen_ids.assign(wrong_terminal.view.battle.fallen_recruit_ids) var failed_settlement := adapter.settle_terminal() if not bool(failed_settlement.get("ok", false)) or String(failed_settlement.view.mode) != "recovery": return failed_result(String(profile.id), "adversarial_settle") clock = record_recovery(recovery_labels, recovery_timestamps, clock, "accept_permanent_loss", decision_seconds) var recovered := adapter.recover() if not bool(recovered.get("ok", false)) or String(recovered.view.mode) != "market": return failed_result(String(profile.id), "adversarial_recover") recoveries += 1 clock = record_recovery(recovery_labels, recovery_timestamps, clock, "retry_same_disclosed_route", decision_seconds) route = route_for_risk(adapter.view_state().routes, risk) prepared = adapter.prepare_route(String(route.offer_id)) if not bool(prepared.get("ok", false)): return failed_result(String(profile.id), "adversarial_retry_prepare") var plan_id := String(Contract2Battle.PRIMARY_PLAN[market_index]) clock = record_decision(labels, timestamps, clock, "c2_m%d_plan_%s" % [market_index, plan_id], decision_seconds) plan_ids.append(plan_id) var terminal := run_victory(adapter, plan_id) if not bool(terminal.get("ok", false)): return failed_result(String(profile.id), "contract2_battle_%d" % market_index) var duration := battle_seconds(terminal) automatic_seconds += duration clock += duration + 1.0 if not bool(adapter.settle_terminal().get("ok", false)): return failed_result(String(profile.id), "contract2_settle_%d" % market_index) if String(profile.id) == "returning" and market_index in [3, 6, 9, 12]: var reloaded := reload_adapter(adapter, 210 + reloads) if not bool(reloaded.get("ok", false)): return failed_result(String(profile.id), "return_contract2_%d" % market_index) adapter = reloaded.adapter offline_zero = offline_zero and bool(reloaded.offline_zero) reloads += 1 clock += 1.5 reload_timestamps.append(clock) clock = record_decision(labels, timestamps, clock, "boss2_plan_%s" % String(profile.boss2_plan), decision_seconds) var boss2 := run_victory(adapter, String(profile.boss2_plan)) if not bool(boss2.get("ok", false)): return failed_result(String(profile.id), "boss2") var boss2_seconds := battle_seconds(boss2) automatic_seconds += boss2_seconds clock += boss2_seconds + 1.0 var boss2_settled := adapter.settle_terminal() if not bool(boss2_settled.get("ok", false)) or String(boss2_settled.view.mode) != "region_record": return failed_result(String(profile.id), "boss2_settle") clock = record_decision(labels, timestamps, clock, "record2_commit", decision_seconds) var closed := adapter.continue_result() if not bool(closed.get("ok", false)) or String(closed.view.mode) != "region_closed": return failed_result(String(profile.id), "region_close") if String(profile.id) == "returning": var terminal_reload := reload_adapter(adapter, 210 + reloads) if not bool(terminal_reload.get("ok", false)): return failed_result(String(profile.id), "return_terminal") adapter = terminal_reload.adapter offline_zero = offline_zero and bool(terminal_reload.offline_zero) reloads += 1 clock += 1.5 reload_timestamps.append(clock) var view := adapter.view_state() var result := { "id": String(profile.id), "failed": false, "artifact": String(profile.artifact), "boss_plans": [String(profile.boss1_plan), String(profile.boss2_plan)], "route_risks": route_risks, "route_ids": route_ids, "plan_ids": plan_ids, "labels": labels, "timestamps": timestamps, "decision_count": timestamps.size(), "first_decision": float(timestamps[0]) if not timestamps.is_empty() else 999.0, "max_gap": maximum_gap(timestamps), "total_seconds": clock, "automatic_seconds": automatic_seconds, "recoveries": recoveries, "recovery_labels": recovery_labels, "recovery_timestamps": recovery_timestamps, "fallen_ids": fallen_ids, "reloads": reloads, "reload_timestamps": reload_timestamps, "offline_zero": offline_zero, "records": int(view.records_count), "unlocked_rg03": view.unlocked_region_ids.count("RG-03"), "phase": String(view.mode), "company_size": view.company.size(), "coin": int(view.coin), } print("FG3_PROFILE_RESULT profile=%s artifact=%s routes=%s bosses=%s decisions=%s recovery=%s reloads=%s first=%.1f max_gap=%.1f auto=%.1f total=%.1f company=%d coin=%d records=%d rg03=%d" % [result.id, result.artifact, result.route_risks, result.boss_plans, format_timestamps(labels, timestamps), format_timestamps(recovery_labels, recovery_timestamps), result.reload_timestamps, result.first_decision, result.max_gap, result.automatic_seconds, result.total_seconds, result.company_size, result.coin, result.records, result.unlocked_rg03]) return result func run_victory(adapter, plan_id: String) -> Dictionary: var started: Dictionary = adapter.start_plan(plan_id) if not bool(started.get("ok", false)): return {"ok": false, "code": String(started.get("code", "start_failed"))} var terminal: Dictionary = adapter.run_battle_to_terminal() if not bool(terminal.get("ok", false)) or String(terminal.view.battle.outcome) != "victory": return {"ok": false, "code": "victory_required", "terminal": terminal} return terminal func reload_adapter(adapter, sequence: int) -> Dictionary: var encoded := SaveV10.new().encode_snapshot(adapter.snapshot(), sequence) if not bool(encoded.get("ok", false)): return {"ok": false, "code": "encode"} var decoded := SaveV10.new().decode_text(String(encoded.text), sequence) if not bool(decoded.get("ok", false)) or decoded.snapshot != adapter.snapshot(): return {"ok": false, "code": "decode"} var replacement := Adapter.new() var configured := replacement.configure(decoded.snapshot) return { "ok": bool(configured.get("ok", false)), "code": String(configured.get("code", "configure")), "adapter": replacement, "offline_zero": decoded.offline_delta == SaveV10.OFFLINE_ZERO, } func route_for_risk(routes: Array, risk: int) -> Dictionary: for route_value in routes: var route: Dictionary = route_value if int(route.get("risk", -1)) == risk: return route return {} func battle_seconds(terminal: Dictionary) -> float: return float(terminal.get("view", {}).get("battle", {}).get("event_count", 0)) * 0.1 func record_decision(labels: Array[String], timestamps: Array[float], clock: float, label: String, dwell: float) -> float: clock += dwell labels.append(label) timestamps.append(clock) return clock func record_recovery(labels: Array[String], timestamps: Array[float], clock: float, label: String, dwell: float) -> float: return record_decision(labels, timestamps, clock, label, dwell) func maximum_gap(timestamps: Array[float]) -> float: var prior := 0.0 var result := 0.0 for timestamp in timestamps: result = maxf(result, float(timestamp) - prior) prior = float(timestamp) return result func format_timestamps(labels: Array[String], timestamps: Array[float]) -> String: var rows: Array[String] = [] for index in range(labels.size()): rows.append("%s@%.1f" % [labels[index], timestamps[index]]) return "[" + ",".join(rows) + "]" func verify_results(results: Array[Dictionary]) -> void: expect(results.size() == 5, "all five profiles return results") var signatures := {} var artifacts := {} var boss1_plans := {} var boss2_plans := {} var risks_by_slot: Array[Dictionary] = [] for _index in range(17): risks_by_slot.append({}) for result in results: expect(not bool(result.get("failed", true)), "%s completes without harness failure" % String(result.get("id", "unknown"))) if bool(result.get("failed", true)): continue expect(int(result.decision_count) == 39, "%s records the exact 39 core decisions" % result.id) expect(float(result.first_decision) <= 20.0, "%s reaches the first decision by 20 seconds" % result.id) expect(float(result.max_gap) <= 45.0, "%s keeps the longest nonbattle decision gap within 45 seconds" % result.id) expect(strictly_increasing(result.timestamps), "%s decision timestamps are strictly increasing" % result.id) expect(result.route_ids.size() == 17 and result.route_risks.size() == 17, "%s exercises all 17 ordinary decisions" % result.id) expect(result.plan_ids.size() == 17 and result.boss_plans.size() == 2, "%s records all ordinary and Boss plans" % result.id) expect(int(result.records) == 2 and int(result.unlocked_rg03) == 1 and String(result.phase) == "region_closed", "%s reaches the exact persistent Region close" % result.id) expect(int(result.coin) >= 0 and int(result.company_size) >= 1, "%s avoids insolvency and total Company loss" % result.id) expect(bool(result.offline_zero), "%s reloads never invent offline value" % result.id) var signature := "%s:%s:%s" % [result.artifact, result.route_risks, result.boss_plans] signatures[signature] = true artifacts[String(result.artifact)] = true boss1_plans[String(result.boss_plans[0])] = true boss2_plans[String(result.boss_plans[1])] = true for index in range(17): risks_by_slot[index][int(result.route_risks[index])] = true if String(result.id) == "adversarial": expect(int(result.recoveries) == 1 and result.fallen_ids.size() == 1, "adversarial play loses one Recruit and actively recovers") expect(result.recovery_labels == ["wrong_plan_interrupt_pulse", "accept_permanent_loss", "retry_same_disclosed_route"], "adversarial recovery names failure, acceptance and retry") expect(strictly_increasing(result.recovery_timestamps), "adversarial recovery timestamps are ordered") if String(result.id) == "returning": expect(int(result.reloads) >= 3 and strictly_increasing(result.reload_timestamps), "returning play reloads at least three stable boundaries") expect(signatures.size() == 5, "five profile policies remain strategically distinct") expect(artifacts.size() == 3, "five profiles cover all three legal inherited Artifact contexts") expect(boss1_plans.size() == 2 and boss2_plans.size() == 2, "both legal Boss responses are exercised") for index in range(risks_by_slot.size()): expect(risks_by_slot[index].size() >= 2, "ordinary decision %d changes route risk across profiles" % (index + 1)) func strictly_increasing(values: Array) -> bool: for index in range(1, values.size()): if float(values[index]) <= float(values[index - 1]): return false return true func failed_result(profile_id: String, code: String) -> Dictionary: print("FG3_PROFILE_FAILURE profile=%s code=%s" % [profile_id, code]) return {"id": profile_id, "failed": true, "code": code} func expect(condition: bool, message: String) -> void: if not condition: failures.append("FG-3 five-profile simulation: %s" % message)