feat: enhance navigation and shop utilities with improved visibility checks and price reading retries
This commit is contained in:
parent
ffcacc8738
commit
a15ac6111e
@ -772,6 +772,18 @@ SHOP_CHECKED_RGB = ((60, 130, 80), (220, 245, 115))
|
|||||||
# for the largest configured price without bleeding into the neighboring
|
# for the largest configured price without bleeding into the neighboring
|
||||||
# column's card.
|
# column's card.
|
||||||
SHOP_PRICE_OCR_OFFSET = (48, 166, 145, 195)
|
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_BUY_BUTTON = (1751, 1112)
|
||||||
SHOP_CANCEL_BUTTON = (1525, 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
|
# was confirmed live to uniquely hold on true home and fail on every other
|
||||||
# captured state (both title-screen background arts, the attendance card,
|
# captured state (both title-screen background arts, the attendance card,
|
||||||
# and home with the news dialog still open and dimming this same area).
|
# 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))
|
# Promoted to navigation.is_home_nav_bar_visible/HOME_NAV_BAR_* (2026-07-31)
|
||||||
LOGIN_HOME_NAV_BAR_MIN_CHANNEL = 220
|
# once battle_pass.py needed the same positive signal for the same reason
|
||||||
LOGIN_HOME_NAV_BAR_MAX_SPREAD = 25
|
# -- 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
|
LOGIN_POLL_INTERVAL = 2
|
||||||
# Generous per-attempt budget: a normal run (title tap -> loading ->
|
# Generous per-attempt budget: a normal run (title tap -> loading ->
|
||||||
|
|||||||
@ -95,6 +95,33 @@ def is_header_bar_visible(driver):
|
|||||||
return True
|
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):
|
def _not_home(driver):
|
||||||
# "Home" means neither on a subscreen NOR under an open modal. Checking
|
# "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
|
# only is_on_subscreen was found live (2026-07-11, ba_daily.py's
|
||||||
|
|||||||
@ -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
|
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
|
here spends AP/credits/tickets, so it belongs in ba_daily.py's
|
||||||
DEFAULT_ORDER rather than staying opt-in.
|
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
|
OPEN_RETRIES = 3
|
||||||
MISSION_RETRIES = 3
|
MISSION_RETRIES = 3
|
||||||
CLAIM_MAX_ROUNDS = 3
|
CLAIM_MAX_ROUNDS = 3
|
||||||
|
DISMISS_EXTRA_PRESSES = 2
|
||||||
EXIT_MAX_ROUNDS = 4
|
EXIT_MAX_ROUNDS = 4
|
||||||
|
|
||||||
|
|
||||||
@ -123,11 +148,19 @@ def _claim_all(driver, config):
|
|||||||
print("[battle_pass] claiming reward")
|
print("[battle_pass] claiming reward")
|
||||||
driver.keypress("Return")
|
driver.keypress("Return")
|
||||||
driver.wait(1.5)
|
driver.wait(1.5)
|
||||||
# dismiss the reward-reveal ("報酬獲得!"/TOUCH) popup; harmless
|
# Dismiss the reward-reveal ("報酬獲得!"/TOUCH) popup -- pressed
|
||||||
# no-op if nothing is actually showing, same assumption as
|
# DISMISS_EXTRA_PRESSES times rather than once, each a harmless
|
||||||
# stamina.py's identical second press
|
# 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.keypress("Return")
|
||||||
driver.wait(1.5)
|
driver.wait(1.2)
|
||||||
claimed_any = True
|
claimed_any = True
|
||||||
return claimed_any
|
return claimed_any
|
||||||
|
|
||||||
@ -153,10 +186,25 @@ def run(driver, config):
|
|||||||
else:
|
else:
|
||||||
print("[battle_pass] could not confirm the pass menu for the level-reward claim, skipping")
|
print("[battle_pass] could not confirm the pass menu for the level-reward claim, skipping")
|
||||||
|
|
||||||
for _ in range(EXIT_MAX_ROUNDS):
|
if not _exit_to_home(driver, config):
|
||||||
if not _inside_battle_pass(driver, config):
|
print("[battle_pass] warning: could not confirm return to home screen after exiting the pass menu")
|
||||||
break
|
|
||||||
driver.keypress("Escape")
|
|
||||||
driver.wait(1.5)
|
|
||||||
|
|
||||||
print("[battle_pass] Done.")
|
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)
|
||||||
|
|||||||
@ -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)
|
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):
|
def _true_home(driver, config):
|
||||||
return (
|
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_on_subscreen(driver)
|
||||||
and not navigation.is_modal_open(driver)
|
and not navigation.is_modal_open(driver)
|
||||||
and not _news_dialog_open(driver, config)
|
and not _news_dialog_open(driver, config)
|
||||||
|
|||||||
@ -42,11 +42,25 @@ def select_targets(driver, config, targets):
|
|||||||
it registers as checked. Returns (selected, skipped); a price mismatch
|
it registers as checked. Returns (selected, skipped); a price mismatch
|
||||||
or an unconfirmed checkbox skips just that target rather than aborting
|
or an unconfirmed checkbox skips just that target rather than aborting
|
||||||
the whole run (mirrors story_sweep's per-target skip-not-abort design).
|
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 = []
|
selected = []
|
||||||
skipped = []
|
skipped = []
|
||||||
for row, col, name, expected_price in targets:
|
for row, col, name, expected_price in targets:
|
||||||
|
price = None
|
||||||
|
for attempt in range(1, config.SHOP_PRICE_OCR_RETRIES + 1):
|
||||||
price = detector.read_int(_price_rect(config, row, col))
|
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:
|
if price != expected_price:
|
||||||
print(f"[shop] '{name}' price read as {price}, expected {expected_price} -- skipping (catalog may have changed)")
|
print(f"[shop] '{name}' price read as {price}, expected {expected_price} -- skipping (catalog may have changed)")
|
||||||
skipped.append((name, "price_mismatch"))
|
skipped.append((name, "price_mismatch"))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user