Files
aetherbound-guild/runtime/tests/p9_7/three_round_five_profile_test.gd
T

238 lines
13 KiB
GDScript

extends SceneTree
const LegacyService = preload("res://p9_3/core_loop_service.gd")
const LegacyState = preload("res://p9_3/core_loop_state.gd")
const MarketService = preload("res://p9_7/market_service.gd")
const MarketState = preload("res://p9_7/market_state.gd")
const BattleService = preload("res://p9_7/battle_service.gd")
const SaveService = preload("res://p9_7/save_service.gd")
const PROFILES := {
"normal": {
"recruits": ["P97-M5-R2", "P97-M6-R2", "P97-M7-R3"],
"routes": ["P97-R5-Q0", "P97-R6-Q0", "P97-R7-Q0"],
"gear": "EQ-021", "wearer": "P97-M5-R2", "speed": 1,
},
"active": {
"recruits": ["P97-M5-R3", "P97-M6-R1", "P97-M7-R3"],
"routes": ["P97-R5-Q1", "P97-R6-Q1", "P97-R7-Q2"],
"gear": "EQ-036", "wearer": "P97-M5-R3", "speed": 2,
},
"efficient": {
"recruits": ["P97-M5-R3", "P97-M6-R2", "P97-M7-R1"],
"routes": ["P97-R5-Q0", "P97-R6-Q0", "P97-R7-Q1"],
"gear": "", "wearer": "", "speed": 2,
},
"adversarial": {
"recruits": ["P97-M5-R2", "P97-M6-R3", "P97-M7-R1"],
"routes": ["P97-R5-Q0", "P97-R6-Q2", "P97-R7-Q2"],
"gear": "EQ-028", "wearer": "P97-M5-R2", "speed": 1,
},
"returning": {
"recruits": ["P97-M5-R2", "P97-M6-R2", "P97-M7-R3"],
"routes": ["P97-R5-Q1", "P97-R6-Q0", "P97-R7-Q1"],
"gear": "EQ-023", "wearer": "P97-M5-R2", "speed": 1,
},
}
var failures: Array[String] = []
var represented_route_risks: Dictionary = {}
var represented_gear: Dictionary = {}
var longest_gap := 0.0
var timestamp_count := 0
var defeat_recoveries := 0
var interruption_reloads := 0
var invalid_mutations := 0
var save_root := ""
func _initialize() -> void:
call_deferred("run_test")
func run_test() -> void:
save_root = OS.get_environment("ABG_P9_7_PROFILE_SAVE_ROOT")
if save_root.is_empty():
save_root = OS.get_user_data_dir().path_join("p9_7_profiles")
DirAccess.make_dir_recursive_absolute(save_root)
for profile_name in ["normal", "active", "efficient", "adversarial", "returning"]:
run_profile(profile_name)
expect(represented_route_risks.size() == 3, "profiles cover steady, pressured and elite routes")
expect(represented_gear.size() == 5 and represented_gear.has("none"), "profiles cover four distinct purchases plus no purchase")
expect(defeat_recoveries == 1, "adversarial profile proves exactly one defeat/recovery/replan")
expect(interruption_reloads == 1, "returning profile proves exactly one Encounter interruption reload")
expect(longest_gap <= 25.0, "longest automatic battle gap remains at most 25 seconds")
if failures.is_empty():
print("ABG_P9_7_THREE_ROUND_FIVE_PROFILE_OK profiles=5 route_risks=steady,pressured,elite gear=EQ-021,EQ-023,EQ-028,EQ-036,none timestamps=%d longest_gap=%.2f defeat_recoveries=%d interruption_reloads=%d offline_rewards=0 starvation=0 invalid_mutations=%d relay=5/5" % [timestamp_count, longest_gap, defeat_recoveries, interruption_reloads, invalid_mutations])
quit(0)
else:
for failure in failures:
push_error(failure)
quit(1)
func run_profile(profile_name: String) -> void:
var profile: Dictionary = PROFILES[profile_name]
var market = MarketService.new(complete_p96())
var labels: Array[String] = ["market5_read"]
var times: Array[float] = [0.0]
var profile_defeats := 0
var profile_interruptions := 0
represented_gear[String(profile.gear) if not String(profile.gear).is_empty() else "none"] = true
for round_index in range(3):
var market_index := 5 + round_index
var recruit_id := String(profile.recruits[round_index])
var route_id := String(profile.routes[round_index])
var route: Dictionary = market.content.routes[route_id]
represented_route_risks[int(route.risk)] = true
append_time(labels, times, "m%d_focus_recruit" % market_index, 0.7)
expect(market.focus_recruit_offer(recruit_id).ok, "%s Market %d focuses Recruit" % [profile_name, market_index])
append_time(labels, times, "m%d_hire" % market_index, 0.5)
expect(market.hire("%s-hire-%d" % [profile_name, market_index], recruit_id).ok, "%s Market %d hires Recruit" % [profile_name, market_index])
if round_index == 0 and not String(profile.gear).is_empty():
buy_profile_gear(market, profile_name, profile, labels, times)
var deployment: Array[String] = []
deployment.assign(market.auto_order_candidate())
if profile_name == "adversarial" and market_index == 6:
deployment.assign(["Hearthguard", "Farstring", "Kindhand", "P97-M6-R3"])
append_time(labels, times, "m%d_commit_line" % market_index, 0.7)
expect(market.commit_deployment("%s-deploy-%d" % [profile_name, market_index], deployment).ok, "%s Market %d commits Line" % [profile_name, market_index])
append_time(labels, times, "m%d_focus_route" % market_index, 1.0)
expect(market.focus_route(route_id).ok, "%s Market %d focuses route" % [profile_name, market_index])
append_time(labels, times, "m%d_commit_route" % market_index, 0.5)
expect(market.commit_route("%s-route-%d" % [profile_name, market_index], route_id).ok, "%s Market %d commits route" % [profile_name, market_index])
if profile_name == "returning" and market_index == 6:
market = interrupt_and_reload(market, profile_name, market_index, route_id, labels, times)
profile_interruptions += 1
var battle_result: Dictionary = run_battle(market, int(profile.speed))
append_time(labels, times, "m%d_%s" % [market_index, battle_result.outcome], float(battle_result.ticks) * 0.08 / float(profile.speed))
var settlement: Dictionary = market.settle_battle("%s-settle-%d-%d" % [profile_name, market_index, profile_defeats], battle_result.snapshot)
expect(settlement.ok, "%s Round %d settles" % [profile_name, market_index])
append_time(labels, times, "m%d_settle" % market_index, 0.5)
if battle_result.outcome == "defeat":
profile_defeats += 1
defeat_recoveries += 1
expect(profile_name == "adversarial" and market_index == 6, "only the declared adversarial Round 6 may defeat")
append_time(labels, times, "m%d_recover8" % market_index, 0.8)
expect(market.recover_injured_company("%s-defeat-recover-%d" % [profile_name, market_index]).ok, "%s Round %d recovers defeat" % [profile_name, market_index])
append_time(labels, times, "m%d_replan_line" % market_index, 0.7)
expect(market.commit_deployment("%s-redeploy-%d" % [profile_name, market_index], market.auto_order_candidate()).ok, "%s Round %d commits recovered full Line" % [profile_name, market_index])
append_time(labels, times, "m%d_retry_route" % market_index, 0.8)
expect(market.focus_route(route_id).ok and market.commit_route("%s-retry-route-%d" % [profile_name, market_index], route_id).ok, "%s Round %d recommits route" % [profile_name, market_index])
var retry: Dictionary = run_battle(market, int(profile.speed))
append_time(labels, times, "m%d_retry_%s" % [market_index, retry.outcome], float(retry.ticks) * 0.08 / float(profile.speed))
expect(retry.outcome == "victory", "%s Round %d recovered full Line wins" % [profile_name, market_index])
expect(market.settle_battle("%s-retry-settle-%d" % [profile_name, market_index], retry.snapshot).ok, "%s Round %d retry settles" % [profile_name, market_index])
append_time(labels, times, "m%d_retry_settle" % market_index, 0.5)
if market.state.recovery_required:
append_time(labels, times, "m%d_injury_recovery" % market_index, 0.6)
expect(market.recover_injured_company("%s-injury-recover-%d" % [profile_name, market_index]).ok, "%s Round %d recovers fallen members" % [profile_name, market_index])
if market_index < 7:
append_time(labels, times, "market%d_read" % (market_index + 1), 0.9)
var snapshot: Dictionary = market.state.snapshot()
expect(snapshot.current_market == 8 and snapshot.relay_restored and snapshot.continuation_locked, "%s reaches the Relay threshold" % profile_name)
expect(int(snapshot.coin) >= 0, "%s never starves below zero Coin" % profile_name)
expect(snapshot.battle_settlement_receipts.size() == 3 + profile_defeats, "%s settlement count includes only committed attempts" % profile_name)
var duplicate_before := snapshot.duplicate(true)
var duplicate_recovery: Dictionary = market.recover_injured_company("%s-last-recovery" % profile_name)
expect(not duplicate_recovery.ok and market.state.snapshot() == duplicate_before, "%s invalid final recovery mutates nothing" % profile_name)
invalid_mutations += 1
var profile_gap := max_gap(times)
longest_gap = maxf(longest_gap, profile_gap)
timestamp_count += times.size()
interruption_reloads += profile_interruptions
expect(profile_defeats == (1 if profile_name == "adversarial" else 0), "%s has the declared defeat count" % profile_name)
print("P97_PROFILE_RESULT profile=%s routes=%s gear=%s speed=%dx timestamps=%s final_coin=%d settlements=%d max_gap=%.2f" % [profile_name, ",".join(profile.routes), String(profile.gear) if not String(profile.gear).is_empty() else "none", int(profile.speed), format_times(labels, times), int(snapshot.coin), snapshot.battle_settlement_receipts.size(), profile_gap])
func buy_profile_gear(market, profile_name: String, profile: Dictionary, labels: Array[String], times: Array[float]) -> void:
var catalog_id := String(profile.gear)
var offer_id := "P97-M5-%s" % catalog_id
append_time(labels, times, "m5_focus_%s" % catalog_id, 0.4)
expect(market.focus_equipment_offer(offer_id).ok, "%s focuses %s" % [profile_name, catalog_id])
append_time(labels, times, "m5_buy_%s" % catalog_id, 0.4)
var purchase: Dictionary = market.buy_equipment("%s-buy-%s" % [profile_name, catalog_id], offer_id)
expect(purchase.ok, "%s buys %s" % [profile_name, catalog_id])
if not purchase.ok:
return
append_time(labels, times, "m5_equip_%s" % catalog_id, 0.5)
expect(market.equip("%s-equip-%s" % [profile_name, catalog_id], String(purchase.receipt.instance_id), String(profile.wearer)).ok, "%s equips %s" % [profile_name, catalog_id])
func interrupt_and_reload(market, profile_name: String, market_index: int, route_id: String, labels: Array[String], times: Array[float]):
var saves = SaveService.new()
saves.save_path = save_root.path_join("%s.json" % profile_name)
saves.clear()
expect(saves.save_snapshot(market.state.snapshot(), 2000).ok, "%s saves committed Encounter" % profile_name)
append_time(labels, times, "m%d_interrupted" % market_index, 2.5)
var loaded: Dictionary = saves.load_snapshot(2015)
expect(loaded.ok and loaded.battle_interrupted and loaded.offline_coin == 0 and loaded.offline_xp == 0 and loaded.offline_battle_ticks == 0, "%s interruption restores pre-battle Market without offline progress" % profile_name)
var restored = MarketService.new()
expect(restored.state.restore_snapshot(loaded.snapshot), "%s restores interruption checkpoint" % profile_name)
expect(int(restored.state.current_market) == market_index and restored.state.phase == MarketState.Phase.MARKET, "%s returns to the same Market decision" % profile_name)
append_time(labels, times, "m%d_resume_route" % market_index, 0.8)
expect(restored.focus_route(route_id).ok and restored.commit_route("%s-resume-route-%d" % [profile_name, market_index], route_id).ok, "%s recommits route after reload" % profile_name)
return restored
func run_battle(market, speed: int) -> Dictionary:
var battle = BattleService.new()
expect(battle.start_from_market(market.state).ok, "%s battle starts" % market.state.selected_route)
var guard := 0
while not battle.state.terminal and guard < 2400:
battle.advance_ticks(1)
guard += 1
expect(guard < 2400 and battle.state.outcome in ["victory", "defeat"], "%s battle terminates" % market.state.selected_route)
return {"outcome": battle.state.outcome, "ticks": guard, "speed": speed, "snapshot": battle.state.snapshot()}
func complete_p96() -> Dictionary:
var service = LegacyService.new()
service.enter_hall()
service.focus_offer("Farstring")
service.hire("Farstring")
complete_legacy_battle(service, "open_road")
service.focus_offer("Kindhand")
service.hire("Kindhand")
complete_legacy_battle(service, "crosswind")
service.focus_equipment("EQ-002")
service.equip_focused("Hearthguard")
complete_legacy_battle(service, "bell_breach")
service.choose_rift_tide_plan("press_on")
complete_legacy_battle(service, "rift_tide")
return service.state.snapshot()
func complete_legacy_battle(service, encounter_id: String) -> void:
service.select_encounter(encounter_id)
service.start_battle()
var guard := 0
while service.state.phase == LegacyState.Phase.BATTLE and guard < 4000:
service.advance_battle_ticks(1)
guard += 1
service.claim_and_return()
func append_time(labels: Array[String], times: Array[float], label: String, delta: float) -> void:
labels.append(label)
times.append(times[-1] + delta)
func max_gap(times: Array[float]) -> float:
var result := 0.0
for index in range(1, times.size()):
result = maxf(result, times[index] - times[index - 1])
return result
func format_times(labels: Array[String], times: Array[float]) -> String:
var values: Array[String] = []
for index in range(times.size()):
values.append("%s@%.2f" % [labels[index], times[index]])
return ",".join(values)
func expect(condition: bool, message: String) -> void:
if not condition:
failures.append("P9.7 five profile: %s" % message)