Refactor story_sweep.py to align with story_sweep_hard.py best practices and add campaign-active guard
- Rewrote story_sweep.py to incorporate fixes from story_sweep_hard.py's Phase 21, addressing latent bugs and improving functionality. - Introduced a campaign-active guard for Normal mode, ensuring AP is only spent when the reward campaign banner is visible. - Added explicit Normal-tab navigation and self-healing checks to maintain task screen integrity. - Updated click functions for MAX and PLUS buttons to remove reliance on visible count increases, addressing false-negative issues. - Implemented OCR verification for the sweep confirmation dialog to prevent misclicks on stacked buttons. - Enhanced region reading with retry logic for transient OCR failures. - Added force parameter to run function, allowing bypass of campaign checks via story_sweep_force command. - Updated ba_daily.py to include new force commands for both story_sweep and story_sweep_hard. - Documented changes and live-tested functionality to ensure reliability and correctness.
This commit is contained in:
parent
7f752d5422
commit
a5d70e5ec6
@ -668,9 +668,9 @@ When debugging on `nik-gpu`, copy relevant screenshots or debug images back into
|
|||||||
Current project state: mailbox, cafe, stamina, story_sweep, story_sweep_hard, event_sweep, shop_common, shop_tactical, lesson, arena, and bounty are all migrated to real Python. No Bash feature logic remains. (This list has drifted behind a few other migrated tasks -- login, gem_shop, circle, exit_game -- not tracked here yet; see `ba_daily.py`'s own `TASKS` dict for the true current set.)
|
Current project state: mailbox, cafe, stamina, story_sweep, story_sweep_hard, event_sweep, shop_common, shop_tactical, lesson, arena, and bounty are all migrated to real Python. No Bash feature logic remains. (This list has drifted behind a few other migrated tasks -- login, gem_shop, circle, exit_game -- not tracked here yet; see `ba_daily.py`'s own `TASKS` dict for the true current set.)
|
||||||
|
|
||||||
- `ba_dailies.sh` is a thin launcher that execs `ba_daily.py`
|
- `ba_dailies.sh` is a thin launcher that execs `ba_daily.py`
|
||||||
- `ba_daily.py` dispatches `mailbox`, `cafe`, `stamina`, `story_sweep`, `story_sweep_hard`/`story_sweep_hard_force`, `event_sweep`, `shop_common`, `shop_tactical`, `lesson`, `arena`, `bounty`, and default flow to `ba_auto/tasks/`
|
- `ba_daily.py` dispatches `mailbox`, `cafe`, `stamina`, `story_sweep`/`story_sweep_force`, `story_sweep_hard`/`story_sweep_hard_force`, `event_sweep`, `shop_common`, `shop_tactical`, `lesson`, `arena`, `bounty`, and default flow to `ba_auto/tasks/`
|
||||||
- default flow is `mailbox`, `cafe`, `stamina`
|
- default flow is `mailbox`, `cafe`, `stamina`
|
||||||
- `story_sweep`, `story_sweep_hard` (and its `story_sweep_hard_force` override), `event_sweep`, `shop_common`, `shop_tactical`, `lesson`, `arena`, and `bounty` are opt-in only since they spend AP/credits/tactical coin/lesson tickets/an arena ticket/a bounty ticket rather than reclaiming something free. `story_sweep_hard` additionally refuses to run at all unless a Hard-task reward campaign is currently active, unless overridden via `story_sweep_hard_force` -- see `ba_auto/tasks/story_sweep_hard.py` and `plan.md` Phase 21
|
- `story_sweep` (and its `story_sweep_force` override), `story_sweep_hard` (and its `story_sweep_hard_force` override), `event_sweep`, `shop_common`, `shop_tactical`, `lesson`, `arena`, and `bounty` are opt-in only since they spend AP/credits/tactical coin/lesson tickets/an arena ticket/a bounty ticket rather than reclaiming something free. Both `story_sweep` and `story_sweep_hard` additionally refuse to run at all unless their tab's reward campaign is currently active, unless overridden via `story_sweep_force`/`story_sweep_hard_force` -- see `ba_auto/tasks/story_sweep.py`/`story_sweep_hard.py` and `plan.md` Phases 21-22
|
||||||
- every task, whether run individually or as part of the default flow, self-heals back to the home screen both before it starts and after it ends — `ba_daily.py`'s `_run_task()` calls a retrying `_ensure_home()` before dispatch and wraps the dispatch itself in a `try/finally` calling `navigation.return_to_home()`, so cleanup runs regardless of success, an early-return failure, or an uncaught exception. The pre-task check is self-healing, not a hard gate, per explicit user direction: if `_ensure_home()` still can't confirm home after its own bounded retries, the task is attempted anyway rather than aborted, trusting each task's own click-then-verify steps to fail safely if the starting state really was bad. This is centralized rather than duplicated per-task; see plan.md's "Return-to-home audit" (and its self-heal-not-abort follow-up) for why (a real audit found most tasks had little to no reliable cleanup on several paths) and for a real bug this surfaced and fixed in `navigation.is_on_subscreen`/`return_to_home` itself (a modal open on top of a subscreen was indistinguishable from the true home screen using the header-brightness probe alone — fixed by also checking `is_modal_open`)
|
- every task, whether run individually or as part of the default flow, self-heals back to the home screen both before it starts and after it ends — `ba_daily.py`'s `_run_task()` calls a retrying `_ensure_home()` before dispatch and wraps the dispatch itself in a `try/finally` calling `navigation.return_to_home()`, so cleanup runs regardless of success, an early-return failure, or an uncaught exception. The pre-task check is self-healing, not a hard gate, per explicit user direction: if `_ensure_home()` still can't confirm home after its own bounded retries, the task is attempted anyway rather than aborted, trusting each task's own click-then-verify steps to fail safely if the starting state really was bad. This is centralized rather than duplicated per-task; see plan.md's "Return-to-home audit" (and its self-heal-not-abort follow-up) for why (a real audit found most tasks had little to no reliable cleanup on several paths) and for a real bug this surfaced and fixed in `navigation.is_on_subscreen`/`return_to_home` itself (a modal open on top of a subscreen was indistinguishable from the true home screen using the header-brightness probe alone — fixed by also checking `is_modal_open`)
|
||||||
- `ba_auto/tasks/mailbox.py` and `ba_auto/tasks/cafe.py` click with `ba_auto/driver.py` primitives and verify state with `driver.color_at` and `ba_auto/navigation.py`
|
- `ba_auto/tasks/mailbox.py` and `ba_auto/tasks/cafe.py` click with `ba_auto/driver.py` primitives and verify state with `driver.color_at` and `ba_auto/navigation.py`
|
||||||
- mailbox and cafe were ported from the reference patterns around `module/mail.py` and `module/cafe_reward.py`
|
- mailbox and cafe were ported from the reference patterns around `module/mail.py` and `module/cafe_reward.py`
|
||||||
@ -680,6 +680,7 @@ Current project state: mailbox, cafe, stamina, story_sweep, story_sweep_hard, ev
|
|||||||
- `ba_auto/tasks/story_sweep.py` sweeps a config-driven list of exact `(region, stage, count)` targets from `config.STORY_SWEEP_TARGETS`, plus one daily-rotating target (`config.STORY_SWEEP_ROTATION_*`) that cycles through a fixed region's stages one per day
|
- `ba_auto/tasks/story_sweep.py` sweeps a config-driven list of exact `(region, stage, count)` targets from `config.STORY_SWEEP_TARGETS`, plus one daily-rotating target (`config.STORY_SWEEP_ROTATION_*`) that cycles through a fixed region's stages one per day
|
||||||
- story sweep navigates via OCR, using region-number readout and stage-label matching, rather than random selection
|
- story sweep navigates via OCR, using region-number readout and stage-label matching, rather than random selection
|
||||||
- see `plan.md` Phase 10 for the OCR-based story sweep port, and its "Phase 10 follow-up" for the rotation-target fix
|
- see `plan.md` Phase 10 for the OCR-based story sweep port, and its "Phase 10 follow-up" for the rotation-target fix
|
||||||
|
- `story_sweep.py` also has its own campaign-active guard (`story_sweep_force` to override), an explicit Normal-tab click-and-verify (needed now that `story_sweep_hard` can run earlier in the same preset and leave Hard tab selected), and the same `_confirm_dialog_is_sweep`/self-heal-navigation/no-false-negative-MAX-verify fixes `story_sweep_hard.py` needed — see `plan.md` Phase 22 for the full rewrite, live-tested successfully on the first real attempt (a real MAX sweep of the day's rotation target, correctly detected as swept)
|
||||||
- `ba_auto/tasks/story_sweep_hard.py` sweeps a fixed, user-supplied priority-ordered list of Hard-mode `(region, stage)` targets (`config.HARD_STORY_SWEEP_TARGETS`) via the in-game MAX button (capped at 3x by the game itself), reusing story_sweep.py's region-nav and stage-info-modal machinery directly (confirmed pixel-identical) but with its own fixed 3-row stage list (no scrolling/OCR-label search — Hard always has exactly missions 1-3) and its own campaign-active guard: it refuses to spend any AP unless the pink "キャンペーン中" reward-campaign banner is showing on the region-info card, unless run via the separate `story_sweep_hard_force` command
|
- `ba_auto/tasks/story_sweep_hard.py` sweeps a fixed, user-supplied priority-ordered list of Hard-mode `(region, stage)` targets (`config.HARD_STORY_SWEEP_TARGETS`) via the in-game MAX button (capped at 3x by the game itself), reusing story_sweep.py's region-nav and stage-info-modal machinery directly (confirmed pixel-identical) but with its own fixed 3-row stage list (no scrolling/OCR-label search — Hard always has exactly missions 1-3) and its own campaign-active guard: it refuses to spend any AP unless the pink "キャンペーン中" reward-campaign banner is showing on the region-info card, unless run via the separate `story_sweep_hard_force` command
|
||||||
- a real gold-button money hazard exists in this same modal for Hard specifically: a stage that already used all 3 of today's auto-sweep clears still shows a clickable 入場 button, and attempting to sweep it raises a real "spend 40 Pyroxene/blue gems to refill today's clear count?" dialog, not just an AP-insufficient prompt — guarded by two independent layers (an OCR pre-check on the modal's own count field before ever clicking MAX, plus an OCR fallback on the dialog's own text, both declining via Escape rather than any positional click)
|
- a real gold-button money hazard exists in this same modal for Hard specifically: a stage that already used all 3 of today's auto-sweep clears still shows a clickable 入場 button, and attempting to sweep it raises a real "spend 40 Pyroxene/blue gems to refill today's clear count?" dialog, not just an AP-insufficient prompt — guarded by two independent layers (an OCR pre-check on the modal's own count field before ever clicking MAX, plus an OCR fallback on the dialog's own text, both declining via Escape rather than any positional click)
|
||||||
- see `plan.md` Phase 21 for the full live-testing writeup, including the navigation-cascade bug, the false-negative MAX-click abort bug, and the gem-refill hazard, all found and fixed against the real game
|
- see `plan.md` Phase 21 for the full live-testing writeup, including the navigation-cascade bug, the false-negative MAX-click abort bug, and the gem-refill hazard, all found and fixed against the real game
|
||||||
|
|||||||
@ -196,6 +196,18 @@ WORK_ICON = (1793, 1138)
|
|||||||
# screenshot, not just a hunch. (1250, 380) sits solidly mid-card, on the
|
# screenshot, not just a hunch. (1250, 380) sits solidly mid-card, on the
|
||||||
# "任務" title text itself, well clear of every edge.
|
# "任務" title text itself, well clear of every edge.
|
||||||
TASK_CARD = (1250, 380)
|
TASK_CARD = (1250, 380)
|
||||||
|
# Normal/Hard toggle tab atop the region browser's stage-list panel.
|
||||||
|
# story_sweep.py explicitly clicks+verifies this (mirroring
|
||||||
|
# story_sweep_hard.py's own HARD_TAB/_open_hard_tab) rather than assuming
|
||||||
|
# Normal is always the screen's default -- added 2026-07-21 once
|
||||||
|
# story_sweep_hard.py started running earlier in the same q4h preset
|
||||||
|
# sequence and could plausibly leave Hard selected. NORMAL_TAB_ACTIVE_PROBE
|
||||||
|
# is sampled clear of the "Normal" glyph itself (dark navy background,
|
||||||
|
# confirmed live 2026-07-21); note this is a DIFFERENT active-tab color than
|
||||||
|
# Hard's own red (HARD_TAB_ACTIVE_RGB below) -- each tab has its own accent.
|
||||||
|
NORMAL_TAB = (1200, 297)
|
||||||
|
NORMAL_TAB_ACTIVE_PROBE = (1100, 297)
|
||||||
|
NORMAL_TAB_ACTIVE_RGB = ((20, 40, 60), (100, 120, 140))
|
||||||
REGION_RIGHT_ARROW = (1862, 598)
|
REGION_RIGHT_ARROW = (1862, 598)
|
||||||
# Pixel-scanline-scanned (not visually estimated -- see plan.md Phase 8's
|
# Pixel-scanline-scanned (not visually estimated -- see plan.md Phase 8's
|
||||||
# lesson) from scratchpad/stage_info.png: the "<" chevron's navy-blue pixel
|
# lesson) from scratchpad/stage_info.png: the "<" chevron's navy-blue pixel
|
||||||
@ -295,6 +307,16 @@ STAGE_MODAL_CLOSE_BUTTON = (1691, 225)
|
|||||||
# yet; confirm it before relying on a non-"max" configured count (see
|
# yet; confirm it before relying on a non-"max" configured count (see
|
||||||
# plan.md Phase 10).
|
# plan.md Phase 10).
|
||||||
SWEEP_PLUS_BUTTON = (1520, 550)
|
SWEEP_PLUS_BUTTON = (1520, 550)
|
||||||
|
# "MIN" stepper button -- forces a known baseline of 1 before applying "+"
|
||||||
|
# raises for a configured exact (non-"max") count. Added 2026-07-21: the
|
||||||
|
# stepper remembers its last-used value across opens (confirmed live via
|
||||||
|
# bounty.py's own real overspend incident, see plan.md Phase 15 and
|
||||||
|
# CLAUDE.md's stepper-default guidance), so blindly clicking "+" count-1
|
||||||
|
# times on top of an unknown starting value could under- or over-shoot the
|
||||||
|
# intended count. Same row/layout as SWEEP_MAX_BUTTON/SWEEP_PLUS_BUTTON,
|
||||||
|
# pixel-scanned directly (confirmed live, the leftmost button in the
|
||||||
|
# MIN/-/count/+/MAX row).
|
||||||
|
SWEEP_MIN_BUTTON = (1180, 555)
|
||||||
|
|
||||||
# Clicking 掃討開始 (start sweep) always raises an "AP<N>使用して、掃討を
|
# Clicking 掃討開始 (start sweep) always raises an "AP<N>使用して、掃討を
|
||||||
# <M>回行いますか?" usage-confirmation dialog before the sweep actually
|
# <M>回行いますか?" usage-confirmation dialog before the sweep actually
|
||||||
@ -419,27 +441,33 @@ HARD_STAGE_ENTER_PROBE_HALF_SIZE = (25, 15)
|
|||||||
HARD_COUNT_FIELD_RECT = (1338, 535, 1474, 575)
|
HARD_COUNT_FIELD_RECT = (1338, 535, 1474, 575)
|
||||||
HARD_CHALLENGE_ALERT_TEXT_RECT = (600, 385, 1330, 470)
|
HARD_CHALLENGE_ALERT_TEXT_RECT = (600, 385, 1330, 470)
|
||||||
|
|
||||||
# Hard-only reward campaign banner, rendered directly on the region-info card
|
# Reward campaign banner, rendered directly on the region-info card (left
|
||||||
# (left panel) whenever a reward campaign is active for Hard tasks --
|
# panel) whenever a reward campaign is active for whichever tab (Normal or
|
||||||
# confirmed live 2026-07-20 on both Area 30 and Area 29 with the exact text
|
# Hard) is currently selected -- confirmed live both times: "任務Hardで獲得
|
||||||
# "任務Hardで獲得できる報酬量が2倍(+100%)になっています。", so this reads as
|
# できる報酬量が2倍(+100%)になっています。" (2026-07-20, Area 30/29) and
|
||||||
# account-wide, not per-region. No reference equivalent exists at all
|
# "任務Normalで獲得できる報酬量が2倍(+100%)になっています。" (2026-07-21,
|
||||||
|
# Area 30), pixel-identical rect/color in both cases, only the tab name in
|
||||||
|
# the text differs. Reads as account-wide, not per-region (same text on
|
||||||
|
# multiple regions each time). No reference equivalent exists at all
|
||||||
# (baas-reference has no campaign/drop-rate concept anywhere) -- per explicit
|
# (baas-reference has no campaign/drop-rate concept anywhere) -- per explicit
|
||||||
# user direction, this task refuses to spend AP unless this banner is
|
# user direction, both story_sweep.py and story_sweep_hard.py refuse to spend
|
||||||
# showing, unless explicitly overridden (see story_sweep_hard.py's
|
# AP unless this banner is showing for their own tab, unless explicitly
|
||||||
# force= parameter / the story_sweep_hard_force CLI command). The home
|
# overridden (force= parameter / the story_sweep_force / story_sweep_hard_force
|
||||||
# screen's own smaller "キャンペーン中" badge (top-right notice stack) was
|
# CLI commands). The home screen's own smaller "キャンペーン中" badge (top-right
|
||||||
# considered but rejected as the actual gate signal: it can point at any of
|
# notice stack) was considered but rejected as the actual gate signal: it can
|
||||||
# several concurrent campaigns (e.g. a totally unrelated "大決戦開催中"
|
# point at any of several concurrent campaigns (e.g. a totally unrelated
|
||||||
# banner sits right above it), not necessarily Hard task rewards
|
# "大決戦開催中" banner sits right above it), not necessarily story-task
|
||||||
# specifically, while this in-panel banner explicitly names "任務Hard".
|
# rewards specifically, while this in-panel banner explicitly names the tab.
|
||||||
# Detected via a flat color-presence scan (not OCR) since the pink chip is
|
# Detected via a flat color-presence scan (not OCR) since the pink chip is
|
||||||
# clean and high-contrast and the banner is either fully rendered or entirely
|
# clean and high-contrast and the banner is either fully rendered or entirely
|
||||||
# absent (never a different overlapping color) -- matching this project's
|
# absent (never a different overlapping color) -- matching this project's
|
||||||
# own established convention for unambiguous binary state signals (e.g.
|
# own established convention for unambiguous binary state signals (e.g.
|
||||||
# gem_shop.py's claimed/unclaimed probe) rather than OCR'ing the banner text.
|
# gem_shop.py's claimed/unclaimed probe) rather than OCR'ing the banner text.
|
||||||
HARD_CAMPAIGN_BADGE_RECT = (164, 426, 344, 464)
|
# Named generically (not HARD_-prefixed) since both story_sweep.py and
|
||||||
HARD_CAMPAIGN_BADGE_RGB = ((230, 100, 160), (256, 210, 256))
|
# story_sweep_hard.py share it -- originally added for Hard only, renamed
|
||||||
|
# 2026-07-21 once story_sweep.py's own campaign check reused it unchanged.
|
||||||
|
TASK_CAMPAIGN_BADGE_RECT = (164, 426, 344, 464)
|
||||||
|
TASK_CAMPAIGN_BADGE_RGB = ((230, 100, 160), (256, 210, 256))
|
||||||
|
|
||||||
# (region, stage) priority-ordered targets to sweep, in the exact order given
|
# (region, stage) priority-ordered targets to sweep, in the exact order given
|
||||||
# by the user (highest farming priority first, not region-ascending) --
|
# by the user (highest farming priority first, not region-ascending) --
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -27,16 +27,65 @@ constants.
|
|||||||
The MAX-button click-then-verify and the modal's own X-button close (both
|
The MAX-button click-then-verify and the modal's own X-button close (both
|
||||||
calibrated and confirmed live in Phase 9) are reused unchanged -- see
|
calibrated and confirmed live in Phase 9) are reused unchanged -- see
|
||||||
plan.md Phase 9/10.
|
plan.md Phase 9/10.
|
||||||
|
|
||||||
|
**2026-07-21 rewrite**: this module predates story_sweep_hard.py's own live
|
||||||
|
debugging (plan.md Phase 21), which found several real bugs in patterns this
|
||||||
|
module had originally established and story_sweep_hard.py had copied
|
||||||
|
verbatim. Per explicit user direction ("match the hard implementation on
|
||||||
|
best practices"), the same fixes are applied here too, even though most were
|
||||||
|
never actually triggered in production (this module's own
|
||||||
|
config.STORY_SWEEP_TARGETS is empty by default, so historically only one
|
||||||
|
rotation target ever ran per invocation -- never enough iterations to expose
|
||||||
|
a multi-target bug):
|
||||||
|
|
||||||
|
- An explicit Normal-tab guard (`_open_normal_tab`, config.NORMAL_TAB) and a
|
||||||
|
loop-level self-heal (`_ensure_task_screen`), replacing the removed
|
||||||
|
per-path "if on subscreen, press Escape" cleanup that used to fire after
|
||||||
|
*every* target (success or abort) and would back all the way out of the
|
||||||
|
region browser -- confirmed live as a real bug in story_sweep_hard.py,
|
||||||
|
same copied code here. The Normal-tab guard is new, not just a bugfix:
|
||||||
|
story_sweep_hard.py can now run earlier in the same `q4h` preset sequence
|
||||||
|
and leave the Hard tab selected, which this module never had to account
|
||||||
|
for before both tasks coexisted.
|
||||||
|
- `_click_max`/`_click_plus` no longer require the sweep count to visibly
|
||||||
|
rise above 1 as proof a click landed -- low AP can legitimately cap the
|
||||||
|
count at its floor, which is visually indistinguishable from "the click
|
||||||
|
didn't register." `_click_plus` also now forces a known baseline via
|
||||||
|
`_click_min` before applying "+" raises, porting bounty.py's own
|
||||||
|
`_click_min_and_verify` fix for a stepper that remembers its last-used
|
||||||
|
value across opens (see CLAUDE.md/plan.md Phase 15) -- this module's
|
||||||
|
original `_click_plus_and_verify` never did that, a latent version of the
|
||||||
|
exact overspend risk bounty.py hit for real.
|
||||||
|
- `_confirm_dialog_is_sweep` OCR-verifies the confirm dialog's own text
|
||||||
|
before the one irreversible click, closing the same two-stacked-button
|
||||||
|
hazard (cyan 掃討開始 directly above a gold 任務開始 real-battle button)
|
||||||
|
config.py's own SWEEP_MAX_BUTTON comment already documented for this
|
||||||
|
exact tabbed modal layout but this module never actually guarded against.
|
||||||
|
- `_watch_sweep_result` gained the same `clicked_any`-gated second `ends`
|
||||||
|
condition event_sweep.py/story_sweep_hard.py needed: the post-sweep flow
|
||||||
|
can land back on the bare region browser instead of leaving the modal
|
||||||
|
open, which the original "swept" condition required.
|
||||||
|
- `_read_current_region` retries a None OCR read a couple of times before
|
||||||
|
giving up, matching story_sweep_hard.py's own REGION_READ_RETRIES fix for
|
||||||
|
a transient render/settle race confirmed live on the same region browser.
|
||||||
|
- A campaign-active guard, genuinely new (no reference equivalent -- see
|
||||||
|
story_sweep_hard.py's own docstring for why): refuses to spend AP unless
|
||||||
|
the pink "キャンペーン中" reward-campaign banner is showing for the Normal
|
||||||
|
tab specifically (config.TASK_CAMPAIGN_BADGE_RECT, shared with
|
||||||
|
story_sweep_hard.py's own check -- pixel-identical rect/color, confirmed
|
||||||
|
live 2026-07-21, only the tab name in the banner text differs). Pass
|
||||||
|
force=True (wired to the `story_sweep_force` CLI command) to bypass it.
|
||||||
"""
|
"""
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from ba_auto import detector, navigation
|
from ba_auto import detector, navigation
|
||||||
|
|
||||||
OPEN_RETRIES = 3
|
OPEN_RETRIES = 3
|
||||||
|
NORMAL_TAB_RETRIES = 3
|
||||||
POST_SWEEP_DISMISS_ROUNDS = 6
|
POST_SWEEP_DISMISS_ROUNDS = 6
|
||||||
STAGE_MODAL_DIM_MAX_CHANNEL = 150
|
STAGE_MODAL_DIM_MAX_CHANNEL = 150
|
||||||
MAX_BUTTON_RETRIES = 3
|
|
||||||
MODAL_CLOSE_RETRIES = 3
|
MODAL_CLOSE_RETRIES = 3
|
||||||
|
REGION_READ_RETRIES = 3
|
||||||
|
|
||||||
|
|
||||||
def _is_stage_modal_open(driver, config):
|
def _is_stage_modal_open(driver, config):
|
||||||
@ -65,6 +114,25 @@ def _is_ap_purchase_prompt(driver, config):
|
|||||||
return _color_in_range(driver.color_at(*config.SWEEP_CONFIRM_BUTTON), config.SWEEP_CONFIRM_GOLD)
|
return _color_in_range(driver.color_at(*config.SWEEP_CONFIRM_BUTTON), config.SWEEP_CONFIRM_GOLD)
|
||||||
|
|
||||||
|
|
||||||
|
def _confirm_dialog_is_sweep(driver, config):
|
||||||
|
"""OCR-verify the confirm dialog reached after clicking 掃討開始 is
|
||||||
|
genuinely the sweep-usage confirm ("APをN使用して、掃討をN回行いますか?"),
|
||||||
|
not some other cyan-styled confirmation that happens to satisfy
|
||||||
|
_is_sweep_usage_confirm's color-only check -- ports bounty.py's own
|
||||||
|
_confirm_dialog_is_sweep fix (see that module and story_sweep_hard.py for
|
||||||
|
the hazard class this guards against: this exact tabbed modal has a gold
|
||||||
|
任務開始 "start mission" button directly beneath the cyan 掃討開始 one,
|
||||||
|
both showing an identical AP-cost preview -- see config.py's
|
||||||
|
SWEEP_MAX_BUTTON comment). Reuses config.BOUNTY_SWEEP_CONFIRM_TEXT_RECT
|
||||||
|
directly, confirmed live to crop this modal's confirm text correctly too
|
||||||
|
since it's the same shared "通知" dialog component. Substring match on
|
||||||
|
"掃討" rather than an exact match, matching this project's own
|
||||||
|
established dialog-classification convention.
|
||||||
|
"""
|
||||||
|
text = detector.read_text(config.BOUNTY_SWEEP_CONFIRM_TEXT_RECT, psm=6, lang="jpn")
|
||||||
|
return "掃討" in text
|
||||||
|
|
||||||
|
|
||||||
def _find_result_button(driver, config):
|
def _find_result_button(driver, config):
|
||||||
# The "掃討完了" (sweep complete) results screen shows a SKIP button
|
# The "掃討完了" (sweep complete) results screen shows a SKIP button
|
||||||
# (skips the reward-reveal animation) and then, once settled, a final
|
# (skips the reward-reveal animation) and then, once settled, a final
|
||||||
@ -74,15 +142,6 @@ 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.SWEEP_RESULT_BUTTON_REGION, *config.SWEEP_CONFIRM_CYAN)
|
||||||
|
|
||||||
|
|
||||||
def _count_raised_above_one(driver, config):
|
|
||||||
# The "-" stepper button is flat grey while count == 1 (its default,
|
|
||||||
# disabled at the minimum) and turns vivid orange once raised -- cheap
|
|
||||||
# way to confirm a MAX/"+" click actually registered without needing OCR
|
|
||||||
# on the count itself.
|
|
||||||
r, g, b = driver.color_at(*config.SWEEP_MINUS_BUTTON_PROBE)
|
|
||||||
return r > 200 and g < 180 and b < 100
|
|
||||||
|
|
||||||
|
|
||||||
def _open_task_screen(driver, config):
|
def _open_task_screen(driver, config):
|
||||||
for attempt in range(1, OPEN_RETRIES + 1):
|
for attempt in range(1, OPEN_RETRIES + 1):
|
||||||
driver.click(*config.WORK_ICON)
|
driver.click(*config.WORK_ICON)
|
||||||
@ -102,8 +161,49 @@ def _open_task_screen(driver, config):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _open_normal_tab(driver, config):
|
||||||
|
for attempt in range(1, NORMAL_TAB_RETRIES + 1):
|
||||||
|
driver.click(*config.NORMAL_TAB)
|
||||||
|
driver.wait(1)
|
||||||
|
if _color_in_range(driver.color_at(*config.NORMAL_TAB_ACTIVE_PROBE), config.NORMAL_TAB_ACTIVE_RGB):
|
||||||
|
return True
|
||||||
|
print(f"[story_sweep] Normal tab not confirmed active (attempt {attempt}/{NORMAL_TAB_RETRIES})")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _campaign_active(driver, config):
|
||||||
|
return detector.region_contains_color(config.TASK_CAMPAIGN_BADGE_RECT, *config.TASK_CAMPAIGN_BADGE_RGB)
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_task_screen(driver, config):
|
||||||
|
"""Re-verify we're still on the task screen's Normal tab before each
|
||||||
|
target, self-healing from any navigation drift between targets the same
|
||||||
|
way ba_daily.py's own _ensure_home does before each task, and the same
|
||||||
|
way story_sweep_hard.py's own _ensure_hard_screen does between its
|
||||||
|
targets -- see that function's docstring for the real incident this
|
||||||
|
pattern was built to fix (a leftover per-path Escape press that backed
|
||||||
|
out of the region browser after every single target).
|
||||||
|
"""
|
||||||
|
if _color_in_range(driver.color_at(*config.NORMAL_TAB_ACTIVE_PROBE), config.NORMAL_TAB_ACTIVE_RGB):
|
||||||
|
return True
|
||||||
|
print("[story_sweep] task screen (Normal tab) not confirmed before target -- re-opening")
|
||||||
|
return _open_task_screen(driver, config) and _open_normal_tab(driver, config)
|
||||||
|
|
||||||
|
|
||||||
def _read_current_region(driver, config):
|
def _read_current_region(driver, config):
|
||||||
return detector.read_int(config.REGION_NUMBER_OCR_RECT)
|
"""OCR the current region number, retrying a None read a couple of times
|
||||||
|
before giving up -- matching story_sweep_hard.py's own fix for a
|
||||||
|
transient render/settle race confirmed live on this same region browser
|
||||||
|
(a screenshot that read as None was, moments later, confirmed by eye to
|
||||||
|
be a perfectly clean and legible digit).
|
||||||
|
"""
|
||||||
|
for attempt in range(1, REGION_READ_RETRIES + 1):
|
||||||
|
cur = detector.read_int(config.REGION_NUMBER_OCR_RECT)
|
||||||
|
if cur is not None:
|
||||||
|
return cur
|
||||||
|
if attempt < REGION_READ_RETRIES:
|
||||||
|
driver.wait(1)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _region_arrow_visible(driver, config, center):
|
def _region_arrow_visible(driver, config, center):
|
||||||
@ -206,31 +306,51 @@ def _find_stage_row(driver, config, region, stage):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _click_max_and_verify(driver, config):
|
def _click_min(driver, config):
|
||||||
for attempt in range(1, MAX_BUTTON_RETRIES + 1):
|
driver.click(*config.SWEEP_MIN_BUTTON)
|
||||||
|
driver.wait(0.8)
|
||||||
|
|
||||||
|
|
||||||
|
def _click_max(driver, config):
|
||||||
|
"""Click the MAX button once. Deliberately does NOT require the sweep
|
||||||
|
count to visibly rise above 1 as proof the click landed -- see
|
||||||
|
story_sweep_hard.py's own _click_max for the real incident this fixes
|
||||||
|
(low AP can legitimately cap the count at its floor, indistinguishable
|
||||||
|
from "the click didn't register"; 11 real, affordable targets were
|
||||||
|
wrongly skipped by the old verify-and-abort design in that module before
|
||||||
|
this fix). The downstream confirm-dialog checks (_is_ap_purchase_prompt/
|
||||||
|
_is_sweep_usage_confirm/_confirm_dialog_is_sweep) are the real safety net
|
||||||
|
regardless of what the stepper visually showed here.
|
||||||
|
"""
|
||||||
driver.click(*config.SWEEP_MAX_BUTTON)
|
driver.click(*config.SWEEP_MAX_BUTTON)
|
||||||
driver.wait(0.8)
|
driver.wait(0.8)
|
||||||
if _count_raised_above_one(driver, config):
|
|
||||||
return True
|
|
||||||
print(f"[story_sweep] MAX click not detected (attempt {attempt}/{MAX_BUTTON_RETRIES})")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _click_plus_and_verify(driver, config, count):
|
def _click_plus(driver, config, count):
|
||||||
for attempt in range(1, MAX_BUTTON_RETRIES + 1):
|
"""Raise the sweep count to an exact configured value via "+".
|
||||||
|
|
||||||
|
Forces a known baseline of 1 via _click_min first -- the stepper
|
||||||
|
remembers its last-used value across opens (confirmed live for real via
|
||||||
|
bounty.py's own overspend incident, plan.md Phase 15/CLAUDE.md), so
|
||||||
|
clicking "+" count-1 times on top of an unknown starting value could
|
||||||
|
under- or over-shoot the intended count. This module's original
|
||||||
|
_click_plus_and_verify never reset to a baseline first -- a latent
|
||||||
|
version of the exact bug bounty.py hit for real, never triggered here
|
||||||
|
only because config.STORY_SWEEP_TARGETS has always been empty in
|
||||||
|
practice. Same as _click_max, does not require a visible post-click
|
||||||
|
raise as proof of success.
|
||||||
|
"""
|
||||||
|
_click_min(driver, config)
|
||||||
for _ in range(count - 1):
|
for _ in range(count - 1):
|
||||||
driver.click(*config.SWEEP_PLUS_BUTTON)
|
driver.click(*config.SWEEP_PLUS_BUTTON)
|
||||||
driver.wait(0.8)
|
driver.wait(0.8)
|
||||||
if _count_raised_above_one(driver, config):
|
|
||||||
return True
|
|
||||||
print(f"[story_sweep] count-raise via '+' not detected (attempt {attempt}/{MAX_BUTTON_RETRIES})")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _set_sweep_count(driver, config, count):
|
def _set_sweep_count(driver, config, count):
|
||||||
if count == "max":
|
if count == "max":
|
||||||
return _click_max_and_verify(driver, config)
|
_click_max(driver, config)
|
||||||
return _click_plus_and_verify(driver, config, count)
|
else:
|
||||||
|
_click_plus(driver, config, count)
|
||||||
|
|
||||||
|
|
||||||
def _close_stage_modal(driver, config):
|
def _close_stage_modal(driver, config):
|
||||||
@ -246,25 +366,28 @@ def _close_stage_modal(driver, config):
|
|||||||
|
|
||||||
|
|
||||||
def _watch_sweep_result(driver, config):
|
def _watch_sweep_result(driver, config):
|
||||||
# The reference's start_sweep returns one of "inadequate_ap",
|
"""Confirmed live 2026-07-20/21 (story_sweep_hard.py, plan.md Phase 21)
|
||||||
# "charge_challenge_counts", or "sweep_complete" so its caller reacts
|
that this shared modal's post-sweep flow can land all the way back on
|
||||||
# appropriately -- this ports that same named-outcome contract via
|
the bare region browser instead of leaving the 任務情報 modal open --
|
||||||
# navigation.wait_for_state instead of the old single generic "Done".
|
the same terminal-state gap event_sweep.py hit historically. Ports
|
||||||
#
|
event_sweep.py's/story_sweep_hard.py's own `clicked_any`-gated second
|
||||||
# Called only after the usage-confirm dialog is already accepted (see
|
`ends` condition: only treat "no modal, no result button" as "swept"
|
||||||
# _sweep_target), so from here it's purely "click through the
|
once we've actually clicked through at least one result-screen button,
|
||||||
# 掃討完了 SKIP/OK screens until the bare stage-info modal reappears."
|
so an immediate read on the very first check (before any SKIP/OK
|
||||||
# Clicking the found button by color (not a keypress) means this never
|
sequence has started) still can't be mistaken for a genuine completion.
|
||||||
# risks landing on the underlying "任務開始" button the way a blind
|
"""
|
||||||
# Enter-press loop would.
|
clicked_any = {"value": False}
|
||||||
|
|
||||||
def click_result_button(d):
|
def click_result_button(d):
|
||||||
pos = _find_result_button(d, config)
|
pos = _find_result_button(d, config)
|
||||||
if pos:
|
if pos:
|
||||||
d.click(*pos)
|
d.click(*pos)
|
||||||
|
clicked_any["value"] = True
|
||||||
d.wait(1.5)
|
d.wait(1.5)
|
||||||
|
|
||||||
ends = {
|
ends = {
|
||||||
(lambda d, c: _is_stage_modal_open(d, c) and _find_result_button(d, c) is None): "swept",
|
(lambda d, c: _is_stage_modal_open(d, c) and _find_result_button(d, c) is None): "swept",
|
||||||
|
(lambda d, c: clicked_any["value"] and not _is_stage_modal_open(d, c) and _find_result_button(d, c) is None): "swept",
|
||||||
}
|
}
|
||||||
reactions = {
|
reactions = {
|
||||||
(lambda d, c: _find_result_button(d, c) is not None): click_result_button,
|
(lambda d, c: _find_result_button(d, c) is not None): click_result_button,
|
||||||
@ -292,18 +415,9 @@ def _sweep_target(driver, config, region, stage, count):
|
|||||||
|
|
||||||
if not _is_stage_modal_open(driver, config):
|
if not _is_stage_modal_open(driver, config):
|
||||||
print("[story_sweep] stage info panel not detected, aborting")
|
print("[story_sweep] stage info panel not detected, aborting")
|
||||||
if navigation.is_on_subscreen(driver):
|
|
||||||
driver.keypress("Escape")
|
|
||||||
driver.wait(1.5)
|
|
||||||
return "unrecognized_state"
|
return "unrecognized_state"
|
||||||
|
|
||||||
if not _set_sweep_count(driver, config, count):
|
_set_sweep_count(driver, config, count)
|
||||||
print("[story_sweep] could not confirm sweep count was raised, aborting without spending AP")
|
|
||||||
_close_stage_modal(driver, config)
|
|
||||||
if navigation.is_on_subscreen(driver):
|
|
||||||
driver.keypress("Escape")
|
|
||||||
driver.wait(1.5)
|
|
||||||
return "unrecognized_state"
|
|
||||||
|
|
||||||
driver.click(*config.SWEEP_START_BUTTON)
|
driver.click(*config.SWEEP_START_BUTTON)
|
||||||
driver.wait(1.5)
|
driver.wait(1.5)
|
||||||
@ -313,17 +427,22 @@ def _sweep_target(driver, config, region, stage, count):
|
|||||||
driver.click(*config.SWEEP_CONFIRM_CANCEL_BUTTON)
|
driver.click(*config.SWEEP_CONFIRM_CANCEL_BUTTON)
|
||||||
driver.wait(1)
|
driver.wait(1)
|
||||||
_close_stage_modal(driver, config)
|
_close_stage_modal(driver, config)
|
||||||
if navigation.is_on_subscreen(driver):
|
|
||||||
driver.keypress("Escape")
|
|
||||||
driver.wait(1.5)
|
|
||||||
return "inadequate_ap"
|
return "inadequate_ap"
|
||||||
|
|
||||||
if not _is_sweep_usage_confirm(driver, config):
|
if not _is_sweep_usage_confirm(driver, config):
|
||||||
print("[story_sweep] sweep-usage confirmation not detected, aborting without further input")
|
print("[story_sweep] sweep-usage confirmation not detected, aborting without further input")
|
||||||
_close_stage_modal(driver, config)
|
_close_stage_modal(driver, config)
|
||||||
if navigation.is_on_subscreen(driver):
|
return "unrecognized_state"
|
||||||
driver.keypress("Escape")
|
|
||||||
driver.wait(1.5)
|
# Hard safety gate before the one irreversible click in this whole flow
|
||||||
|
# -- see module docstring / bounty.py's own real incident. Color already
|
||||||
|
# matched above (_is_sweep_usage_confirm); verify the actual dialog text
|
||||||
|
# too before committing rather than trusting color alone.
|
||||||
|
if not _confirm_dialog_is_sweep(driver, config):
|
||||||
|
print("[story_sweep] confirm dialog text did not read as a sweep confirmation -- cancelling without confirming")
|
||||||
|
driver.click(*config.SWEEP_CONFIRM_CANCEL_BUTTON)
|
||||||
|
driver.wait(1)
|
||||||
|
_close_stage_modal(driver, config)
|
||||||
return "unrecognized_state"
|
return "unrecognized_state"
|
||||||
|
|
||||||
driver.click(*config.SWEEP_CONFIRM_BUTTON)
|
driver.click(*config.SWEEP_CONFIRM_BUTTON)
|
||||||
@ -336,11 +455,6 @@ def _sweep_target(driver, config, region, stage, count):
|
|||||||
print("[story_sweep] warning: could not confirm stage info modal closed -- leaving it open rather than pressing further keys blindly")
|
print("[story_sweep] warning: could not confirm stage info modal closed -- leaving it open rather than pressing further keys blindly")
|
||||||
return outcome
|
return outcome
|
||||||
|
|
||||||
if navigation.is_on_subscreen(driver):
|
|
||||||
driver.keypress("Escape")
|
|
||||||
driver.wait(1.5)
|
|
||||||
return outcome
|
|
||||||
|
|
||||||
|
|
||||||
def _rotation_target(config):
|
def _rotation_target(config):
|
||||||
region = getattr(config, "STORY_SWEEP_ROTATION_REGION", None)
|
region = getattr(config, "STORY_SWEEP_ROTATION_REGION", None)
|
||||||
@ -351,7 +465,7 @@ def _rotation_target(config):
|
|||||||
return (region, stage, config.STORY_SWEEP_ROTATION_COUNT)
|
return (region, stage, config.STORY_SWEEP_ROTATION_COUNT)
|
||||||
|
|
||||||
|
|
||||||
def run(driver, config):
|
def run(driver, config, force=False):
|
||||||
driver.focus_game()
|
driver.focus_game()
|
||||||
|
|
||||||
targets = list(config.STORY_SWEEP_TARGETS)
|
targets = list(config.STORY_SWEEP_TARGETS)
|
||||||
@ -368,7 +482,22 @@ def run(driver, config):
|
|||||||
print("[story_sweep] could not confirm task screen is open, aborting without pressing further keys")
|
print("[story_sweep] could not confirm task screen is open, aborting without pressing further keys")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not _open_normal_tab(driver, config):
|
||||||
|
print("[story_sweep] could not confirm Normal tab is selected, aborting without pressing further keys")
|
||||||
|
return
|
||||||
|
|
||||||
|
if force:
|
||||||
|
print("[story_sweep] force=True -- skipping campaign check")
|
||||||
|
else:
|
||||||
|
if not _campaign_active(driver, config):
|
||||||
|
print("[story_sweep] no active Normal-task reward campaign detected -- skipping sweep (run story_sweep_force to override)")
|
||||||
|
return
|
||||||
|
print("[story_sweep] campaign confirmed active, proceeding")
|
||||||
|
|
||||||
for region, stage, count in targets:
|
for region, stage, count in targets:
|
||||||
|
if not _ensure_task_screen(driver, config):
|
||||||
|
print(f"[story_sweep] could not confirm/recover the task screen before target {region}-{stage} -- stopping")
|
||||||
|
break
|
||||||
outcome = _sweep_target(driver, config, region, stage, count)
|
outcome = _sweep_target(driver, config, region, stage, count)
|
||||||
if outcome == "inadequate_ap":
|
if outcome == "inadequate_ap":
|
||||||
print("[story_sweep] insufficient AP -- stopping, not attempting remaining targets")
|
print("[story_sweep] insufficient AP -- stopping, not attempting remaining targets")
|
||||||
|
|||||||
@ -17,9 +17,11 @@ always-exactly-3 fixed stage rows with no scrolling/OCR-label search needed
|
|||||||
campaign-active guard with no reference equivalent at all (baas-reference has
|
campaign-active guard with no reference equivalent at all (baas-reference has
|
||||||
no concept of a drop-rate campaign) -- per explicit user direction, this
|
no concept of a drop-rate campaign) -- per explicit user direction, this
|
||||||
checks for the pink "キャンペーン中" reward-campaign banner (config.
|
checks for the pink "キャンペーン中" reward-campaign banner (config.
|
||||||
HARD_CAMPAIGN_BADGE_RECT) before spending any AP, since Hard sweep is only
|
TASK_CAMPAIGN_BADGE_RECT, shared with story_sweep.py's own Normal-mode
|
||||||
worth running while Hard-task rewards are boosted. Pass force=True (wired to
|
campaign check added 2026-07-21 -- pixel-identical rect/color, confirmed
|
||||||
the `story_sweep_hard_force` CLI command) to bypass that guard.
|
live) before spending any AP, since Hard sweep is only worth running while
|
||||||
|
Hard-task rewards are boosted. Pass force=True (wired to the
|
||||||
|
`story_sweep_hard_force` CLI command) to bypass that guard.
|
||||||
|
|
||||||
Also carries forward a hard lesson from bounty.py's own real incident (see
|
Also carries forward a hard lesson from bounty.py's own real incident (see
|
||||||
CLAUDE.md/plan.md): the stage-info modal has TWO stacked action buttons --
|
CLAUDE.md/plan.md): the stage-info modal has TWO stacked action buttons --
|
||||||
@ -158,7 +160,7 @@ def _open_hard_tab(driver, config):
|
|||||||
|
|
||||||
|
|
||||||
def _campaign_active(driver, config):
|
def _campaign_active(driver, config):
|
||||||
return detector.region_contains_color(config.HARD_CAMPAIGN_BADGE_RECT, *config.HARD_CAMPAIGN_BADGE_RGB)
|
return detector.region_contains_color(config.TASK_CAMPAIGN_BADGE_RECT, *config.TASK_CAMPAIGN_BADGE_RGB)
|
||||||
|
|
||||||
|
|
||||||
def _ensure_hard_screen(driver, config):
|
def _ensure_hard_screen(driver, config):
|
||||||
|
|||||||
29
ba_daily.py
29
ba_daily.py
@ -13,9 +13,14 @@ TASKS = {
|
|||||||
"gem_shop": gem_shop.run,
|
"gem_shop": gem_shop.run,
|
||||||
"circle": circle.run,
|
"circle": circle.run,
|
||||||
"story_sweep": story_sweep.run,
|
"story_sweep": story_sweep.run,
|
||||||
|
# Bypasses story_sweep's campaign-active guard -- see that module's
|
||||||
|
# docstring and config.TASK_CAMPAIGN_BADGE_RECT for why the guard exists
|
||||||
|
# (no reference equivalent; per explicit user direction, added 2026-07-21
|
||||||
|
# to match story_sweep_hard's own guard).
|
||||||
|
"story_sweep_force": lambda d, c: story_sweep.run(d, c, force=True),
|
||||||
"story_sweep_hard": story_sweep_hard.run,
|
"story_sweep_hard": story_sweep_hard.run,
|
||||||
# Bypasses story_sweep_hard's campaign-active guard -- see that module's
|
# Bypasses story_sweep_hard's campaign-active guard -- see that module's
|
||||||
# docstring and config.HARD_CAMPAIGN_BADGE_RECT for why the guard exists
|
# docstring and config.TASK_CAMPAIGN_BADGE_RECT for why the guard exists
|
||||||
# (no reference equivalent; per explicit user direction, Hard sweep
|
# (no reference equivalent; per explicit user direction, Hard sweep
|
||||||
# normally refuses to spend AP without an active reward campaign).
|
# normally refuses to spend AP without an active reward campaign).
|
||||||
"story_sweep_hard_force": lambda d, c: story_sweep_hard.run(d, c, force=True),
|
"story_sweep_hard_force": lambda d, c: story_sweep_hard.run(d, c, force=True),
|
||||||
@ -27,15 +32,16 @@ TASKS = {
|
|||||||
"bounty": bounty.run,
|
"bounty": bounty.run,
|
||||||
"exit_game": exit_game.run,
|
"exit_game": exit_game.run,
|
||||||
}
|
}
|
||||||
# story_sweep, story_sweep_hard (and its story_sweep_hard_force variant),
|
# story_sweep (and its story_sweep_force variant), story_sweep_hard (and its
|
||||||
# event_sweep, both shop tasks, lesson, arena, and bounty are opt-in only
|
# story_sweep_hard_force variant), event_sweep, both shop tasks, lesson,
|
||||||
# (not in the default flow): they spend AP/credits/tactical coin/lesson
|
# arena, and bounty are opt-in only (not in the default flow): they spend
|
||||||
# tickets/an arena ticket/a bounty ticket on an automated choice rather than
|
# AP/credits/tactical coin/lesson tickets/an arena ticket/a bounty ticket on
|
||||||
# reclaiming something free, which is a real resource decision the default
|
# an automated choice rather than reclaiming something free, which is a real
|
||||||
# unattended run shouldn't make blindly. story_sweep_hard additionally has
|
# resource decision the default unattended run shouldn't make blindly. Both
|
||||||
# its own campaign-active guard on top of being opt-in (see that module's
|
# story_sweep and story_sweep_hard additionally have their own campaign-active
|
||||||
# docstring) -- story_sweep_hard_force is a second, separate opt-in command
|
# guard on top of being opt-in (see each module's own docstring) --
|
||||||
# for bypassing that guard, not something DEFAULT_ORDER/PRESETS should ever
|
# story_sweep_force/story_sweep_hard_force are separate opt-in commands for
|
||||||
|
# bypassing those guards, not something DEFAULT_ORDER/PRESETS should ever
|
||||||
# invoke unattended. Arena specifically
|
# invoke unattended. Arena specifically
|
||||||
# fights a real ranked PvP battle each run -- see ba_auto/tasks/arena.py's
|
# fights a real ranked PvP battle each run -- see ba_auto/tasks/arena.py's
|
||||||
# module docstring. gem_shop and circle are the opposite case -- like
|
# module docstring. gem_shop and circle are the opposite case -- like
|
||||||
@ -77,7 +83,8 @@ PRESETS = {
|
|||||||
"gem_shop", "mailbox", "stamina", "event_sweep", "exit_game",
|
"gem_shop", "mailbox", "stamina", "event_sweep", "exit_game",
|
||||||
],
|
],
|
||||||
"q4h": [
|
"q4h": [
|
||||||
"login", "cafe", "mailbox", "stamina", "story_sweep_hard", "event_sweep", "exit_game"
|
"login", "cafe", "mailbox", "stamina", "story_sweep_hard",
|
||||||
|
"event_sweep", "story_sweep", "exit_game"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
27
plan.md
27
plan.md
@ -1003,6 +1003,33 @@ Implemented `ba_auto/tasks/story_sweep_hard.py` and `config.HARD_STORY_SWEEP_TAR
|
|||||||
|
|
||||||
**Net real result across the session**: multiple genuine Hard sweeps confirmed for real -- H18-3 and H30-3 both fully exhausted their daily 3/3 clears (60 AP each), and six more targets (H28-3, H17-3, H13-3, H23-2, H16-1, H20-3) swept for real in the final run before AP ran low. Zero AP was wasted once the false-negative MAX-click bug was fixed. Zero gems were ever spent despite deliberately reproducing the real-money hazard twice for calibration. By the final run, all eight already-exhausted-today targets (H18-3, H30-3, H17-2, H15-2, H14-3, H10-3, H14-2, H7-1) correctly self-skipped via the new pre-check without ever reaching the gem-refill dialog.
|
**Net real result across the session**: multiple genuine Hard sweeps confirmed for real -- H18-3 and H30-3 both fully exhausted their daily 3/3 clears (60 AP each), and six more targets (H28-3, H17-3, H13-3, H23-2, H16-1, H20-3) swept for real in the final run before AP ran low. Zero AP was wasted once the false-negative MAX-click bug was fixed. Zero gems were ever spent despite deliberately reproducing the real-money hazard twice for calibration. By the final run, all eight already-exhausted-today targets (H18-3, H30-3, H17-2, H15-2, H14-3, H10-3, H14-2, H7-1) correctly self-skipped via the new pre-check without ever reaching the gem-refill dialog.
|
||||||
|
|
||||||
|
### Phase 22: story_sweep.py rewrite to match story_sweep_hard.py's Phase 21 fixes, plus its own campaign guard (2026-07-21)
|
||||||
|
|
||||||
|
Direct follow-up to Phase 21, per explicit user direction: "story_sweep was implemented long time ago so it might have old/buggy code. Try to match the hard implementation on best practices." This module predates story_sweep_hard.py's own live debugging and had several of the same latent bugs, since story_sweep_hard.py had copied its patterns verbatim in the first place.
|
||||||
|
|
||||||
|
The user also asked for a campaign-active guard on Normal mode, matching Hard's -- Normal mode was confirmed to be under its own real 2x reward campaign at the time.
|
||||||
|
|
||||||
|
Live investigation on nik-gpu (2026-07-21) before making any change confirmed:
|
||||||
|
|
||||||
|
- The reward-campaign banner is pixel-identical (same rect, same color) between the Normal and Hard tabs -- only the tab name in the text differs ("任務Normalで獲得できる報酬量が2倍(+100%)になっています" vs Hard's own "任務Hard..."). `config.HARD_CAMPAIGN_BADGE_RECT`/`_RGB` were renamed to the generic `config.TASK_CAMPAIGN_BADGE_RECT`/`_RGB` and are now shared by both modules rather than duplicated.
|
||||||
|
- Normal mode's own tabbed stage-info modal has the identical two-stacked-action-button hazard (gold 任務開始 directly beneath cyan 掃討開始) that `config.py`'s own `SWEEP_MAX_BUTTON` comment already documented for this exact layout, but `story_sweep.py` had never actually added an OCR gate against it (unlike bounty.py/story_sweep_hard.py, both of which have `_confirm_dialog_is_sweep`).
|
||||||
|
- Normal mode has no per-stage daily clear-count cap at all -- no "残り回数" indicator anywhere in its modal, confirming the Hard-specific gem-refill hazard (Phase 21, item 3) genuinely does not apply here; nothing else needed on that front.
|
||||||
|
- The Normal/Hard toggle tab needed its own explicit guard for a new reason Hard never had to consider: with `story_sweep_hard` now able to run earlier in the same `q4h` preset sequence (the user's own recent edit), it can leave the Hard tab selected, and `story_sweep.py` had always implicitly assumed Normal was already active.
|
||||||
|
|
||||||
|
Rewrote `ba_auto/tasks/story_sweep.py`, porting every fix from Phase 21 even though most had never actually been triggered in production (this module's own `config.STORY_SWEEP_TARGETS` has always been empty, so historically only one rotation target ever ran per invocation -- never enough iterations for a multi-target bug to surface):
|
||||||
|
|
||||||
|
- `_open_normal_tab`/`_ensure_task_screen` (new): explicit Normal-tab click-and-verify, plus a loop-level self-heal check before each target, replacing the removed per-path "if on subscreen, press Escape" cleanup -- the same latent bug Phase 21 found and fixed in story_sweep_hard.py, copied here originally, never triggered here only because of the single-target-per-run history above.
|
||||||
|
- `_click_max`/`_click_plus` (renamed from `_click_max_and_verify`/`_click_plus_and_verify`): dropped the "must visibly raise above the floor" abort gate, same false-negative class Phase 21 found under low AP.
|
||||||
|
- `_click_plus` also gained a `_click_min` reset-to-baseline step (new `config.SWEEP_MIN_BUTTON`) before applying "+" raises -- porting bounty.py's own real fix (plan.md Phase 15) for a stepper that remembers its last-used value across opens. This module's original `_click_plus_and_verify` never did this; a latent version of the exact bug bounty.py hit for real, just never exercised since no configured target has ever used a non-"max" count in practice.
|
||||||
|
- `_confirm_dialog_is_sweep` (new): OCR gate against the two-stacked-button hazard, reusing `config.BOUNTY_SWEEP_CONFIRM_TEXT_RECT` directly (confirmed live to crop this modal's own confirm text correctly too).
|
||||||
|
- `_watch_sweep_result` gained the same `clicked_any`-gated second `ends` condition.
|
||||||
|
- `_read_current_region` gained the same `REGION_READ_RETRIES` retry-on-`None` wrapper.
|
||||||
|
- `_campaign_active` (new): the actual feature request, using the shared `config.TASK_CAMPAIGN_BADGE_RECT`/`_RGB`. `run()` gained a `force=False` parameter; `story_sweep_force` was added to `ba_daily.py`'s `TASKS` as the override command, mirroring `story_sweep_hard_force`.
|
||||||
|
|
||||||
|
`config.py` additions: `NORMAL_TAB`/`NORMAL_TAB_ACTIVE_PROBE`/`NORMAL_TAB_ACTIVE_RGB` (live-calibrated 2026-07-21 -- Normal's own active-tab color is dark navy, a different accent than Hard's red), `SWEEP_MIN_BUTTON` (live-calibrated against the same MIN/-/+/MAX row as the existing MAX/PLUS/MINUS-probe constants).
|
||||||
|
|
||||||
|
**Live-tested for real the same session**: a real run swept the day's rotation target (30-2) end-to-end successfully on the first attempt -- campaign guard correctly passed (banner was genuinely showing), region navigation succeeded, the OCR stage-row search matched correctly despite the same known leading-digit misread already documented in this module ("20-2" was read for the "30-2" row, but `_label_suffix`'s suffix-only comparison still matched on "2"), the sweep-usage confirm dialog passed both the color and the new OCR text gate, and the result was correctly detected as `"swept"` (not the old cosmetic misreport story_sweep_hard.py hit) on the very first real attempt. AP dropped from ~139 to 1 and credits rose by 13,966, confirming a real, substantial MAX sweep. Clean return to home confirmed by screenshot. No new bugs found -- unlike Phase 21, this rewrite worked correctly the first time it touched the real game, likely because every fix ported in was already proven live in story_sweep_hard.py rather than being newly speculative here.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
### OCR
|
### OCR
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user