"""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.")