410 lines
18 KiB
GDScript
410 lines
18 KiB
GDScript
extends SceneTree
|
|
|
|
const BattleService = preload("res://p10_3/deep_channels_battle_service.gd")
|
|
const GrowthService = preload("res://p10_3/growth_service.gd")
|
|
const PreparationService = preload("res://p10_3/deep_channels_preparation_service.gd")
|
|
const ResolutionService = preload("res://p10_3/deep_channels_resolution_service.gd")
|
|
const SaveV9 = preload("res://p10_3/save_v9_service.gd")
|
|
const Fixture = preload("res://tests/p10_3/deep_channels_fixture.gd")
|
|
|
|
const PROFILES := [
|
|
{"id": "normal", "risks": [0, 0, 0], "artifact": "AR-015", "promotion": "defer"},
|
|
{"id": "active", "risks": [1, 1, 1], "artifact": "AR-011", "promotion": "PF-A14"},
|
|
{"id": "efficient", "risks": [2, 2, 2], "artifact": "AR-012", "promotion": "PF-A13"},
|
|
{"id": "adversarial", "risks": [0, 1, 2], "artifact": "AR-012", "promotion": "PF-A13"},
|
|
{"id": "returning", "risks": [1, 2, 0], "artifact": "AR-015", "promotion": "defer"},
|
|
]
|
|
|
|
const WRONG_PLANS := {
|
|
"RO-R2-M05-Q0": "guard_center",
|
|
"RO-R2-M05-Q1": "follow_tide",
|
|
"RO-R2-M05-Q2": "follow_tide",
|
|
"RO-R2-M06-Q0": "guard_center",
|
|
"RO-R2-M06-Q1": "hold_line",
|
|
"RO-R2-M06-Q2": "guard_center",
|
|
"RO-R2-M07-Q0": "focus_source",
|
|
"RO-R2-M07-Q1": "follow_tide",
|
|
"RO-R2-M07-Q2": "focus_source",
|
|
}
|
|
|
|
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))
|
|
var counterfactuals := verify_counterfactuals()
|
|
verify_results(results, counterfactuals)
|
|
if failures.is_empty():
|
|
var decisions := 0
|
|
var longest_gap := 0.0
|
|
var recoveries := 0
|
|
var reloads := 0
|
|
var route_ids := {}
|
|
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)
|
|
for route_id in result.route_ids:
|
|
route_ids[String(route_id)] = true
|
|
print("ABG_P10_3_FIVE_PROFILE_OK profiles=normal,active,efficient,adversarial,returning routes=%d artifacts=3 promotion_branches=2 decisions=%d longest_gap=%.1f automatic_battle_excluded=true route_reversals=%d artifact_reversals=%d promotion_reversals=%d recoveries=%d reloads=%d starvation=0 resurrection=0 offline_rewards=0 market8=8" % [route_ids.size(), decisions, longest_gap, int(counterfactuals.route_reversals), int(counterfactuals.artifact_reversals), int(counterfactuals.promotion_reversals), recoveries, reloads])
|
|
quit(0)
|
|
else:
|
|
for failure in failures:
|
|
push_error(failure)
|
|
quit(1)
|
|
|
|
|
|
func simulate_profile(profile: Dictionary) -> Dictionary:
|
|
var snapshot_value := Fixture.new().migrated_snapshot()
|
|
if snapshot_value.is_empty():
|
|
return failed_result(String(profile.id), "fixture")
|
|
var service = PreparationService.new(snapshot_value)
|
|
var labels: Array[String] = []
|
|
var timestamps: Array[float] = []
|
|
var clock := 0.0
|
|
var automatic_seconds := 0.0
|
|
var reloads := 0
|
|
var recoveries := 0
|
|
var fallen_ids: Array[String] = []
|
|
var company_before: Array = Array(service.state.current_run.company).duplicate()
|
|
var route_ids: Array[String] = []
|
|
var decisive_kinds: Array[String] = []
|
|
var chosen_artifact := ""
|
|
var chosen_promotion := ""
|
|
|
|
for market_index in [5, 6, 7]:
|
|
clock = record_decision(labels, timestamps, clock, "market_%d_read" % market_index, 1.0)
|
|
if market_index == 7:
|
|
var growth_result := apply_growth_choice(service.state.snapshot(), profile)
|
|
if not growth_result.ok:
|
|
return failed_result(String(profile.id), String(growth_result.code))
|
|
service = PreparationService.new(growth_result.snapshot)
|
|
chosen_artifact = String(growth_result.artifact)
|
|
chosen_promotion = String(growth_result.promotion)
|
|
clock = record_decision(labels, timestamps, clock, "artifact_%s" % chosen_artifact, 2.0)
|
|
clock = record_decision(labels, timestamps, clock, "promotion_%s" % chosen_promotion, 2.0)
|
|
|
|
if String(profile.id) == "returning":
|
|
var reloaded := save_roundtrip(service.state.snapshot(), 300 + reloads)
|
|
if reloaded.is_empty():
|
|
return failed_result(String(profile.id), "reload_before_route")
|
|
service = PreparationService.new(reloaded)
|
|
reloads += 1
|
|
clock = record_decision(labels, timestamps, clock, "market_%d_reload" % market_index, 1.5)
|
|
|
|
var risk := int(profile.risks[market_index - 5])
|
|
var route: Dictionary = service.state.current_run.market_board.battle_offers[risk]
|
|
route_ids.append(String(route.route_id))
|
|
var prepared := service.commit_route(
|
|
"profile-%s-route-%d" % [profile.id, market_index],
|
|
service.state.revision,
|
|
String(route.offer_id),
|
|
String(service.state.current_run.market_board.board_hash)
|
|
)
|
|
if not prepared.ok:
|
|
return failed_result(String(profile.id), "route_%s" % prepared.code)
|
|
clock = record_decision(labels, timestamps, clock, "market_%d_route_risk_%d" % [market_index, risk], 3.0)
|
|
var preferred := String(BattleService.PREFERRED_PLAN[String(route.route_id)])
|
|
clock = record_decision(labels, timestamps, clock, "round_%d_plan_%s" % [market_index, preferred], 2.0)
|
|
|
|
if String(profile.id) == "adversarial" and market_index == 7:
|
|
var wrong := BattleService.new(service.state.snapshot()).run_to_terminal(String(WRONG_PLANS[String(route.route_id)]))
|
|
if not wrong.ok or String(wrong.snapshot.outcome) != "defeat":
|
|
return failed_result(String(profile.id), "expected_defeat")
|
|
automatic_seconds += float(wrong.snapshot.tick) / 10.0
|
|
var failed_resolver = ResolutionService.new(service.state.snapshot())
|
|
var failed_settlement := failed_resolver.settle(
|
|
"profile-adversarial-fail",
|
|
failed_resolver.state.revision,
|
|
wrong.snapshot
|
|
)
|
|
if not failed_settlement.ok:
|
|
return failed_result(String(profile.id), "defeat_settlement")
|
|
fallen_ids.assign(wrong.snapshot.fallen_recruit_ids)
|
|
clock = record_decision(labels, timestamps, clock, "round_7_accept_loss", 4.0)
|
|
var recovered := failed_resolver.recover(
|
|
"profile-adversarial-recover",
|
|
failed_resolver.state.revision,
|
|
String(wrong.snapshot.snapshot_hash),
|
|
int(wrong.snapshot.recovery_coin)
|
|
)
|
|
if not recovered.ok:
|
|
return failed_result(String(profile.id), "recovery_%s" % recovered.code)
|
|
recoveries += 1
|
|
service = PreparationService.new(failed_resolver.state.snapshot())
|
|
clock = record_decision(labels, timestamps, clock, "round_7_recover", 2.0)
|
|
route = service.state.current_run.market_board.battle_offers[risk]
|
|
prepared = service.commit_route(
|
|
"profile-adversarial-retry",
|
|
service.state.revision,
|
|
String(route.offer_id),
|
|
String(service.state.current_run.market_board.board_hash)
|
|
)
|
|
if not prepared.ok:
|
|
return failed_result(String(profile.id), "retry_route")
|
|
clock = record_decision(labels, timestamps, clock, "round_7_retry_%s" % preferred, 2.0)
|
|
|
|
var battle := BattleService.new(service.state.snapshot()).run_to_terminal(preferred)
|
|
if not battle.ok or String(battle.snapshot.outcome) != "victory":
|
|
return failed_result(String(profile.id), "market_%d_preferred_battle" % market_index)
|
|
automatic_seconds += float(battle.snapshot.tick) / 10.0
|
|
decisive_kinds.append(String(battle.snapshot.first_decisive_event.kind))
|
|
var resolver = ResolutionService.new(service.state.snapshot())
|
|
var settled := resolver.settle(
|
|
"profile-%s-settle-%d" % [profile.id, market_index],
|
|
resolver.state.revision,
|
|
battle.snapshot
|
|
)
|
|
if not settled.ok:
|
|
return failed_result(String(profile.id), "settle_%s" % settled.code)
|
|
service = PreparationService.new(resolver.state.snapshot())
|
|
clock = record_decision(labels, timestamps, clock, "round_%d_settle" % market_index, 4.0)
|
|
|
|
if String(profile.id) == "returning":
|
|
var returned := save_roundtrip(service.state.snapshot(), 320 + reloads)
|
|
if returned.is_empty():
|
|
return failed_result(String(profile.id), "reload_after_settlement")
|
|
service = PreparationService.new(returned)
|
|
reloads += 1
|
|
clock = record_decision(labels, timestamps, clock, "market_%d_return" % int(service.state.current_run.market_index), 1.5)
|
|
|
|
if int(service.state.current_run.coin) < 0:
|
|
return failed_result(String(profile.id), "negative_coin")
|
|
|
|
var final_run: Dictionary = service.state.current_run
|
|
var result := {
|
|
"id": String(profile.id),
|
|
"failed": false,
|
|
"policy_signature": "%s:%s:%s" % [profile.risks, profile.artifact, profile.promotion],
|
|
"route_ids": route_ids,
|
|
"decisive_kinds": decisive_kinds,
|
|
"labels": labels,
|
|
"timestamps": timestamps,
|
|
"decision_count": timestamps.size(),
|
|
"max_gap": maximum_gap(timestamps),
|
|
"automatic_seconds": automatic_seconds,
|
|
"recoveries": recoveries,
|
|
"reloads": reloads,
|
|
"fallen_ids": fallen_ids,
|
|
"company_before": company_before,
|
|
"company_after": Array(final_run.company).duplicate(),
|
|
"coin": int(final_run.coin),
|
|
"phase": String(final_run.phase),
|
|
"market_index": int(final_run.market_index),
|
|
"line_capacity": int(final_run.line_capacity),
|
|
"artifact": chosen_artifact,
|
|
"promotion": chosen_promotion,
|
|
"route_history": final_run.route_history.size(),
|
|
}
|
|
print("P103_PROFILE_RESULT profile=%s policy=%s routes=%s decisions=%s max_gap=%.1f auto=%.1f coin=%d company=%d fallen=%d reloads=%d artifact=%s promotion=%s market=%d" % [result.id, result.policy_signature, route_ids, format_timestamps(labels, timestamps), result.max_gap, result.automatic_seconds, result.coin, result.company_after.size(), fallen_ids.size(), reloads, chosen_artifact, chosen_promotion, result.market_index])
|
|
return result
|
|
|
|
|
|
func apply_growth_choice(snapshot_value: Dictionary, profile: Dictionary) -> Dictionary:
|
|
var growth := GrowthService.new(snapshot_value)
|
|
if not growth.valid:
|
|
return {"ok": false, "code": "growth_state_invalid"}
|
|
var artifact_id := String(profile.artifact)
|
|
var artifact := growth.select_artifact(
|
|
"profile-%s-artifact" % profile.id,
|
|
growth.state.revision,
|
|
String(growth.state.current_run.artifact_draft.draft_hash),
|
|
artifact_id
|
|
)
|
|
if not artifact.ok:
|
|
return {"ok": false, "code": "artifact_%s" % artifact.code}
|
|
var opened := growth.open_promotion_draft(
|
|
"profile-%s-promotion-open" % profile.id,
|
|
growth.state.revision
|
|
)
|
|
if not opened.ok:
|
|
return {"ok": false, "code": "promotion_open_%s" % opened.code}
|
|
var draft: Dictionary = growth.state.current_run.promotion_draft
|
|
var promotion_id := String(profile.promotion)
|
|
var promotion: Dictionary
|
|
if promotion_id == "defer":
|
|
promotion = growth.defer_promotion(
|
|
"profile-%s-promotion-defer" % profile.id,
|
|
growth.state.revision,
|
|
String(draft.draft_hash)
|
|
)
|
|
else:
|
|
promotion = growth.confirm_promotion(
|
|
"profile-%s-promotion" % profile.id,
|
|
growth.state.revision,
|
|
String(draft.draft_hash),
|
|
String(draft.recruit_id),
|
|
String(draft.current_profession_id),
|
|
promotion_id
|
|
)
|
|
if not promotion.ok:
|
|
return {"ok": false, "code": "promotion_%s" % promotion.code}
|
|
return {
|
|
"ok": true,
|
|
"code": "growth_chosen",
|
|
"snapshot": growth.state.snapshot(),
|
|
"artifact": artifact_id,
|
|
"promotion": promotion_id,
|
|
}
|
|
|
|
|
|
func verify_counterfactuals() -> Dictionary:
|
|
var route_reversals := 0
|
|
var preferred_plans := {}
|
|
for market_index in [5, 6, 7]:
|
|
for risk in range(3):
|
|
var service = Fixture.new().prepared_service(market_index, risk)
|
|
if service == null:
|
|
continue
|
|
var route_id := String(service.state.current_run.pending_encounter.route_id)
|
|
var preferred_plan := String(BattleService.PREFERRED_PLAN[route_id])
|
|
var preferred := BattleService.new(service.state.snapshot()).run_to_terminal(preferred_plan)
|
|
var wrong := BattleService.new(service.state.snapshot()).run_to_terminal(String(WRONG_PLANS[route_id]))
|
|
preferred_plans[preferred_plan] = true
|
|
if preferred.ok and wrong.ok and String(preferred.snapshot.outcome) == "victory" and String(wrong.snapshot.outcome) == "defeat":
|
|
route_reversals += 1
|
|
|
|
var artifact_winners := {}
|
|
var artifact_reversals := 0
|
|
for risk in range(3):
|
|
var modifiers := {}
|
|
for artifact_id in ["AR-011", "AR-012", "AR-015"]:
|
|
var service = Fixture.new().prepared_service(7, risk, artifact_id, "", true)
|
|
if service == null:
|
|
continue
|
|
var route_id := String(service.state.current_run.pending_encounter.route_id)
|
|
var battle := BattleService.new(service.state.snapshot()).run_to_terminal(String(BattleService.PREFERRED_PLAN[route_id]))
|
|
if battle.ok:
|
|
modifiers[artifact_id] = int(battle.snapshot.artifact_modifier)
|
|
if modifiers.size() == 3:
|
|
var ranked: Array = modifiers.keys()
|
|
ranked.sort_custom(func(left, right): return int(modifiers[left]) > int(modifiers[right]))
|
|
if int(modifiers[ranked[0]]) > int(modifiers[ranked[-1]]):
|
|
artifact_reversals += 1
|
|
artifact_winners[String(ranked[0])] = true
|
|
|
|
var promotion_winners := {}
|
|
var promotion_reversals := 0
|
|
for risk in [1, 2]:
|
|
var modifiers := {}
|
|
for promotion_id in ["PF-A13", "PF-A14"]:
|
|
var service = Fixture.new().prepared_service(7, risk, "AR-012", promotion_id, true)
|
|
if service == null:
|
|
continue
|
|
var route_id := String(service.state.current_run.pending_encounter.route_id)
|
|
var battle := BattleService.new(service.state.snapshot()).run_to_terminal(String(BattleService.PREFERRED_PLAN[route_id]))
|
|
if battle.ok:
|
|
modifiers[promotion_id] = int(battle.snapshot.promotion_modifier)
|
|
if modifiers.size() == 2:
|
|
var winner := "PF-A13" if int(modifiers["PF-A13"]) > int(modifiers["PF-A14"]) else "PF-A14"
|
|
if int(modifiers["PF-A13"]) != int(modifiers["PF-A14"]):
|
|
promotion_reversals += 1
|
|
promotion_winners[winner] = true
|
|
return {
|
|
"route_reversals": route_reversals,
|
|
"preferred_plans": preferred_plans,
|
|
"artifact_reversals": artifact_reversals,
|
|
"artifact_winners": artifact_winners,
|
|
"promotion_reversals": promotion_reversals,
|
|
"promotion_winners": promotion_winners,
|
|
}
|
|
|
|
|
|
func verify_results(results: Array[Dictionary], counterfactuals: 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 signatures := {}
|
|
var routes := {}
|
|
var artifacts := {}
|
|
var promotions := {}
|
|
for result in results:
|
|
signatures[String(result.policy_signature)] = true
|
|
for route_id in result.route_ids:
|
|
routes[String(route_id)] = true
|
|
artifacts[String(result.artifact)] = true
|
|
if String(result.promotion) != "defer":
|
|
promotions[String(result.promotion)] = true
|
|
expect(int(result.decision_count) >= 14, "%s records every deep-channel decision" % result.id)
|
|
expect(float(result.max_gap) <= 12.0, "%s nonbattle decision gap is bounded" % result.id)
|
|
expect(strictly_increasing(result.timestamps), "%s decision timestamps are strictly increasing" % result.id)
|
|
expect(int(result.coin) >= 0, "%s never has negative Coin" % result.id)
|
|
expect(result.phase == "market_8" and result.market_index == 8 and result.line_capacity == 7 and result.route_history == 7, "%s reaches stable Market 8" % result.id)
|
|
expect(signatures.size() == 5, "profiles use five distinct policies")
|
|
expect(routes.size() == 9, "profiles cover all nine route IDs")
|
|
expect(artifacts.size() == 3, "profiles select all three Artifact rules")
|
|
expect(promotions.size() == 2, "profiles commit both promotion branches")
|
|
var adversarial := result_by_id(results, "adversarial")
|
|
expect(adversarial.recoveries == 1 and adversarial.fallen_ids.size() == 1, "adversarial profile proves one bounded casualty recovery")
|
|
for fallen_id in adversarial.fallen_ids:
|
|
expect(fallen_id not in adversarial.company_after, "recovery never resurrects %s" % fallen_id)
|
|
var returning := result_by_id(results, "returning")
|
|
expect(returning.reloads == 6, "returning profile reloads before and after all three Rounds")
|
|
expect(int(counterfactuals.route_reversals) == 9 and counterfactuals.preferred_plans.size() == 4, "all routes reverse and use all four plans")
|
|
expect(int(counterfactuals.artifact_reversals) == 3 and counterfactuals.artifact_winners.size() == 3, "each Artifact wins one opposed risk comparison")
|
|
expect(int(counterfactuals.promotion_reversals) == 2 and counterfactuals.promotion_winners.size() == 2, "both promotion branches win one opposed comparison")
|
|
|
|
|
|
func save_roundtrip(snapshot_value: Dictionary, sequence: int) -> Dictionary:
|
|
var encoded := SaveV9.new().encode_snapshot(snapshot_value, sequence)
|
|
if not encoded.ok:
|
|
failures.append("Save v9 encode rejected at sequence %d" % sequence)
|
|
return {}
|
|
var decoded := SaveV9.new().decode_text(String(encoded.text), sequence)
|
|
if not decoded.ok or decoded.snapshot != snapshot_value or decoded.offline_delta != SaveV9.OFFLINE_ZERO:
|
|
failures.append("Save v9 roundtrip changed state or offline value at sequence %d" % sequence)
|
|
return {}
|
|
return Dictionary(decoded.snapshot).duplicate(true)
|
|
|
|
|
|
func record_decision(labels: Array[String], timestamps: Array[float], clock: float, label: String, delta: float) -> float:
|
|
var next := clock + delta
|
|
labels.append(label)
|
|
timestamps.append(next)
|
|
return next
|
|
|
|
|
|
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 failed_result(profile_id: String, code: String) -> Dictionary:
|
|
failures.append("profile %s failed at %s" % [profile_id, code])
|
|
return {"id": profile_id, "failed": true, "code": code}
|
|
|
|
|
|
func expect(condition: bool, message: String) -> void:
|
|
if not condition:
|
|
failures.append("P10.3 profiles: %s" % message)
|