"""Circle (サークル / guild) daily check-in. Ports module/group.py. Reference flow (`implement`): to_main_page -> to_group. `to_group` reacts to the 'main_page' rgb state by clicking a fixed screen position to open the circle from the main page, polling (`picture.co_detect`) until one of three image-template terminal states is reached: `group_sign-up-reward` (first entry today, +10 AP granted, claimable from mailbox), `group_menu` (already checked in today, just the plain circle menu), or `group_join-club` (account isn't in a circle at all). This project's client reaches the same screen via home -> bottom-nav ソーシャル (Social) icon -> サークル (Circle) card (the reference's own single fixed click is a single fixed *screen position* too, but on a different, earlier menu layout than this client's bottom-nav + hub-page structure). If this is the first entry today, a "今日のサークルへの参加報酬" (today's circle participation reward) modal appears automatically (AP x10, "報酬はメールボックスから受け取ることができます" -- claim later from mailbox, matching the reference's own sign-up-reward outcome and this task's explicit scope: enter only, no mailbox claim here) -- dismissed via Enter/OK. If already checked in today, the same click sequence lands directly on the サークル chat/member screen with no modal. Told apart via the existing shared `navigation.is_modal_open`/`is_on_subscreen` -- no new probe needed, pixel-confirmed live against real captures: the reward modal dims `MODAL_DIM_PROBE` to ~(110,115,115) (is_modal_open true), the already-checked-in screen has no modal but a proper opaque header bar (~(247,250,252) at `SUBSCREEN_HEADER_PROBE`, is_on_subscreen true) -- `is_modal_open(driver) or is_on_subscreen(driver)` is true in both real "reached the circle screen" states and false on both the true home screen and the intermediate ソーシャル hub page (confirmed live: that hub page keeps the home screen's own background art dimly visible behind its card grid rather than a proper opaque header, so it reads dark/false at `SUBSCREEN_HEADER_PROBE` too, same as home -- it does NOT have its own reliable single-point signal distinct from home, unlike every other subscreen in this project. Rather than inventing an uncalibrated probe for that intermediate state, `_enter_circle` treats "click Social, click Circle" as one combined navigation attempt and retries the whole pair if the *final* destination isn't confirmed -- safe to repeat: both the Social icon and the Circle card stay clickable no-ops if we're already partway through this navigation on a retry). The "not in a circle" case (reference's `group_join-club`) is NOT ported -- this account is already a circle member (confirmed live), and per explicit user scope this task only performs the entry, nothing else. If ever run on an account with no circle, whatever join-prompt screen appears is only guaranteed to abort cleanly if it fails BOTH is_modal_open/is_on_subscreen -- `run()` now calls navigation.return_to_home at the very start (see below) specifically to close the more likely false-positive source instead (a leftover subscreen/modal from a stuck prior run), but a join-prompt that happens to render as an ordinary subscreen or modal could still be misread as "already checked in" rather than "not in a circle". Acceptable given this account is always a real member and the task's own low stakes (silently skipping a free reward once, not spending anything) -- not acceptable to leave unexamined if this is ever run against a different account. Live-calibrated 2026-07-15 on nik-gpu by driving the real flow end-to-end (see plan.md's Circle phase): confirmed both real outcomes (the actual first-entry-today reward dialog, +10 AP; and the already-checked-in straight-to-chat state on immediate re-entry), and confirmed a single Escape from the サークル screen returns directly to the true home screen in one step (skipping back through the ソーシャル hub page) -- per explicit user direction, this task presses Escape directly rather than clicking `navigation.BACK_BUTTON`, matching mailbox.py's own end-of-task convention. A `reference-parity-reviewer` pass the same day caught three real gaps in the first version, all fixed: 1. `_reached_circle`'s generic is_modal_open/is_on_subscreen check could false-positive from a stuck non-home starting state (a leftover modal/ subscreen from a prior run), reading "already checked in" on attempt 1 without either click having done anything -- the exact class of bug arena.py/bounty.py already fixed for themselves by calling navigation.return_to_home at the top of their own run(). Ported the same fix here. 2. The reward-dismiss Enter press and the closing Escape were never verified to have worked -- same "unverified keypress assumed to work" shape as the cafe.py/gem_shop.py bugs fixed earlier this same session. A dropped Enter press would leave the reward modal open, then the very next line's unconditional Escape would risk cancelling it instead of having claimed it -- silently declining the day's free AP with no warning logged, non-recoverable until the next daily reset. Fixed with `_dismiss_reward`/`_leave_circle`, both bounded retry-until-verified loops matching gem_shop.py's `_claim_free_package`/`_close_gem_shop`. 3. `_enter_circle`'s retry loop had no driver.focus_game() escalation for the XIGNCODE anti-cheat overlay -- confirmed to have actually occurred live during this task's own calibration (it stole a BACK_BUTTON click mid-session). Every other retry loop in this project bitten by this (navigation.return_to_home, navigation.click_back, arena's own navigation) escalates via focus_game() partway through its budget; this one now does too, right before the final attempt, matching click_back's own convention. """ from ba_auto import navigation ENTER_RETRIES = 3 def _reached_circle(driver, config): return navigation.is_modal_open(driver) or navigation.is_on_subscreen(driver) def _enter_circle(driver, config): for attempt in range(1, ENTER_RETRIES + 1): if attempt == ENTER_RETRIES: driver.focus_game() driver.click(*config.SOCIAL_ICON) driver.wait(1.5) driver.click(*config.CIRCLE_CARD) driver.wait(2) if _reached_circle(driver, config): return True print(f"[circle] circle screen not detected after click (attempt {attempt}/{ENTER_RETRIES})") return False def _dismiss_reward(driver, config): for attempt in range(1, ENTER_RETRIES + 1): driver.keypress("Return") driver.wait(1.5) if not navigation.is_modal_open(driver): return True print(f"[circle] reward dialog still open after Enter (attempt {attempt}/{ENTER_RETRIES})") return not navigation.is_modal_open(driver) def _leave_circle(driver, config): for attempt in range(1, ENTER_RETRIES + 1): driver.keypress("Escape") driver.wait(1) if not _reached_circle(driver, config): return True print(f"[circle] circle screen still open after Escape (attempt {attempt}/{ENTER_RETRIES})") return not _reached_circle(driver, config) def run(driver, config): driver.focus_game() navigation.return_to_home(driver) if not _enter_circle(driver, config): print("[circle] could not confirm the circle screen opened, aborting without pressing further keys") return if navigation.is_modal_open(driver): print("[circle] claiming today's circle check-in reward (+10 AP, claim later from mailbox)") if not _dismiss_reward(driver, config): print("[circle] warning: could not confirm the reward dialog closed") else: print("[circle] circle already checked in today") if not _leave_circle(driver, config): print("[circle] warning: could not confirm return to the home screen") print("[circle] Done.")