ba-auto-daily/ba_auto/navigation.py
Nik Afiq a8ff95d6f5 feat: Implement OCR-driven story AP sweep with deterministic stage targeting
- Added `wait_for_state` function in `navigation.py` for state monitoring and reaction handling.
- Updated `mapping.md` to reflect changes in story sweep implementation and OCR usage.
- Refactored `story_sweep.py` to utilize OCR for region and stage identification, replacing random selection with configured targets.
- Enhanced modal handling and confirmation checks for AP usage in `story_sweep.py`.
- Updated setup script to require `tesseract` for OCR functionality and included installation instructions.
- Revised `plan.md` to document the transition from heuristic to OCR-based stage targeting and the associated findings from live testing.
2026-07-06 01:41:58 +09:00

57 lines
2.4 KiB
Python

"""Shared navigation helpers (home, menu, popups, back/escape)."""
# The mailbox/cafe/shop-style header bar renders a plain light background
# here; the home screen shows character art instead.
SUBSCREEN_HEADER_PROBE = (500, 10)
SUBSCREEN_HEADER_MIN_CHANNEL = 200
# Any modal dialog dims the screen behind it to roughly this darkness.
MODAL_DIM_PROBE = (960, 200)
MODAL_DIM_MAX_CHANNEL = 150
def is_on_subscreen(driver):
r, g, b = driver.color_at(*SUBSCREEN_HEADER_PROBE)
return r > SUBSCREEN_HEADER_MIN_CHANNEL and g > SUBSCREEN_HEADER_MIN_CHANNEL and b > SUBSCREEN_HEADER_MIN_CHANNEL
def is_modal_open(driver):
r, g, b = driver.color_at(*MODAL_DIM_PROBE)
return r < MODAL_DIM_MAX_CHANNEL and g < MODAL_DIM_MAX_CHANNEL and b < MODAL_DIM_MAX_CHANNEL
def wait_for_state(driver, config, reactions, ends, max_iterations=30, poll_interval=1.0):
"""Generic "watch the screen, react to anything recognized, stop once a
recognized destination is reached" loop -- the local equivalent of the
reference's core/picture.py::co_detect, scoped to what this project
actually needs (a handful of named checks) rather than co_detect's full
generality (which spans the whole reference project via ~20 image
template assets this project doesn't have).
`ends`: {check_fn(driver, config) -> bool: outcome_name}. Checked first,
every iteration; the first match stops the loop and returns its name.
`reactions`: {check_fn(driver, config) -> bool: action(driver)}. Checked
if no end matched; the first match runs its action (a click, a keypress,
whatever the recognized state calls for) and the loop continues.
If neither an end nor a reaction matches, the loop just waits and retries
-- it never falls back to a blind click/keypress guess (see CLAUDE.md's
exit-game-dialog writeup for why that was a real bug elsewhere).
Returns the matched end's outcome name, or None once max_iterations is
exhausted without reaching a recognized end -- callers should treat None
as "unrecognized state, abort safely."
"""
for _ in range(max_iterations):
for check_fn, outcome_name in ends.items():
if check_fn(driver, config):
return outcome_name
for check_fn, action in reactions.items():
if check_fn(driver, config):
action(driver)
break
else:
driver.wait(poll_interval)
return None