fix(event_sweep): resolve OCR misreads for stages 08/09 and improve result button detection logic
This commit is contained in:
parent
634d77d9d2
commit
166a34825a
@ -319,10 +319,25 @@ EVENT_STAGE_MODAL_CLOSE_BUTTON = (1704, 271)
|
|||||||
|
|
||||||
# The AP-usage confirmation dialog raised by 掃討開始, and the post-sweep
|
# The AP-usage confirmation dialog raised by 掃討開始, and the post-sweep
|
||||||
# "掃討完了" SKIP/OK result screen, reuse SWEEP_CONFIRM_BUTTON/
|
# "掃討完了" SKIP/OK result screen, reuse SWEEP_CONFIRM_BUTTON/
|
||||||
# SWEEP_CONFIRM_CANCEL_BUTTON/SWEEP_CONFIRM_CYAN/SWEEP_CONFIRM_GOLD/
|
# SWEEP_CONFIRM_CANCEL_BUTTON/SWEEP_CONFIRM_CYAN/SWEEP_CONFIRM_GOLD directly
|
||||||
# SWEEP_RESULT_BUTTON_REGION directly (defined above under story_sweep) --
|
# (defined above under story_sweep) -- confirmed live pixel-identical
|
||||||
# confirmed live pixel-identical position and color, since it's the same
|
# position and color, since it's the same shared dialog component both
|
||||||
# shared dialog component both tasks reach after their own 掃討開始 click.
|
# 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
|
# 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
|
# request (2026-07-10) -- "the current event has up to 12 stages, randomly
|
||||||
|
|||||||
@ -202,6 +202,41 @@ def read_int(region, psm=7):
|
|||||||
return int(digits) if digits else None
|
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):
|
def read_int_on_heart_badge(region, psm=7):
|
||||||
"""OCR a small dark-navy digit rendered on lesson.py's pink/magenta
|
"""OCR a small dark-navy digit rendered on lesson.py's pink/magenta
|
||||||
heart-shaped affection badge.
|
heart-shaped affection badge.
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -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
|
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
|
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.
|
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 datetime
|
||||||
import os
|
import os
|
||||||
@ -206,7 +254,7 @@ def _is_ap_purchase_prompt(driver, config):
|
|||||||
|
|
||||||
|
|
||||||
def _find_result_button(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):
|
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):
|
def _scan_stage_rows_once(driver, config, stage):
|
||||||
saw_any_valid_row = False
|
saw_any_valid_row = False
|
||||||
for row_y in config.EVENT_STAGE_ROW_Y:
|
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}'")
|
print(f"[event_sweep] row @ {row_y}: read '{label}'")
|
||||||
if label is not None:
|
if label is not None:
|
||||||
saw_any_valid_row = True
|
saw_any_valid_row = True
|
||||||
|
|||||||
14
plan.md
14
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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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.
|
**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
|
## Prerequisites
|
||||||
|
|
||||||
### OCR
|
### OCR
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user