"""Exit the Blue Archive client. No baas-reference counterpart -- see ba_auto/reference_notes/mapping.md's "Exit game" row for the full search: the reference project runs as a persistent background scheduler and never deliberately exits the app between task cycles. This is new, desktop-specific convenience logic for this project's different (cron, one-shot-per-preset) architecture, meant to run as the last task in a preset. navigation.return_to_home's own docstring already documents the mechanism this relies on: an Escape press on the confirmed true home screen (nowhere else) raises Blue Archive's own "exit the game?" confirmation dialog. Every other task treats that as a hazard to avoid triggering by accident; this is the one task that wants it, so it only fires Escape after verifying true home first, never blindly. Enter is this project's established confirm/OK key (circle.py/gem_shop.py's own reward dismissal). Success is verified against driver.window_exists() actually going False, not just against the keypress having been sent, matching every other verified-keypress helper in this project. No force-kill fallback if the graceful path doesn't verify -- matches this project's "abort cleanly on unknown state rather than guess" convention (gem_shop/bounty) rather than reaching for driver.kill_game(). Confirmed live (2026-07-18) via a standalone `exit_game` run: the is_modal_open dim-probe correctly detected the exit-confirmation dialog after Escape, and the game closed cleanly after Enter. Not yet exercised as the tail end of a full daily/q4h preset run specifically. """ from ba_auto import navigation CONFIRM_RETRIES = 3 CLOSE_WAIT_ITERATIONS = 15 CLOSE_WAIT_INTERVAL = 1 def _raise_exit_dialog(driver, config): for attempt in range(1, CONFIRM_RETRIES + 1): if attempt == CONFIRM_RETRIES and driver.window_exists(): driver.focus_game() driver.keypress("Escape") driver.wait(1) if navigation.is_modal_open(driver): return True print(f"[exit_game] exit-confirmation dialog not detected after Escape (attempt {attempt}/{CONFIRM_RETRIES})") return False def _confirm_exit(driver, config): for attempt in range(1, CONFIRM_RETRIES + 1): driver.keypress("Return") driver.wait(1) for _ in range(CLOSE_WAIT_ITERATIONS): if not driver.window_exists(): return True driver.wait(CLOSE_WAIT_INTERVAL) print(f"[exit_game] game window still present after Enter (attempt {attempt}/{CONFIRM_RETRIES})") return not driver.window_exists() def run(driver, config): driver.focus_game() if not navigation.return_to_home(driver): print("[exit_game] could not confirm the true home screen -- aborting without pressing Escape") return if not _raise_exit_dialog(driver, config): print("[exit_game] could not confirm the exit-confirmation dialog opened -- aborting without pressing Enter") return if not _confirm_exit(driver, config): print("[exit_game] warning: could not confirm the game actually closed") return print("[exit_game] game closed.")