ba-auto-daily/CLAUDE.md
Nik Afiq c5873f4e58 Refactor arena and lesson tasks for improved efficiency and reliability
- Updated arena.py to allow multiple battles per invocation, looping until tickets are exhausted or a rank-1 condition is met. Introduced _fight_one function for single battle logic and added cooldown handling between fights.
- Enhanced lesson.py to implement a tiered priority system for scheduling lessons based on student slots available, replacing the previous highest affection value selection. Introduced functions for scanning all regions and building a priority queue for lesson scheduling.
- Centralized return-to-home logic in ba_daily.py to ensure the game returns to the home screen before and after each task, improving robustness against navigation issues.
- Added retry mechanism for returning to home, allowing for transient navigation issues to be handled gracefully without aborting tasks.
2026-07-13 10:32:53 +09:00

33 KiB
Raw Blame History

CLAUDE.md

This file provides guidance to Claude Code when working with code in this repository.

What this project is

This repository is a personal Blue Archive JP daily-automation project.

It controls the real Blue Archive PC/Steam/Proton client running on a Linux machine named nik-gpu. The development machine is a MacBook named nik-macbookair.

The original goal is a backend port, not a rewrite. The reference project (~/repo/baas-reference/) only works against an Android emulator via ADB/uiautomator2, typically run on a Windows PC. This project's job is to make that same automation logic work against the real JP client running under Proton on Linux instead — porting the reference's decision logic, navigation, and state-detection approach, including its use of OCR, as faithfully as practical, and replacing only the parts that are genuinely Android-specific with local desktop equivalents.

Android-specific parts include:

  • ADB input
  • uiautomator2 taps
  • Android UI-object-tree queries
  • emulator-specific assumptions

Local desktop equivalents include:

  • xdotool
  • scrot
  • OpenCV
  • OCR against screenshots
  • local image/color/template detection

Inventing a different, simpler approach to avoid porting a piece of reference logic is a deviation from the project goal, not a shortcut toward it.

The automation backend is local desktop control:

  • xdotool for mouse/keyboard/window control
  • scrot for screenshots
  • Python/OpenCV for image matching and color/template detection
  • OCR, using Tesseract or PaddleOCR, wherever the reference implementation uses OCR for a feature
  • no Android emulator control
  • no ADB
  • no uiautomator2

OCR policy

OCR is not an Android-specific concern. The reference's OCR-driven region, stage-name, and currency matching runs against a screenshot and can be ported to this backend using scrot captures. Only the reference's input mechanism is Android-specific.

Two features, Stamina/AP mission claim and Normal/Hard story AP sweep, were originally built without OCR by substituting ad hoc pixel probes, fixed coordinates, or randomized selection for the reference's OCR-driven navigation. For story sweep, that substitution produced more real bugs during live testing than porting the reference's actual approach would have:

  • a wrong modal-open probe
  • an unverified button click that silently under-spent AP
  • a modal that does not close on Escape
  • a latent hazard where a mistimed keypress could have started a real battle

See plan.md Phase 9's retrospective for the full writeup.

That issue has since been fixed: story_sweep.py now ports the reference's actual OCR-based region/stage targeting. See plan.md Phase 10. The retrospective stays here as the concrete, lived reason for the OCR policy, not as a description of the current state of that task.

Going forward: when a reference feature's control flow depends on OCR, set up OCR and port that logic. Do not invent a non-OCR workaround just to avoid the setup cost. Only skip OCR for a specific step if the reference itself does not use OCR there.

The reference project is located at:

~/repo/baas-reference/

That repository contains the full Blue Archive Auto Script implementation. It should be treated as the behavioral reference for this project.

Core architecture decision

This project must be Python-first.

Do not implement new Blue Archive automation logic in ba_dailies.sh.

ba_dailies.sh is only a thin runtime/deployment launcher for convenience on nik-gpu.

All feature logic must live in Python.

The intended structure is:

ba-auto-daily/
├── ba_dailies.sh
├── ba_daily.py
├── ba_auto/
│   ├── __init__.py
│   ├── driver.py
│   ├── detector.py
│   ├── navigation.py
│   ├── config.py
│   ├── tasks/
│   │   ├── __init__.py
│   │   ├── mailbox.py
│   │   ├── cafe.py
│   │   ├── stamina.py
│   │   ├── group.py
│   │   └── ...
│   └── reference_notes/
│       └── mapping.md
├── assets/
├── screenshots/
├── scratchpad/
├── setup.sh
├── plan.md
└── CLAUDE.md

scripts/ previously held one-off Bash/Python helpers. It was deleted once mailbox and cafe migrated off it. Do not recreate scripts/ as a place to stash feature logic.

The exact layout can evolve, but the architectural rule should not change:

  • Bash launches Python.
  • Python owns automation logic.
  • The reference repository guides feature behavior.
  • Local driver primitives adapt that behavior to the PC/Steam/Proton setup.

Reference-first rule

Before implementing any new feature, inspect the matching reference implementation in:

~/repo/baas-reference/module/

Do not start by inventing a Bash click sequence.

For every feature, first identify:

  1. Which reference file implements it.
  2. Which class/function contains the main control flow.
  3. What the reference uses for state detection.
  4. What the reference uses for retries/failure handling.
  5. Which parts depend on Android/uiautomator2 and must be replaced.
  6. Which parts can be ported directly as Python control flow.
  7. Which local driver primitives are missing.

Then implement the feature in Python under:

ba_auto/tasks/

Reference repository usage

The reference repository is read-only.

Allowed

  • read reference modules
  • inspect control flow
  • reuse architecture ideas
  • reuse retry/state-machine structure
  • reuse task decomposition ideas
  • reuse constants/config concepts when appropriate
  • write local notes describing how a reference module maps to this project

Not allowed

  • edit files in ~/repo/baas-reference/
  • reimplement a reference feature from scratch in Bash
  • create a local solution that ignores the reference flow when a reference implementation already exists
  • substitute a pixel probe, fixed coordinate, or randomized shortcut for reference logic that uses OCR, just to avoid setting up OCR

Two-machine architecture

Development happens on:

nik-macbookair

Runtime happens on:

nik-gpu

The Blue Archive client and X display live on nik-gpu.

Assume:

DISPLAY=:0
GDM XAUTHORITY under /run/user/1000

The game runs under Steam/Proton on the Linux desktop.

There is no reliable local execution path on macOS. Anything that interacts with the game must be deployed to nik-gpu.

Deployment model

During iteration, files are pushed from nik-macbookair to nik-gpu.

Typical paths on nik-gpu:

~/ba_dailies.sh
~/ba_daily.py
~/ba_auto/
~/ba_assets/
~/.venvs/ba-auto-daily/

The current setup may still contain older paths such as:

~/ba_scripts/detect_and_click.py

When refactoring, prefer consolidating Python code into ba_auto/.

setup.sh should bootstrap or update the runtime layout on nik-gpu.

For a fresh checkout on nik-gpu, run from the repo root:

./setup.sh

After initial setup, individual changes may be pushed with scp or rsync.

Prefer syncing the project from the repository root:

rsync -av \
  --exclude='.git/' \
  --exclude='__pycache__/' \
  --exclude='*.pyc' \
  --exclude='.claude/settings.local.json' \
  --exclude='graphify-out/' \
  --exclude='screenshots/' \
  --exclude='scratchpad/' \
  ./ nik-gpu:~/repo/ba-auto-daily/

Do not sync Claude Code internal temporary folders.

Runtime dependencies on nik-gpu

These are host-level dependencies. Confirm they exist before assuming a bug is in the project code.

Required now:

  • xdotool
  • scrot
  • python3

Required Python packages:

  • opencv-python or opencv-python-headless
  • numpy
  • pytesseract

Expected venv:

~/.venvs/ba-auto-daily/bin/python3

Quick check:

ssh nik-gpu "which xdotool scrot tesseract && ~/.venvs/ba-auto-daily/bin/python3 -c 'import cv2, numpy, pytesseract; print(cv2.__version__)'"

OCR engine dependency is set up as of Phase 10:

  • Tesseract via the tesseract-ocr apt package
  • pytesseract in the venv

Installing tesseract-ocr needs interactive sudo, so setup.sh should check for it but should not assume it can install it automatically.

As of Phase 14's live-debugging (see plan.md's Phase 14 follow-up #5), the tesseract-ocr-jpn language pack is also needed, but only for event_sweep.py's _is_finished_event_page check (Japanese free-text OCR to detect a finished event's own "イベント期間が終了しました" page) -- every other task's OCR only ever needed the base eng data (digit/whitelisted-character reads), so this is a soft dependency specific to that one task, not a project-wide requirement. setup.sh checks for it via tesseract --list-langs and prints the install command (sudo apt-get install -y tesseract-ocr-jpn) if missing, but does not hard-fail setup over it, same interactive-sudo caveat as the base package.

ba_auto/detector.py's read_text() and read_int() wrap OCR for occasional single-crop reads. See story_sweep.py for the first real usage. read_text() takes a lang parameter (default "eng") for this.

Bash policy

ba_dailies.sh should be a thin launcher only.

Preferred shape:

#!/usr/bin/env bash
set -euo pipefail

VENV_PYTHON="${VENV_PYTHON:-$HOME/.venvs/ba-auto-daily/bin/python3}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

exec "$VENV_PYTHON" "$SCRIPT_DIR/ba_daily.py" "$@"

Acceptable Bash responsibilities:

  • choose Python binary
  • set environment variables
  • call Python entry point
  • provide compatibility with old command names
  • fail early if the Python entry point is missing

Not acceptable in Bash:

  • feature state machines
  • long click sequences
  • OpenCV logic
  • OCR logic
  • retry loops for game states
  • feature-specific navigation
  • shop/sweep/battle logic
  • new do_<feature> game automation functions

If an existing Bash function exists, migrate it to Python rather than extending it.

Python entry point

The intended Python CLI entry point is:

ba_daily.py

It should support commands such as:

./ba_dailies.sh
./ba_dailies.sh mailbox
./ba_dailies.sh cafe
./ba_dailies.sh stamina
./ba_dailies.sh story_sweep
./ba_dailies.sh group

No argument should run the default daily flow.

Current default flow in ba_daily.py's DEFAULT_ORDER:

  1. focus game
  2. mailbox
  3. cafe
  4. stamina
  5. future daily tasks

story_sweep spends AP rather than reclaiming something free, so it is deliberately excluded from the default flow. It must be invoked explicitly.

The CLI should dispatch into task modules under:

ba_auto/tasks/

Driver layer

Create and maintain a driver layer in:

ba_auto/driver.py

The driver layer should wrap the PC/Steam/Proton backend.

It should provide reusable primitives such as:

focus_game()
screenshot()
click(x, y)
double_click(x, y)
drag(start, end, duration)
swipe(start, end, duration)
keypress(key)
wait(seconds)
wait_until(...)
color_at(...)
region_average_color(...)
template_match(...)
find_and_click_template(...)

Feature modules should not directly shell out to xdotool or scrot unless a driver primitive is missing and is being added.

Prefer:

driver.click(x, y)

over:

subprocess.run(["xdotool", "click", "1"])

This keeps the project close to the reference architecture while replacing only the backend-control layer.

Detector layer

Image matching, color matching, and OCR wrappers should live in:

ba_auto/detector.py

or in clearly named helper classes/functions.

The cafe sparkle-matching logic formerly in scripts/detect_and_click.py has been migrated into ba_auto/detector.py as find_cafe_sparkle(). It should be called in-process rather than as a per-click subprocess.

The detector should support:

  • screenshot input
  • template matching
  • threshold tuning
  • masked matching
  • click-offset handling
  • OCR crop reads
  • debug image output to scratchpad/

Avoid one Python cold start per click attempt where possible. Prefer long-running Python task logic that can take repeated screenshots and click repeatedly from one process.

Navigation layer

Common navigation should live in:

ba_auto/navigation.py

Use this for shared flows such as:

  • returning home
  • opening main menu
  • opening mailbox
  • opening cafe
  • opening shop
  • opening lesson/schedule
  • closing popups
  • generic back/escape handling
  • waiting for known UI states

Do not duplicate navigation click sequences inside every task if they can be shared.

Task modules

Each feature should have a task module:

ba_auto/tasks/<feature>.py

Examples:

ba_auto/tasks/mailbox.py
ba_auto/tasks/cafe.py
ba_auto/tasks/stamina.py
ba_auto/tasks/story_sweep.py
ba_auto/tasks/group.py

Each task module should expose a clear function such as:

run(driver, config)

or:

run_mailbox(driver, config)

Keep task files feature-focused.

Reference mapping notes

Maintain a mapping file at:

ba_auto/reference_notes/mapping.md

Before or during implementation of a feature, update the mapping.

Use this format:

| Local feature | Reference file | Reference functions/classes | Local file | Backend replacements | Status |
|---|---|---|---|---|---|
| Cafe | `module/cafe.py` or relevant file | `...` | `ba_auto/tasks/cafe.py` | uiautomator2 tap -> xdotool click, screenshot -> scrot/OpenCV | In progress |

Working conventions

Always work from the repository root:

cd ~/repo/ba-auto-daily

Use scratchpad/ in the project root for all temporary, generated, diagnostic, calibration, and investigation files.

Examples:

  • cropped calibration images
  • debug screenshots
  • annotated match results
  • temporary Python probes
  • temporary investigation notes
  • one-off image-analysis scripts
  • temporary OCR experiments
  • screenshots copied back from nik-gpu for analysis

Create it if missing:

mkdir -p scratchpad

Do not use Claude Code's internal temporary directories such as:

/private/tmp/claude-*
/tmp/claude-*

Do not run probes from /private/tmp/claude-* or write temporary scripts there. These paths are outside the repository and often trigger extra permission prompts, interrupting the development flow.

When a temporary Python probe is needed, write it into scratchpad/ first, then run it from the repository root.

Preferred pattern:

cat > scratchpad/probe_image.py <<'PY'
from PIL import Image

img = Image.open("scratchpad/shop_probe_04_checked.png")
print(img.size)

points = [
    ("checkbox_checked_center", (972, 297)),
    ("checkbox_checked_bg", (975, 300)),
    ("card_border_left", (940, 380)),
    ("card_border_unselected", (1165, 380)),
    ("buy_button", (1751, 1112)),
    ("cancel_button", (1525, 1112)),
]

for name, xy in points:
    print(name, xy, img.getpixel(xy))
PY

python3 scratchpad/probe_image.py

Avoid this pattern:

cd /private/tmp/claude-*/scratchpad && python3 -c "..."

Also avoid large multiline python3 -c "..." commands when they contain comments or complex quoting. Prefer a temporary script under scratchpad/ because it is easier to inspect, rerun, and keep within workspace permissions.

screenshots/ contains human reference captures. They are useful for calibration and documentation, but they are not necessarily automated test fixtures.

assets/ contains local template images. These should be captured from the local game setup where possible.

Temporary probe policy

Temporary probes are allowed, but they must be workspace-local.

For non-trivial investigation, use:

scratchpad/<short_descriptive_name>.py

Examples:

scratchpad/probe_shop_checkbox.py
scratchpad/probe_cafe_income.py
scratchpad/probe_story_modal.py
scratchpad/probe_ocr_region.py

A temporary probe should:

  • run from the repository root
  • read inputs from scratchpad/, screenshots/, or assets/
  • write outputs to scratchpad/
  • avoid /tmp, /private/tmp, and Claude Code internal paths
  • avoid relying on absolute /private/tmp/claude-* paths
  • be deletable once the investigation is complete

Prefer:

python3 scratchpad/probe_shop_checkbox.py

Do not prefer:

python3 -c "large multiline script..."

For very small one-liners, python3 -c is acceptable only when it does not require changing directories into /private/tmp/claude-*.

Testing and checks

Because the game only runs on nik-gpu, local macOS testing is limited.

Before deploying, run static/syntax checks locally:

bash -n ba_dailies.sh
python3 -m py_compile ba_daily.py
python3 -m py_compile ba_auto/*.py
python3 -m py_compile ba_auto/tasks/*.py

For temporary investigation scripts, use:

python3 scratchpad/<script_name>.py

Do not run temporary investigation scripts from Claude Code's internal /private/tmp/claude-* workspace.

On nik-gpu, run real integration tests against the live game.

Example:

ssh nik-gpu "~/repo/ba-auto-daily/ba_dailies.sh cafe"

When debugging image matching, write debug images to:

scratchpad/

When debugging on nik-gpu, copy relevant screenshots or debug images back into the local project scratchpad/ if they need local analysis.

Existing features

Current project state: mailbox, cafe, stamina, story_sweep, event_sweep, shop_common, shop_tactical, lesson, arena, and bounty 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, event_sweep, shop_common, shop_tactical, lesson, arena, bounty, and default flow to ba_auto/tasks/
  • default flow is mailbox, cafe, stamina
  • story_sweep, 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
  • 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
  • mailbox and cafe were ported from the reference patterns around module/mail.py and module/cafe_reward.py
  • 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, 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, 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
  • 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
  • a later regression broke LESSON_TICKET_OCR_RECT entirely (a stray katakana fragment at the crop's left edge made tesseract drop the leading digit, "7/7" reading as "/7", aborting every run) — fixed by tightening the rect; re-validated live with 7 real tickets spent and correct re-reads after every schedule. See plan.md's "Phase 12 follow-up"
  • a second regression then surfaced: clicking the schedule icon doesn't always land on the Location Select list — the game can resume directly on whichever region's per-region isometric map was last open (a previous run's Ctrl-C interruption left it stuck there), which broke navigation for every region identically since the list's scroll/row-click logic doesn't apply to that screen. Fixed via lesson._ensure_location_select_list, which detects the per-region map's own "all schedules" button already showing and returns via the back button before sweeping. Validated by deliberately reproducing the stuck state and confirming recovery with a real ticket spend. See plan.md's "Phase 12 follow-up #2"
  • ba_auto/tasks/arena.py fights ranked Tactical Challenge (Arena) battles in a loop until the OCR'd ticket count reaches 0 (per explicit user direction, 2026-07-12 — this reverses the original one-battle-per-invocation design, which matched the reference's own per-call pacing via its background scheduler; see plan.md's Phase 13 follow-up #2), waiting config.ARENA_POST_BATTLE_COOLDOWN (30s) between fights for the real in-game lockout, then collects both reward slots once at the end
  • Tactical Challenge is reached via a card inside the お仕事 (Work) hub, not a bottom-nav icon; the reference's separate opponent-info and formation-edit screens are merged into one modal here with a live ticket-count preview confirming the real fight-commit click
  • arena was live-tested for real across all 5 of the account's daily tickets (2 WIN, 1 LOSE, 2 spent debugging), which surfaced three real bugs: a level-OCR crop too small for tesseract despite looking legible to the eye, level text being bright-on-dark unlike every other OCR read in this project (fixed via a new detector.read_int_white_on_dark), and — most importantly — the post-fight WIN/LOSE result modal proving undetectable by precisely locating its own confirm button (WIN and LOSE are different heights; widening the search region to cover both then caught stray cyan-ish pixels in the opponent list's own portrait art, false-positive-clicking into an unrelated opponent's info modal). Fixed by abandoning per-button color detection for a bounded blind-Enter-press loop (matching lesson.py's own _run_one_schedule pattern), gated by a hard safety check against the one modal where Enter is genuinely dangerous — the opponent-info modal's own attack-formation button is also Enter-bound and spends a real ticket. See plan.md Phase 13 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, and find_template()/template_visible(), a generalized named-template matcher built for arena but ultimately unused there — state detection stayed OCR/color-probe-driven throughout, like every other task
  • scripts/ba_dailies_legacy.sh and scripts/detect_and_click.py have been deleted
  • the scripts/ directory itself no longer exists
  • ba_auto/driver.py primitives are wired into all migrated task modules
  • existing driver primitives include run_command, focus_game, click, move_mouse, scroll, keypress, screenshot, wait, and color_at
  • ba_auto/navigation.py has shared state probes used across tasks:
    • is_on_subscreen
    • is_modal_open
    • wait_for_state()

wait_for_state() is a scoped port of the reference's core/picture.py::co_detect: watch for any of several named states, react to known non-terminal ones, and stop on a recognized terminal one.

story_sweep.py additionally has its own local modal probes and close logic because the stage-info modal is wide enough to break is_modal_open's default probe point, and it does not close on Escape. See plan.md Phase 9.

The story sweep modal renders at least two different internal layouts:

  • a plain one for the bonus -A stage
  • a taller tabbed one for regular numbered stages

Their button coordinates differ. This was discovered live in Phase 10.

Live testing found both the mailbox-icon and cafe-icon fixed coordinates were flaky. They missed the first click and worked on retry. Neither task originally verified state before proceeding, so a missed click cascaded into blind actions and could reach an unverified Escape press on the home screen, which triggers Blue Archive's own "exit the game?" confirmation.

See plan.md Phases 56 for the full writeup. This is the concrete reason every task now verifies state before acting rather than trusting fixed coordinates or a single click blindly.

is_on_subscreen's header-brightness probe alone cannot tell "the true home screen" apart from "a subscreen with a modal open on top" — a modal's own screen-wide dimming overlay darkens the header probe point the same way home's own background art can, confirmed live via direct pixel comparison (2026-07-12). navigation.return_to_home now checks a combined _not_home helper (is_on_subscreen(driver) or is_modal_open(driver)) instead of is_on_subscreen alone, so it can no longer mistake a stuck modal for having reached home. See plan.md's "Return-to-home audit" for the live incident this was found from (a task's fixed home-relative click coordinates landed on the wrong screen entirely because of this exact false reading) and the fix.

Rank-up popups mid-pat-loop are now handled.

A pat that crosses an affection-rank threshold shows a full-screen 絆ランクアップ! cutscene with no cafe header. find_cafe_sparkle() can never recognize this because it is nothing like the sparkle template. The loop used to spin uselessly against it for the rest of the room's click budget.

cafe.py's _dismiss_rank_up_if_shown() reuses the existing navigation.is_on_subscreen header-brightness probe. This was confirmed against screenshots/cafe/student/01-02:

  • rank-up cutscene: header probe reads r < 200
  • normal cafe screen: header probe reads r > 200

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

  • whether camera zoom/pan can drift over a long unattended run
  • the reference project zooms out before detecting
  • this project currently does not
  • testing did not reproduce a failure from skipping it
  • see plan.md Phase 6 "Not verified" list

Known cafe gaps to verify during migration

When migrating or modifying cafe logic, verify these manually against the actual current behavior:

  • whether student rotation popups are handled
  • whether rank-up popups are handled
  • whether the view is zoomed/centered before sparkle detection
  • whether detection still works when text overlaps the student
  • whether one Python process can handle repeated sparkle detection faster than repeated cold starts
  • whether both cafe rooms are handled consistently
  • whether cafe income claim is robust against popup timing

Do not assume the old Bash implementation handles these correctly.

Migration goal

The migration goal is:

  • preserve existing behavior
  • move feature logic from Bash into Python
  • keep ba_dailies.sh as launcher only
  • make detection logic reusable
  • prepare for future features by copying the reference project's structure where appropriate

Anti-patterns

Do not:

  • add new do_<feature> Bash functions
  • build a giant Bash automation script
  • recreate reference logic as fixed-coordinate Bash click chains
  • skip reading the reference module before implementing a feature
  • put OCR in Bash
  • put OpenCV state machines in Bash
  • make every feature shell out independently to xdotool
  • duplicate common navigation in every task
  • copy reference image assets blindly
  • edit ~/repo/baas-reference/
  • implement event-specific features before the generic reusable machinery exists
  • invent a pixel probe, fixed-coordinate, or randomized substitute for reference logic that uses OCR, just to avoid setting up OCR
  • treat OCR as an Android-specific concern that can be deferred indefinitely
  • run temporary probes from /private/tmp/claude-* instead of the repository scratchpad/
  • create investigation scripts outside the repository when they can live in scratchpad/
  • use multiline python3 -c "..." commands for non-trivial probes
  • rely on Claude Code internal temporary paths in committed code, notes, commands, or debugging workflow
  • write generated debug files into the repository root when they belong in scratchpad/

Feature implementation workflow

For every new feature:

  1. Read the relevant ~/repo/baas-reference/module/... file.
  2. Summarize the upstream feature flow in notes or comments.
  3. Add or update the reference mapping table.
  4. Identify missing local driver primitives.
  5. Implement or improve those primitives in ba_auto/driver.py or ba_auto/detector.py.
  6. Implement the feature in ba_auto/tasks/<feature>.py.
  7. Add CLI dispatch in ba_daily.py.
  8. Keep ba_dailies.sh unchanged unless launcher behavior changes.
  9. Run syntax checks.
  10. Deploy to nik-gpu.
  11. Test against the live game.
  12. Update plan.md status.
  13. Move temporary investigation files into scratchpad/, or delete them if they are no longer useful.

Priority when uncertain

When uncertain, prefer this order:

  1. Preserve existing working behavior.
  2. Follow the reference project's control flow, including its use of OCR.
  3. Do not deprioritize OCR-based navigation/matching in favor of a simpler non-OCR substitute.
  4. Keep logic in Python.
  5. Add reusable driver primitives instead of feature-specific hacks.
  6. Use local screenshots/assets only when backend differences require it.
  7. Use scratchpad/ for temporary work and diagnostics.
  8. Avoid large rewrites that do not move the project closer to reference-driven Python architecture.

User preference

The user wants this project to be as close to the original Blue Archive Auto Script architecture as practical, without recreating logic that already exists.

The user specifically does not want Claude Code to keep converting feature work into Bash.

The user also wants Claude Code to avoid using /private/tmp/claude-* for development probes when a repository-local scratchpad/ directory can be used instead, because those paths trigger permission prompts and interrupt the development flow.

Respect those preferences.