ba-auto-daily/ba_auto/tasks/exit_game.py
Nik Afiq 68a7ed2529 feat: cafe lowest-affection invite for room 2, exit_game AP-left-unspent alert, login window-flicker fix
- cafe.py: room 2 now invites the lowest-affection candidate instead of
  highest (room 1 unchanged), plus OCR'd invited-student name logging
- exit_game.py: alert (AP_ALERT: log tag, picked up by ba_cron_run.sh's
  Discord alerting) if AP is still unspent above threshold when the game
  closes
- login.py: re-check window_exists() after the stabilization wait to catch
  a startup window-flicker race that could crash a run
- config.py/mapping.md: supporting constants and reference-mapping updates

Also rewrites plan.md: retires the phase-by-phase changelog (now fully
superseded by CLAUDE.md's own documentation) down to forward-looking
backlog items, and adds a performance-improvement plan from a cron-log
time analysis (lesson.py's per-cell screenshot redundancy, event_sweep's
multi-call design).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 19:28:59 +09:00

96 lines
4.1 KiB
Python

"""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 _alert_if_ap_left_unspent(driver, config):
"""Print a distinctly-tagged, greppable log line if AP is still above
config.EXIT_AP_ALERT_THRESHOLD right as the game is about to close --
ba_cron_run.sh greps its own captured run output for the "AP_ALERT:"
tag and fires an extra Discord alert on it (see plan.md Phase 30).
Alert-only: never blocks or delays the exit itself.
Must run while still confirmed on true home (navigation.current_ap's
OCR rect is only meaningful there, same precondition already documented
on that function). An unreadable OCR result (None) is skipped silently,
matching this project's "don't guess on unreadable OCR" convention
rather than alerting on a possibly-wrong value.
"""
ap = navigation.current_ap(driver)
if ap is not None and ap > config.EXIT_AP_ALERT_THRESHOLD:
print(f"[exit_game] AP_ALERT: exiting with {ap} AP unspent (threshold {config.EXIT_AP_ALERT_THRESHOLD})")
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
_alert_if_ap_left_unspent(driver, config)
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.")