diff --git a/CLAUDE.md b/CLAUDE.md index 3fa4e6a..3921135 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -650,9 +650,9 @@ Current project state: mailbox, cafe, stamina, story_sweep, shop_common, shop_ta - no legacy bridge remains - `ba_auto/tasks/stamina.py` claims the Mission panel's bulk `一括受取` button - see `plan.md` Phase 8 for stamina details -- `ba_auto/tasks/story_sweep.py` sweeps a config-driven list of exact `(region, stage, count)` targets from `config.STORY_SWEEP_TARGETS` +- `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 -- see `plan.md` Phase 10 for the OCR-based story sweep port +- see `plan.md` Phase 10 for the OCR-based story sweep port, and its "Phase 10 follow-up" for the rotation-target fix - `ba_auto/tasks/shop_common.py` and `ba_auto/tasks/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 (`config.COMMON_SHOP_TARGETS` / `config.TACTICAL_SHOP_TARGETS`) - item identification is by fixed grid position, not per-item OCR — the reference's own `get_item_position` indexes an external static price table this repo doesn't have, so a locally pixel-scanned position table is the faithful port, not an OCR-avoidance shortcut; price-digit OCR is layered on top as an extra catalog-drift safety check the reference doesn't even do per-item - both shops were live-tested with real purchases (see `plan.md` Phase 11), which also surfaced a real, previously-unknown per-refresh-cycle purchase cap on these items (not shown as a visible counter) — the task correctly detected the now-unselectable items and safely declined rather than misfiring diff --git a/ba_auto/config.py b/ba_auto/config.py index 2d15219..ed6a98a 100644 --- a/ba_auto/config.py +++ b/ba_auto/config.py @@ -179,13 +179,24 @@ SWEEP_RESULT_BUTTON_REGION = (700, 700, 1300, 1050) # region % 3 == 0. `count` is a positive int (uses the "+" stepper) or the # literal string "max" (uses the in-game MAX button). # -# PLACEHOLDER -- region 1 stage 1 is unlikely to be what you actually want -# swept (it may not even be 3-starred/cleared on this account yet). Edit -# this list with your own already-cleared stage(s) before running -# story_sweep for real. -STORY_SWEEP_TARGETS = [ - (1, 1, "max"), -] +# Empty by default -- the earlier placeholder here was (1, 1, "max"), which +# a real run then dutifully swept region 1 stage 1 instead of the account's +# actual last region, since story_sweep has no "find the latest region" +# heuristic anymore (see plan.md Phase 9's retrospective for why that +# heuristic was removed). Add entries here for any additional fixed targets +# you want swept every run, on top of the daily rotation target below. +STORY_SWEEP_TARGETS = [] + +# Daily-rotating target: sweeps a single region, cycling through its stages +# one per day rather than grinding the same stage every run, per explicit +# user direction. `ROTATION_STAGE_COUNT` is how many stages that region has +# (1..N); which one runs today is `today's date -> N` via a plain date +# ordinal modulo, not the calendar day-of-year, so the cycle doesn't skip or +# repeat around a year boundary. Set STORY_SWEEP_ROTATION_REGION to None to +# disable this and only sweep STORY_SWEEP_TARGETS. +STORY_SWEEP_ROTATION_REGION = 30 +STORY_SWEEP_ROTATION_STAGE_COUNT = 6 +STORY_SWEEP_ROTATION_COUNT = "max" # Common Shop / Tactical Challenge Shop. Both tabs share the same underlying # checkbox-grid-then-bulk-buy UI (the live equivalent of the reference's diff --git a/ba_auto/tasks/story_sweep.py b/ba_auto/tasks/story_sweep.py index d560b68..46fe20f 100644 --- a/ba_auto/tasks/story_sweep.py +++ b/ba_auto/tasks/story_sweep.py @@ -7,6 +7,13 @@ navigating to each one deterministically instead of the previous "latest unlocked region, then a random stage" heuristic -- see plan.md Phase 9's retrospective for why that heuristic was a mistake. +`_rotation_target` adds a second, date-derived target on top of that static +list, per explicit user direction: rather than grinding one fixed stage in +their current last region every run, it cycles through that region's stages +one per day (a plain date-ordinal modulo, so the cycle doesn't skip or +repeat around a year boundary). See config.py's STORY_SWEEP_ROTATION_* +constants. + - `_go_to_region` ports task_utils.py::to_region: OCR the current region number, click the exact delta, re-check, bounded loop. - `_find_stage_row` is a scoped-down port of core/image.py's @@ -21,6 +28,8 @@ 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 plan.md Phase 9/10. """ +import datetime + from ba_auto import detector, navigation OPEN_RETRIES = 3 @@ -333,18 +342,33 @@ def _sweep_target(driver, config, region, stage, count): return outcome +def _rotation_target(config): + region = getattr(config, "STORY_SWEEP_ROTATION_REGION", None) + if not region: + return None + stage_count = config.STORY_SWEEP_ROTATION_STAGE_COUNT + stage = (datetime.date.today().toordinal() % stage_count) + 1 + return (region, stage, config.STORY_SWEEP_ROTATION_COUNT) + + def run(driver, config): driver.focus_game() - if not config.STORY_SWEEP_TARGETS: - print("[story_sweep] no targets configured (config.STORY_SWEEP_TARGETS is empty), nothing to do") + targets = list(config.STORY_SWEEP_TARGETS) + rotation = _rotation_target(config) + if rotation: + print(f"[story_sweep] today's rotation target: region {rotation[0]} stage {rotation[1]}") + targets.append(rotation) + + if not targets: + print("[story_sweep] no targets configured (config.STORY_SWEEP_TARGETS is empty and rotation is disabled), nothing to do") return if not _open_task_screen(driver, config): print("[story_sweep] could not confirm task screen is open, aborting without pressing further keys") return - for region, stage, count in config.STORY_SWEEP_TARGETS: + for region, stage, count in targets: outcome = _sweep_target(driver, config, region, stage, count) if outcome == "inadequate_ap": print("[story_sweep] insufficient AP -- stopping, not attempting remaining targets") diff --git a/plan.md b/plan.md index 15ac4ba..96e3d08 100644 --- a/plan.md +++ b/plan.md @@ -369,6 +369,8 @@ Also fixed in passing, found only because live testing exercised the actual home Verified live end-to-end at least once, real AP spent: a genuine 5x sweep of 30-3 (AP 53→~5, confirmed via the "掃討完了" results screen's reward totals), including clicking through the usage-confirm dialog, the SKIP animation-skip screen, and the final reward-totals OK, landing back on the bare stage-info modal afterward. The genuine insufficient-AP path was also verified live (correctly cancelled the real "AP購入" purchase prompt without spending Pyroxene). Not yet re-verified end-to-end with the final rewritten code specifically (the color-based dynamic button-finding in `_watch_sweep_result`) at a nonzero AP balance — the manual walkthrough that discovered the dialogs used direct scripted clicks before the code was rewritten to match; the rewritten code's color-matching logic was separately verified offline against the exact screenshots captured live (all four dialog states correctly classified), but a fresh live run once AP regenerates would close that last gap. Not verified: sweeping a "-A" bonus stage (needs its own layout re-check, see above), an integer (non-"max") configured count actually being clicked via `SWEEP_PLUS_BUTTON`, and Hard-mode tab stages. +**Phase 10 follow-up (user-reported):** the user ran `story_sweep` for real and it spent AP on region 1 stage 1 instead of region 30 (their actual current last region). Not a code bug — `config.STORY_SWEEP_TARGETS` still held the literal placeholder `(1, 1, "max")` shipped with this phase, and the user hadn't edited it yet. Rather than just filling in one static `(30, N, "max")` entry, the user asked for the stage within region 30 to rotate daily across all 6 of that region's stages instead of grinding one fixed stage every run. Added `config.STORY_SWEEP_ROTATION_REGION`/`STORY_SWEEP_ROTATION_STAGE_COUNT`/`STORY_SWEEP_ROTATION_COUNT` and `story_sweep._rotation_target()`, which computes `(region, stage, count)` from `datetime.date.today().toordinal() % stage_count` — a plain date-ordinal modulo rather than calendar day-of-year, so the 6-day cycle doesn't skip or repeat around a year boundary. This target is appended to (not a replacement for) whatever's in `STORY_SWEEP_TARGETS`, which is now empty by default. Verified the computed target offline (region 30, stage 6, on the date this was fixed) but not yet re-run against the live game since AP hadn't regenerated. + ### Phase 11: Common Shop + Tactical Shop **Status: Done.** Ported `module/shop/common_shop.py` / `module/shop/tactical_challenge_shop.py`'s `implement()` and the shared `module/shop/shop_utils.py` (`to_common_shop`, `get_item_position`/`ensure_choose`/`buy`) to `ba_auto/tasks/shop_common.py` / `shop_tactical.py`, sharing control flow through a new `ba_auto/tasks/shop_utils.py`.