diff --git a/ba_auto/config.py b/ba_auto/config.py index 2591104..a6298b8 100644 --- a/ba_auto/config.py +++ b/ba_auto/config.py @@ -573,22 +573,33 @@ HARD_STORY_SWEEP_TARGETS = [ EVENT_BADGE_ICON = (1787, 300) # A finished event's Quest tab shows plain "イベント期間が終了しました。" -# (event period has ended) text instead of any stage-row cards. This rect -# and the lang="jpn" OCR read against it are LIVE-CONFIRMED both ways -# (2026-07-10, against "嵐過天晴", a genuinely finished event reachable via -# the badge carousel's 2nd dot position at that time): the OCR returned the -# exact phrase verbatim on the finished-event page (`detector.read_text` -# with psm=6), and on the correct/current event's page the same rect reads -# unrelated stage-list text with no false-positive "終了" substring match. -# Used as an early-exit, authoritative-when-positive check inside -# _find_stage_row's scan loop -- a positive match ends the wait immediately -# as a confirmed wrong page; a negative match does NOT prove the page is -# right (a different finished event's layout might differ), so patient -# rescanning still continues regardless either way up to the existing -# budget. If that budget is ever fully exhausted with no rows and no -# confirmed finished-page text, _find_stage_row saves a debug screenshot to +# (event period has ended) text instead of any stage-row cards. Used as an +# early-exit, authoritative-when-positive check inside _find_stage_row's +# scan loop -- a positive match ends the wait immediately as a confirmed +# wrong page; a negative match does NOT prove the page is right (a +# different finished event's layout might differ), so patient rescanning +# still continues regardless either way up to the existing budget. If that +# budget is ever fully exhausted with no rows and no confirmed +# finished-page text, _find_stage_row saves a debug screenshot to # scratchpad/ (event_sweep_no_rows_debug.png) for further diagnosis. -EVENT_FINISHED_TEXT_RECT = (1030, 600, 1810, 750) +# +# Re-calibrated 2026-07-29 (user report + user-supplied screenshot, +# scratchpad/event_event.png, against "シャーレの総決算 with 連邦生徒会", the +# event that succeeded "鉄道爆走事件"/"嵐過天晴"): the previous rect, +# (1030, 600, 1810, 750), was live-confirmed against an EARLIER finished +# event back on 2026-07-10, but re-tested live against this real capture +# read pure garbage with no "終了" match at all -- not just clipped text, +# almost entirely background office-art below the real text line, which +# actually sits noticeably higher (y~585-635, not 600-750). Whether the +# panel's own text position genuinely shifted between events or the +# original calibration was never quite right, this rect is now confirmed +# directly against a real live capture of the CURRENT finished-event page +# (scratchpad/probe_event_finished_text.py, since deleted): the old rect +# produced unusable noise ("glに月7」ししだに。..."), the new one reads +# clean text containing "終了" ("4時回終了しました。"). If this drifts again +# on some future finished event, re-run that same probe pattern against a +# fresh capture rather than assuming the rect is still centered correctly. +EVENT_FINISHED_TEXT_RECT = (1080, 585, 1760, 635) # Top-right tab bar inside the event screen (reference's activity_menu # story/mission/challenge tabs -- rendered as English labels "Story / Quest / diff --git a/ba_auto/reference_notes/mapping.md b/ba_auto/reference_notes/mapping.md index 7ca0b6c..7307fd0 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/...` | Stamina/AP | `module/collect_daily_task_power.py` | `to_tasks`/`implement` | `ba_auto/tasks/stamina.py` | `color.rgb_in_range` → `driver.color_at`; reference's per-tab claim loop → live UI's single "一括受取" bulk-claim button + Enter | Migrated: Mission-panel claim done (see `plan.md` Phase 8). The gem shop's own Daily Free Power flow (`module/collect_daily_free_power.py`) is a separate task now — see the "Gem shop daily free package" row below. **Daily gem reward bugfix (2026-07-16)**: reported live by the user against a real account state — the reference's own `implement` checks TWO separate button regions (the main 一括受取 area, then a second, separate "claim daily pyroxenes" button checked independently after), but only the first was ported originally. Fixed by porting the second region too: a new `MISSION_DAILY_GEM_CLAIM_BUTTON` (config.py), reusing the same bright-yellow-vs-grey color-probe style as the existing `MISSION_CLAIM_PROBE`, clicked via `driver.click` (not Enter — unlike 一括受取, this button has no keybind shown on screen) with a click-then-verify retry loop matching this project's established discipline. **Confirmed live**: gem balance went from 12,384 → 12,404 (exactly the reward's stated +20), clean return to the true home screen, no warnings. | | 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. **Recalibrated 2026-07-29** after a real client UI update replaced the old 3-top-tab overlay dialog with a genuine full subscreen (left sidebar + デイリー/ウィークリー/マンスリー sub-tabs under 一般商品). The free card's own status bar now reads flat light blue (~(226,236,246)) when available vs flat dark red (~(190,56,66)) once claimed today — still no OCR needed, same high-contrast-flat-color reasoning as before, just re-read off the new layout. The old dedicated `GEM_SHOP_DIALOG_PROBES` 4-point "is this dialog showing" check is gone entirely: the redesigned screen is a real subscreen (own header, back arrow at the shared `navigation.BACK_BUTTON` position), confirmed live that `navigation.is_on_subscreen`/`return_to_home` now work correctly here, so `gem_shop.py` was simplified to share that machinery with mailbox/cafe/shop/lesson instead of the old bespoke Escape-only close. | Done, recalibrated. Original: live-calibrated against nik-gpu 2026-07-15 by manually driving the real dialog end-to-end via raw xdotool/scrot — genuinely claimed the account's real free package for the day (0 yen, confirmed +10 AP / +10,000 credits). Recalibration (2026-07-29): the new "already claimed" status color and every new coordinate (icon, sidebar tab, daily sub-tab, buy button, status probe, screen-open/close detection) were re-verified live on nik-gpu against the real redesigned screen (see `scratchpad/gem_shop_after_click.png`/`gem_shop_weekly_tab.png`/`gem_shop_after_close.png`) — confirmed the icon click still opens it, confirmed `is_on_subscreen`/`return_to_home` now correctly detect/close it, and confirmed the "already claimed" red status color directly off the real card (still claimed from the 2026-07-15 session's calibration run, today's own reset hadn't been claimed yet at investigation time but the account's daily reset had already occurred, so it was showing the post-claim state). The new "available" light-blue color was inferred from three other real, currently-purchasable cards under the ウィークリー tab (same shared status-bar UI component, all three reading identically) rather than from the daily free card itself, since that card wasn't in the available state at recalibration time — **not yet re-confirmed against the daily free card's own available state, or against the full claim-confirmation/reward-banner flow post-redesign**; worth a deliberate 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; 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; rewritten Phase 22 to match story_sweep_hard.py's Phase 21 fixes and add its own campaign guard). Config-driven exact `(region, stage, count)` targets (`config.STORY_SWEEP_TARGETS`), not latest-region/random-stage. Opt-in only, not in default flow. **2026-07-21 rewrite (Phase 22)**: per explicit user direction ("match the hard implementation on best practices"), ported story_sweep_hard.py's fixes back into this module — an explicit Normal-tab guard (`config.NORMAL_TAB`/`_open_normal_tab`, needed now that story_sweep_hard can run earlier in the same `q4h` preset and leave Hard selected), a loop-level self-heal (`_ensure_task_screen`) replacing the old per-path "Escape if on subscreen" cleanup that would have backed out of the region browser after every target (never triggered in production since this module only ever swept one rotation target per run, but a real latent bug), MAX/+ clicks no longer requiring a visible stepper raise as proof of success (same low-AP false-negative class Hard hit), `_click_plus` now resets to a known baseline via a new `SWEEP_MIN_BUTTON` before raising (closing a latent version of bounty.py's real overspend bug that this module's original `_click_plus_and_verify` never guarded against), a newly-added `_confirm_dialog_is_sweep` OCR gate against the two-stacked-button hazard (confirmed live 2026-07-21 that Normal's own tabbed modal has the identical gold 任務開始 button beneath 掃討開始 that config.py's own `SWEEP_MAX_BUTTON` comment already documented but this module never actually guarded against), the same `clicked_any`-gated `_watch_sweep_result` fix, and a `REGION_READ_RETRIES` OCR-read retry. Also added a genuinely new campaign-active guard (`config.TASK_CAMPAIGN_BADGE_RECT`, renamed from `HARD_CAMPAIGN_BADGE_RECT` once shared) — confirmed live to be pixel-identical rect/color to Hard's own banner, just reading "任務Normalで獲得できる報酬量が2倍(+100%)になっています" instead of "Hard". Confirmed live that Normal mode has no per-stage daily clear cap (no "残り回数" indicator anywhere), so Hard's gem-refill hazard does not apply here. `story_sweep_force` added as the override command. **Live-tested for real the same day**: a real MAX sweep of the day's rotation target (30-2) succeeded cleanly end-to-end on the first attempt — campaign guard passed, OCR stage-row match succeeded despite the known leading-digit misread ("20-2" read but the "2" suffix still matched), sweep confirmed and correctly detected as `"swept"` (not the old cosmetic misreport), clean return home. AP dropped from ~139 to 1 and credits rose by 13,966, confirming a real substantial MAX sweep. | -| 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. | +| 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. **Seventh round (2026-07-29), direct user bug report with exact log**: "when it enter wrong page it will just wait then exit on false positive instead of return home and retry" -- the log showed a single stray/partial OCR digit ('7', matching nothing real) appearing at one row position while the other 4 were still `None`, which the old `_find_stage_row` trusted as proof of a genuinely-rendered page (`stage_not_found`), a terminal outcome `run()` never retried (only `wrong_page` was retryable). Also found live while investigating (user supplied a real screenshot, `scratchpad/event_event.png`, of the actual finished-event page): `EVENT_FINISHED_TEXT_RECT`'s "authoritative" check had been silently broken since whatever event succeeded the one it was calibrated against -- re-tested live against a real capture, it read pure background-art garbage with no "終了" match at all, not just clipped text. Fixed three ways: (1) `_find_stage_row` now requires a "some rows read, target absent" conclusion to read back identically on two consecutive scans before trusting it (the all-`None` case already had this stabilize-before-trusting protection; the partial-read case didn't); (2) `EVENT_FINISHED_TEXT_RECT` re-calibrated from (1030,600,1810,750) to (1080,585,1760,635) against a fresh live capture; (3) `run()` now retries on `stage_not_found` the same as `wrong_page`. **Confirmed live immediately after deploying**: a real run landed on the finished-event page first (now correctly and immediately detected via the fixed text rect, instead of silently falling through to the fragile row-count heuristic), returned home, retried, landed on the actual current event ("出航!万魔紐", which succeeded the event this module's stage-list layout was originally calibrated against but happens to share the identical 12-stage/last-5-rows-visible template), read all 5 rows cleanly, found the target stage, and safely declined without spending AP once the MAX-count-raise couldn't be confirmed (that stage isn't sweep-eligible yet) -- the full wrong-page-to-correct-page recovery loop worked exactly as intended end to end. | | 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). **Real hazard found and fixed via a real unattended `daily` cron run (2026-07-17)**: reported live by the user -- a real "Battle Complete" screen (a live ~3-minute combat timer) was found on the account instead of an instant sweep, with `_watch_sweep_result` having reported "swept" cleanly and no warnings anywhere in the log; every task that ran for the rest of that `daily` invocation failed to open its own screen. Manually reproducing the flow live (reaching the real confirm dialog and cancelling before confirming, the same safe-calibration pattern the original live-testing used) found the actual hazard: the 任務情報 modal has TWO separate action buttons stacked vertically -- the intended cyan 掃討開始 (start sweep, instant) and a separate gold 任務開始 (start mission, a REAL manual battle) directly below it, both showing an identical ticket-cost preview. Every check along the commit path (`_count_raised_above_one`/`_is_sweep_usage_confirm`/`_watch_sweep_result`'s own "swept" conditions) is a generic color/position probe with no verification of what's actually showing. Fixed with `_confirm_dialog_is_sweep`, an OCR text check (`config.BOUNTY_SWEEP_CONFIRM_TEXT_RECT`, captured live from the real dialog: "指名手配チケットをN使用して、掃討をN回行いますか?") gating the one irreversible click in this flow -- cancels rather than confirms if the dialog doesn't read as a sweep. The exact mechanism that let a real run reach 任務開始 instead of 掃討開始 was not fully reproduced live (would mean deliberately repeating a real battle); the fix closes the hazard regardless of the exact upstream cause. Not yet re-verified live against a fresh real sweep. | | Login | `core/Baas_thread.py`, `module/restart.py` | `to_main_page` (generic post-launch arrival routine, reused by every other reference feature's own navigation -- no separate "login" module exists in the reference), `restart.py`'s `implement`/`start` (check app running, launch if not) | `ba_auto/tasks/login.py` | Reference detects every one-off popup (~20 named `img_reactions`/`rgb_possibles`) via `picture.co_detect` image-template matching; this port scopes down to what was actually confirmed live (title screen via a fixed-chrome logo-color probe, a real network-error notice, the daily attendance card, S.C.H.A.L.E NEWS) plus a bounded generic Enter-press fallback for anything else recognized, mirroring co_detect's own "blind action once nothing matches" fallback shape but using this project's own established Enter-dismiss idiom rather than a blind coordinate click. `module/restart.py`'s kill-then-relaunch pattern ported directly via new `driver.kill_game`/`launch_game`/`is_game_running`/`window_exists` primitives (`pkill -f`/the account's own `/usr/local/bin/launch-blue-archive.sh`/`pgrep -f`/`xdotool search`) | Done. Live-calibrated 2026-07-16 on nik-gpu against the account's real overnight login-screen state (native 1920x1200 captures throughout, not the non-native `screenshots/daily_login/*.png` reference photos originally supplied -- same not-1:1 gap already documented for `screenshots/gem_shop/`/`screenshots/cafe/student/`). Confirmed live: the title screen's own logo reads a fixed brand-chrome color independent of rotating seasonal background art (confirmed across two different pieces), bright when clean and uniformly dimmed when a real notice is open on top of it (a genuine "ネットワークへの接続に失敗しました" network error surfaced unprompted during calibration); the daily attendance card only appears once per day (confirmed absent on an immediate same-day re-run after being claimed) and, like the network notice, responds to a plain Enter with no dedicated detection needed; the S.C.H.A.L.E NEWS popup needed a dedicated header-color probe since `navigation.is_on_subscreen`/`is_modal_open` both proved unreliable on it (same class of mismatch as `gem_shop.py`'s own dialog -- confirmed live by direct pixel comparison). **A real stuck-loading incident hit live during calibration itself**: the loading transition (a full-bleed variant with no chrome, distinct from a brief chrome-visible variant also seen) stalled past 6 minutes with zero progress, confirmed not a network/process-health issue; the user's own live guidance ("kill the game and rerun it") identified the relaunch script, and a second attempt after manually killing+relaunching completed the entire remaining flow (title tap -> attendance card -> home) in well under 15 seconds, confirming the stall was a genuine stuck state now handled automatically by `_recover`. Also observed live (twice, non-deterministically) but deliberately NOT worked around: a known pre-existing client rendering bug (per the user) where the news popup's own promo image can get stuck as a blank white rectangle after closing -- confirmed harmless to this module specifically (sits clear of every probe point used here, and the shared home-check probes still read correctly through it), and the user's own fix for it (reload the app) is already this module's existing stuck-recovery path. Added as the very first step in `DEFAULT_ORDER`, ahead of mailbox, since no other task can reach home from the title/loading/attendance-card states on its own. **Not yet live-confirmed**: the infrequent 業務復帰ログインボーナス welcome-back login bonus card from the user's own reference screenshots (account wasn't in that state during calibration) -- expected to fall through to the same generic-Enter path already confirmed for the attendance card and network notice, but not yet exercised for real; and the kill+relaunch recovery path has only been exercised once, manually, not yet through a fresh invocation of the actual `login.py` module hitting a real stuck state on its own. **Connectivity-probe fix, `_connectivity_confirmed` (2026-07-17)**: reported live by the user via real pulled cron logs -- a 4:30 AM `daily` fire logged a clean `[login] reached home`, but every task that ran afterward (cafe, event_sweep, circle, lesson, arena, both shops, gem_shop, mailbox, stamina) failed to open its own screen, persisting across multiple consecutive `q4h` fires spanning hours. Per the user's own direct knowledge: the server's daily reset or a concurrent login from another device can silently kill the session while the client keeps showing a cached-looking, visually normal home screen -- the error only surfaces as the game's own "connection lost" popup once an actual navigation/API call is attempted, which `_true_home`'s purely-visual checks never triggered. Fixed per the user's own suggested design ("enter the stamina claim/reward area then go back home"): `_connectivity_confirmed` now opens the Mission panel and closes it again before `_wait_for_home` will return success. Reproducing this live (the user logged into the account on their phone to trigger a real session kill) surfaced two FURTHER false positives in the same family, both from an unusually long, multi-frame animated loading sequence that never resolved on its own: first with `navigation.is_on_subscreen` (single-pixel) as the panel-opened check, then again after upgrading to `navigation.is_header_bar_visible` (8-point) -- each independently got fooled by a different coincidental splash frame within the same run. Fixed by requiring `_true_home` to re-verify a second time, after a short wait, before finally trusting a passed connectivity check. **Confirmed live**: the stuck session was cleared with a manual kill+relaunch, then a genuinely fresh cold-start `login` run (game process not running at all) correctly launched the game, passed through the title screen, and reached independently-verified true home (full HUD -- Lv/AP/credits/gems/bottom nav all visible, confirmed via direct screenshot and a separate `_true_home`/`is_header_bar_visible` check, not just the module's own printed message). Not yet re-confirmed against a fresh instance of the original silent-session-death failure mode specifically (the live reproduction available this session was the full-kick-to-login-screen variant, not the "still looks like home but isn't" variant from the cron logs), though the same `_connectivity_confirmed` mechanism covers both by design. **Faster stuck-loading detection, `_loading_buffer_visible` (2026-07-19)**: a manual `ba_cron_run.sh daily` run left the game stuck at login; `_recover`'s existing kill+relaunch DID fire correctly once the generic 240s timeout elapsed (confirmed live -- the user reported "the game did relaunched"), but waiting the full generic timeout for this specific, recognizable state is slower than necessary. Live-captured the real stuck screen and pixel-probed it (`screenshots/daily_login/stuck_loading_buffer.png`, calibrated via `scratchpad/probe_login_buffer.py`): the full-bleed loading variant's center spinner badge reads a flat, exact neutral gray `(118,118,118)` at a fixed screen-center position, independent of whichever splash-art frame is rotating behind it -- unlike the earlier `_true_home`/`_connectivity_confirmed` false positives, this doesn't share the "different frame fools a single-snapshot check" failure class, since the badge is a fixed UI overlay, not art. `_wait_for_home` now tracks continuous badge visibility and bails out to `_recover` after `config.LOGIN_LOADING_BUFFER_STUCK_SECONDS` (60s) instead of the full 240s once this exact state is confidently recognized. Verified offline against the real capture (all 6 probe points matched). Not yet live-confirmed against a fresh stuck instance recovering via this specific faster path (the triggering instance had already recovered via the pre-existing generic timeout by the time the fix was written). | diff --git a/ba_auto/tasks/event_sweep.py b/ba_auto/tasks/event_sweep.py index 6460c0a..fdc361d 100644 --- a/ba_auto/tasks/event_sweep.py +++ b/ba_auto/tasks/event_sweep.py @@ -295,6 +295,66 @@ across all 3 trials. This confirms the navigation half of the fix directly; the full sweep-and-result path wasn't re-exercised in this same check (that needs spending real AP, deferred pending the user's own next real run). + +A real run (2026-07-29, direct user report with the exact log) surfaced a +new failure mode in a genuinely different part of the wrong-page handling: +"When it enter wrong page it will just wait then exit on false positive +instead of return home and retry to enter correct page." The log showed +exactly why: scan 1 read all 5 rows `None` (correctly treated as "still +settling"), but scan 2 read a single stray/partial digit ('7', matching +nothing real on this event) at just one row position while the other 4 +were still `None`. The old `_find_stage_row` treated ANY non-`None` read +as proof the page was genuinely, fully rendered -- so it immediately +concluded "confirmed right page, stage 11 just isn't in it" +(`stage_not_found`) off that single transient misread. `stage_not_found` +was never in the retryable set in `run()`'s loop (only `wrong_page` was), +so the whole task exited immediately instead of returning home and +retrying -- the exact behavior reported. + +By coincidence, live investigation into this (clicking the event badge at +the exact instant it displayed the OLD/finished event's promotional text, +which -- unlike every earlier attempt in this file's history -- actually +DID open that finished event's own page this time, confirming the timing +really is the deciding factor, not the dot-clicking this project already +gave up on) also surfaced a second, independent real bug: the finished- +event page's own confirmed real capture (user-supplied +scratchpad/event_event.png, and a fresh live re-capture used for +calibration) showed `EVENT_FINISHED_TEXT_RECT`'s OCR read returning pure +garbage with no "終了" match at all -- not merely clipped, almost entirely +background office-art below the real text line, which actually sits +higher on screen than the old rect assumed. This means the "authoritative" +finished-page check had been silently broken (always reading false- +negative) at least since whatever event succeeded the one it was last +calibrated against, quietly falling through to the same fragile +saw-any-valid-row heuristic that caused the bug above. Re-calibrated via +scratchpad/probe_event_finished_text.py (since deleted) against the live +page directly -- confirmed the corrected rect reads clean text containing +"終了". + +Fixed three ways: +1. `_scan_stage_rows_once` now returns the full labels list, not just a + `saw_any_valid_row` bool. `_find_stage_row` no longer trusts a "some + rows read, target not among them" conclusion the first time it appears + -- it now requires the exact same labels to read back identically on + two consecutive scans before treating "genuinely not found" as settled, + the same stabilize-before-trusting fix already applied to the all- + `None` case. A single transient misread can no longer end the scan + early. +2. `config.EVENT_FINISHED_TEXT_RECT` corrected from (1030,600,1810,750) to + (1080,585,1760,635) -- see that constant's own comment for the before/ + after OCR reads. +3. `run()`'s retry loop now treats `stage_not_found` the same as + `wrong_page` (return home, retry immediately) rather than as terminal -- + this event's target range is always supposed to be in the visible + bottom rows of a genuinely correct, fully-rendered page, so failing to + find it there (post-stabilization) is much more likely to mean "wrong + page" than "this event really lacks it." + +Not yet re-confirmed against a fresh real failure of the exact original +kind (the live investigation above spent its effort reproducing/fixing the +finished-text rect instead, since that page happened to be reachable at +the time) -- next real `stage_not_found`-triggering run should confirm the +stability fix directly. """ import datetime import os @@ -382,15 +442,15 @@ def _row_number_rect(config, row_y): def _scan_stage_rows_once(driver, config, stage): - saw_any_valid_row = False + labels = [] + row_y_match = None for row_y in config.EVENT_STAGE_ROW_Y: label = detector.read_int_bordered(_row_number_rect(config, row_y)) print(f"[event_sweep] row @ {row_y}: read '{label}'") - if label is not None: - saw_any_valid_row = True + labels.append(label) if label == stage: - return row_y, saw_any_valid_row - return None, saw_any_valid_row + row_y_match = row_y + return row_y_match, labels def _is_finished_event_page(driver, config): @@ -422,20 +482,46 @@ def _find_stage_row(driver, config, stage): # page) is the AUTHORITATIVE signal now. All-empty reads alone just # mean "keep waiting patiently", not "wrong page" -- see module # docstring for the full history of this getting fixed twice. + # + # A real run's log (2026-07-29, user report) surfaced a related false + # positive this all-empty handling didn't cover: scan 1 read all 5 rows + # None (correctly treated as "still settling"), but scan 2 read a single + # stray/partial digit ('7', not matching any real row on this event) at + # just ONE row position while the other 4 were still None -- one non- + # None read was enough to satisfy the old "saw ANY valid row -> trust + # it" check, so the loop immediately concluded "genuinely on the right + # page, target stage just isn't in it" (stage_not_found) off a single + # transient misread, instead of recognizing the list was still mid- + # render. Fixed the same way the all-empty case already was: don't + # trust a "some rows read, target not among them" conclusion the first + # time it appears either -- require the exact same set of labels to + # read back identically on two consecutive scans first. A genuinely + # rendered (even wrong) page's OCR reads are stable frame to frame; a + # mid-render one isn't, which is exactly the signal that already fixed + # the all-empty version of this same problem class. x, y = config.EVENT_STAGE_LIST_SCROLL_POINT driver.scroll(x, y, "down", config.EVENT_STAGE_LIST_SCROLL_CLICKS) driver.wait(0.5) + previous_labels = None for attempt in range(1, STAGE_ROW_SCAN_ATTEMPTS + 1): - row_y, saw_any_valid_row = _scan_stage_rows_once(driver, config, stage) - if row_y is not None or saw_any_valid_row: - return row_y, saw_any_valid_row, False + row_y, labels = _scan_stage_rows_once(driver, config, stage) + if row_y is not None: + return row_y, True, False + + saw_any_valid_row = any(label is not None for label in labels) + if saw_any_valid_row and labels == previous_labels: + return None, True, False if _is_finished_event_page(driver, config): print("[event_sweep] finished-event page text detected -- confirmed wrong page") return None, False, True - print(f"[event_sweep] no stage-row numbers recognized on scan {attempt}/{STAGE_ROW_SCAN_ATTEMPTS} -- screen may still be settling") + if saw_any_valid_row: + print(f"[event_sweep] some rows read but not yet stable across scans (scan {attempt}/{STAGE_ROW_SCAN_ATTEMPTS}): {labels}") + else: + print(f"[event_sweep] no stage-row numbers recognized on scan {attempt}/{STAGE_ROW_SCAN_ATTEMPTS} -- screen may still be settling") + previous_labels = labels if attempt < STAGE_ROW_SCAN_ATTEMPTS: driver.wait(STAGE_ROW_SCAN_RETRY_WAIT) @@ -672,14 +758,26 @@ def run(driver, config): continue outcome = _sweep_target(driver, config, stage, count) - if outcome == "wrong_page": - print(f"[event_sweep] landed on the wrong event page (attempt {attempt}/{WRONG_PAGE_RETRIES}) -- retrying immediately") + if outcome in ("wrong_page", "stage_not_found"): + # stage_not_found is treated as retryable too, not just + # wrong_page: this event's target range (9-12) is always + # supposed to sit within the last 5 rows of a fully-rendered + # correct page (see _find_stage_row), so failing to find it + # there -- once a stable, settled read confirms it's genuinely + # absent, not just a still-rendering page -- is far more likely + # to mean "this isn't actually the event we think it is" than + # "the right page legitimately lacks this stage." Real user + # report (2026-07-29): a single stray/unstable OCR read used to + # get treated as "confirmed right page, stage missing" and exit + # immediately with no retry at all -- see _find_stage_row's own + # stability fix and this module's docstring for the full story. + print(f"[event_sweep] landed on the wrong event page (attempt {attempt}/{WRONG_PAGE_RETRIES}, outcome={outcome}) -- retrying immediately") continue break else: print(f"[event_sweep] could not reach the current event's stage list after {WRONG_PAGE_RETRIES} attempts, giving up") - if outcome is not None and outcome not in ("swept", "wrong_page"): + if outcome is not None and outcome not in ("swept", "wrong_page", "stage_not_found"): print(f"[event_sweep] target stage {stage} ended in '{outcome}'") if not navigation.return_to_home(driver): diff --git a/plan.md b/plan.md index 40bfca5..3998771 100644 --- a/plan.md +++ b/plan.md @@ -708,6 +708,18 @@ This overturns follow-up #5's own theory. That fix assumed the carousel's auto-r **Confirmed live at zero AP cost**: 3 separate trials of `return_to_home` → `_open_event_screen` (no dot, no wait) → checking `_is_finished_event_page`/reading the stage rows, run immediately after deploying. All 3 landed on the correct current event on the very first attempt — no `wrong_page` outcome at all across any trial (previously, the same account's badge was landing wrong 3 times out of 4 attempts with the old design). The full sweep-and-result path through this navigation wasn't re-exercised in the same check (that needs spending real AP) — deferred to the user's next real run. +#### Phase 14 follow-up #9: stray single-digit misread treated as a confirmed "stage not found" + `EVENT_FINISHED_TEXT_RECT` silently broken (2026-07-29) + +**Reported** directly by the user with the exact log: "When it enter wrong page it will just wait then exit on false positive instead of return home and retry to enter correct page." The log showed precisely why: scan 1 read all 5 stage rows `None` (correctly treated as "still settling"); scan 2 read a single stray/partial digit (`'7'`, matching nothing real on this event) at just one row position while the other 4 stayed `None`. The old `_scan_stage_rows_once`/`_find_stage_row` treated ANY non-`None` read, even one, as proof the page was genuinely, fully rendered — so it immediately concluded "confirmed right page, target stage just isn't in it" (`stage_not_found`) off that single transient misread. Unlike `wrong_page`, `stage_not_found` was never in `run()`'s retryable set, so the whole task exited immediately instead of returning home and retrying. + +While investigating (reproducing wrong-page states live, plus a real screenshot the user supplied of the actual finished-event page, `scratchpad/event_event.png`), a second, independent bug surfaced: `EVENT_FINISHED_TEXT_RECT`'s "authoritative" check — previously live-confirmed back on 2026-07-10 against an earlier finished event — read pure background-art garbage with no "終了" match at all when re-tested against a real capture of the CURRENT finished event ("シャーレの総決算 with 連邦生徒会"), not just clipped text, almost entirely the reception-desk art below the real text line. Whether the panel's text position genuinely shifted between different finished events or the original calibration was never quite centered, this meant the authoritative check had been silently broken for some unknown span of time, quietly falling back to the same fragile row-count heuristic that caused the bug above. Re-calibrated directly against a fresh live capture (`scratchpad/probe_event_finished_text.py`, since deleted, tested several candidate rects via the real `lang="jpn"` OCR call): corrected from `(1030, 600, 1810, 750)` to `(1080, 585, 1760, 635)`. + +Also notable: reproducing the finished-event page required clicking the home-screen event badge at the exact instant it displayed that page's own promotional text — every earlier attempt in this same investigation (clicking a moment later) landed on the actual current event instead, further confirming follow-up #8's "click immediately, don't wait" finding rather than contradicting it. The current event turned out to have moved on since this module was first calibrated — "出航!万魔紐 マコト議長の華麗なる避暑" started 2026-07-29, succeeding whatever was running when `EVENT_STAGE_ROW_Y` etc. were last tuned — but it happens to share the identical 12-stage/last-5-rows-visible template, so no navigation constants needed retuning, just these two logic/calibration bugs. + +**Fixed three ways**: (1) `_scan_stage_rows_once` now returns the full labels list instead of a `saw_any_valid_row` bool; `_find_stage_row` no longer trusts a "some rows read, target not among them" conclusion the first time it appears — it now requires the exact same labels to read back identically on two consecutive scans first, the same stabilize-before-trusting protection the all-`None` case already had. (2) `config.EVENT_FINISHED_TEXT_RECT` re-calibrated as above. (3) `run()`'s retry loop now treats `stage_not_found` the same as `wrong_page` (return home, retry immediately) rather than as terminal — this event's target range is always supposed to be in the visible bottom rows of a genuinely correct, fully-rendered page, so failing to find it there, once stabilized, is far more likely to mean "wrong page" than "this event really lacks it." + +**Confirmed live immediately after deploying, real run**: attempt 1 landed on the finished-event page and this time correctly, immediately identified it via the fixed text rect (previously would have silently fallen through to the broken heuristic); returned home and retried immediately; attempt 2 landed on the actual current event, read all 5 rows cleanly (8,9,10,11,12, no stray misreads), found the target stage; the MAX-count-raise couldn't be confirmed (that stage isn't sweep-eligible yet on this account), so it safely declined without spending AP — a different, legitimate outcome, not a bug. Clean return to home confirmed by screenshot. The full wrong-page-to-correct-page recovery loop the user asked for worked exactly as intended, end to end, on the very first post-fix run. + ### Phase 15: Bounty Reference: `module/rewarded_task.py`. Local: `ba_auto/tasks/bounty.py`.