ba-auto-daily/ba_daily.py
Nik Afiq c5873f4e58 Refactor arena and lesson tasks for improved efficiency and reliability
- Updated arena.py to allow multiple battles per invocation, looping until tickets are exhausted or a rank-1 condition is met. Introduced _fight_one function for single battle logic and added cooldown handling between fights.
- Enhanced lesson.py to implement a tiered priority system for scheduling lessons based on student slots available, replacing the previous highest affection value selection. Introduced functions for scanning all regions and building a priority queue for lesson scheduling.
- Centralized return-to-home logic in ba_daily.py to ensure the game returns to the home screen before and after each task, improving robustness against navigation issues.
- Added retry mechanism for returning to home, allowing for transient navigation issues to be handled gracefully without aborting tasks.
2026-07-13 10:32:53 +09:00

148 lines
6.8 KiB
Python

#!/usr/bin/env python3
"""Python CLI entry point: ba_dailies.sh -> ba_daily.py -> ba_auto/tasks/*.py"""
import sys
from ba_auto import config, driver, navigation
from ba_auto.tasks import arena, bounty, cafe, event_sweep, lesson, mailbox, shop_common, shop_tactical, stamina, story_sweep
TASKS = {
"mailbox": mailbox.run,
"cafe": cafe.run,
"stamina": stamina.run,
"story_sweep": story_sweep.run,
"event_sweep": event_sweep.run,
"shop_common": shop_common.run,
"shop_tactical": shop_tactical.run,
"lesson": lesson.run,
"arena": arena.run,
"bounty": bounty.run,
}
# story_sweep, event_sweep, both shop tasks, lesson, arena, and bounty are
# opt-in only (not in the default flow): they spend AP/credits/tactical
# coin/lesson tickets/an arena ticket/a bounty ticket on an automated choice
# rather than reclaiming something free, which is a real resource decision
# the default unattended run shouldn't make blindly. Arena specifically
# fights a real ranked PvP battle each run -- see ba_auto/tasks/arena.py's
# module docstring.
DEFAULT_ORDER = ["mailbox", "cafe", "stamina"]
# How many times _ensure_home retries navigation.return_to_home as a whole
# (not to be confused with that function's own internal
# RETURN_HOME_MAX_ROUNDS press-loop) before giving up, and how long it waits
# between attempts. A single return_to_home call already presses through
# ordinary stuck subscreens/modals -- this outer retry exists for the
# genuinely transient case a single pass can't help with (a screen still
# mid-transition/loading at the moment it was checked, a slow network
# hiccup dialog that needs a few seconds to settle), giving that condition
# real time to resolve on its own between attempts rather than hammering
# the same check back-to-back.
PRE_TASK_HOME_RETRIES = 3
PRE_TASK_HOME_RETRY_WAIT = 5
def _ensure_home(name):
"""Self-healing pre-task recovery: retry navigation.return_to_home
across multiple bounded attempts (with a wait between, for a genuinely
transient condition to clear) rather than giving up after a single
pass. Returns True once home is confirmed, False if still not home
after PRE_TASK_HOME_RETRIES attempts -- callers should NOT abort on
False (see _run_task's own docstring for why), just proceed with
whatever confidence they have.
"""
for attempt in range(1, PRE_TASK_HOME_RETRIES + 1):
if navigation.return_to_home(driver):
return True
print(f"[{name}] still not confirmed home after recovery attempt {attempt}/{PRE_TASK_HOME_RETRIES}")
if attempt < PRE_TASK_HOME_RETRIES:
driver.wait(PRE_TASK_HOME_RETRY_WAIT)
return False
def _run_task(name):
"""Run one task, guaranteeing the game is at the home screen both before
it starts and after it ends -- success, a task's own early "abort
without pressing further keys" return, or an uncaught exception.
Centralized here rather than duplicated inside every task's own run()
so it can't be missed by an early-return path a task's own author
didn't think to guard against, or skipped entirely by a new task that
forgets to add it. An audit (2026-07-11, per explicit user request)
found this was a real, widespread gap: mailbox/cafe/stamina/shop_*/
lesson only pressed a single conditional Escape on some paths (nothing
at all on several early-failure paths), story_sweep.py had no home-
return cleanup anywhere including its own success path, and arena.py
only called navigation.return_to_home at the START of a run (recovering
from the *previous* run's leftover state) rather than at the end of its
own. Only event_sweep.py and bounty.py already called it on every path
-- see plan.md's "Return-to-home audit" entry for the full writeup.
The pre-task call turned out to matter just as much as the post-task
one, confirmed live the same session: every task's own navigation
(MAILBOX_ICON, WORK_ICON, etc.) is a fixed, home-screen-relative
coordinate. mailbox.py was run once with the game left mid-navigation
on an unrelated Event Quest screen (a leftover from an unrelated
network-disconnect popup, nothing to do with this project) -- its
MAILBOX_ICON click landed on that screen instead, and a chain of
false-positive state checks (see navigation.return_to_home's own
_not_home fix from the same incident) let it click blindly into a
completely unrelated event stage-info modal rather than the mailbox.
No resource was actually spent (confirmed by unchanged AP/credits), but
calling return_to_home before every task closes the gap outright rather
than relying on chance.
Per explicit user direction (2026-07-12): this is self-healing, not a
hard gate. _ensure_home retries across several bounded attempts rather
than giving up after one, and even if it still can't confirm home, the
task is attempted anyway -- never hard-aborted here. A task's own
individual navigation steps already verify their own state before
acting (mailbox's MAILBOX_ICON click, story_sweep's task-screen check,
etc.), so a task started from an unconfirmed state still fails safely
at its own first click-verify step rather than cascading blindly, the
same protection every task already has for a missed click mid-run.
A bare try/finally (no except) is used deliberately for the post-task
call: navigation.return_to_home always runs before control leaves this
function, but any exception a task raises still propagates normally
afterward rather than being swallowed -- a crash should still surface
as a crash, just with the game safely back at home first instead of
stuck wherever it failed.
"""
if not _ensure_home(name):
print(f"[{name}] warning: could not confirm starting from the home screen after {PRE_TASK_HOME_RETRIES} attempts -- proceeding anyway")
try:
TASKS[name](driver, config)
finally:
if not navigation.return_to_home(driver):
print(f"[{name}] warning: could not confirm return to home screen after task finished")
def main(argv):
args = argv[1:]
if args and args[0] == "--list-commands":
# Machine-readable command list for shell completion (see
# completions/ba_dailies.bash) -- kept as a thin read of TASKS
# itself so the completion list can never drift from what's
# actually dispatchable.
print(" ".join(TASKS.keys()))
return 0
if not args:
for name in DEFAULT_ORDER:
_run_task(name)
print("All done.")
return 0
command = args[0]
if command not in TASKS:
print(f"Unknown phase: {command}", file=sys.stderr)
print(f"Valid phases: {' '.join(TASKS.keys())}", file=sys.stderr)
return 1
_run_task(command)
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))