155 lines
7.9 KiB
Python
155 lines
7.9 KiB
Python
"""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.")
|