extends SceneTree const Fixture = preload("res://tests/p10_1/first_crossing_fixture.gd") const BattleService = preload("res://p10_1/first_crossing_battle_service.gd") const ResolutionService = preload("res://p10_1/first_crossing_resolution_service.gd") const SaveV7 = preload("res://p10_1/save_v7_service.gd") const PROFILES := [ {"id": "normal", "route": 1, "plan": "hold_line", "times": [0.0, 2.0, 4.0, 6.0, 8.0]}, {"id": "active", "route": 2, "plan": "follow_tide", "times": [0.0, 1.0, 2.0, 3.0, 4.0]}, {"id": "efficient", "route": 0, "plan": "hold_line", "times": [0.0, 1.5, 3.0, 4.5, 6.0]}, {"id": "adversarial", "route": 2, "plan": "hold_line", "times": [0.0, 2.0, 4.0, 6.0, 8.0]}, {"id": "returning", "route": 1, "plan": "follow_tide", "times": [0.0, 3.0, 6.0, 9.0, 11.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) verify_counterfactuals() if failures.is_empty(): var decisions := 0 var longest_gap := 0.0 var recoveries := 0 var reloads := 0 for result in results: decisions += int(result.decision_count) longest_gap = maxf(longest_gap, float(result.max_gap)) recoveries += int(result.recoveries) reloads += int(result.reloads) print("ABG_P10_1_FIVE_PROFILE_OK profiles=normal,active,efficient,adversarial,returning policies=5 risks=0,1,2 plans=hold_line,follow_tide decisions=%d longest_gap=%.1f counterfactual=opposed recoveries=%d reloads=%d starvation=0 resurrection=0 offline_rewards=0" % [decisions, longest_gap, recoveries, reloads]) quit(0) else: for failure in failures: push_error(failure) quit(1) func simulate_profile(profile: Dictionary) -> Dictionary: var prepared: Dictionary = Fixture.new().prepared_snapshot(int(profile.route)) var reloads := 0 if String(profile.id) == "returning": prepared = save_roundtrip(prepared, 40) reloads += 1 var battle_result: Dictionary = BattleService.new(prepared).run_to_terminal(String(profile.plan)) if not battle_result.ok: failures.append("%s battle rejected: %s" % [profile.id, battle_result.code]) return {"id": profile.id, "failed": true} var battle: Dictionary = battle_result.snapshot var resolver = ResolutionService.new(prepared) var settled: Dictionary = resolver.settle("profile-%s-settle" % profile.id, resolver.state.revision, battle) if not settled.ok: failures.append("%s settlement rejected: %s" % [profile.id, settled.code]) return {"id": profile.id, "failed": true} var fallen_ids: Array = Array(battle.fallen_recruit_ids).duplicate() var recoveries := 0 var recovered_ids: Array = [] if String(battle.outcome) == "defeat": var recovered: Dictionary = resolver.recover( "profile-%s-recover" % profile.id, resolver.state.revision, String(battle.snapshot_hash), int(battle.recovery_coin) ) if not recovered.ok: failures.append("%s recovery rejected: %s coin=%d cost=%d" % [profile.id, recovered.code, resolver.state.current_run.coin, battle.recovery_coin]) return {"id": profile.id, "failed": true} recoveries = 1 recovered_ids = Array(resolver.state.current_run.company).duplicate() var final_snapshot: Dictionary = resolver.state.snapshot() if String(profile.id) == "returning": final_snapshot = save_roundtrip(final_snapshot, 41) reloads += 1 var labels: Array[String] = ["inspect", "hire_pair", "equipment", "route", "plan"] var timestamps: Array[float] = [] for value in profile.times: timestamps.append(float(value)) var terminal_time := timestamps[-1] + float(battle.tick) / 10.0 labels.append("battle_result") timestamps.append(terminal_time) labels.append("settle") timestamps.append(terminal_time + 1.0) if recoveries > 0: labels.append("recover") timestamps.append(terminal_time + 2.0) var result := { "id": String(profile.id), "failed": false, "policy": "%d:%s" % [profile.route, profile.plan], "risk": int(battle.risk), "plan": String(battle.plan_id), "outcome": String(battle.outcome), "score": int(battle.score), "total_hp": total_hp(battle), "fallen_ids": fallen_ids, "recovered_ids": recovered_ids, "recoveries": recoveries, "reloads": reloads, "coin": int(final_snapshot.current_run.coin), "phase": String(final_snapshot.current_run.phase), "first_decisive_kind": String(battle.first_decisive_event.kind), "labels": labels, "timestamps": timestamps, "decision_count": timestamps.size(), "max_gap": maximum_gap(timestamps), } print("P101_PROFILE_RESULT profile=%s policy=%s outcome=%s score=%d timestamps=%s max_gap=%.1f coin=%d fallen=%d reloads=%d" % [result.id, result.policy, result.outcome, result.score, format_timestamps(labels, timestamps), result.max_gap, result.coin, fallen_ids.size(), reloads]) return result func verify_results(results: Array[Dictionary]) -> void: expect(results.size() == 5 and results.all(func(result): return not bool(result.get("failed", true))), "all five profiles complete") if results.size() != 5 or results.any(func(result): return bool(result.get("failed", true))): return var policies := {} var decisive_kinds := {} for result in results: policies[String(result.policy)] = true decisive_kinds[String(result.first_decisive_kind)] = true expect(int(result.decision_count) >= 7, "%s records every decision timestamp" % result.id) expect(float(result.max_gap) <= 12.0, "%s maximum no-decision gap is bounded" % result.id) expect(int(result.coin) >= 0, "%s never starves below zero Coin" % result.id) expect(result.labels.size() == result.timestamps.size() and strictly_increasing(result.timestamps), "%s timestamps are complete and strictly increasing" % result.id) expect(policies.size() == 5, "five profiles use five distinct route-plan policies") expect(results.any(func(result): return int(result.risk) == 0) and results.any(func(result): return int(result.risk) == 1) and results.any(func(result): return int(result.risk) == 2), "profiles collectively use every disclosed risk") expect(decisive_kinds.size() >= 3, "profiles expose pull, healing and tide decision causes") var efficient: Dictionary = result_by_id(results, "efficient") var active: Dictionary = result_by_id(results, "active") var adversarial: Dictionary = result_by_id(results, "adversarial") expect(efficient.outcome == "victory" and efficient.plan == "hold_line", "safe-route stable order wins") expect(active.outcome == "victory" and active.plan == "follow_tide", "high-route deliberate movement wins") expect(adversarial.outcome == "defeat" and adversarial.recoveries == 1 and not adversarial.fallen_ids.is_empty(), "wrong high-route response fails and recovers") for fallen_id in adversarial.fallen_ids: expect(fallen_id not in adversarial.recovered_ids, "recovery never resurrects Fallen %s" % fallen_id) var returning: Dictionary = result_by_id(results, "returning") expect(returning.reloads == 2 and returning.outcome == "victory" and returning.phase == "market_2", "returning profile reloads before battle and after settlement") func verify_counterfactuals() -> void: var safe: Dictionary = Fixture.new().prepared_snapshot(0) var safe_hold: Dictionary = BattleService.new(safe).run_to_terminal("hold_line").snapshot var safe_move: Dictionary = BattleService.new(safe).run_to_terminal("follow_tide").snapshot expect(safe_hold.outcome == "victory" and safe_move.outcome == "victory" and total_hp(safe_hold) > total_hp(safe_move), "safe route rewards stable order with more preserved vitality") var high: Dictionary = Fixture.new().prepared_snapshot(2) var high_hold: Dictionary = BattleService.new(high).run_to_terminal("hold_line").snapshot var high_move: Dictionary = BattleService.new(high).run_to_terminal("follow_tide").snapshot expect(high_hold.outcome == "defeat" and high_move.outcome == "victory" and high_move.score > high_hold.score, "high route reverses the plan preference") func save_roundtrip(snapshot: Dictionary, sequence: int) -> Dictionary: var encoded: Dictionary = SaveV7.new().encode_snapshot(snapshot, sequence) if not encoded.ok: failures.append("Save-v7 encode rejected at sequence %d" % sequence) return {} var decoded: Dictionary = SaveV7.new().decode_text(String(encoded.text), sequence) if not decoded.ok or decoded.snapshot != snapshot or decoded.offline_delta != SaveV7.OFFLINE_ZERO: failures.append("Save-v7 roundtrip changed state or offline value at sequence %d" % sequence) return {} return Dictionary(decoded.snapshot).duplicate(true) func total_hp(battle: Dictionary) -> int: var value := 0 for actor in battle.actors.values(): value += int(actor.hp) return value func maximum_gap(timestamps: Array[float]) -> float: var result := 0.0 for index in range(1, timestamps.size()): result = maxf(result, timestamps[index] - timestamps[index - 1]) return result func strictly_increasing(timestamps: Array) -> bool: for index in range(1, timestamps.size()): if float(timestamps[index]) <= float(timestamps[index - 1]): return false return true 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 result_by_id(results: Array[Dictionary], profile_id: String) -> Dictionary: for result in results: if String(result.id) == profile_id: return result return {} func expect(condition: bool, message: String) -> void: if not condition: failures.append("P10.1 five-profile: %s" % message)