"""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