feat: Implement Lesson/Schedule task
- Added `lesson.py` to handle the scheduling of lessons based on affection values. - Integrated OCR functionality to read affection counts from heart badges. - Updated `README.md` to include the new lesson task in the task status section. - Enhanced `config.py` with necessary configurations for the lesson task. - Modified `detector.py` to include a new function for reading heart badge values accurately. - Updated `mapping.md` to reflect the new lesson task implementation. - Adjusted `ba_daily.py` to include the lesson task in the command dispatch. - Updated `plan.md` to document the completion of the lesson task and its testing outcomes. - Modified `setup.sh` to include the lesson task in the run command instructions.
This commit is contained in:
parent
1bb2ccdd6f
commit
51ad297f69
13
CLAUDE.md
13
CLAUDE.md
@ -639,12 +639,12 @@ When debugging on `nik-gpu`, copy relevant screenshots or debug images back into
|
||||
|
||||
## Existing features
|
||||
|
||||
Current project state: mailbox, cafe, stamina, story_sweep, shop_common, and shop_tactical are all migrated to real Python. No Bash feature logic remains.
|
||||
Current project state: mailbox, cafe, stamina, story_sweep, shop_common, shop_tactical, and lesson are all migrated to real Python. No Bash feature logic remains.
|
||||
|
||||
- `ba_dailies.sh` is a thin launcher that execs `ba_daily.py`
|
||||
- `ba_daily.py` dispatches `mailbox`, `cafe`, `stamina`, `story_sweep`, `shop_common`, `shop_tactical`, and default flow to `ba_auto/tasks/`
|
||||
- `ba_daily.py` dispatches `mailbox`, `cafe`, `stamina`, `story_sweep`, `shop_common`, `shop_tactical`, `lesson`, and default flow to `ba_auto/tasks/`
|
||||
- default flow is `mailbox`, `cafe`, `stamina`
|
||||
- `story_sweep`, `shop_common`, and `shop_tactical` are opt-in only since they spend AP/credits/tactical coin rather than reclaiming something free
|
||||
- `story_sweep`, `shop_common`, `shop_tactical`, and `lesson` are opt-in only since they spend AP/credits/tactical coin/lesson tickets rather than reclaiming something free
|
||||
- `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`
|
||||
- no legacy bridge remains
|
||||
@ -656,6 +656,9 @@ Current project state: mailbox, cafe, stamina, story_sweep, shop_common, and sho
|
||||
- `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
|
||||
- `ba_auto/tasks/lesson.py` sweeps every unlocked region's schedule grid (a scrollable list of 12 named regions, each opening a grid modal of up to 9 location cards), picking the highest-affection available lesson each time until lesson tickets or lessons run out
|
||||
- per-cell affection is read via `detector.read_int_on_heart_badge`, a dedicated OCR path for the pink/magenta heart-shaped badge — the project's normal grayscale-threshold OCR (`read_int`) misreads it, because the badge's own outline stroke survives the same threshold as the digit glyph; a "done today" portrait keeps its number and gets a green checkmark added alongside it rather than losing the number, so done-ness is checked via that checkmark's color, not inferred from a failed OCR read
|
||||
- lesson was live-tested with real tickets spent (see `plan.md` Phase 12), which surfaced two real bugs from that assumption gap plus an OCR contamination issue — both fixed; see Phase 12 for the full writeup
|
||||
- `ba_auto/detector.py` has `find_cafe_sparkle()`, the sparkle template-match ported in-process from the now-deleted `scripts/detect_and_click.py`
|
||||
- `scripts/ba_dailies_legacy.sh` and `scripts/detect_and_click.py` have been deleted
|
||||
- the `scripts/` directory itself no longer exists
|
||||
@ -692,7 +695,9 @@ A pat that crosses an affection-rank threshold shows a full-screen `絆ランク
|
||||
|
||||
It presses Enter until the cutscene clears, porting the reference's own `to_cafe()` navigation handling of `relationship_rank_up` from `module/cafe_reward.py`.
|
||||
|
||||
This is not yet live-confirmed against a real rank-up trigger. It is grounded in real captured screenshots, but treat it as implemented-but-unverified until one happens naturally during a real run.
|
||||
This is not yet live-confirmed against a real cafe-pat rank-up trigger specifically. It is grounded in real captured screenshots, but treat it as implemented-but-unverified until one happens naturally during a real cafe run.
|
||||
|
||||
The same full-screen `絆ランクアップ!` cutscene *was* confirmed live during Lesson/Schedule work (see `plan.md` Phase 12), and that work found `is_on_subscreen`'s fixed header probe reads inconsistently across different characters' cutscene art — bright for one, which would make cafe's "keep pressing Enter while NOT on subscreen" loop think the cutscene had already cleared when it hadn't. Not necessarily hazardous on its own (it would just stop dismissing early and let the pat loop retry against a still-showing cutscene), but worth a more robust probe if cafe's rank-up path ever misbehaves live.
|
||||
|
||||
Not yet verified for cafe:
|
||||
|
||||
|
||||
@ -33,6 +33,7 @@ Current task status:
|
||||
| `story_sweep` | Real Python (`ba_auto/tasks/story_sweep.py`). **Spends AP** — opt-in only, not part of the default flow. Sweeps a config-driven list of exact `(region, stage, count)` targets (`config.STORY_SWEEP_TARGETS` — edit this before running for real, it ships with a placeholder), navigating to each via OCR (region-number readout + stage-label matching) rather than a random pick. Verifies the MAX/`+` click actually raised the count before starting the sweep, clicks through the AP-usage-confirmation dialog, and closes the stage-info modal via its own X button afterward (Escape doesn't close it — confirmed live). See `plan.md` Phase 10. |
|
||||
| `shop_common` | Real Python (`ba_auto/tasks/shop_common.py`). **Spends credits** — opt-in only, not part of the default flow. Buys a config-driven list of exact `(row, col, name, expected_price)` targets (`config.COMMON_SHOP_TARGETS`) from the 通常アイテム tab: OCR-verifies each item's price before checking its box, then clicks the bulk 購入 button and confirms through the purchase dialog. Live-tested with real purchases. See `plan.md` Phase 11. |
|
||||
| `shop_tactical` | Real Python (`ba_auto/tasks/shop_tactical.py`). **Spends tactical coin** — opt-in only, not part of the default flow. Same flow as `shop_common` against the 戦術対抗戦 tab and `config.TACTICAL_SHOP_TARGETS`. Live-tested with real purchases. See `plan.md` Phase 11. |
|
||||
| `lesson` | Real Python (`ba_auto/tasks/lesson.py`). **Spends lesson tickets** — opt-in only, not part of the default flow. Sweeps every unlocked region's schedule grid, picking the highest-affection available lesson each time (via `detector.read_int_on_heart_badge` OCR on each portrait's heart-shaped badge) until tickets or lessons run out. Live-tested with real tickets spent. See `plan.md` Phase 12. |
|
||||
|
||||
## Prerequisites on nik-gpu
|
||||
|
||||
@ -73,6 +74,7 @@ The game must already be running on `nik-gpu` (window titled `BlueArchive`). The
|
||||
~/ba_dailies.sh story_sweep # spends AP -- not in the default flow, run explicitly
|
||||
~/ba_dailies.sh shop_common # spends credits -- not in the default flow, run explicitly
|
||||
~/ba_dailies.sh shop_tactical # spends tactical coin -- not in the default flow, run explicitly
|
||||
~/ba_dailies.sh lesson # spends lesson tickets -- not in the default flow, run explicitly
|
||||
```
|
||||
|
||||
You can also run these remotely without a separate `ssh` login step:
|
||||
|
||||
@ -264,3 +264,99 @@ TACTICAL_SHOP_TARGETS = [
|
||||
(0, 0, "初級栄養ドリンク(AP30)", 15),
|
||||
(0, 1, "中級栄養ドリンク(AP60)", 30),
|
||||
]
|
||||
|
||||
# Lesson/Schedule (module/lesson.py). This client renders the reference's
|
||||
# paged single-region view as a scrollable "Location Select" list of 12
|
||||
# named regions instead -- these names are the reference's own
|
||||
# lesson_region_name.JP list (core/config/default_config.py), embedded there
|
||||
# directly rather than fetched externally, so hardcoding it locally isn't an
|
||||
# external-data problem the way the shop price table was. They're used only
|
||||
# for logging here, not for OCR identification -- unlike the reference's
|
||||
# paged arrows, this list's scroll position is deterministic (see
|
||||
# REGION_ROW_Y below), so there's nothing to OCR-locate.
|
||||
LESSON_ICON = (314, 1100) # bottom nav "スケジュール" icon on the home screen
|
||||
LESSON_BACK_BUTTON = (85, 55) # shared by both the region-list and per-region map screens
|
||||
|
||||
LESSON_REGION_NAMES = [
|
||||
"シャーレオフィス", "シャーレ居住区", "ゲヘナ学園・中央区", "アビドス高等学校",
|
||||
"ミレニアム・スタディーエリア", "トリニティ・スクエア", "レッドウインター連邦学園",
|
||||
"百鬼夜行中心部", "D.U.シラトリ区", "山海経中央特区", "春葉原", "ワイルドハント総合芸術地区",
|
||||
]
|
||||
# The region list only ever settles at two scroll positions -- scrolled fully
|
||||
# to top (regions 0-5 visible) or fully to bottom (regions 6-11) -- confirmed
|
||||
# live: 15 scroll-down clicks always lands on the same bottom state, it
|
||||
# doesn't keep scrolling past it. Each row's card is clickable at this
|
||||
# center-ish Y regardless of which of the two scroll states is showing.
|
||||
LESSON_REGION_ROW_Y = [265, 420, 585, 745, 900, 1060]
|
||||
LESSON_REGION_LIST_SCROLL_POINT = (1400, 700)
|
||||
LESSON_REGION_LIST_SCROLL_CLICKS = 15
|
||||
LESSON_REGION_ROW_X = 1400
|
||||
|
||||
# Per-region isometric map screen -- opens the "全てのスケジュール" grid
|
||||
# modal (this client's rendering of the reference's per-region 3x3
|
||||
# get_lesson_each_region_status/get_lesson_relationship_counts grid).
|
||||
LESSON_ALL_SCHEDULES_BUTTON = (1770, 1118)
|
||||
LESSON_GRID_MODAL_CLOSE_BUTTON = (1710, 211)
|
||||
|
||||
# Grid modal cell layout: up to 3 columns x 3 rows of location cards, each
|
||||
# showing up to 3 student portraits with a heart-shaped affection-count
|
||||
# badge at bottom-right. Pixel-scanned live against two different regions'
|
||||
# modals (Gehenna Central: 8 cells, Schale Office: 7 cells) -- consistent
|
||||
# across both. A region with more than 9 currently-unlocked locations would
|
||||
# need scrolling inside this modal, which isn't implemented (not yet seen
|
||||
# live on this account; see plan.md).
|
||||
LESSON_GRID_COL_X = [270, 786, 1302] # portrait-slot-0 center per column
|
||||
LESSON_GRID_ROW_HEADER_Y = [380, 608, 836] # click target to open a cell's info panel
|
||||
LESSON_GRID_ROW_PORTRAIT_Y = [486, 713, 940]
|
||||
LESSON_GRID_PORTRAIT_STEP_X = 109 # slot 1/2 center = slot 0 center + N * this
|
||||
# Badge center sits below-right of each portrait-slot's own center point.
|
||||
LESSON_GRID_BADGE_OFFSET = (38, 24)
|
||||
LESSON_GRID_BADGE_OCR_HALF_SIZE = (26, 20) # crop half-width/half-height around badge center
|
||||
# A green checkmark can appear at top-right of a portrait ALONGSIDE its
|
||||
# unchanged heart badge number, not instead of it (confirmed live -- an
|
||||
# earlier assumption that "done" always blanks the number was wrong) --
|
||||
# this is the only reliable "already done today" signal, so it's checked
|
||||
# separately rather than inferred from the badge OCR.
|
||||
LESSON_GRID_CHECKMARK_OFFSET = (41, -31)
|
||||
LESSON_GRID_CHECKMARK_RGB = ((130, 190, 60), (220, 255, 160))
|
||||
LESSON_GRID_CHECKMARK_HALF_SIZE = (14, 13)
|
||||
# A masked, digit-only OCR read of the heart badge can still occasionally
|
||||
# fuse a stray leading digit from portrait art bleeding into the crop's left
|
||||
# edge (confirmed live: "13" -> "413", "18" -> "418", reproducible across
|
||||
# every psm mode -- see detector.read_int_on_heart_badge). Real affection
|
||||
# values never reach this range in practice, so treat anything this large
|
||||
# as contamination and discard it rather than trust it.
|
||||
LESSON_GRID_BADGE_MAX_PLAUSIBLE = 99
|
||||
|
||||
# "保有チケット N/M" readout, top-left of the region-list/map screens.
|
||||
LESSON_TICKET_OCR_RECT = (295, 133, 385, 172)
|
||||
|
||||
# Per-cell info panel ("スケジュール情報"), opened by clicking a grid cell.
|
||||
# Its Start button and the post-schedule report's OK button (below) are the
|
||||
# same bright-cyan gradient button at nearly the same position -- confirmed
|
||||
# by direct pixel sample, sharing one color range.
|
||||
LESSON_ACTION_BUTTON_RGB = ((90, 195, 235), (150, 240, 255))
|
||||
LESSON_INFO_START_BUTTON = (960, 890)
|
||||
LESSON_INFO_CLOSE_BUTTON = (1435, 240)
|
||||
|
||||
# After clicking Start, a variable sequence of intermediate screens can
|
||||
# appear before settling back on the grid modal -- a bond-rank-up full-
|
||||
# screen cutscene (confirmed live, see screenshots/lesson/ -- its art is
|
||||
# character-dependent, so no fixed color/position reliably identifies it)
|
||||
# and/or a "スケジュールレポート" results modal. A transient "run the
|
||||
# schedule twice" campaign multiplier was also observed live, doubling how
|
||||
# many of these screens appear in a row -- rather than special-case any of
|
||||
# this, `lesson.py` presses Enter in a bounded loop and re-checks two fixed
|
||||
# markers each round:
|
||||
#
|
||||
# - the grid modal's own title underline (this exact yellow-gold, confirmed
|
||||
# live) is visible ONLY when the grid modal is frontmost and idle -- both
|
||||
# the report modal and the cutscene cover it, confirmed live against all
|
||||
# three states.
|
||||
LESSON_GRID_IDLE_PROBE_RECT = (790, 244, 1130, 252)
|
||||
LESSON_GRID_IDLE_RGB = ((235, 220, 70), (255, 250, 130))
|
||||
# - the report modal's OK button -- same color as LESSON_ACTION_BUTTON_RGB,
|
||||
# its own confirmed fixed position -- clicked directly rather than folded
|
||||
# into the blind Enter-press fallback, since we can verify it precisely.
|
||||
LESSON_REPORT_OK_BUTTON = (960, 895)
|
||||
LESSON_POST_SCHEDULE_MAX_ENTER_PRESSES = 8
|
||||
|
||||
@ -131,3 +131,32 @@ def read_int(region, psm=7):
|
||||
"""Local equivalent of the reference's recognize_int."""
|
||||
digits = "".join(ch for ch in read_text(region, whitelist="0123456789", psm=psm) if ch.isdigit())
|
||||
return int(digits) if digits else None
|
||||
|
||||
|
||||
def read_int_on_heart_badge(region, psm=7):
|
||||
"""OCR a small dark-navy digit rendered on lesson.py's pink/magenta
|
||||
heart-shaped affection badge.
|
||||
|
||||
read_int's plain grayscale threshold (tuned for this UI's normal
|
||||
dark-text-on-light-card look) misreads these: the heart's own outline
|
||||
stroke is a darker, saturated magenta whose grayscale value happens to
|
||||
fall on the same side of the threshold as the digit glyph, so it
|
||||
survives as stray black marks tesseract sometimes fuses into extra
|
||||
digits -- confirmed live, "13" -> "113", "19" -> "119". The digit color
|
||||
is reliably R < G (navy/blue-grey) while every pink/magenta badge tone
|
||||
sampled (fill and outline, light and dark) is R > G, so masking on that
|
||||
channel relationship instead of raw brightness cleanly drops the badge
|
||||
shape and keeps just the glyph.
|
||||
"""
|
||||
x1, y1, x2, y2 = region
|
||||
driver.screenshot(OCR_SHOT_PATH)
|
||||
img = cv2.imread(OCR_SHOT_PATH)
|
||||
crop = img[y1:y2, x1:x2]
|
||||
b, g, r = crop[:, :, 0].astype(np.int16), crop[:, :, 1].astype(np.int16), crop[:, :, 2].astype(np.int16)
|
||||
ink = (r < g) & (np.maximum(np.maximum(r, g), b) < 170)
|
||||
binary = np.where(ink, 0, 255).astype(np.uint8)
|
||||
upscaled = cv2.resize(binary, None, fx=OCR_UPSCALE, fy=OCR_UPSCALE, interpolation=cv2.INTER_CUBIC)
|
||||
tess_config = f"--psm {psm} -c tessedit_char_whitelist=0123456789"
|
||||
text = pytesseract.image_to_string(upscaled, lang="eng", config=tess_config).strip()
|
||||
digits = "".join(ch for ch in text if ch.isdigit())
|
||||
return int(digits) if digits else None
|
||||
|
||||
@ -14,6 +14,6 @@ Maps each local feature to the corresponding `~/repo/baas-reference/module/...`
|
||||
| Arena | `module/arena.py` | Need to inspect | `ba_auto/tasks/arena.py` | auto-fight + OCR + local driver | Not started |
|
||||
| Common Shop | `module/shop/common_shop.py`, `module/shop/shop_utils.py` | `implement`, `to_common_shop`, `get_item_position`/`ensure_choose`/`buy` (shared, see Tactical Shop row) | `ba_auto/tasks/shop_common.py`, `ba_auto/tasks/shop_utils.py` | `get_item_position`'s color+template item-state scan → fixed grid-position targets (`config.COMMON_SHOP_TARGETS`) + price-digit OCR verify, since the reference's own item-identification here indexes an external static price table (`self.static_config.common_shop_price_list`, fetched from a remote resource) this repo doesn't have — not per-item OCR, so this isn't an OCR-avoidance shortcut. Purchase-confirm dialog + reward-acquired banner handled via a single overlay-darkness probe (`config.SHOP_OVERLAY_PROBE`) instead of tracking each dialog's own layout | Done. Live-tested with real purchases (all 8 configured targets bought, cost matched exactly). Discovered live: these items have a per-refresh-cycle purchase cap not shown as a visible counter (unlike the 青輝石 tab's "あと1回購入可能" labels) — confirmed by re-running the task after purchase and observing it correctly detect the now-unselectable items (checkbox + individual 購入 button both unresponsive) and safely decline rather than guess. A fresh, everything-available run hasn't been re-verified since the account had already exhausted this cycle's purchases via that same test |
|
||||
| Tactical Shop | `module/shop/tactical_challenge_shop.py`, `module/shop/shop_utils.py` | `implement`, `goto_shop_by_name`, shared `get_item_position`/`ensure_choose`/`buy` | `ba_auto/tasks/shop_tactical.py`, `ba_auto/tasks/shop_utils.py` | `goto_shop_by_name`'s OCR swipe-search over the shop-tab list → fixed click (`config.SHOP_TAB_TACTICAL`): this account's tab list is only 7 entries and fits on screen with no scroll needed, confirmed live, so there's nothing to search for — not an OCR-avoidance shortcut. Same grid-position + price-OCR-verify + overlay-probe design as Common Shop, sharing `shop_utils.run_shop_tab` | Done. Live-tested with real purchases (both configured AP-recovery drinks bought; AP and tactical-coin balance changes matched exactly) |
|
||||
| Lesson/Schedule | `module/lesson.py` | Need to inspect | `ba_auto/tasks/lesson.py` | OCR + template/portrait search + local driver | Not started |
|
||||
| Lesson/Schedule | `module/lesson.py` | `implement`, `to_lesson_location_select`/`to_select_location`/`to_all_locations` (nav state machine), `get_lesson_region_num`/`switch_lesson_region_page`/`to_lesson_region` (paged region nav), `get_lesson_each_region_status`+`check_region_availability` (per-cell status via isometric-parallelogram pixel scan), `get_lesson_relationship_counts` (per-cell affection pip count via color count), `choose_lesson` (selection policy), `execute_lesson`/`to_location_info`/`start_lesson` (click cell -> info panel -> start -> result) | `ba_auto/tasks/lesson.py` | `picture.co_detect` -> `navigation.wait_for_state`-style bounded Enter-press loop (see below); the reference's paged-arrow region nav (needing OCR to know current position) -> this client renders the 12 regions as a scrollable list instead, which only ever settles at two scroll positions (`config.LESSON_REGION_ROW_Y`), so navigation is direct index-based clicking with nothing to OCR-locate; the reference's isometric `Parallelogram`/`Triangle` per-cell scan (tuned to the reference's own screen layout) -> reading each portrait's heart-shaped affection badge via a dedicated OCR path (`detector.read_int_on_heart_badge`) needs no isometric geometry at all | Done. Config-driven scope only in the sense of the *policy* (affection-first selection, sweep every unlocked region until tickets/lessons run out, no ticket purchasing, no favor-student targeting) -- unlike shop, no user-specific target list was needed since the reference's own `lesson_region_name.JP` (embedded directly in its `default_config.py`, not externally fetched) already names all 12 regions, used here only for logging. Live-tested for real: 5 real tickets spent across 3 regions with correct outcomes (ticket count, cleanup navigation, home-screen return all verified). Two real bugs were found and fixed from that run -- see below and `plan.md`'s Lesson phase |
|
||||
|
||||
Do not implement a feature without filling at least the relevant row.
|
||||
|
||||
247
ba_auto/tasks/lesson.py
Normal file
247
ba_auto/tasks/lesson.py
Normal file
@ -0,0 +1,247 @@
|
||||
"""Lesson/Schedule. Reference: baas-reference/module/lesson.py.
|
||||
|
||||
Scoped for v1 per plan.md's "Suggested first version" and explicit user
|
||||
direction: affection-first selection (mirrors the reference's
|
||||
lesson_relationship_first=True), sweep every unlocked region in a fixed
|
||||
order until either lesson tickets or scoreable lessons run out, no
|
||||
lesson-ticket purchasing (same real-currency-adjacent caution as Daily Free
|
||||
Power / the shop's manual refresh button -- see CLAUDE.md), no favor-student
|
||||
targeting (deferred, matching plan.md).
|
||||
|
||||
This client renders the reference's paged single-region view + isometric
|
||||
3x3 status grid (get_lesson_each_region_status/get_lesson_relationship_counts,
|
||||
built on an isometric Parallelogram/Triangle pixel scan tuned to the
|
||||
reference's own screen layout) as a two-level UI instead: a scrollable list
|
||||
of the same 12 named regions (config.LESSON_REGION_NAMES, taken directly
|
||||
from the reference's own lesson_region_name.JP list -- used here only for
|
||||
logging, since unlike the reference's paged arrows this list's scroll
|
||||
position is deterministic and needs no OCR to locate), each opening a grid
|
||||
modal of up to 9 location cards. Each card shows up to 3 student portraits
|
||||
with a heart-shaped affection-count badge -- reading those via OCR (this
|
||||
project's local equivalent of the reference's pip-counting
|
||||
get_lesson_relationship_counts) needs no isometric geometry at all. A locked
|
||||
location isn't listed in the grid modal at all, so there's nothing to do
|
||||
there; a "no relationship yet" portrait has no badge and its OCR read comes
|
||||
back empty, scoring as a non-candidate. An "already done today" portrait
|
||||
was initially assumed to also blank its badge, but live testing showed that
|
||||
assumption was wrong: it keeps showing its (unchanged) number and gets a
|
||||
green checkmark added at top-right instead -- so done-ness is checked
|
||||
separately via that checkmark's color, not inferred from the OCR read.
|
||||
|
||||
Reading the badge itself also needed its own OCR path
|
||||
(detector.read_int_on_heart_badge): a plain grayscale-threshold read (this
|
||||
project's usual approach) misreads it, because the heart's own outline
|
||||
stroke happens to survive the same threshold as the digit glyph and
|
||||
tesseract sometimes fuses the two into extra digits (see that function's
|
||||
docstring, and config.py's LESSON_GRID_BADGE_MAX_PLAUSIBLE for the
|
||||
plausibility-cap backstop this project keeps as a second line of defense).
|
||||
"""
|
||||
from ba_auto import detector, navigation
|
||||
|
||||
OPEN_RETRIES = 3
|
||||
REGIONS_PER_SCREEN = 6
|
||||
TOTAL_REGIONS = 12
|
||||
GRID_ROWS = 3
|
||||
GRID_COLS = 3
|
||||
GRID_SLOTS = 3
|
||||
|
||||
|
||||
def _read_ticket_count(driver, config):
|
||||
text = detector.read_text(config.LESSON_TICKET_OCR_RECT, whitelist="0123456789/", psm=7)
|
||||
head = text.split("/")[0] if "/" in text else text
|
||||
digits = "".join(ch for ch in head if ch.isdigit())
|
||||
return int(digits) if digits else None
|
||||
|
||||
|
||||
def _open_schedule_screen(driver, config):
|
||||
for attempt in range(1, OPEN_RETRIES + 1):
|
||||
driver.click(*config.LESSON_ICON)
|
||||
driver.wait(2)
|
||||
if navigation.is_on_subscreen(driver):
|
||||
return True
|
||||
print(f"[lesson] schedule screen not detected after click (attempt {attempt}/{OPEN_RETRIES})")
|
||||
return False
|
||||
|
||||
|
||||
def _scroll_region_list(driver, config, to_bottom):
|
||||
x, y = config.LESSON_REGION_LIST_SCROLL_POINT
|
||||
direction = "down" if to_bottom else "up"
|
||||
driver.scroll(x, y, direction, config.LESSON_REGION_LIST_SCROLL_CLICKS)
|
||||
driver.wait(0.8)
|
||||
|
||||
|
||||
def _is_grid_idle(driver, config):
|
||||
lo, hi = config.LESSON_GRID_IDLE_RGB
|
||||
return detector.region_contains_color(config.LESSON_GRID_IDLE_PROBE_RECT, lo, hi)
|
||||
|
||||
|
||||
def _is_action_button_showing(driver, config, position):
|
||||
r, g, b = driver.color_at(*position)
|
||||
lo, hi = config.LESSON_ACTION_BUTTON_RGB
|
||||
return lo[0] <= r <= hi[0] and lo[1] <= g <= hi[1] and lo[2] <= b <= hi[2]
|
||||
|
||||
|
||||
def _open_region_grid(driver, config, region_index):
|
||||
# Retries the whole click-row -> click-all-schedules sequence from
|
||||
# scratch rather than trying to separately verify "did the isometric map
|
||||
# open" -- that screen shares the same bright header as the plain list
|
||||
# (navigation.is_on_subscreen can't tell them apart), so the grid modal
|
||||
# actually opening is the only reliable signal either step worked.
|
||||
screen_bottom = region_index >= REGIONS_PER_SCREEN
|
||||
row = region_index % REGIONS_PER_SCREEN
|
||||
for attempt in range(1, OPEN_RETRIES + 1):
|
||||
_scroll_region_list(driver, config, to_bottom=screen_bottom)
|
||||
driver.click(config.LESSON_REGION_ROW_X, config.LESSON_REGION_ROW_Y[row])
|
||||
driver.wait(1.5)
|
||||
driver.click(*config.LESSON_ALL_SCHEDULES_BUTTON)
|
||||
driver.wait(1.2)
|
||||
if _is_grid_idle(driver, config):
|
||||
return True
|
||||
print(f"[lesson] schedule grid not detected for region index {region_index} (attempt {attempt}/{OPEN_RETRIES})")
|
||||
return False
|
||||
|
||||
|
||||
def _close_grid_modal(driver, config):
|
||||
driver.click(*config.LESSON_GRID_MODAL_CLOSE_BUTTON)
|
||||
driver.wait(1)
|
||||
|
||||
|
||||
def _slot_center(config, row, col, slot):
|
||||
cx = config.LESSON_GRID_COL_X[col] + slot * config.LESSON_GRID_PORTRAIT_STEP_X
|
||||
cy = config.LESSON_GRID_ROW_PORTRAIT_Y[row]
|
||||
return cx, cy
|
||||
|
||||
|
||||
def _badge_rect(config, row, col, slot):
|
||||
cx, cy = _slot_center(config, row, col, slot)
|
||||
ox, oy = config.LESSON_GRID_BADGE_OFFSET
|
||||
hx, hy = config.LESSON_GRID_BADGE_OCR_HALF_SIZE
|
||||
cx, cy = cx + ox, cy + oy
|
||||
return (cx - hx, cy - hy, cx + hx, cy + hy)
|
||||
|
||||
|
||||
def _checkmark_rect(config, row, col, slot):
|
||||
cx, cy = _slot_center(config, row, col, slot)
|
||||
ox, oy = config.LESSON_GRID_CHECKMARK_OFFSET
|
||||
hx, hy = config.LESSON_GRID_CHECKMARK_HALF_SIZE
|
||||
cx, cy = cx + ox, cy + oy
|
||||
return (cx - hx, cy - hy, cx + hx, cy + hy)
|
||||
|
||||
|
||||
def _is_slot_already_done(driver, config, row, col, slot):
|
||||
lo, hi = config.LESSON_GRID_CHECKMARK_RGB
|
||||
return detector.region_contains_color(_checkmark_rect(config, row, col, slot), lo, hi)
|
||||
|
||||
|
||||
def _read_slot_affection(driver, config, row, col, slot):
|
||||
if _is_slot_already_done(driver, config, row, col, slot):
|
||||
return None
|
||||
value = detector.read_int_on_heart_badge(_badge_rect(config, row, col, slot))
|
||||
if value is not None and value > config.LESSON_GRID_BADGE_MAX_PLAUSIBLE:
|
||||
# Contamination from portrait art bleeding into the crop's edge,
|
||||
# not a real affection value -- see config.py's comment.
|
||||
return None
|
||||
return value
|
||||
|
||||
|
||||
def _find_best_cell(driver, config):
|
||||
best_score, best_cell = -1, None
|
||||
for row in range(GRID_ROWS):
|
||||
for col in range(GRID_COLS):
|
||||
cell_score = -1
|
||||
for slot in range(GRID_SLOTS):
|
||||
value = _read_slot_affection(driver, config, row, col, slot)
|
||||
if value is not None and value > cell_score:
|
||||
cell_score = value
|
||||
if cell_score > best_score:
|
||||
best_score, best_cell = cell_score, (row, col)
|
||||
return best_cell, best_score
|
||||
|
||||
|
||||
def _click_cell(driver, config, row, col):
|
||||
driver.click(config.LESSON_GRID_COL_X[col], config.LESSON_GRID_ROW_HEADER_Y[row])
|
||||
driver.wait(1.2)
|
||||
|
||||
|
||||
def _run_one_schedule(driver, config, row, col):
|
||||
"""Click a grid cell through to a completed schedule. Returns True once
|
||||
settled back at the idle grid modal, False if it never got there."""
|
||||
_click_cell(driver, config, row, col)
|
||||
if not _is_action_button_showing(driver, config, config.LESSON_INFO_START_BUTTON):
|
||||
print("[lesson] info panel start button not detected, aborting this cell without pressing further keys")
|
||||
return False
|
||||
|
||||
driver.click(*config.LESSON_INFO_START_BUTTON)
|
||||
driver.wait(1.5)
|
||||
|
||||
# A variable number of intermediate screens can follow (a bond-rank-up
|
||||
# cutscene, a results modal, occasionally both twice under a "2x
|
||||
# schedule" campaign multiplier -- see config.py's comment). Neither
|
||||
# intermediate screen has a reliable fixed marker of its own except the
|
||||
# results modal's OK button, so anything that isn't "back at the idle
|
||||
# grid" or "OK button showing" gets a plain Enter press, bounded.
|
||||
for _ in range(config.LESSON_POST_SCHEDULE_MAX_ENTER_PRESSES):
|
||||
if _is_grid_idle(driver, config):
|
||||
return True
|
||||
if _is_action_button_showing(driver, config, config.LESSON_REPORT_OK_BUTTON):
|
||||
driver.click(*config.LESSON_REPORT_OK_BUTTON)
|
||||
else:
|
||||
driver.keypress("Return")
|
||||
driver.wait(1.5)
|
||||
|
||||
return _is_grid_idle(driver, config)
|
||||
|
||||
|
||||
def _sweep_region(driver, config, region_index, remaining_tickets):
|
||||
name = config.LESSON_REGION_NAMES[region_index]
|
||||
|
||||
if not _open_region_grid(driver, config, region_index):
|
||||
print(f"[lesson] could not confirm schedule grid opened for {name}, skipping")
|
||||
return remaining_tickets
|
||||
|
||||
while remaining_tickets > 0:
|
||||
cell, score = _find_best_cell(driver, config)
|
||||
if cell is None:
|
||||
print(f"[lesson] no schedulable lesson found in {name}")
|
||||
break
|
||||
row, col = cell
|
||||
print(f"[lesson] {name}: best available affection {score} at row {row} col {col}")
|
||||
if not _run_one_schedule(driver, config, row, col):
|
||||
print(f"[lesson] warning: could not confirm return to the schedule grid after {name} row {row} col {col} -- stopping this region")
|
||||
break
|
||||
new_count = _read_ticket_count(driver, config)
|
||||
remaining_tickets = new_count if new_count is not None else remaining_tickets - 1
|
||||
print(f"[lesson] tickets remaining: {remaining_tickets}")
|
||||
|
||||
_close_grid_modal(driver, config)
|
||||
driver.wait(0.5)
|
||||
driver.click(*config.LESSON_BACK_BUTTON)
|
||||
driver.wait(1.5)
|
||||
return remaining_tickets
|
||||
|
||||
|
||||
def run(driver, config):
|
||||
driver.focus_game()
|
||||
|
||||
if not _open_schedule_screen(driver, config):
|
||||
print("[lesson] could not confirm schedule screen is open, aborting without pressing further keys")
|
||||
return
|
||||
|
||||
tickets = _read_ticket_count(driver, config)
|
||||
if tickets is None:
|
||||
print("[lesson] could not OCR ticket count, aborting without pressing further keys")
|
||||
return
|
||||
|
||||
print(f"[lesson] starting tickets: {tickets}")
|
||||
if tickets <= 0:
|
||||
print("[lesson] no lesson tickets available, nothing to do")
|
||||
else:
|
||||
for region_index in range(TOTAL_REGIONS):
|
||||
if tickets <= 0:
|
||||
print("[lesson] out of lesson tickets -- stopping")
|
||||
break
|
||||
tickets = _sweep_region(driver, config, region_index, tickets)
|
||||
|
||||
driver.click(*config.LESSON_BACK_BUTTON)
|
||||
driver.wait(1.5)
|
||||
print("[lesson] Done.")
|
||||
11
ba_daily.py
11
ba_daily.py
@ -3,7 +3,7 @@
|
||||
import sys
|
||||
|
||||
from ba_auto import config, driver
|
||||
from ba_auto.tasks import cafe, mailbox, shop_common, shop_tactical, stamina, story_sweep
|
||||
from ba_auto.tasks import cafe, lesson, mailbox, shop_common, shop_tactical, stamina, story_sweep
|
||||
|
||||
TASKS = {
|
||||
"mailbox": mailbox.run,
|
||||
@ -12,11 +12,12 @@ TASKS = {
|
||||
"story_sweep": story_sweep.run,
|
||||
"shop_common": shop_common.run,
|
||||
"shop_tactical": shop_tactical.run,
|
||||
"lesson": lesson.run,
|
||||
}
|
||||
# story_sweep and both shop tasks are opt-in only (not in the default flow):
|
||||
# they spend AP/credits/tactical coin on a configured buy list rather than
|
||||
# reclaiming something free, which is a real resource decision the default
|
||||
# unattended run shouldn't make blindly.
|
||||
# story_sweep, both shop tasks, and lesson are opt-in only (not in the
|
||||
# default flow): they spend AP/credits/tactical coin/lesson tickets on an
|
||||
# automated choice rather than reclaiming something free, which is a real
|
||||
# resource decision the default unattended run shouldn't make blindly.
|
||||
DEFAULT_ORDER = ["mailbox", "cafe", "stamina"]
|
||||
|
||||
|
||||
|
||||
53
plan.md
53
plan.md
@ -157,8 +157,9 @@ Do not implement a feature without filling at least the relevant row.
|
||||
| Stamina/AP (mission claim) | Migrated (partial, Phase 8): `ba_auto/tasks/stamina.py` claims the Mission panel's bulk "一括受取" button. Daily Free Power (real-money purchase menu) intentionally not implemented | Daily Free Power still not started |
|
||||
| Normal/Hard story AP sweep | Done (Phase 10, supersedes Phase 9's random-pick design): `ba_auto/tasks/story_sweep.py` sweeps a config-driven list of exact `(region, stage, count)` targets (`config.STORY_SWEEP_TARGETS`), navigating to each via OCR (region-number read + delta-click, stage-label OCR match) instead of "latest region, random stage." Opt-in only (`story_sweep` command), not part of the default daily flow | Done |
|
||||
| Common Shop / Tactical Shop | Done (Phase 11): `ba_auto/tasks/shop_common.py` / `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, price-OCR-verified before each click. Live-tested with real purchases in both shops. Opt-in only (`shop_common`/`shop_tactical` commands), not part of the default daily flow | Done |
|
||||
| Shared driver | `ba_auto/driver.py` built (`run_command`, `focus_game`, `click`, `move_mouse`, `scroll`, `keypress`, `screenshot`, `wait`, `color_at`); `click()` now splits `mousemove`/`click` into two xdotool calls (Phase 8 finding — fixes a real source of click flakiness); wired into `mailbox.py`, `cafe.py`, `stamina.py`, `story_sweep.py`, `shop_common.py`, `shop_tactical.py` | Extend with new primitives as future tasks need them |
|
||||
| Python CLI | Built: `ba_daily.py` dispatches `mailbox`/`cafe`/`stamina`/`story_sweep`/`shop_common`/`shop_tactical`/default flow | Extend as new tasks are added |
|
||||
| Lesson/Schedule | Done (Phase 12): `ba_auto/tasks/lesson.py` sweeps every unlocked region's schedule grid, picking the highest-affection available lesson each time via a dedicated heart-badge OCR read (`detector.read_int_on_heart_badge`) until tickets or lessons run out. Live-tested with real tickets spent; two real bugs (checkmark-doesn't-blank-the-number, badge OCR misreads) found and fixed. Opt-in only (`lesson` command), not part of the default daily flow | Done |
|
||||
| Shared driver | `ba_auto/driver.py` built (`run_command`, `focus_game`, `click`, `move_mouse`, `scroll`, `keypress`, `screenshot`, `wait`, `color_at`); `click()` now splits `mousemove`/`click` into two xdotool calls (Phase 8 finding — fixes a real source of click flakiness); wired into `mailbox.py`, `cafe.py`, `stamina.py`, `story_sweep.py`, `shop_common.py`, `shop_tactical.py`, `lesson.py` | Extend with new primitives as future tasks need them |
|
||||
| Python CLI | Built: `ba_daily.py` dispatches `mailbox`/`cafe`/`stamina`/`story_sweep`/`shop_common`/`shop_tactical`/`lesson`/default flow | Extend as new tasks are added |
|
||||
| Reference mapping | Built: `ba_auto/reference_notes/mapping.md` | Fill in reference file/function columns per feature |
|
||||
| Everything else | Not started | Implement reference-first in Python |
|
||||
|
||||
@ -384,6 +385,27 @@ What was found live, captured before writing any code (see the session's live-ca
|
||||
- **Discovered live, not anticipated going in: these shop items have a per-refresh-cycle purchase cap that isn't shown as a visible counter** (unlike the Pyroxene-shop tab's explicit "あと1回購入可能" labels — Common Shop items just look normal until you've already bought them, then go quietly unresponsive). Found by re-running the actual `shop_common` CLI task shortly after the manual purchase above: every configured item's checkbox failed to register as checked, and a direct test of the individual per-item "購入" button and the page's own "全て選択" (select-all) control confirmed those specific items are genuinely non-interactive right now (select-all successfully picked up *other*, not-yet-purchased items further down the list), while credits stayed unchanged throughout. The task correctly reported "nothing selected, cancelling" and spent nothing, rather than misfiring — a real edge case the price-verify + checkbox-confirm design caught safely, not a bug in it. Net effect: a fully "fresh, everything-available" unattended run hasn't been re-verified end-to-end today, since this account's cycle allowance for these specific items was already spent via the same session's manual calibration. Next run after the shop's own refresh timer (~5h, shown in-game as "更新まで") should exercise that path for real.
|
||||
- **Not verified**: non-fully-visible target rows requiring a scroll (both current buy lists happen to be fully visible without scrolling, so `shop_utils` has no scroll/pagination logic yet — would need porting `buy()`'s `last_checked_idx`/swipe-diff tracking if a future buy list needs it); the pre-purchase "insufficient assets" abort path (both currencies were comfortably sufficient this run); the paid manual shop-refresh flow (`更新` button) — deliberately not automated for v1, same reasoning as Daily Free Power (real-currency spend needs explicit human intent, not a default unattended path).
|
||||
|
||||
### Phase 12: Lesson/Schedule
|
||||
|
||||
**Status: Done.** Ported `module/lesson.py`'s control flow (`implement`, the `to_*` navigation state machine, `get_lesson_each_region_status`/`get_lesson_relationship_counts`, `choose_lesson`, `execute_lesson`) to `ba_auto/tasks/lesson.py`.
|
||||
|
||||
Scope, decided with the user before writing any code (see the two `AskUserQuestion` answers): affection-first selection (mirrors `lesson_relationship_first=True` — matches this feature's own "affection farming" framing, not raw reward-tier grinding), sweep every unlocked region in a fixed order until either lesson tickets or scoreable lessons run out (no per-region target list needed from the user, unlike shop's per-item buy list), no lesson-ticket purchasing and no favor-student targeting (both deferred, matching plan.md's original "Suggested first version").
|
||||
|
||||
Key finding before writing any code: **this client's UI is structurally the same two-level layout the reference describes (12 named regions, each with up to 9 individual lesson locations) but rendered completely differently** — a scrollable list of regions instead of the reference's paged single-region swipe view, and a clean "すべてのスケジュール" grid-card modal instead of the reference's raw isometric map + `Parallelogram`/`Triangle` pixel-scanned status grid. Two consequences:
|
||||
|
||||
- **No OCR is needed for region navigation at all.** The reference OCRs the current region name because its paged arrows leave position ambiguous. This client's region list only ever settles at two scroll positions (scrolled to top: regions 0-5; scrolled to bottom: regions 6-11, confirmed live — repeated scroll-down clicks don't keep scrolling past this), so navigation is just "scroll to the right state, click the row at a fixed Y" — deterministic, nothing to locate. The reference's own 12 JP region names (`core/config/default_config.py`'s `lesson_region_name.JP` — embedded directly there, not fetched externally like the shop price table) are kept locally purely for log readability, confirmed to match this account's list exactly, row for row.
|
||||
- **No isometric geometry is needed for per-cell status either.** Each grid-modal card shows up to 3 student portraits with a heart-shaped affection-count badge — reading that number via OCR needs no geometry at all, replacing the reference's isometric pixel-scan outright.
|
||||
|
||||
Three real bugs were found and fixed via a live test run (5 real tickets spent across 3 regions, ticket accounting and cleanup navigation both confirmed correct throughout):
|
||||
|
||||
- **A checkmark does not blank the badge number — it renders alongside it.** The initial design assumed an "already done today" portrait would fail the digit-whitelisted OCR read (no number left to read), the same way a "no relationship yet" portrait does, and treated both as identically "not a candidate." Live testing showed this was wrong: a done portrait keeps its unchanged number and gets a small green checkmark added at top-right instead. Without separately checking for that checkmark, the same already-done cell could be re-picked immediately after being completed. Fixed by detecting the checkmark directly via its own fixed color/offset (`config.LESSON_GRID_CHECKMARK_*`, `lesson._is_slot_already_done`) rather than inferring done-ness from the OCR read.
|
||||
- **The badge's own OCR read was unreliable, and not for the reason first suspected.** Reusing the project's existing generic `detector.read_int` (grayscale + hard threshold, tuned for this UI's normal dark-text-on-light-card look) on the pink/magenta heart badge produced wildly oversized results live — "13" read as "113", "19" as "119", "18" as "418". Root cause, confirmed by rendering the exact same threshold step locally: the heart's own darker outline stroke has a grayscale value that happens to land on the same side of the threshold as the digit glyph, surviving as stray black marks that tesseract sometimes fuses into extra leading digits. Fixed with a dedicated `detector.read_int_on_heart_badge`, which masks on the color relationship "R < G" (true for the navy digit glyph in every sample, false for every pink/magenta badge tone, light or dark) instead of raw brightness — this alone fixed most cases. A few slots still occasionally fuse a stray digit from the character's own portrait art bleeding into the crop's edge (this is character-art-dependent, not fixable by OCR config alone — confirmed reproducible across every psm mode tried). Rather than chase perfect crop geometry per-character, `config.LESSON_GRID_BADGE_MAX_PLAUSIBLE` discards any reading of 100+ as certain contamination (real affection values never reach that range in practice) — a second line of defense, not the primary fix.
|
||||
- **A transient "2x schedule" campaign event (active on this account, ~6 days remaining) doubles how many intermediate screens appear after starting a lesson**, and a bond-rank-up cutscene can appear too (same full-screen "絆ランクアップ!" style already handled in `cafe.py`, confirmed live here for the first time against a real trigger). Rather than special-case either, `lesson._run_one_schedule` presses Enter in a bounded loop, checking two fixed markers each round: the grid modal's own title underline color (visible only when it's frontmost and idle — both the results modal and the cutscene cover it, confirmed live against screenshots of all three states) as the "done" signal, and the results modal's OK button color (shared with the info panel's Start button, confirmed by direct pixel sample) as the one intermediate state worth clicking precisely rather than folding into the blind-Enter fallback.
|
||||
|
||||
Also confirmed live: the "保有チケット N/M" ticket counter is directly visible on the region-list screen (no submenu needed, unlike the reference's `to_purchase_lesson_ticket`/OCR-a-modal approach) — `lesson._read_ticket_count` just OCRs it directly and re-reads it after every schedule to track spend, rather than assuming exactly 1 ticket per schedule (the 2x campaign event doesn't change the ticket cost, only the reward/cutscene count, but re-reading rather than assuming keeps this robust either way).
|
||||
|
||||
**Not verified**: a region with more than 9 currently-unlocked locations (would need scroll support inside the grid modal — not implemented, not yet seen on this account, both regions tested topped out at 7-8); the "no lesson tickets" abort-immediately path (tickets hit exactly 0 mid-sweep during the real test, not at the start); a full 12-region sweep in one run (the test's 5 tickets ran out partway through region 3 of 12); a truly fresh "everything available, nothing done yet" run (this account had already done some lessons manually during calibration before the automated run started). Worth re-running `~/ba_dailies.sh lesson` after tickets next refill to exercise the untested tail of the region list.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### OCR
|
||||
@ -594,7 +616,7 @@ Implemented version:
|
||||
- safe purchase confirmation logic — done, a single overlay-darkness probe covers both the confirm dialog and the reward-acquired banner
|
||||
- no-refresh (the paid manual `更新` refresh button is deliberately not automated, same reasoning as Daily Free Power)
|
||||
|
||||
### 9. Lesson / Schedule
|
||||
### 9. Lesson / Schedule — Done (Phase 12)
|
||||
|
||||
Affection farming via classes.
|
||||
|
||||
@ -604,22 +626,18 @@ Reference:
|
||||
~/repo/baas-reference/module/lesson.py
|
||||
```
|
||||
|
||||
Local target: `ba_auto/tasks/lesson.py`
|
||||
Local target: `ba_auto/tasks/lesson.py` — implemented, live-tested with real tickets. See Phase 12 above for the full writeup.
|
||||
|
||||
Complexity: High
|
||||
- region/area identification — done without OCR: this client's region list only settles at two fixed scroll positions, so navigation is deterministic index-based clicking, not the reference's OCR-a-name-then-page approach
|
||||
- multi-page swipe search — not needed for the same reason
|
||||
- student detection/portrait matching for specific students — deferred, per the original suggested scope below
|
||||
- isometric grid location logic — done without porting the reference's geometry: per-cell status/affection reads via `detector.read_int_on_heart_badge` OCR + a checkmark color probe instead
|
||||
|
||||
Needs:
|
||||
Suggested first version (as originally scoped, and what shipped):
|
||||
|
||||
- OCR for region/area names
|
||||
- multi-page swipe search
|
||||
- student detection/portrait matching if targeting specific students
|
||||
- isometric grid location logic if following reference fully
|
||||
|
||||
Suggested first version:
|
||||
|
||||
- pick a fixed region
|
||||
- select available/highest visible lesson
|
||||
- avoid favorite-student targeting at first
|
||||
- pick a fixed region — expanded to all 12, swept in order
|
||||
- select available/highest visible lesson — affection-first, per explicit user direction
|
||||
- avoid favorite-student targeting at first — still deferred
|
||||
|
||||
## Low priority backlog
|
||||
|
||||
@ -866,7 +884,8 @@ should run the default daily sequence.
|
||||
13. Implement group/club AP. — Not started
|
||||
14. Set up OCR and port it for whichever remaining feature's reference implementation depends on it — not a blanket "only when needed" deferral; see `CLAUDE.md` → "OCR policy"
|
||||
15. Implement Common Shop + Tactical Shop. — Done (Phase 11)
|
||||
16. Attempt Arena/Lesson once OCR is in place, since their reference implementations depend on it.
|
||||
16. Implement Lesson/Schedule. — Done (Phase 12)
|
||||
17. Attempt Arena once OCR is in place, since its reference implementation depends on it.
|
||||
|
||||
## Claude Code guidance summary
|
||||
|
||||
|
||||
2
setup.sh
2
setup.sh
@ -60,5 +60,5 @@ rm -rf "$HOME/ba_auto"
|
||||
cp -r ba_auto "$HOME/ba_auto"
|
||||
|
||||
echo "== Done =="
|
||||
echo "Run with: ~/ba_dailies.sh [mailbox|cafe|stamina|story_sweep|shop_common|shop_tactical]"
|
||||
echo "Run with: ~/ba_dailies.sh [mailbox|cafe|stamina|story_sweep|shop_common|shop_tactical|lesson]"
|
||||
echo "(requires the game already running, window titled 'BlueArchive')"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user