- Added `ba_auto/tasks/gem_shop.py` to automate claiming the daily free package from the gem shop. - Integrated color probes for state detection instead of template matching. - Updated `ba_daily.py` to include the new gem shop task in the default execution order. - Documented the implementation and calibration process in `plan.md`, including live testing results and fixes for dialog handling. - Addressed potential bugs related to dialog state verification and tab layout changes.
144 lines
6.6 KiB
Python
144 lines
6.6 KiB
Python
"""Gem shop (青輝石購入) daily free package. Ports module/collect_daily_free_power.py.
|
|
|
|
Reference flow (`implement`): to_main_page -> to_purchase_pyroxenes_menu
|
|
(open the dialog via the home-screen 青輝石購入 icon) -> to_purchase_type
|
|
("package") -> detect_free_power_availability -> if purchasable,
|
|
collect_daily_free_power (click the free card, confirm the 0-yen purchase,
|
|
wait for the reward) else log "already collected" -> return_to_main_page
|
|
(close the dialog). Skips entirely on CN server (JP/Global only) -- no
|
|
equivalent server concept in this project, so not ported.
|
|
|
|
The reference detects every step via image template matching (no OCR
|
|
anywhere in this flow) -- this port uses plain color probes for the same
|
|
states instead (see config.py's "Gem shop daily free package" section for
|
|
the full live-calibration writeup and exact pixel values), matching this
|
|
project's own established equivalent for a simple, high-contrast
|
|
state-A/state-B visual difference, same as cafe.py's CLAIM_DISABLED_RGB or
|
|
stamina.py's MISSION_CLAIM_PROBE.
|
|
|
|
Live-calibrated 2026-07-15 by manually driving the real dialog end-to-end
|
|
on nik-gpu, which genuinely claimed the account's real free package for the
|
|
day (0 yen, confirmed +10 AP / +10,000 credits via before/after counter
|
|
values) -- see scratchpad/gem_shop_*.png for the captures. That exercised
|
|
the "available -> claim it" path for real, via raw xdotool/scrot rather
|
|
than this actual module. This module was then live-tested for real
|
|
against the resulting "already claimed today" state (see plan.md's Gem
|
|
shop phase) -- the "available -> claim" code path itself is implemented
|
|
against the same coordinates/logic already confirmed live, but has not
|
|
yet been exercised by this actual module end-to-end; treat as
|
|
implemented-but-unverified for that one path until a run happens naturally
|
|
on a day the package hasn't been claimed yet.
|
|
|
|
A `reference-parity-reviewer` pass the same day caught a real bug in the
|
|
first version of this module: the final dialog-close step verified success
|
|
via `navigation.is_on_subscreen`, but config.py's own writeup on
|
|
`GEM_SHOP_DIALOG_PROBES` already documents that probe as blind to this
|
|
specific dialog (reads the same whether it's open or closed) -- so that
|
|
check could never fail, making a stuck-open dialog unrecoverable by this
|
|
module (and, per the same blindness, by `navigation.return_to_home`'s
|
|
shared fallback too). Fixed by adding `_close_gem_shop`, which verifies
|
|
against the module's own correctly-calibrated `_dialog_open` instead, with
|
|
a bounded retry -- reused for both closing paths (the "unknown status"
|
|
abort and the normal end-of-run close), replacing the original's second,
|
|
separately-broken blind Escape in the abort path too.
|
|
"""
|
|
|
|
def _dialog_open(driver, config):
|
|
for x, y in config.GEM_SHOP_DIALOG_PROBES:
|
|
r, g, b = driver.color_at(x, y)
|
|
spread = max(r, g, b) - min(r, g, b)
|
|
if not (min(r, g, b) > config.GEM_SHOP_DIALOG_PROBE_MIN_CHANNEL and spread < config.GEM_SHOP_DIALOG_PROBE_MAX_SPREAD):
|
|
return False
|
|
return True
|
|
|
|
|
|
def _open_gem_shop(driver, config):
|
|
for attempt in range(1, config.GEM_SHOP_ICON_RETRIES + 1):
|
|
driver.click(*config.GEM_SHOP_ICON)
|
|
driver.wait(2)
|
|
if _dialog_open(driver, config):
|
|
return True
|
|
print(f"[gem_shop] dialog not detected after click (attempt {attempt}/{config.GEM_SHOP_ICON_RETRIES})")
|
|
return False
|
|
|
|
|
|
def _open_package_tab(driver, config):
|
|
for attempt in range(1, config.GEM_SHOP_ICON_RETRIES + 1):
|
|
driver.click(*config.GEM_SHOP_PACKAGE_TAB)
|
|
driver.wait(1.5)
|
|
status = _read_free_card_status(driver, config)
|
|
if status != "unknown":
|
|
return status
|
|
print(f"[gem_shop] package tab's free card not recognized after click (attempt {attempt}/{config.GEM_SHOP_ICON_RETRIES})")
|
|
return "unknown"
|
|
|
|
|
|
def _color_matches(rgb, target, tolerance):
|
|
return all(abs(c - t) <= tolerance for c, t in zip(rgb, target))
|
|
|
|
|
|
def _read_free_card_status(driver, config):
|
|
rgb = driver.color_at(*config.GEM_SHOP_FREE_CARD_STATUS_PROBE)
|
|
if _color_matches(rgb, config.GEM_SHOP_STATUS_CLAIMED_RGB, config.GEM_SHOP_STATUS_TOLERANCE):
|
|
return "claimed"
|
|
if _color_matches(rgb, config.GEM_SHOP_STATUS_AVAILABLE_RGB, config.GEM_SHOP_STATUS_TOLERANCE):
|
|
return "available"
|
|
return "unknown"
|
|
|
|
|
|
def _close_gem_shop(driver, config):
|
|
# Verify with the module's own _dialog_open probe rather than
|
|
# navigation.is_on_subscreen/is_modal_open -- both are documented above
|
|
# (see the "Generic... dialog... showing" comment in config.py) to read
|
|
# the same regardless of whether this specific dialog is open or closed,
|
|
# so a check against either can never fail and would silently mask a
|
|
# stuck-open dialog (caught in review before this was live-tested: the
|
|
# original version of this function used is_on_subscreen for exactly
|
|
# that dead check).
|
|
for attempt in range(1, config.GEM_SHOP_ICON_RETRIES + 1):
|
|
driver.keypress("Escape")
|
|
driver.wait(1.5)
|
|
if not _dialog_open(driver, config):
|
|
return True
|
|
print(f"[gem_shop] dialog still open after Escape (attempt {attempt}/{config.GEM_SHOP_ICON_RETRIES})")
|
|
return False
|
|
|
|
|
|
def _claim_free_package(driver, config):
|
|
driver.click(*config.GEM_SHOP_FREE_CARD_BUY_BUTTON)
|
|
driver.wait(2)
|
|
for _ in range(config.GEM_SHOP_CLAIM_MAX_ATTEMPTS):
|
|
if _read_free_card_status(driver, config) == "claimed":
|
|
return True
|
|
driver.keypress("Return")
|
|
driver.wait(2)
|
|
return _read_free_card_status(driver, config) == "claimed"
|
|
|
|
|
|
def run(driver, config):
|
|
driver.focus_game()
|
|
|
|
if not _open_gem_shop(driver, config):
|
|
print("[gem_shop] could not confirm the gem shop dialog opened, aborting without pressing further keys")
|
|
return
|
|
|
|
status = _open_package_tab(driver, config)
|
|
if status == "unknown":
|
|
print("[gem_shop] could not read the free package card's status, aborting without pressing further keys")
|
|
if not _close_gem_shop(driver, config):
|
|
print("[gem_shop] warning: could not confirm the dialog closed")
|
|
return
|
|
|
|
if status == "claimed":
|
|
print("[gem_shop] daily free package already collected today")
|
|
else:
|
|
print("[gem_shop] claiming daily free package (+10 AP, +10,000 credits)")
|
|
if _claim_free_package(driver, config):
|
|
print("[gem_shop] claimed daily free package")
|
|
else:
|
|
print("[gem_shop] warning: could not confirm the free package was claimed after retries")
|
|
|
|
if not _close_gem_shop(driver, config):
|
|
print("[gem_shop] warning: could not confirm the dialog closed")
|
|
print("[gem_shop] Done.")
|