Files
aetherbound-guild/runtime/tests/p9_10/five_profile_test.gd
T

187 lines
10 KiB
GDScript

extends SceneTree
const Service = preload("res://p9_10/boss_service.gd")
const BattleService = preload("res://p9_10/battle_service.gd")
const BossBattleService = preload("res://p9_10/boss_battle_service.gd")
const SaveService = preload("res://p9_10/save_service.gd")
const Fixture = preload("res://tests/p9_10/runtime_fixture.gd")
const PROFILES := [
{"id": "normal", "claim": "CH-RG01-CATALOG-01", "risks": [0, 0, 0, 0, 0, 0, 0, 0, 0], "plan": "break_spindle", "gap": 12, "hire_pick": 0, "equipment": true},
{"id": "active", "claim": "CH-RG01-THREAT-01", "risks": [0, 0, 0, 0, 0, 0, 0, 0, 0], "plan": "hold_line", "gap": 7, "hire_pick": 0, "equipment": true},
{"id": "efficient", "claim": "CH-RG01-CATALOG-01", "risks": [2, 1, 0, 2, 1, 0, 2, 1, 0], "plan": "break_spindle", "gap": 5, "hire_pick": 1, "equipment": true},
{"id": "adversarial", "claim": "CH-RG01-THREAT-01", "risks": [0, 0, 0, 0, 0, 0, 0, 0, 0], "plan": "hold_line", "gap": 18, "hire_pick": 0, "equipment": false, "boss_defeat": true},
{"id": "returning", "claim": "CH-RG01-CATALOG-01", "risks": [0, 0, 0, 0, 0, 0, 0, 0, 0], "plan": "break_spindle", "gap": 15, "hire_pick": 0, "equipment": true, "interrupt": true},
]
var failures: Array[String] = []
var timestamps := 0
var longest_gap := 0
var boss_recoveries := 0
var interruption_reloads := 0
var save_root := ""
func _initialize() -> void:
call_deferred("run_test")
func run_test() -> void:
save_root = OS.get_environment("ABG_P9_10_PROFILE_SAVE_ROOT")
if save_root.is_empty():
save_root = "/private/tmp/abg-p910-profile-save"
DirAccess.make_dir_recursive_absolute(save_root)
for profile in PROFILES:
run_profile(profile)
if failures.is_empty():
print("ABG_P9_10_FIVE_PROFILE_OK profiles=normal,active,efficient,adversarial,returning timestamps=%d longest_gap=%d markets=45 hires=25 equipment=both routes=risks0,1,2 plans=break_spindle,hold_line counterfactual_builds=projectile,stable boss_recoveries=%d interruption_reloads=%d close=5/5 offline_rewards=0 starvation=0 invalid_mutations=0" % [timestamps, longest_gap, boss_recoveries, interruption_reloads])
quit(0)
else:
for failure in failures:
push_error(failure)
quit(1)
func run_profile(profile: Dictionary) -> void:
var service = Service.new(Fixture.p99_market_four(String(profile.claim)))
var clock := 0
var trace: Array[String] = []
for round_index in range(4, 13):
clock = decision(profile, clock, trace, "market_%d" % round_index)
if not service.state.available_recruit_offer_ids.is_empty() and service.state.company.size() < service.state.line_capacity:
clock = decision(profile, clock, trace, "hire_%d" % round_index)
var options: Array = service.state.available_recruit_offer_ids
var pick := mini(int(profile.hire_pick), options.size() - 1)
var offer_id := String(options[pick])
if String(profile.plan) == "break_spindle" and round_index == 11:
offer_id = "P910-M11-H2"
expect(service.hire("%s-hire-%d" % [profile.id, round_index], offer_id).ok, "%s Market %d hire" % [profile.id, round_index])
if bool(profile.equipment) and not service.state.available_equipment_offer_ids.is_empty():
clock = decision(profile, clock, trace, "equipment_%d" % round_index)
var offer_id := String(service.state.available_equipment_offer_ids[0])
var target := String(service.state.deployed_recruit_ids[-1])
expect(service.buy_and_equip("%s-equip-%d" % [profile.id, round_index], offer_id, target).ok, "%s Market %d equipment" % [profile.id, round_index])
if round_index == 12:
clock = decision(profile, clock, trace, "final_order")
var order: Array[String] = service.state.deployed_recruit_ids.duplicate()
if String(profile.id) in ["active", "adversarial"]:
order.reverse()
expect(service.propose_final_order("%s-order-propose" % profile.id, order).ok, "%s order proposal" % profile.id)
expect(service.confirm_final_order("%s-order-confirm" % profile.id).ok, "%s order confirmation" % profile.id)
var risk := int(profile.risks[round_index - 4])
var route_id := "P910-R%d-Q%d" % [round_index, risk]
clock = decision(profile, clock, trace, "route_%d_risk_%d" % [round_index, risk])
expect(service.commit_route("%s-route-%d" % [profile.id, round_index], route_id).ok, "%s Round %d route" % [profile.id, round_index])
if bool(profile.get("interrupt", false)) and round_index == 8:
service = interrupt_encounter(service, profile, round_index, route_id)
var battle = BattleService.new(String(profile.claim))
expect(battle.start_from_market(service.state).ok, "%s Round %d starts" % [profile.id, round_index])
var guard := 0
while not battle.state.terminal and guard < 2000:
battle.advance_ticks(1)
guard += 1
if guard >= 2000 or battle.state.outcome != "victory":
expect(false, "%s Round %d wins at risk %d" % [profile.id, round_index, risk])
return
clock = decision(profile, clock, trace, "settle_%d" % round_index)
expect(service.settle_battle("%s-settle-%d" % [profile.id, round_index], battle.state.snapshot()).ok, "%s Round %d settles" % [profile.id, round_index])
if service.state.current_market != 13 or service.state.route_history.size() != 12:
expect(false, "%s reaches one stable Boss Muster" % profile.id)
return
if bool(profile.get("interrupt", false)):
service = interrupt_boss(service, profile)
if bool(profile.get("boss_defeat", false)):
clock = run_boss_defeat_and_recovery(service, profile, clock, trace)
clock = decision(profile, clock, trace, "boss_plan_%s" % String(profile.plan))
if not service.commit_boss_plan("%s-boss-plan-final" % profile.id, String(profile.plan)).ok:
expect(false, "%s final Boss plan" % profile.id)
return
var boss_battle = BossBattleService.new()
if not boss_battle.start_from_contract(service.state).ok:
expect(false, "%s Boss starts" % profile.id)
return
var boss_guard := 0
while not boss_battle.state.terminal and boss_guard < 1900:
boss_battle.advance_ticks(1)
boss_guard += 1
if boss_guard >= 1900 or boss_battle.state.outcome != "victory":
expect(false, "%s Boss wins" % profile.id)
return
clock = decision(profile, clock, trace, "boss_result")
if not service.settle_boss("%s-boss-settle-final" % profile.id, boss_battle.state.snapshot()).ok:
expect(false, "%s Boss settles" % profile.id)
return
clock = decision(profile, clock, trace, "close")
if not service.close_contract("%s-close" % profile.id).ok:
expect(false, "%s closes" % profile.id)
return
var expected_renown := int(Fixture.p99_market_four(String(profile.claim)).profile.persistent_renown) + 18
expect(service.state.run_closed and service.state.persistent_renown == expected_renown, "%s persistent Renown is exact" % profile.id)
expect(service.state.company_records[-1].round_settlements.size() == 12 and service.state.unlocked_region_ids == ["RG-01", "RG-02"], "%s Record and RG-02 are exact" % profile.id)
print("P910_PROFILE_RESULT profile=%s claim=%s plan=%s clock=%d routes=%s trace=%s renown=%d" % [String(profile.id), String(profile.claim), String(profile.plan), clock, ",".join(Array(profile.risks).map(func(value): return str(value))), ",".join(trace), service.state.persistent_renown])
func interrupt_encounter(service, profile: Dictionary, round_index: int, route_id: String):
var saves = fresh_saves("%s-round-%d" % [profile.id, round_index])
expect(saves.save_snapshot(service.state.snapshot(), 8000).ok, "%s Encounter interruption saves" % profile.id)
var loaded: Dictionary = saves.load_snapshot(8010)
expect(loaded.ok and loaded.battle_interrupted and not loaded.boss_interrupted and loaded.offline_battle_ticks == 0, "%s Encounter rolls back with zero offline ticks" % profile.id)
var restored = Service.new()
expect(restored.restore_snapshot(loaded.snapshot), "%s interrupted Market restores" % profile.id)
expect(restored.commit_route("%s-route-%d-retry" % [profile.id, round_index], route_id).ok, "%s same route recommits" % profile.id)
interruption_reloads += 1
return restored
func interrupt_boss(service, profile: Dictionary):
expect(service.commit_boss_plan("%s-boss-plan-interrupt" % profile.id, String(profile.plan)).ok, "%s interrupt Boss plan" % profile.id)
var muster_hash := String(service.state.boss_muster.muster_hash)
var saves = fresh_saves("%s-boss" % profile.id)
expect(saves.save_snapshot(service.state.snapshot(), 9000).ok, "%s Boss interruption saves" % profile.id)
var loaded: Dictionary = saves.load_snapshot(9010)
expect(loaded.ok and loaded.boss_interrupted and String(loaded.snapshot.boss_muster.muster_hash) == muster_hash, "%s Boss interruption restores same Muster" % profile.id)
var restored = Service.new()
expect(restored.restore_snapshot(loaded.snapshot), "%s interrupted Boss Muster restores" % profile.id)
interruption_reloads += 1
return restored
func run_boss_defeat_and_recovery(service, profile: Dictionary, clock: int, trace: Array[String]) -> int:
clock = decision(profile, clock, trace, "boss_plan_failed")
expect(service.commit_boss_plan("%s-boss-plan-failed" % profile.id, String(profile.plan)).ok, "%s failed Boss plan commits" % profile.id)
var battle = BossBattleService.new()
expect(battle.start_from_contract(service.state).ok, "%s failed Boss starts" % profile.id)
var defeated: Dictionary = battle.state.snapshot()
for recruit_id in defeated.member_ids:
defeated.member_hp[recruit_id] = 0
defeated.terminal = true
defeated.outcome = "defeat"
defeated.cause = "company_zero"
defeated.revision = int(defeated.revision) + 1
expect(service.settle_boss("%s-boss-settle-failed" % profile.id, defeated).ok, "%s failed Boss settles" % profile.id)
clock = decision(profile, clock, trace, "boss_recovery")
expect(service.recover_boss_defeat("%s-boss-recover" % profile.id).ok, "%s Boss recovery" % profile.id)
boss_recoveries += 1
return clock
func decision(profile: Dictionary, clock: int, trace: Array[String], label: String) -> int:
var gap := int(profile.gap)
longest_gap = maxi(longest_gap, gap)
timestamps += 1
var next := clock + gap
trace.append("%s@%d" % [label, next])
return next
func fresh_saves(name: String):
var saves = SaveService.new()
saves.save_path = save_root.path_join(name + ".json")
saves.clear()
return saves
func expect(condition: bool, message: String) -> void:
if not condition:
failures.append("P9.10 profiles: %s" % message)