diff --git a/ba_auto/config.py b/ba_auto/config.py index 9de89fe..da4cd3b 100644 --- a/ba_auto/config.py +++ b/ba_auto/config.py @@ -1148,3 +1148,14 @@ GEM_SHOP_ICON_RETRIES = 3 # animation was still playing, so this needs real patience, not just 1-2 # tries. GEM_SHOP_CLAIM_MAX_ATTEMPTS = 6 + +# Circle (サークル / guild), ported from module/group.py. Home -> bottom-nav +# ソーシャル (Social) icon -> サークル (Circle) card, opening the circle's +# chat/member screen. Live-calibrated 2026-07-15 on nik-gpu (real account, +# already a circle member -- "not in a circle" per the reference's own +# group_join-club outcome was not exercised or ported, see circle.py's +# module docstring). +SOCIAL_ICON = (812, 1080) +# サークル card, leftmost of three (サークル/フレンド/助っ人) on the +# ソーシャル hub page. +CIRCLE_CARD = (463, 613) diff --git a/ba_auto/reference_notes/mapping.md b/ba_auto/reference_notes/mapping.md index 89e2e2a..4458df0 100644 --- a/ba_auto/reference_notes/mapping.md +++ b/ba_auto/reference_notes/mapping.md @@ -10,7 +10,7 @@ Maps each local feature to the corresponding `~/repo/baas-reference/module/...` | Gem shop daily free package | `module/collect_daily_free_power.py` | `implement`, `to_purchase_pyroxenes_menu`, `to_purchase_type`, `detect_free_power_availability`, `collect_daily_free_power`, `return_to_main_page` | `ba_auto/tasks/gem_shop.py` | Reference detects every step via `core.picture.co_detect` + fixed-region OpenCV template matching (no OCR anywhere in this reference flow) — ported here as plain color probes instead, matching this project's own established equivalent for a simple flat-color state-A/state-B difference (see `cafe.py`'s `CLAIM_DISABLED_RGB`, `stamina.py`'s `MISSION_CLAIM_PROBE`) rather than building new template assets. The free card's own status bar reads flat dark navy (~(41,65,90)) when available vs flat dark red (~(144,38,47)) once claimed today — a clean, high-contrast signal pixel-scanned from real captures, no OCR of the "0"/"1" count text needed. A dedicated 4-point "is this dialog (or a nested confirm/notice card) showing" check (`GEM_SHOP_DIALOG_PROBES`) was needed because `navigation.is_on_subscreen`/`is_modal_open`'s default probes both proved unreliable on this dialog (it overlays the home screen directly rather than being a full subscreen, and its own opaque white card sits right on top of `is_modal_open`'s probe point) — same class of default-probe mismatch `story_sweep.py`/`bounty.py` already documented for their own wide modals, fixed the same multi-point-beats-single-point way `navigation.is_header_bar_visible` was fixed for cafe's rank-up cutscene the same day. | Done. Live-calibrated against nik-gpu 2026-07-15 by manually driving the real dialog end-to-end via raw xdotool/scrot (see `scratchpad/gem_shop_*.png`) — this **genuinely claimed the account's real free package for the day** (0 yen, confirmed +10 AP / +10,000 credits via before/after counter values: AP 64→74, credits 161,391,144→161,401,144), which calibrated both the "available" and "already claimed" visual states from real data. The actual `gem_shop.py` module was then live-tested for real against the resulting "already claimed today" state. The "available → claim" code path itself uses the same coordinates/logic already confirmed live via the manual walkthrough, but has not yet been exercised by the module itself end-to-end — worth a follow-up live check the next time the package resets and hasn't been claimed yet. Added to `DEFAULT_ORDER` (unlike most recent additions): it only ever reclaims a genuinely free, once-per-day resource with no choice to make, matching mailbox/cafe/stamina's own "reclaim something free" category rather than the "spends a resource, needs opt-in" category. | | Normal/Hard story AP sweep | `module/explore_tasks/sweep_task.py`, `module/explore_tasks/task_utils.py` | `to_region` (ported: OCR region-number readout + delta-click), a scoped-down `swipe_search_target_str` (ported: OCR stage-row label matching), `start_sweep`'s named-outcome contract (ported via `navigation.wait_for_state`, this project's scoped `co_detect` port) | `ba_auto/tasks/story_sweep.py` | OCR region/stage-name matching, ported for real (Phase 10) — replaces Phase 9's "next-region arrow stops advancing, then random stage" heuristic; MAX click verified via `SWEEP_MINUS_BUTTON_PROBE` color check (reused, still correct), modal closed via its own X button (Escape doesn't close it; X-button position re-calibrated per stage-layout variant, see Phase 10) | Done (see `plan.md` Phase 10, supersedes Phase 9). Config-driven exact `(region, stage, count)` targets (`config.STORY_SWEEP_TARGETS`), not latest-region/random-stage. Opt-in only, not in default flow | | Event sweep | `module/sweep_activity.py`, `module/activities/activity_utils.py` | `activity_sweep` (main flow), `to_activity` (nav to the event's Story/Mission/Challenge tabs), `check_sweep_availability`/`color.check_sweep_availability` (SSS gate), `start_sweep`'s named-outcome contract (shared with story_sweep's, ported the same way via `navigation.wait_for_state`) | `ba_auto/tasks/event_sweep.py`, `ba_auto/navigation.py` (`return_to_home`) | `to_activity`'s bottom-nav-icon entry -> this client's home-screen event badge (`config.EVENT_BADGE_ICON`, confirmed live to be a rotating carousel -- see Status); the reference's config-string sweep-list parsing (arbitrary stage lists, per-stage float/fraction counts via `preprocess_activity_region`/`preprocess_activity_sweep_times`) -> a single date-ordinal-modulo rotation target over a fixed 9-12 sub-range, mirroring `story_sweep.py`'s own rotation, per explicit user direction; stage-number OCR (`detector.read_int`) replaces the reference's `swipe_search_target_str` template-button search, since this client's stage list only ever needs its bottom scroll extreme for the 9-12 target range | Done, with a live-reported bug fixed (see `plan.md`'s Phase 14 follow-up). Live-calibrated against nik-gpu 2026-07-10 against the currently-running "鉄道爆走事件" event (12 stages) -- zero real AP spent during calibration (every confirm dialog reached was cancelled via Escape, verified by the AP counter). The stage-info modal is structurally identical to story_sweep's (MIN/-/+/MAX stepper, same AP-confirm dialog reusing `SWEEP_CONFIRM_*`/`SWEEP_RESULT_BUTTON_REGION`), but simpler: no region navigation, one fixed modal layout confirmed across two different stages (09 and 12), and the modal closes on Escape (story_sweep's doesn't). The reference's SSS-availability gate for a never-cleared stage was never exercised (every stage 9-12 on this account was already 3-starred) -- `event_sweep.py` handles that gate the same defensive way story_sweep handles an unavailable target: if the MAX-button count-raise can't be verified, it aborts without spending AP rather than guessing. **Bug found on a real run**: the home-screen event badge turned out to be a rotating carousel (cycles between the current event's countdown and other notices, e.g. a finished event's leftover reward-claim reminder), so a click could land on a stale event's page instead. Fixed via a new shared `navigation.return_to_home` primitive (bounded press-back-until-home loop, built generically so other tasks can reuse it, per explicit user request) plus wrong-page detection in `_find_stage_row` (zero stage-row numbers OCR'd at all -> retry via `return_to_home`, up to 3 attempts). **Second bug found on the next real run** (after the above fix correctly recovered and correctly OCR'd the target row): the row's own 入場 (enter) button click had no retry, unlike every other click-then-confirm step in this module -- missed once, aborted the whole run. Fixed via `_open_stage_modal`, the same click-then-verify-then-retry pattern already used everywhere else in the file. **Third bug found on a third real run**: `_find_stage_row`'s wrong-page detection false-positived twice (a fresh navigation's list hadn't finished rendering on the first OCR pass) before self-correcting on the 3rd attempt, wasting expensive return-home retries on what was really just a timing race -- fixed with a cheap in-place rescan before concluding "wrong page." Also, once past that, the 掃討開始 (start sweep) click turned out to be the last bare, unretried click in the file -- fixed via `_click_sweep_start_and_verify`. **Fourth round, self-driven live iteration per explicit user direction (fix/deploy/run/screenshot/diagnose in a loop, no stopping to report)**: found two more root causes and reached the first confirmed real live sweep. (1) The badge carousel's auto-rotate timer is far slower than the retry window -- a run hit "wrong page" on all 3 attempts genuinely, confirmed by screenshot; fixed by discovering and clicking the carousel's own pagination dots directly (`config.EVENT_BADGE_DOT_X`) instead of hoping the ambiguous badge shows the right item. (2) A genuine cold-start settle delay (up to ~20s for the Quest tab's stage list to populate after a fresh navigation, most likely a one-time server round-trip), not a flaky race -- confirmed via standalone probe scripts polling the real OCR pipeline once/second for a minute; fixed by widening `STAGE_ROW_SCAN_ATTEMPTS`/`_RETRY_WAIT` to match. With both fixes, a real run completed end-to-end: 200 AP spent (10x MAX sweep), credits gained, clean return home, confirmed by screenshot -- see `plan.md`'s Phase 14 follow-up #4 for the full writeup. **Fifth round, reported by the user a day later**: the exact same wrong-page-looping symptom recurred. Per the user's explicit request, added a DIRECT check for the finished event's own "イベント期間が終了しました" text (Japanese OCR, newly installed on nik-gpu since this project previously only needed digit/English reads) instead of the indirect "zero valid rows" heuristic -- `config.EVENT_FINISHED_TEXT_RECT`/`_is_finished_event_page`, live-calibrated and confirmed both ways (exact phrase match on a real finished page, no false positive on the correct page). This immediately proved the badge carousel's dot-clicking (follow-up #4's fix) is NOT reliably controllable after all -- sometimes worked, sometimes didn't, on the identical badge state -- while simply waiting longer let the carousel's own auto-rotate timer land on the correct item independently. Fixed by widening `WRONG_PAGE_RETRIES`/`_WAIT` (3x3s -> 6x12s) to give the timer real room to cycle, keeping dot-clicking as a harmless supplementary nudge. Two more real sweeps confirmed this fully working (11x MAX sweep, 219 AP spent, credits gained, clean home return, confirmed by screenshot -- three real confirmed sweeps total across this whole investigation). Also found and fixed the real root cause of the lingering `"unrecognized_state"` cosmetic bug (not a budget issue after all): `_watch_sweep_result`'s "swept" condition required the stage modal to still be open, but this event's flow can auto-return all the way to the Quest list instead, a terminal state it never anticipated -- fixed with a second `ends` condition gated on having clicked at least one result button first. Not yet re-verified live (AP exhausted by the successful sweep). See `plan.md`'s Phase 14 follow-up #5 for the full writeup. **Sixth round (2026-07-11)**: the daily rotation picked stage 9 for the first time, hitting a previously-flagged-but-never-exercised gap -- rows 366/538 ("08"/"09") consistently misread by OCR ("2" and empty/None) on every psm mode, even though the crop looked completely clean by eye; confirmed NOT a navigation/timing bug since rows 710/883/1055 ("10"/"11"/"12") read fine in the same run. Root cause (`scratchpad/probe_ocr_fix_08_09.py`): tesseract's segmentation fails on this tight edge-to-edge crop (no whitespace margin) for a leading-zero digit pair specifically -- adding a plain white border around the upscaled crop before OCR fixed both digits exactly, at every psm mode, without affecting "10". Fixed via new `detector.read_int_bordered`, now used for all `EVENT_STAGE_ROW_Y` reads. **Confirmed live**: stage 9 found, sweep executed for real (AP 233->14, credits +5,892, screenshot-confirmed safe return home) -- the originally reported bug is fixed. The same run again logged `unrecognized_state`, proving follow-up #5's `clicked_any`-gate fix addressed a real but secondary issue, not the full story. Diagnosed at zero AP cost (`scratchpad/probe_result_button_fp.py`, checks `_find_result_button` against the plain Quest list with no sweep running): the shared `SWEEP_RESULT_BUTTON_REGION` (borrowed from `story_sweep.py`) reaches into this event's own character-art panel and false-positive-matches `SWEEP_CONFIRM_CYAN` there with no dialog showing at all, confirmed on both a wrong event page and the correct one's own plain list -- so `_watch_sweep_result` kept "finding" a result button after the real one was already dismissed, and its "modal closed, no result button" end condition could never match. Fixed with a new event_sweep-only `config.EVENT_SWEEP_RESULT_BUTTON_REGION` (x narrowed to exclude the character-art panel, still comfortably covering the real buttons), confirmed live against the actual false-positive condition (now returns `None` where it previously didn't) -- not yet re-confirmed via a fresh full sweep since AP was too low that day. See `plan.md`'s Phase 14 follow-up #6. | -| Group/Club AP | `module/group.py` | Need to inspect | `ba_auto/tasks/group.py` | fixed click + state check via local driver | Not started | +| Circle (Group/Club) daily check-in | `module/group.py` | `implement`, `to_group` | `ba_auto/tasks/circle.py` | Reference's `picture.co_detect` polling against fixed screen positions + `rgb_possible`/`img_possible` template states → this client's own 2-click nav (home → bottom-nav ソーシャル icon → サークル card) verified with the existing shared `navigation.is_modal_open`/`is_on_subscreen` (no new probe needed — pixel-confirmed both real "reached the circle screen" states, with or without the reward modal, satisfy that combined check) | Done. Live-calibrated 2026-07-15 on nik-gpu by driving the real flow end-to-end via raw xdotool/scrot — this **genuinely claimed the account's real circle check-in reward for the day** (+10 AP, confirmed via the real reward dialog "今日のサークルへの参加報酬... AP x10... 報酬はメールボックスから受け取ることができます"), which calibrated both real states from real data: the first-entry reward modal, and (by re-entering immediately after) the already-checked-in straight-to-chat state with no modal. Also live-confirmed a real `XIGNCODE` anti-cheat overlay stole a `BACK_BUTTON`-coordinate click mid-calibration (same recurring gotcha documented elsewhere in this project) — recovered via the standard `windowactivate`+`windowraise` escalation. Per explicit user direction, the task presses Escape directly to return home (confirmed live: a single Escape from the サークル screen returns straight to true home, skipping back through the intermediate ソーシャル hub page) rather than clicking `navigation.BACK_BUTTON`. The reference's `group_join-club` ("not in a circle") outcome is deliberately not ported — this account is already a member, and the task's scope (per explicit user direction) is entry only, no mailbox claim. The actual `circle.py` module was then live-tested for real via `./ba_dailies.sh circle`, correctly reading the "already checked in today" state left over from calibration and returning cleanly home. **Not yet live-tested**: the "first entry → claim reward" code path itself, since the account was already checked in for today by the time the module existed — same disclosed gap shape as gem_shop's own "available → claim" path. Added to `DEFAULT_ORDER` alongside mailbox/cafe/stamina/gem_shop, since it's a pure free reclaim with no decision to make. **A `reference-parity-reviewer` pass the same day caught and fixed three real gaps**: a too-generic terminal-state check that could false-positive "already checked in" from a stuck non-home starting state (fixed via `navigation.return_to_home(driver)` at the top of `run()`, matching arena.py/bounty.py precedent); an unverified reward-dismiss Enter press and closing Escape (fixed via `_dismiss_reward`/`_leave_circle`, bounded retry-until-verified loops directly porting `gem_shop.py`'s `_claim_free_package`/`_close_gem_shop`); and a missing `driver.focus_game()` escalation in the entry retry loop for the `XIGNCODE` overlay (which had already struck live during this task's own calibration) — fixed to match `navigation.click_back`'s own escalation convention. Re-confirmed live via `./ba_dailies.sh circle` after all three fixes. See `plan.md`'s Phase 17 for the full writeup. | | Bounty | `module/rewarded_task.py` | `implement` (main flow, `get_task_count`/`purchase_bounty_ticket`/per-area `rewarded_task_status` loop, not ported -- see Status), `to_bounty`/`to_choose_bounty` (nav), `get_los`/`one_detect`/`bounty_common_operation` (per-row SSS-color scan + sweep) | `ba_auto/tasks/bounty.py` | `to_bounty`'s bottom-nav "bus" icon -> this client's Work-hub `指名手配` card (`config.BOUNTY_CARD`), landing directly on Location Select with no separate bus-icon sub-navigation step; `get_los`/`one_detect`'s per-row `color.check_sweep_availability` SSS-color scan across however many rows are visible -> a fixed bottom-most row click (`config.BOUNTY_LATEST_STAGE_ROW_Y`), since this client's 3 areas each have exactly 10 stages (confirmed live, scrolling past the 10th is a no-op) all already SSS-cleared, making "scroll to the bottom extreme, click the last row" equivalent to "the latest stage available" by construction; the reference's config-string per-area sweep-count list (`rewarded_task_times`, `get_task_count`) and its loop across all 3 areas -> a single date-ordinal-modulo rotation choosing ONE area per run, mirroring story_sweep.py/event_sweep.py's own rotation, per explicit user direction (2026-07-11); `purchase_bounty_ticket` (buying more tickets with real currency) -> not ported, matching plan.md's "OCR optional for coin balance/refresh logic, can skip for first version" | Done, live-tested for real -- see `plan.md` Phase 15. Live-calibrated against nik-gpu 2026-07-11 with **zero real tickets spent** during calibration (every ticket-usage confirm dialog reached was cancelled via Escape, verified by the ticket counter (6/6) unchanged before/after, across all 3 areas). All 3 areas (ハイウェイ/砂漠の線路/校舎, matching the reference's OVERPASS/DESSERT RAILWAY/CLASSROOM groupings) confirmed to share an identical layout: same area-row/stage-row/modal-button coordinates, same 10-stage-per-area structure. The 任務情報 modal's MAX stepper, minus-button raised-color check, and ticket-usage confirm dialog are pixel-identical to event_sweep.py's own stage modal / shared `SWEEP_CONFIRM_*` dialog -- confirmed live -- and reused directly. The modal-open probe could NOT be reused from `EVENT_STAGE_MODAL_PROBE`: that corner point reads dark on this screen regardless of modal state (different background art), so a fresh `BOUNTY_STAGE_MODAL_PROBE` was calibrated that does discriminate both states. **2 real sweeps confirmed live** (credits +180,000 then +36,000, clean automatic return home both times), which surfaced and fixed two real bugs: (1) `_set_sweep_count`'s count==1 path wrongly assumed the modal defaults to count=1 on open -- it actually remembers the last-used count -- causing an unintended 5-ticket spend instead of the intended 1; fixed via a new `_click_min_and_verify` that always forces a known baseline first. (2) `BOUNTY_SWEEP_RESULT_BUTTON_REGION`'s first-guess copy of event_sweep's own region overlapped `BOUNTY_SWEEP_START_BUTTON`'s real cyan pixels, causing `_find_result_button` to re-click it once tickets hit 0 -- which surfaced a real Pyroxene ticket-purchase prompt (no gem actually spent, confirmed by an unchanged balance, but a real near-miss). Fixed three ways: the region was corrected to the real pixel-scanned OK-button bbox; a new `min_pixels` parameter on `detector.find_color_centroid` (plus `config.BOUNTY_RESULT_BUTTON_MIN_PIXELS`) filters out a second, smaller contamination source (stray cyan-range pixels in the modal's own reward-icon artwork); and `_watch_sweep_result` now has an explicit `_is_ticket_purchase_prompt` guard as a named `ends` condition. **Not yet re-confirmed live**: a fresh sweep with the fixes deployed (account at 0/6 tickets as of session end) and any bulk/MAX-count sweep's result-screen flow (only count=1 was ever tested; a bulk sweep may show a SKIP-then-OK sequence like story_sweep/event_sweep instead of the single-OK dialog confirmed here). | | Commissions | `module/clear_special_task_power.py` | Need to inspect | `ba_auto/tasks/commission.py` | sweep/color adaptation | Not started | | Arena | `module/arena.py` | `implement` (main flow), `to_tactical_challenge` (nav from main page), `get_tickets` (ticket-count OCR), `choose_enemy` (self/opponent level OCR + bounded refresh-reroll loop), `check_skip_button` (skip-toggle color probe), `fight` (click fight, wait for win/lose), `collect_tactical_challenge_reward` (two reward-slot color probes) | `ba_auto/tasks/arena.py` | Ticket count/self level/opponent level/rank are plain digit OCR (`detector.read_int`/`read_text`, plus a new `read_int_white_on_dark` for the profile card's bright-on-dark level text). Skip-toggle state and the two reward-slot claimed-vs-claimable colors are plain pixel-color probes (`driver.color_at`/`_color_in_range`). The post-fight WIN/LOSE result modal and an unrelated list-refresh-expired notice are NOT detected by precisely locating a button (a color-region search proved unreliable — see Status); they're dismissed via a bounded blind-Enter-press loop matching `lesson.py`'s own `_run_one_schedule` pattern, gated by a hard safety check against the one modal where Enter is dangerous (opponent-info's own attack-formation button, checked via its fixed-position gold button). `choose_enemy`'s refresh-reroll loop is direct Python control flow, bounded by `maxArenaRefreshTimes`. Config knobs carried over from reference defaults: `ArenaComponentNumber`=1, `ArenaLevelDiff`=0, `maxArenaRefreshTimes`=10, `ArenaStopFightWhenRank1`=False | Done. Live-tested for real across all 5 of the account's daily tickets (2 WIN, 1 LOSE, 2 spent debugging the result-modal detection — see `plan.md` Phase 13 for the full writeup). Real navigation differences confirmed live: Tactical Challenge is a Work-hub card, not a bottom-nav icon; the reference's separate opponent-info and formation-edit screens are merged into one modal here with a live ticket-preview; `navigation.is_modal_open`'s shared probe reads *inverted* on this screen (own `_is_modal_open` via `config.ARENA_MODAL_PROBE`). Three real bugs fixed: a level-OCR crop too small for tesseract despite looking legible (fixed by widening the crop, not the pipeline); level text being bright-on-dark unlike every other OCR read in this project (fixed via `read_int_white_on_dark`); and the result-modal detection cycling through two failed color-based designs (fixed by switching to bounded blind-Enter dismissal with a hard safety gate — a real near-miss of the same "mistimed keypress" hazard class `CLAUDE.md` already documents from story_sweep). Deliberately opt-in only, never in `DEFAULT_ORDER` — unlike every other opt-in task so far (which spend a known-safe resource on a config-driven target list), this one fights a real ranked PvP battle that can win or lose and moves the account's actual arena rank. Per explicit user decision: fights exactly one battle per invocation, matching the reference's own per-call pacing (its `next_time = 55` background-thread rescheduling has no equivalent in this project's one-shot CLI). Not yet exercised live: the "no ticket" mid-flow race, an actual reroll click (every opponent offered was already an acceptable level), and `ArenaStopFightWhenRank1`'s rank-1 stop condition — all implemented per the reference's logic, just not yet hit by real game state. `detector.find_template`/`template_visible` (generalized named-template matcher, built during scaffolding) ended up unused — state detection stayed OCR/color-probe-driven throughout, like every other task in this project. Follow-up (2026-07-11): live bug report — this task never returned home at the end, so a second consecutive invocation starting from wherever the first left the game (the Tactical Challenge screen itself) sent `_open_tactical_challenge`'s home-relative clicks to the wrong place and failed all 3 retries. Fixed by calling the shared `navigation.return_to_home(driver)` at the very start of `run()`, reusing the generic Escape-based recovery primitive built for `event_sweep.py`'s own wrong-page recovery. Confirmed live (2026-07-11): reproduced the stuck-on-arena-screen scenario manually, then a real `arena` invocation recovered and completed a full fight normally (rank 14位's opponent-list entry moved 10位→9位, ticket 2→1, credits +1,080), with no repeat of the original failure. Follow-up #2 (2026-07-12), per explicit user request: reversed the original "exactly one battle per invocation" decision — `_fight_one` now holds the single-battle logic and `run()` loops it (`while tickets > 0 and fights < config.ARENA_MAX_FIGHTS_PER_RUN`), re-reading the OCR'd ticket count after each fight and waiting `config.ARENA_POST_BATTLE_COOLDOWN` (30s, per the user's own info about the real in-game lockout between fights) before continuing. `ArenaStopFightWhenRank1` is now re-checked before every fight in the loop, not just once. See `plan.md`'s Phase 13 follow-up #2 — not yet live-tested (spends multiple real tickets, needs the user's go-ahead first). | diff --git a/ba_auto/tasks/circle.py b/ba_auto/tasks/circle.py new file mode 100644 index 0000000..a15aa21 --- /dev/null +++ b/ba_auto/tasks/circle.py @@ -0,0 +1,154 @@ +"""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.") diff --git a/ba_daily.py b/ba_daily.py index 4e5dd21..8303ce1 100644 --- a/ba_daily.py +++ b/ba_daily.py @@ -3,13 +3,14 @@ import sys from ba_auto import config, driver, navigation -from ba_auto.tasks import arena, bounty, cafe, event_sweep, gem_shop, lesson, mailbox, shop_common, shop_tactical, stamina, story_sweep +from ba_auto.tasks import arena, bounty, cafe, circle, event_sweep, gem_shop, lesson, mailbox, shop_common, shop_tactical, stamina, story_sweep TASKS = { "mailbox": mailbox.run, "cafe": cafe.run, "stamina": stamina.run, "gem_shop": gem_shop.run, + "circle": circle.run, "story_sweep": story_sweep.run, "event_sweep": event_sweep.run, "shop_common": shop_common.run, @@ -24,11 +25,11 @@ TASKS = { # rather than reclaiming something free, which is a real resource decision # the default unattended run shouldn't make blindly. Arena specifically # fights a real ranked PvP battle each run -- see ba_auto/tasks/arena.py's -# module docstring. gem_shop is the opposite case -- like mailbox/cafe/ -# stamina, it only ever reclaims a genuinely free (0 yen), once-per-day -# resource with no choice to make (claim it or don't, nothing to select), -# so it belongs in the default flow rather than opt-in. -DEFAULT_ORDER = ["mailbox", "cafe", "stamina", "gem_shop"] +# module docstring. gem_shop and circle are the opposite case -- like +# mailbox/cafe/stamina, they only ever reclaim a genuinely free (0 yen), +# once-per-day resource with no choice to make (claim it or don't, nothing +# to select), so they belong in the default flow rather than opt-in. +DEFAULT_ORDER = ["mailbox", "cafe", "stamina", "gem_shop", "circle"] # How many times _ensure_home retries navigation.return_to_home as a whole # (not to be confused with that function's own internal diff --git a/plan.md b/plan.md index 9fadd70..7c39403 100644 --- a/plan.md +++ b/plan.md @@ -698,6 +698,34 @@ Added to `ba_daily.py`'s `TASKS` dict and, unlike every task added since Phase 8 1. **Dead close-verification (fixed).** The first version's final dialog-close step verified success via `navigation.is_on_subscreen` — but that probe is documented (in the same session's config.py writeup) to read identically whether this specific dialog is open or closed, since the dialog overlays the home screen directly rather than being a full subscreen. The check could therefore never fail, silently masking a stuck-open dialog that neither this module nor `navigation.return_to_home`'s shared fallback (same blind-probe class) could then detect — the exact "unverified action assumed to work" shape the cafe.py bugs earlier in this same session were also caught in. Fixed by adding `_close_gem_shop`, which verifies with the module's own correctly-calibrated `_dialog_open` check instead, bounded-retry, reused for both the normal end-of-run close and the "unknown status" abort path (which had the same unverified-Escape bug separately). **Re-confirmed live** after the fix: same real "already claimed" run via the actual CLI, no warnings, confirmed via screenshot back at the true home screen. 2. **`GEM_SHOP_PACKAGE_TAB` is a single coordinate calibrated only against today's temporary 3-tab layout (documented, not yet fixable).** The reference's own `to_purchase_type` branches on whether the 期間限定 (time-limited) tab is present, because removing it reflows the remaining tabs — and `screenshots/gem_shop/2.png` (captured live the same session) shows that tab's own countdown ("終了まであと6日", 26.06.24〜26.07.29), meaning this account's 3-tab layout is itself due to change around 2026-07-21. No real screenshot of the resulting 2-tab layout exists yet, so a second coordinate was deliberately NOT guessed in rather than grounded in a real capture — flagged with a dated comment in `config.py` instead. Contained, not a live hazard: if this goes stale, `_open_package_tab`'s existing status-probe check reads "unknown" and aborts cleanly via the now-fixed verified close, without ever clicking a purchase button on whatever tab it actually landed on. Needs a real re-calibration pass once the tab count actually changes. +### Phase 17: Circle (Group/Club) daily check-in (2026-07-15) + +Reference: `module/group.py`. Local: `ba_auto/tasks/circle.py`. Requested directly by the user: "enter circle (guilds)... grant you 10 AP (stamina) once, reset daily. Then you can claim it from mailbox. This implement only do circle enter and return to home screen. No need for mailbox claim." Reached via home → bottom-nav ソーシャル (Social) icon → サークル (Circle) card, per the user's own description. + +Reference flow (`implement`): `to_main_page` → `to_group`, which reacts to the `main_page` rgb state by clicking a fixed screen position, polling (`picture.co_detect`) until one of three image-template terminal states: `group_sign-up-reward` (first entry today, +10 AP granted), `group_menu` (already checked in, plain menu), or `group_join-club` (account isn't in a circle — reference just warns and stops). Every step detected via fixed-region template matching, no OCR. + +**Live-calibrated 2026-07-15** by manually driving the real flow end-to-end on nik-gpu — this **genuinely claimed the account's real circle check-in reward for the day** (the real "今日のサークルへの参加報酬" modal, AP x10, "報酬はメールボックスから受け取ることができます" — matching the reference's own sign-up-reward outcome and the user's own description exactly), then confirmed the "already checked in" state for real by re-entering immediately afterward (same click sequence lands straight on the chat/member screen, no modal). Both real states pixel-sampled and found to satisfy the existing shared `navigation.is_modal_open`/`is_on_subscreen` check with no new probes needed — first genuine case this session where the shared primitives worked as-is rather than needing a dedicated dialog probe (contrast gem_shop.py's `GEM_SHOP_DIALOG_PROBES`). + +One real navigational subtlety found and deliberately *not* over-engineered: the intermediate ソーシャル hub page (between clicking Social and clicking Circle) has no reliable single-point brightness signal of its own — pixel-sampled live, it reads dark at `SUBSCREEN_HEADER_PROBE` same as the true home screen, because it keeps the home screen's own background art dimly visible behind its card grid rather than using a proper opaque header bar like every other subscreen in this project. Rather than inventing an uncalibrated probe for that transient state, `_enter_circle` treats "click Social, click Circle" as one combined attempt and retries the *whole pair* (bounded, `ENTER_RETRIES=3`) if the final destination isn't confirmed — justified as safe because both click targets are harmless no-ops if partially already there on a retry. + +Also hit, live, mid-calibration: the recurring `XIGNCODE` anti-cheat overlay stole a `BACK_BUTTON`-coordinate click (same gotcha documented in the "Return-to-home audit follow-up #2" below) — recovered via the standard `windowactivate`+`windowraise` escalation, no code change needed since `driver.focus_game()`/`navigation.return_to_home` already handle this class of incident project-wide. + +Per explicit user direction mid-session ("Instead of click back you can use Esc button"), the task returns home via a direct `driver.keypress("Escape")` rather than clicking `navigation.BACK_BUTTON` — confirmed live that a single Escape from the サークル screen returns straight to the true home screen in one step (skipping back through the intermediate ソーシャル hub page entirely), matching `mailbox.py`'s own established end-of-task convention rather than inventing a new one. + +The reference's `group_join-club` ("not in a circle") outcome is deliberately not ported — this account is already a circle member (confirmed live), and per explicit user scope this task only performs the entry, nothing else. + +Added to `ba_daily.py`'s `TASKS` dict and `DEFAULT_ORDER` (alongside mailbox/cafe/stamina/gem_shop) — same "reclaims something free, no decision to make" category. + +**Live-tested for real** via the actual `./ba_dailies.sh circle` CLI path: correctly recovered from a flaky first click (same familiar "first click sometimes misses" pattern), correctly read the real "already checked in today" state left over from the manual calibration walkthrough, and returned cleanly to the confirmed true home screen. **Not yet live-tested**: the "first entry → claim reward" code path itself, since the account was already checked in for today by the manual calibration walkthrough before the module existed — same disclosed-gap shape as gem_shop's own "available → claim" path (Phase 16). Worth a follow-up check the next time the check-in resets and hasn't been claimed yet. + +**A `reference-parity-reviewer` pass the same day, before this was reported done, caught three real gaps, all fixed:** + +1. **`_reached_circle`'s generic terminal check could false-positive from a stuck non-home starting state (fixed).** The first version relied solely on `ba_daily.py`'s centralized `_ensure_home()`, which CLAUDE.md documents as explicitly best-effort, not a hard gate — if a run began while some other subscreen/modal was already open (a leftover from a prior task), `_reached_circle` would read true on attempt 1 before either click did anything, and the code would silently report "already checked in today" without ever attempting the real check-in, no warning logged. This also undermined the module's own stated justification for skipping the reference's `group_join-club` ("not in a circle") case — a join-prompt rendered as an ordinary subscreen or modal would trigger the exact same false positive. Fixed by calling `navigation.return_to_home(driver)` at the top of `run()`, porting the same fix `arena.py`/`bounty.py` already made for themselves for this identical class of bug. +2. **The reward-dismiss Enter press and the closing Escape were never verified to have worked (fixed).** Same "unverified keypress assumed to work" shape as the cafe.py/gem_shop.py bugs fixed earlier the 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 claiming it, silently declining the day's free AP with no warning and no way to retry until the next daily reset (amplified by `circle` running unattended in `DEFAULT_ORDER` every day). Fixed with `_dismiss_reward`/`_leave_circle`, both bounded retry-until-verified loops directly porting `gem_shop.py`'s `_claim_free_package`/`_close_gem_shop` pattern. +3. **`_enter_circle`'s retry loop had no `driver.focus_game()` escalation for the `XIGNCODE` overlay (fixed).** That overlay had already struck live during this task's own calibration session (stole a `BACK_BUTTON` click, recovered manually via `windowactivate`+`windowraise`) — every other retry loop bitten by this class of incident in this project (`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. + +**Re-confirmed live** after all three fixes: same real `./ba_dailies.sh circle` run via the actual CLI, correctly read "already checked in today" with the new `return_to_home()` pre-check in place, no warnings from the new verified dismiss/close loops, confirmed via screenshot back at the true home screen. + ### Return-to-home audit (2026-07-12) Per explicit user request: "I want all the script to return to home page after the script ended, no matter the script ended in success or failure. You can use the Esc button to return or use module to reuse the logic."