feat: enhance navigation and shop utilities with improved visibility checks and price reading retries

This commit is contained in:
Nik Afiq 2026-07-31 13:48:25 +09:00
parent ffcacc8738
commit a15ac6111e
5 changed files with 117 additions and 24 deletions

View File

@ -772,6 +772,18 @@ SHOP_CHECKED_RGB = ((60, 130, 80), (220, 245, 115))
# for the largest configured price without bleeding into the neighboring
# column's card.
SHOP_PRICE_OCR_OFFSET = (48, 166, 145, 195)
# A real live run (script_error.md, 2026-07-31) misread '中級レポート's
# price as 1250060 instead of 125000 -- a stray extra digit, not a
# systematically wrong crop (every other target, including three other
# 6-digit prices in this same list, read correctly in that same run, and
# re-summing the run's own "bought N item(s) for TOTAL credits" log against
# every OTHER target's price confirms it). One-off tesseract flake, same
# class of transient misread this project already retries elsewhere (see
# driver.read_screenshot's decode-retry doc) rather than a calibration
# problem -- so shop_utils.select_targets re-reads the price up to this
# many times, taking the first read that matches expected_price, before
# concluding the catalog actually changed.
SHOP_PRICE_OCR_RETRIES = 3
SHOP_BUY_BUTTON = (1751, 1112)
SHOP_CANCEL_BUTTON = (1525, 1112)
@ -1479,9 +1491,10 @@ LOGIN_NEWS_HEADER_TOLERANCE = 35
# was confirmed live to uniquely hold on true home and fail on every other
# captured state (both title-screen background arts, the attendance card,
# and home with the news dialog still open and dimming this same area).
LOGIN_HOME_NAV_BAR_PROBES = ((430, 1150), (720, 1150), (940, 1150), (1230, 1150), (1430, 1150))
LOGIN_HOME_NAV_BAR_MIN_CHANNEL = 220
LOGIN_HOME_NAV_BAR_MAX_SPREAD = 25
# Promoted to navigation.is_home_nav_bar_visible/HOME_NAV_BAR_* (2026-07-31)
# once battle_pass.py needed the same positive signal for the same reason
# -- see that module's docstring. Matches navigation.py's own convention of
# owning its probe constants locally rather than sourcing them from here.
LOGIN_POLL_INTERVAL = 2
# Generous per-attempt budget: a normal run (title tap -> loading ->

View File

@ -95,6 +95,33 @@ def is_header_bar_visible(driver):
return True
# Bottom nav bar's own flat near-white background band -- present ONLY on
# the true home screen (originally calibrated for login.py, see its module
# docstring for the two real false-positive frames this was hardened
# against: a bright loading-transition wipe, and a still-open news dialog).
# Promoted here (2026-07-31) because it is the only POSITIVE "genuinely
# home" signal in this project -- is_on_subscreen/is_modal_open are both
# negative checks, and battle_pass.py found live that BOTH read false/false
# on its own pass-menu/mission screens exactly like true home does (neither
# has a bright subscreen header or a dark modal backdrop). This band sits at
# y=1150, close under battle pass's own dark footer probe row (y=1190,
# config.BATTLE_PASS_INSIDE_PROBES) -- the footer band is dark/flat across
# that whole area, so this row reads correctly non-home while any battle
# pass screen is still open, unlike the ambiguous shared probes above.
HOME_NAV_BAR_PROBES = ((430, 1150), (720, 1150), (940, 1150), (1230, 1150), (1430, 1150))
HOME_NAV_BAR_MIN_CHANNEL = 220
HOME_NAV_BAR_MAX_SPREAD = 25
def is_home_nav_bar_visible(driver):
for r, g, b in driver.colors_at(HOME_NAV_BAR_PROBES):
if min(r, g, b) < HOME_NAV_BAR_MIN_CHANNEL:
return False
if max(r, g, b) - min(r, g, b) > HOME_NAV_BAR_MAX_SPREAD:
return False
return True
def _not_home(driver):
# "Home" means neither on a subscreen NOR under an open modal. Checking
# only is_on_subscreen was found live (2026-07-11, ba_daily.py's

View File

@ -41,11 +41,36 @@ task's two screens behaved the same way live, including a real level-up
This is a pure reclaim like mailbox/cafe/stamina/gem_shop/circle: nothing
here spends AP/credits/tickets, so it belongs in ba_daily.py's
DEFAULT_ORDER rather than staying opt-in.
**Real bug, found from a live `daily` preset run (2026-07-31, see
script_error.md)**: the exit loop below used to stop as soon as
_inside_battle_pass(driver, config) read False, with no further
verification and no failure print. That is a NEGATIVE condition only -- it
proves we're no longer showing battle pass's own dark footer band, not that
we've actually reached true home. A run landed on event_sweep straight
after battle_pass with three consecutive "event screen not detected after
click" retries and no warning logged in between, meaning both this task's
own exit loop AND ba_daily.py's generic post-task navigation.return_to_home
cleanup (which the module docstring above already documents can't tell the
pass menu apart from home either) silently agreed we were home when we
almost certainly weren't -- most likely a still-open reward-reveal card
(e.g. a level-up bundling more than one item, needing more than the single
extra dismiss Enter _claim_all already sends) darkening/covering that
footer probe just enough to misread as "left". Fixed by also requiring
navigation.is_home_nav_bar_visible(driver) -- the same positive bottom-nav-
bar signal login.py needed for the identical class of false-positive -- and
printing a clear warning if the loop still can't confirm true home after
EXIT_MAX_ROUNDS, so a stuck battle pass screen shows up in logs instead of
masquerading as "Done." Not yet re-confirmed live against a real repeat of
this exact stuck state (would need a real level-up to reproduce on demand).
"""
from ba_auto import navigation
OPEN_RETRIES = 3
MISSION_RETRIES = 3
CLAIM_MAX_ROUNDS = 3
DISMISS_EXTRA_PRESSES = 2
EXIT_MAX_ROUNDS = 4
@ -123,11 +148,19 @@ def _claim_all(driver, config):
print("[battle_pass] claiming reward")
driver.keypress("Return")
driver.wait(1.5)
# dismiss the reward-reveal ("報酬獲得!"/TOUCH) popup; harmless
# no-op if nothing is actually showing, same assumption as
# stamina.py's identical second press
driver.keypress("Return")
driver.wait(1.5)
# Dismiss the reward-reveal ("報酬獲得!"/TOUCH) popup -- pressed
# DISMISS_EXTRA_PRESSES times rather than once, each a harmless
# no-op if nothing is actually showing (same assumption as
# stamina.py's own single extra press). Widened to more than one
# after a real stuck run (see this module's docstring) where the
# leading suspect is a level-up bundling more than one item into
# separate stacked cards, which a single dismiss press wouldn't
# fully clear -- leaving a card open that the next round's
# _claim_enabled check (covered by that same card) would then
# misread as "nothing left to claim" rather than "still blocked".
for _ in range(DISMISS_EXTRA_PRESSES):
driver.keypress("Return")
driver.wait(1.2)
claimed_any = True
return claimed_any
@ -153,10 +186,25 @@ def run(driver, config):
else:
print("[battle_pass] could not confirm the pass menu for the level-reward claim, skipping")
for _ in range(EXIT_MAX_ROUNDS):
if not _inside_battle_pass(driver, config):
break
driver.keypress("Escape")
driver.wait(1.5)
if not _exit_to_home(driver, config):
print("[battle_pass] warning: could not confirm return to home screen after exiting the pass menu")
print("[battle_pass] Done.")
def _confirmed_home(driver, config):
# Both conditions matter: _inside_battle_pass alone is a negative check
# (only proves the pass-menu footer band is gone, not that true home was
# actually reached -- see this module's docstring for the real stuck-run
# this fooled), and is_home_nav_bar_visible alone was calibrated against
# login.py's pre-login states, not battle pass's own screens.
return not _inside_battle_pass(driver, config) and navigation.is_home_nav_bar_visible(driver)
def _exit_to_home(driver, config):
for _ in range(EXIT_MAX_ROUNDS):
if _confirmed_home(driver, config):
return True
driver.keypress("Escape")
driver.wait(1.5)
return _confirmed_home(driver, config)

View File

@ -281,18 +281,9 @@ def _loading_buffer_visible(driver, config):
return all(_color_matches(c, config.LOGIN_LOADING_BUFFER_RGB, config.LOGIN_LOADING_BUFFER_TOLERANCE) for c in colors)
def _home_nav_bar_visible(driver, config):
for r, g, b in driver.colors_at(config.LOGIN_HOME_NAV_BAR_PROBES):
if min(r, g, b) < config.LOGIN_HOME_NAV_BAR_MIN_CHANNEL:
return False
if max(r, g, b) - min(r, g, b) > config.LOGIN_HOME_NAV_BAR_MAX_SPREAD:
return False
return True
def _true_home(driver, config):
return (
_home_nav_bar_visible(driver, config)
navigation.is_home_nav_bar_visible(driver)
and not navigation.is_on_subscreen(driver)
and not navigation.is_modal_open(driver)
and not _news_dialog_open(driver, config)

View File

@ -42,11 +42,25 @@ def select_targets(driver, config, targets):
it registers as checked. Returns (selected, skipped); a price mismatch
or an unconfirmed checkbox skips just that target rather than aborting
the whole run (mirrors story_sweep's per-target skip-not-abort design).
Re-reads the price up to config.SHOP_PRICE_OCR_RETRIES times, stopping
early on the first read that matches expected_price -- a real live run
misread one target's price by a single stray digit while every other
target (including other 6-digit prices) read correctly in the same run,
so a single bad frame shouldn't skip a real purchase. A genuine catalog
change still reads the same (wrong) value across every retry and gets
skipped as before. See config.SHOP_PRICE_OCR_RETRIES's own comment.
"""
selected = []
skipped = []
for row, col, name, expected_price in targets:
price = detector.read_int(_price_rect(config, row, col))
price = None
for attempt in range(1, config.SHOP_PRICE_OCR_RETRIES + 1):
price = detector.read_int(_price_rect(config, row, col))
if price == expected_price:
break
if attempt < config.SHOP_PRICE_OCR_RETRIES:
driver.wait(0.3)
if price != expected_price:
print(f"[shop] '{name}' price read as {price}, expected {expected_price} -- skipping (catalog may have changed)")
skipped.append((name, "price_mismatch"))