Fix handling of transient misreads and improve event page checks

- Updated `_scan_stage_rows_once` to return full labels list instead of a boolean, allowing for better validation of stage presence.
- Enhanced `_find_stage_row` to require consistent label readings across consecutive scans to avoid false positives from transient misreads.
- Corrected `EVENT_FINISHED_TEXT_RECT` coordinates to ensure accurate OCR readings for finished events.
- Modified `run()`'s retry logic to treat `stage_not_found` similarly to `wrong_page`, enabling retries for potentially misidentified pages.
- Confirmed fixes through live testing, ensuring robust navigation and accurate event identification.
This commit is contained in:
Nik Afiq 2026-07-29 23:10:09 +09:00
parent 3c02178b3d
commit 6cdce8ccdd
4 changed files with 149 additions and 28 deletions

View File

@ -573,22 +573,33 @@ HARD_STORY_SWEEP_TARGETS = [
EVENT_BADGE_ICON = (1787, 300) EVENT_BADGE_ICON = (1787, 300)
# A finished event's Quest tab shows plain "イベント期間が終了しました。" # A finished event's Quest tab shows plain "イベント期間が終了しました。"
# (event period has ended) text instead of any stage-row cards. This rect # (event period has ended) text instead of any stage-row cards. Used as an
# and the lang="jpn" OCR read against it are LIVE-CONFIRMED both ways # early-exit, authoritative-when-positive check inside _find_stage_row's
# (2026-07-10, against "嵐過天晴", a genuinely finished event reachable via # scan loop -- a positive match ends the wait immediately as a confirmed
# the badge carousel's 2nd dot position at that time): the OCR returned the # wrong page; a negative match does NOT prove the page is right (a
# exact phrase verbatim on the finished-event page (`detector.read_text` # different finished event's layout might differ), so patient rescanning
# with psm=6), and on the correct/current event's page the same rect reads # still continues regardless either way up to the existing budget. If that
# unrelated stage-list text with no false-positive "終了" substring match. # budget is ever fully exhausted with no rows and no confirmed
# Used as an early-exit, authoritative-when-positive check inside # finished-page text, _find_stage_row saves a debug screenshot to
# _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. # 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 # Top-right tab bar inside the event screen (reference's activity_menu
# story/mission/challenge tabs -- rendered as English labels "Story / Quest / # story/mission/challenge tabs -- rendered as English labels "Story / Quest /

File diff suppressed because one or more lines are too long

View File

@ -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 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 check (that needs spending real AP, deferred pending the user's own next
real run). 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 datetime
import os import os
@ -382,15 +442,15 @@ 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 labels = []
row_y_match = None
for row_y in config.EVENT_STAGE_ROW_Y: for row_y in config.EVENT_STAGE_ROW_Y:
label = detector.read_int_bordered(_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: labels.append(label)
saw_any_valid_row = True
if label == stage: if label == stage:
return row_y, saw_any_valid_row row_y_match = row_y
return None, saw_any_valid_row return row_y_match, labels
def _is_finished_event_page(driver, config): 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 # page) is the AUTHORITATIVE signal now. All-empty reads alone just
# mean "keep waiting patiently", not "wrong page" -- see module # mean "keep waiting patiently", not "wrong page" -- see module
# docstring for the full history of this getting fixed twice. # 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 x, y = config.EVENT_STAGE_LIST_SCROLL_POINT
driver.scroll(x, y, "down", config.EVENT_STAGE_LIST_SCROLL_CLICKS) driver.scroll(x, y, "down", config.EVENT_STAGE_LIST_SCROLL_CLICKS)
driver.wait(0.5) driver.wait(0.5)
previous_labels = None
for attempt in range(1, STAGE_ROW_SCAN_ATTEMPTS + 1): for attempt in range(1, STAGE_ROW_SCAN_ATTEMPTS + 1):
row_y, saw_any_valid_row = _scan_stage_rows_once(driver, config, stage) row_y, labels = _scan_stage_rows_once(driver, config, stage)
if row_y is not None or saw_any_valid_row: if row_y is not None:
return row_y, saw_any_valid_row, False 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): if _is_finished_event_page(driver, config):
print("[event_sweep] finished-event page text detected -- confirmed wrong page") print("[event_sweep] finished-event page text detected -- confirmed wrong page")
return None, False, True return None, False, True
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") 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: if attempt < STAGE_ROW_SCAN_ATTEMPTS:
driver.wait(STAGE_ROW_SCAN_RETRY_WAIT) driver.wait(STAGE_ROW_SCAN_RETRY_WAIT)
@ -672,14 +758,26 @@ def run(driver, config):
continue continue
outcome = _sweep_target(driver, config, stage, count) outcome = _sweep_target(driver, config, stage, count)
if outcome == "wrong_page": if outcome in ("wrong_page", "stage_not_found"):
print(f"[event_sweep] landed on the wrong event page (attempt {attempt}/{WRONG_PAGE_RETRIES}) -- retrying immediately") # 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 continue
break break
else: else:
print(f"[event_sweep] could not reach the current event's stage list after {WRONG_PAGE_RETRIES} attempts, giving up") 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}'") print(f"[event_sweep] target stage {stage} ended in '{outcome}'")
if not navigation.return_to_home(driver): if not navigation.return_to_home(driver):

12
plan.md
View File

@ -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. **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 ### Phase 15: Bounty
Reference: `module/rewarded_task.py`. Local: `ba_auto/tasks/bounty.py`. Reference: `module/rewarded_task.py`. Local: `ba_auto/tasks/bounty.py`.