291 lines
11 KiB
GDScript
291 lines
11 KiB
GDScript
extends SceneTree
|
|
|
|
const MainScene = preload("res://main.tscn")
|
|
const PROFILES: Array[String] = ["normal", "active", "efficient", "adversarial", "returning"]
|
|
|
|
var failures: Array[String] = []
|
|
var game: Control
|
|
var touch_count := 0
|
|
var fallback_count := 0
|
|
var direct_count := 0
|
|
var profile_results: Dictionary = {}
|
|
var capture_root := ""
|
|
var recording_only := false
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("run_test")
|
|
|
|
func run_test() -> void:
|
|
capture_root = OS.get_environment("ABG_P9_2_INTEGRATED_CAPTURE_ROOT")
|
|
recording_only = OS.get_environment("ABG_P9_2_INTEGRATED_RECORDING_ONLY") == "1"
|
|
if recording_only:
|
|
await _configure_recording_window()
|
|
var profiles: Array[String] = []
|
|
if recording_only:
|
|
profiles.append("normal")
|
|
else:
|
|
profiles.assign(PROFILES)
|
|
for profile in profiles:
|
|
await run_profile(profile)
|
|
await destroy_game()
|
|
_assert_profiles(profiles)
|
|
if failures.is_empty():
|
|
if recording_only:
|
|
print("ABG_P9_2_INTEGRATED_RECORDING_OK viewport=844x390 profile=normal touch=%d fallback=%d direct=%d handoff=true contact=true inspect=true result=true home=true" % [touch_count, fallback_count, direct_count])
|
|
else:
|
|
print("ABG_P9_2_INTEGRATED_RUNTIME_OK profiles=5 touch=%d fallback=%d direct=%d decision_tick=50 continue=victory retreat=retreat deterministic_continue=true deterministic_retreat=true max_no_decision_ticks=%d zh_CN=true en=true text_130=true reduced_motion=true mute=true low_power=true home=true" % [touch_count, fallback_count, direct_count, _maximum_gap()])
|
|
quit(0)
|
|
else:
|
|
for failure in failures:
|
|
push_error(failure)
|
|
quit(1)
|
|
|
|
func _configure_recording_window() -> void:
|
|
root.content_scale_mode = Window.CONTENT_SCALE_MODE_CANVAS_ITEMS
|
|
root.content_scale_aspect = Window.CONTENT_SCALE_ASPECT_EXPAND
|
|
root.content_scale_size = Vector2i(844, 390)
|
|
root.size = Vector2i(844, 390)
|
|
DisplayServer.window_set_size(Vector2i(844, 390))
|
|
await process_frame
|
|
|
|
func run_profile(profile: String) -> void:
|
|
game = MainScene.instantiate()
|
|
game.auto_boot = false
|
|
game.authoritative_auto_advance = false
|
|
game.audio_enabled = false
|
|
root.add_child(game)
|
|
await settle(8)
|
|
_apply_presentation_profile(profile)
|
|
_prepare_p9_1_result()
|
|
game.render()
|
|
await settle(5)
|
|
var touches_before := touch_count
|
|
await tap("ContinueAuthoritativeBattleButton")
|
|
_expect(game.authoritative_battle_active, "%s enters P9.2 from the visible P9.1 result" % profile)
|
|
_expect(game.authoritative_battle_service.state.member_hp == {"Hearthguard": 72, "Farstring": 46, "Kindhand": 85}, "%s imports exact P9.1 HP" % profile)
|
|
var decision_ticks: Array[int] = []
|
|
|
|
match profile:
|
|
"normal":
|
|
decision_ticks.append(0)
|
|
await tap("P92InspectButton")
|
|
await hold_frames(20)
|
|
await tap("P92InspectButton")
|
|
await capture("01-handoff-controls")
|
|
"active":
|
|
decision_ticks.append(0)
|
|
await tap("P92Speed2Button")
|
|
"efficient":
|
|
decision_ticks.append(0)
|
|
await tap("P92Speed2Button")
|
|
"adversarial":
|
|
decision_ticks.append(0)
|
|
await tap("P92Speed1Button")
|
|
"returning":
|
|
decision_ticks.append(0)
|
|
await tap("P92InspectButton")
|
|
await tap("P92InspectButton")
|
|
await tap("P92Speed2Button")
|
|
|
|
if profile == "active":
|
|
await advance_to_tick(20)
|
|
decision_ticks.append(20)
|
|
var tick_before_pause: int = game.authoritative_battle_service.state.tick
|
|
await tap("P92PauseButton")
|
|
game.advance_authoritative_ticks(5)
|
|
_expect(game.authoritative_battle_service.state.tick == tick_before_pause, "active Pause accepts zero simulation ticks")
|
|
await hold_frames(20)
|
|
await tap("P92PauseButton")
|
|
|
|
await advance_to_tick(24)
|
|
if profile == "normal":
|
|
await capture("02-contact")
|
|
await advance_to_tick(50)
|
|
var decision_projection := _gameplay_projection()
|
|
var decision_hash: int = game.authoritative_battle_service.state.state_hash()
|
|
decision_ticks.append(50)
|
|
|
|
if profile in ["normal", "efficient", "active"]:
|
|
await tap("P92InspectButton")
|
|
if profile == "normal":
|
|
await capture("03-tick-50-inspect")
|
|
await tap("P92InspectButton")
|
|
await advance_to_tick(100)
|
|
decision_ticks.append(100)
|
|
await tap("P92InspectButton")
|
|
await tap("P92InspectButton")
|
|
await advance_to_terminal()
|
|
else:
|
|
await tap("P92RetreatButton")
|
|
if profile == "adversarial":
|
|
await tap("P92RetreatButton")
|
|
await advance_to_terminal()
|
|
|
|
var outcome: String = game.authoritative_battle_service.state.outcome
|
|
var terminal_tick: int = game.authoritative_battle_service.state.tick
|
|
decision_ticks.append(terminal_tick)
|
|
var result_projection := _gameplay_projection()
|
|
var result_hash: int = game.authoritative_battle_service.state.state_hash()
|
|
var event_hash: int = game.authoritative_battle_service.state.event_hash()
|
|
_expect(outcome == ("retreat" if profile in ["adversarial", "returning"] else "victory"), "%s reaches its intended outcome" % profile)
|
|
_expect(find_control(game, "P92ResultPanel") != null, "%s renders the five-line result" % profile)
|
|
for index in range(1, 6):
|
|
_expect(find_control(game, "P92ResultLine%d" % index) != null, "%s result keeps line %d" % [profile, index])
|
|
if profile == "normal":
|
|
await capture("04-victory-result")
|
|
await hold_frames(45)
|
|
await tap("P92HomeButton")
|
|
_expect(not game.authoritative_battle_active and game.page == game.AppPage.MAIN_MENU, "%s Home exits to Main Menu" % profile)
|
|
|
|
profile_results[profile] = {
|
|
"decision_projection": decision_projection,
|
|
"decision_hash": decision_hash,
|
|
"result_projection": result_projection,
|
|
"result_hash": result_hash,
|
|
"event_hash": event_hash,
|
|
"decision_ticks": decision_ticks,
|
|
"max_gap": _max_gap(decision_ticks),
|
|
"outcome": outcome,
|
|
"terminal_tick": terminal_tick,
|
|
"touches": touch_count - touches_before,
|
|
}
|
|
print("ABG_P9_2_PROFILE_RESULT profile=%s outcome=%s terminal_tick=%d decisions=%s max_gap=%d state_hash=%d event_hash=%d" % [profile, outcome, terminal_tick, decision_ticks, _max_gap(decision_ticks), result_hash, event_hash])
|
|
|
|
func _apply_presentation_profile(profile: String) -> void:
|
|
game.locale = "en" if profile in ["active", "returning"] else "zh_CN"
|
|
game.text_scale = 1.3 if profile in ["active", "returning"] else 1.0
|
|
game.reduced_motion = profile in ["active", "returning"]
|
|
game.muted = true
|
|
game.facade.presentation_settings.low_power = profile == "returning"
|
|
|
|
func _prepare_p9_1_result() -> void:
|
|
game.facade.session_state.field_order.assign(["Hearthguard", "Farstring", "Kindhand"])
|
|
game.facade.session_state.starting_pair.assign(["Hearthguard", "Farstring"])
|
|
game.facade.session_state.front_choice = "Hearthguard"
|
|
game.facade.session_state.rear_choice = "Farstring"
|
|
game.facade.session_state.added_rear = "Kindhand"
|
|
game.facade.session_state.member_hp = {"Hearthguard": 72, "Farstring": 46, "Kindhand": 85}
|
|
game.facade.session_state.member_levels = {"Hearthguard": 2, "Farstring": 1, "Kindhand": 1}
|
|
game.page = game.AppPage.THIRD_CYCLE_RESULT
|
|
|
|
func advance_to_tick(target_tick: int) -> void:
|
|
while not game.authoritative_battle_service.state.terminal and game.authoritative_battle_service.state.tick < target_tick:
|
|
game.advance_authoritative_ticks(1)
|
|
if recording_only:
|
|
await hold_frames(4)
|
|
elif game.authoritative_battle_service.state.tick % 10 == 0:
|
|
await process_frame
|
|
await settle(3)
|
|
|
|
func advance_to_terminal() -> void:
|
|
var guard := 0
|
|
while not game.authoritative_battle_service.state.terminal and guard < 900:
|
|
game.advance_authoritative_ticks(1)
|
|
guard += 1
|
|
if recording_only:
|
|
await hold_frames(4)
|
|
elif guard % 10 == 0:
|
|
await process_frame
|
|
await settle(4)
|
|
_expect(game.authoritative_battle_service.state.terminal, "battle reaches a bounded terminal")
|
|
|
|
func _gameplay_projection() -> Dictionary:
|
|
var state = game.authoritative_battle_service.state
|
|
return {
|
|
"tick": state.tick,
|
|
"outcome": state.outcome,
|
|
"cause": state.outcome_cause,
|
|
"member_hp": state.member_hp.duplicate(true),
|
|
"member_positions": state.member_positions.duplicate(true),
|
|
"enemy_hp": state.enemy_hp.duplicate(true),
|
|
"enemy_positions": state.enemy_positions.duplicate(true),
|
|
}
|
|
|
|
func _assert_profiles(profiles: Array[String]) -> void:
|
|
for profile in profiles:
|
|
_expect(profile_results.has(profile), "profile %s reports a result" % profile)
|
|
if profile_results.has(profile):
|
|
_expect(int(profile_results[profile].max_gap) <= 50, "%s no-decision gap stays at or below 50 ticks" % profile)
|
|
if recording_only or not failures.is_empty():
|
|
return
|
|
var decision_projection: Dictionary = profile_results.normal.decision_projection
|
|
for profile in PROFILES:
|
|
_expect(profile_results[profile].decision_projection == decision_projection, "%s reaches the same visible tick-50 counterfactual state" % profile)
|
|
_expect(profile_results.normal.result_projection == profile_results.efficient.result_projection, "speed and Inspect preserve the continue result")
|
|
_expect(profile_results.normal.result_hash == profile_results.efficient.result_hash, "speed and Inspect preserve the continue state hash")
|
|
_expect(profile_results.normal.event_hash == profile_results.efficient.event_hash, "speed and Inspect preserve the continue event hash")
|
|
_expect(profile_results.active.result_projection == profile_results.normal.result_projection, "Pause preserves the gameplay result")
|
|
_expect(profile_results.adversarial.result_projection == profile_results.returning.result_projection, "presentation settings preserve the retreat result")
|
|
_expect(profile_results.adversarial.result_hash == profile_results.returning.result_hash, "same-tick retreat preserves state hash")
|
|
_expect(profile_results.adversarial.event_hash == profile_results.returning.event_hash, "same-tick retreat preserves event hash")
|
|
|
|
func _maximum_gap() -> int:
|
|
var result := 0
|
|
for profile in profile_results:
|
|
result = maxi(result, int(profile_results[profile].max_gap))
|
|
return result
|
|
|
|
func _max_gap(ticks: Array[int]) -> int:
|
|
var result := 0
|
|
for index in range(1, ticks.size()):
|
|
result = maxi(result, ticks[index] - ticks[index - 1])
|
|
return result
|
|
|
|
func tap(node_name: String) -> void:
|
|
var control: Control = find_control(game, node_name)
|
|
if control == null or not control.is_visible_in_tree() or (control is BaseButton and control.disabled):
|
|
failures.append("unavailable parsed-touch target %s" % node_name)
|
|
return
|
|
var position := root.get_final_transform() * control.get_global_rect().get_center()
|
|
var down := InputEventScreenTouch.new()
|
|
down.index = 0
|
|
down.position = position
|
|
down.pressed = true
|
|
Input.parse_input_event(down)
|
|
await process_frame
|
|
var up := InputEventScreenTouch.new()
|
|
up.index = 0
|
|
up.position = position
|
|
up.pressed = false
|
|
Input.parse_input_event(up)
|
|
touch_count += 1
|
|
await settle(6)
|
|
|
|
func find_control(node: Node, node_name: String) -> Control:
|
|
if node.name == node_name and node is Control and not node.is_queued_for_deletion():
|
|
return node
|
|
for child in node.get_children():
|
|
var found := find_control(child, node_name)
|
|
if found != null:
|
|
return found
|
|
return null
|
|
|
|
func capture(name: String) -> void:
|
|
if capture_root.is_empty():
|
|
return
|
|
DirAccess.make_dir_recursive_absolute(capture_root)
|
|
await settle(3)
|
|
await RenderingServer.frame_post_draw
|
|
var image := root.get_texture().get_image()
|
|
var error := image.save_png(capture_root.path_join(name + ".png"))
|
|
_expect(error == OK, "capture %s succeeds" % name)
|
|
|
|
func hold_frames(frames: int) -> void:
|
|
if not recording_only:
|
|
return
|
|
await settle(frames)
|
|
|
|
func destroy_game() -> void:
|
|
if game != null and is_instance_valid(game):
|
|
game.queue_free()
|
|
game = null
|
|
await settle(60)
|
|
|
|
func settle(frames: int) -> void:
|
|
for _frame in frames:
|
|
await process_frame
|
|
|
|
func _expect(condition: bool, message: String) -> void:
|
|
if not condition and failures.size() < 160:
|
|
failures.append("P9.2 integrated: %s" % message)
|