diff --git a/ba_auto/config.py b/ba_auto/config.py index 8a87353..5fb3a2a 100644 --- a/ba_auto/config.py +++ b/ba_auto/config.py @@ -319,10 +319,25 @@ EVENT_STAGE_MODAL_CLOSE_BUTTON = (1704, 271) # The AP-usage confirmation dialog raised by 掃討開始, and the post-sweep # "掃討完了" SKIP/OK result screen, reuse SWEEP_CONFIRM_BUTTON/ -# SWEEP_CONFIRM_CANCEL_BUTTON/SWEEP_CONFIRM_CYAN/SWEEP_CONFIRM_GOLD/ -# SWEEP_RESULT_BUTTON_REGION directly (defined above under story_sweep) -- -# confirmed live pixel-identical position and color, since it's the same -# shared dialog component both tasks reach after their own 掃討開始 click. +# SWEEP_CONFIRM_CANCEL_BUTTON/SWEEP_CONFIRM_CYAN/SWEEP_CONFIRM_GOLD directly +# (defined above under story_sweep) -- confirmed live pixel-identical +# position and color, since it's the same shared dialog component both +# tasks reach after their own 掃討開始 click. +# +# NOT shared: SWEEP_RESULT_BUTTON_REGION. Confirmed live (2026-07-11, +# scratchpad/probe_result_button_fp.py) that region's x-range (700-1300) +# reaches into this event's own Quest-list character-art panel on the +# left, which false-positive-matched SWEEP_CONFIRM_CYAN on the plain list +# with NO sweep-result dialog showing at all -- this is why +# _watch_sweep_result kept "finding" a result button and never reached its +# "modal closed, no result button" end condition after a real sweep +# finished, even though the sweep itself succeeded (AP spent, credits +# gained, confirmed by screenshot). Narrower x-range here (1000-1300) +# still comfortably covers the real buttons (SWEEP_CONFIRM_BUTTON's own +# x=1150 sits well inside it) while excluding the character-art panel, +# confirmed against the same saved false-positive screenshot returning +# no match afterward. +EVENT_SWEEP_RESULT_BUTTON_REGION = (1000, 700, 1300, 1050) # Rotation target: which of stages 9-12 to sweep today, per explicit user # request (2026-07-10) -- "the current event has up to 12 stages, randomly diff --git a/ba_auto/detector.py b/ba_auto/detector.py index ea0e2dc..484b113 100644 --- a/ba_auto/detector.py +++ b/ba_auto/detector.py @@ -202,6 +202,41 @@ def read_int(region, psm=7): return int(digits) if digits else None +def read_int_bordered(region, psm=7, border=20): + """read_int, but with a white margin added around the upscaled crop + before OCR. + + event_sweep.py's stage-row numbers "08"/"09" (a leading-zero digit + tight against the following one) were misread by plain read_int on + every psm mode tried -- "08" consistently as "2", "09" as empty or "9" + with the leading zero silently dropped -- even though the crop and its + thresholded/upscaled version both look completely clean to the eye. + "10"/"11"/"12" at the same crop dimensions read fine. Confirmed live + (scratchpad/probe_ocr_fix_08_09.py) this is specifically a tesseract + segmentation problem with a text blob that fills the crop edge-to-edge, + with no surrounding whitespace context to anchor character boundaries + -- adding a plain white cv2.copyMakeBorder margin (no other pipeline + change) fixed both "08" and "09" to their exact correct digits at every + psm mode tried, and left the already-working "10" unaffected. Kept as + its own function rather than changed in read_int/_ocr_crop, since only + this one tight edge-to-edge crop shape has ever been confirmed to need + it -- every other OCR read in this project already has enough natural + margin. + """ + x1, y1, x2, y2 = region + driver.screenshot(OCR_SHOT_PATH) + img = cv2.imread(OCR_SHOT_PATH) + crop = img[y1:y2, x1:x2] + gray = cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY) + _, thresh = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY) + upscaled = cv2.resize(thresh, None, fx=OCR_UPSCALE, fy=OCR_UPSCALE, interpolation=cv2.INTER_CUBIC) + bordered = cv2.copyMakeBorder(upscaled, border, border, border, border, cv2.BORDER_CONSTANT, value=255) + tess_config = f"--psm {psm} -c tessedit_char_whitelist=0123456789" + text = pytesseract.image_to_string(bordered, lang="eng", config=tess_config).strip() + digits = "".join(ch for ch in text if ch.isdigit()) + return int(digits) if digits else None + + def read_int_on_heart_badge(region, psm=7): """OCR a small dark-navy digit rendered on lesson.py's pink/magenta heart-shaped affection badge. diff --git a/ba_auto/reference_notes/mapping.md b/ba_auto/reference_notes/mapping.md index bc28811..f8d969b 100644 --- a/ba_auto/reference_notes/mapping.md +++ b/ba_auto/reference_notes/mapping.md @@ -8,7 +8,7 @@ Maps each local feature to the corresponding `~/repo/baas-reference/module/...` | Cafe | `module/cafe_reward.py` | `to_cafe` (its `relationship_rank_up` popup-handling now also ported, see below), `interaction_for_cafe_solve_method3`, `collect` | `ba_auto/tasks/cafe.py` | `picture.co_detect`/`color.rgb_in_range` → `driver.color_at` pixel-probe checks; sparkle template match ported in-process into `ba_auto/detector.py` (`find_cafe_sparkle`, now multi-scale) | Migrated: real Python, state-verified via color probes (no legacy bridge). Pat loop now polls for the full attempt budget instead of stopping on the first miss (see `plan.md` Phase 6 follow-up) — not yet confirmed against a live sparkle since none was available during testing. `_dismiss_rank_up_if_shown` reuses `navigation.is_on_subscreen` to detect and clear the full-screen bond-rank-up cutscene after a pat (see `plan.md` Phase 6 follow-up: rank-up popups) — not yet live-confirmed against a real trigger | | Stamina/AP | `module/collect_daily_free_power.py`, `module/collect_daily_task_power.py` | `to_tasks`/`implement` (task-power, ported); `to_purchase_pyroxenes_menu` (free-power, not ported) | `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 | Partially migrated: Mission-panel claim done (see `plan.md` Phase 8). Daily Free Power (real-money purchase menu) deliberately not automated | | 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. | +| 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 | | Bounty | `module/rewarded_task.py` | Need to inspect | `ba_auto/tasks/bounty.py` | sweep/color/OCR adaptation | Not started | | Commissions | `module/clear_special_task_power.py` | Need to inspect | `ba_auto/tasks/commission.py` | sweep/color adaptation | Not started | diff --git a/ba_auto/tasks/event_sweep.py b/ba_auto/tasks/event_sweep.py index e2e2df2..dbb22a7 100644 --- a/ba_auto/tasks/event_sweep.py +++ b/ba_auto/tasks/event_sweep.py @@ -167,6 +167,54 @@ natural rotation a real chance to land on the correct item within the retry window, rather than relying on a fast-but-unreliable dot-click to force it. Dot-clicking is kept as a harmless best-effort nudge alongside the longer wait, not removed, since it did visibly work at least twice. + +Stage 08/09 OCR misread (2026-07-11, previously flagged in plan.md as a +known-but-non-blocking gap, then hit for real once the daily rotation +picked stage 9): row @366 ("08") read as "2", row @538 ("09") read as +empty/None, on every psm mode tried with plain detector.read_int, even +though the crop and its thresholded/upscaled version both looked perfectly +clean by eye (scratchpad/probe_event_stage_ocr.py, probe_ocr_psm_sweep.py) +-- confirmed live NOT a navigation/timing bug this time, since rows +710/883/1055 ("10"/"11"/"12") read correctly in the same run. Root cause, +isolated via scratchpad/probe_ocr_fix_08_09.py: tesseract's segmentation +struggles with this specific tight edge-to-edge crop (no surrounding +whitespace margin) for a leading-zero digit pair specifically -- adding a +plain white border around the upscaled crop before OCR fixed both "08" +and "09" to their exact correct values at every psm mode tried, without +affecting the already-working "10". Ported as detector.read_int_bordered, +now used for all EVENT_STAGE_ROW_Y reads in _scan_stage_rows_once. + +The same live run also confirmed the sweep itself succeeded end-to-end +(AP 233->14, credits +5,892, screenshot-confirmed safe return home) but +still logged "unrecognized_state" instead of "swept" -- the +_watch_sweep_result outcome-logging bug flagged after Phase 14 follow-up +#5 as "not yet re-verified live" was, it turns out, still broken, just for +a DIFFERENT reason than the POST_SWEEP_DISMISS_ROUNDS timing budget that +fix targeted. Root cause this time, isolated with zero AP cost via +scratchpad/probe_result_button_fp.py (checks _find_result_button against +the plain Quest list with no sweep in progress): the shared +SWEEP_RESULT_BUTTON_REGION (700-1300 x-range, borrowed directly from +story_sweep.py) reaches into this event's own Quest-list character-art +panel on the left side of the screen, which false-positive-matched +SWEEP_CONFIRM_CYAN even with no result dialog showing at all -- confirmed +on both a wrong/finished event page and, more importantly, the actual +correct current event's own plain list. This meant _watch_sweep_result's +reaction kept "finding" a result button and clicking it after the real +one had already been fully dismissed, so the "modal closed, no result +button" end condition could never match. Fixed with a new, event_sweep- +only EVENT_SWEEP_RESULT_BUTTON_REGION (x narrowed to 1000-1300, excluding +the character-art panel while still comfortably covering the real +buttons' known x~1150 position) -- confirmed via the same saved +false-positive screenshot returning no match afterward. Deliberately NOT +changed in the shared SWEEP_RESULT_BUTTON_REGION story_sweep.py also uses, +per this file's own established pattern of giving event_sweep its own +constant rather than coupling the two tasks' config together whenever +their actual on-screen content differs (see EVENT_STAGE_MODAL_PROBE's +comment for the earlier instance of the same reasoning). Not yet +re-confirmed against a fresh real bulk sweep (that day's AP was reduced to +14/240 by the sweep that surfaced this, too low to force another MAX +sweep) -- the fix is validated against the actual recorded false-positive +screenshot and the same live code path, just not a full new live sweep. """ import datetime import os @@ -206,7 +254,7 @@ def _is_ap_purchase_prompt(driver, config): def _find_result_button(driver, config): - return detector.find_color_centroid(config.SWEEP_RESULT_BUTTON_REGION, *config.SWEEP_CONFIRM_CYAN) + return detector.find_color_centroid(config.EVENT_SWEEP_RESULT_BUTTON_REGION, *config.SWEEP_CONFIRM_CYAN) def _count_raised_above_one(driver, config): @@ -258,7 +306,7 @@ def _row_number_rect(config, row_y): def _scan_stage_rows_once(driver, config, stage): saw_any_valid_row = False for row_y in config.EVENT_STAGE_ROW_Y: - label = detector.read_int(_row_number_rect(config, 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 diff --git a/plan.md b/plan.md index 65e7874..f20f27e 100644 --- a/plan.md +++ b/plan.md @@ -159,7 +159,7 @@ Do not implement a feature without filling at least the relevant row. | Common Shop / Tactical Shop | Done (Phase 11): `ba_auto/tasks/shop_common.py` / `shop_tactical.py` share a checkbox-grid-then-bulk-buy flow (`ba_auto/tasks/shop_utils.py`) against config-driven `(row, col, name, expected_price)` targets, price-OCR-verified before each click. Live-tested with real purchases in both shops. Opt-in only (`shop_common`/`shop_tactical` commands), not part of the default daily flow | Done | | Lesson/Schedule | Done (Phase 12): `ba_auto/tasks/lesson.py` sweeps every unlocked region's schedule grid, picking the highest-affection available lesson each time via a dedicated heart-badge OCR read (`detector.read_int_on_heart_badge`) until tickets or lessons run out. Live-tested with real tickets spent; two real bugs (checkmark-doesn't-blank-the-number, badge OCR misreads) found and fixed. Opt-in only (`lesson` command), not part of the default daily flow | Done | | Arena / Tactical Challenge | Done (Phase 13): `ba_auto/tasks/arena.py` fights exactly one ranked battle per invocation and collects both reward slots. Live-tested across all 5 of the account's daily tickets (2 WIN, 1 LOSE); the post-fight WIN/LOSE result modal proved undetectable by precise button-color search and was fixed via a bounded blind-Enter loop gated by a hard safety check. A later consecutive-invocation navigation bug (Phase 13 follow-up, fixed via `navigation.return_to_home` at start of `run()`) is also confirmed live. Opt-in only (`arena` command), not part of the default daily flow | Done | -| Event sweep | Done (Phase 14 + follow-ups 1-4): `ba_auto/tasks/event_sweep.py` sweeps one config-rotated stage (9-12) of the currently-running event per invocation. First confirmed real live sweep 2026-07-10 (200 AP spent, 10x MAX sweep, credits gained, confirmed by screenshot) after fixing a badge-carousel navigation bug (pagination dots must be clicked directly, not just the ambiguous rotating badge) and a cold-start OCR timing bug (stage list can take ~20s to populate after a fresh navigation). Opt-in only (`event_sweep` command), not part of the default daily flow | Done, one cosmetic result-outcome-logging fix (`POST_SWEEP_DISMISS_ROUNDS`) not yet re-verified live | +| Event sweep | Done (Phase 14 + follow-ups 1-6): `ba_auto/tasks/event_sweep.py` sweeps one config-rotated stage (9-12) of the currently-running event per invocation. First confirmed real live sweep 2026-07-10 (200 AP spent, 10x MAX sweep, credits gained, confirmed by screenshot) after fixing a badge-carousel navigation bug (pagination dots must be clicked directly, not just the ambiguous rotating badge) and a cold-start OCR timing bug (stage list can take ~20s to populate after a fresh navigation). Follow-up #6 (2026-07-11) fixed a stage 08/09 OCR misread (`detector.read_int_bordered`) and the real root cause of the `unrecognized_state` mislabeling (a false-positive color match on the event's own character art, fixed via `EVENT_SWEEP_RESULT_BUTTON_REGION`) -- both confirmed live, the second via the actual false-positive condition rather than a fresh full sweep (AP was too low that day). Opt-in only (`event_sweep` command), not part of the default daily flow | Done, one outcome-logging fix awaiting one more full live sweep to confirm the log itself reads "swept" | | Shared driver | `ba_auto/driver.py` built (`run_command`, `focus_game`, `click`, `move_mouse`, `scroll`, `keypress`, `screenshot`, `wait`, `color_at`); `click()` now splits `mousemove`/`click` into two xdotool calls (Phase 8 finding — fixes a real source of click flakiness); wired into `mailbox.py`, `cafe.py`, `stamina.py`, `story_sweep.py`, `shop_common.py`, `shop_tactical.py`, `lesson.py` | Extend with new primitives as future tasks need them | | Python CLI | Built: `ba_daily.py` dispatches `mailbox`/`cafe`/`stamina`/`story_sweep`/`shop_common`/`shop_tactical`/`lesson`/default flow | Extend as new tasks are added | | Reference mapping | Built: `ba_auto/reference_notes/mapping.md` | Fill in reference file/function columns per feature | @@ -550,6 +550,18 @@ This is the same failure class `CLAUDE.md` already documents for the mailbox/caf **Status**: the wrong-page-looping bug the user reported twice is now considered definitively fixed, confirmed via two independent real live sweeps in this follow-up alone (three total across the whole Phase 14 investigation). The `_watch_sweep_result` fix is well-reasoned and low-risk (outcome-logging correctness only; the actual sweep behavior was already safe even without it) but awaits live reconfirmation once AP regenerates. +#### Phase 14 follow-up #6: stage 08/09 OCR misread (the flagged non-blocking gap, now actually hit) + the real `_watch_sweep_result` root cause + +**Reported 2026-07-11**: the daily rotation picked stage 9 for the first time since this task existed — exactly the previously-flagged-but-never-hit "stage-list rows for stages 08/09 consistently misread by OCR" gap. Real log: rows 366/538 ("08"/"09") read `None` on the first (wrong-page) attempt, then `'2'` and `None` respectively on the second (correct-page) attempt, while rows 710/883/1055 ("10"/"11"/"12") read correctly — confirmed NOT a navigation/timing bug this time. + +**Root cause** (isolated with `scratchpad/probe_event_stage_ocr.py` and `probe_ocr_psm_sweep.py`, run directly against a live screenshot with no game interaction needed after capture): the crop and its thresholded/upscaled version both looked completely clean by eye, but tesseract's segmentation consistently misreads "08" as "2" and drops/mangles "09" across every psm mode tried. `scratchpad/probe_ocr_fix_08_09.py` isolated why: the crop is edge-to-edge with no whitespace margin around the glyph pair, and adding a plain white border around the upscaled image before OCR fixed both digits to their exact correct values at every psm mode, without affecting the already-working "10". **Fix**: new `detector.read_int_bordered` (same pipeline as `read_int`, plus `cv2.copyMakeBorder`), used for all `EVENT_STAGE_ROW_Y` reads. Kept as its own function rather than changed in the shared `read_int`/`_ocr_crop`, since no other current OCR read in this project has this specific tight-crop shape. + +**Confirmed live**: ran `event_sweep` for real. `row @ 366: read '8'`, `row @ 538: read '9'` — stage 9 found, modal opened, sweep confirmed and executed. AP 233/240 → 14/240, credits +5,892, screenshot-confirmed safe return home. The originally reported bug is fixed. + +**Bonus finding**: the same run again logged `unrecognized_state` instead of `swept`, despite the sweep genuinely succeeding — proving follow-up #5's `clicked_any`-gate fix, while a real improvement, was not the whole story. Diagnosed at zero AP cost via `scratchpad/probe_result_button_fp.py` (checks `_find_result_button` against the plain Quest list with no sweep in progress, no AP needed to reproduce): the shared `SWEEP_RESULT_BUTTON_REGION` (x: 700–1300, borrowed directly from `story_sweep.py`) reaches into this event's own Quest-list character-art panel on the left side of the screen, which false-positive-matches `SWEEP_CONFIRM_CYAN` even with no result dialog showing at all — confirmed on both a wrong/finished event page and, more importantly, the actual correct event's own plain list. This meant `_watch_sweep_result`'s reaction kept "finding" a result button and clicking it after the real one had already been dismissed, so its "modal closed, no result button" end condition could never match. **Fix**: new `config.EVENT_SWEEP_RESULT_BUTTON_REGION` (x narrowed to 1000–1300, excluding the character-art panel while still comfortably covering the real buttons' known x≈1150 position), used only by `event_sweep.py`'s `_find_result_button` — deliberately not changed in the shared region `story_sweep.py` also uses, matching this task's established pattern of giving event_sweep its own constant whenever the two tasks' actual on-screen content differs. **Confirmed live** (zero AP): re-ran the same false-positive probe against the deployed fix on the real correct-event Quest list — `_find_result_button` now correctly returns `None` where it previously returned a false match. Not yet re-confirmed against a fresh full real sweep end-to-end, since that day's AP was down to 14/240 (too low for another MAX sweep) — the fix is validated against the actual live false-positive scenario and the real code path, just not a brand-new full sweep cycle. + +**Status**: both bugs fixed. The originally reported bug (stage 9 unreadable) is fully confirmed via a real successful live sweep. The `_watch_sweep_result` mislabeling's real root cause is now understood and fixed (previous fix attempt addressed a real but secondary issue); confirmed against the actual live false-positive condition, awaiting one more full live sweep to confirm the outcome log itself reads `"swept"` next time AP allows. + ## Prerequisites ### OCR