238 lines
13 KiB
GDScript
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)
|