---
name: automate-aov-reroll
description: Automate and diagnose the demonstrated Arena of Valor (Garena 傳說對決) new-account tutorial and first-draw workflow on macOS BlueStacks Air. Use when Codex needs to discover BlueStacks instances and ADB ports, capture or classify game screens, calibrate click targets, wait through the unattended tutorial battle, save draw results, or run the demonstrated reroll sequence in safe single or adaptive dual mode.
---

# Automate Arena of Valor rerolls

Use ADB for the game canvas and Computer Use only for the BlueStacks Air multi-instance manager. Do not rely on BlueStacks accessibility elements inside the game: it exposes the entire Android canvas as one window.

## Workflow

1. Read `references/recording-notes.md` before changing the demonstrated sequence.
2. For the normal unattended path, run `scripts/aov_runner.py fresh-run --xapk <path> --output-root <dir>`. It checks free space and requires all existing player engines to be stopped, creates a brand-new Tiramisu64 engine through the local BlueStacks manager, sets the recorded resolution, starts it, installs the split APKs, runs the complete game workflow, writes an engine-linked manifest and screenshots, and closes (but does not delete) that engine on success or failure.
3. Run `scripts/aov_runner.py status` to discover instances and their ADB ports from the BlueStacks configuration.
4. Run `scripts/aov_runner.py screenshot --instance <name> --output <path>` to verify native `2560x1440` capture before sending input.
5. For calibration, run `scripts/aov_runner.py observe --instance <name> --output-dir <dir>`. Perform one workflow manually; the observer saves materially changed frames only.
6. Prefer `ocr_text` rules for stable button and page labels. Use OpenCV templates for icons, graphical state, toggle state, or fast fallback. For scrollable lists, tap the OCR/matched-label center and use a bounded bidirectional swipe-on-miss sequence; never assume a fixed row index.
7. The runner compiles `scripts/vision_ocr.swift` into a temporary local helper using Apple's Vision framework. It is offline, requires no API key, and consumes no Codex tokens. Validate recognition with `scripts/aov_runner.py run --instance <name> --workflow references/workflow.json --dry-run`; the log reports whether OCR or a template matched.
8. Run the confirmed workflow on an existing engine with `scripts/aov_runner.py run --instance <name> --launch --output-dir <dir>`. The runner reacts every 0.35 seconds during UI steps, presses `準備` within its 30-second window, sends no hero-selection input, and sends no battle input.
9. A fresh run stores results under `<timestamp>_<engine>/`; its three checkpoint filenames are prefixed with the exact engine name and `run.json` records the engine, ADB port, status, error (if any), screenshot list, and close result. A standalone `fresh-run` closes but never deletes its engine.

## Continuous batch mode

Use `scripts/aov_batch.py` as the failure-isolated outer supervisor. Its default remains one `fresh-run` child with 4 vCPU and 4096 MB. Adaptive dual mode permits at most two explicitly owned children, normally at 3 vCPU and 3072 MB each. It records a permanent round number and exact configuration snapshot before each child, monitors worker heartbeat/output independently, terminates a silent or over-time child without stopping its sibling, analyzes every checkpoint reached, and then either preserves a jackpot candidate or deletes that exact batch-created engine through BlueStacks Manager.

All create/configure/start and Manager-delete operations share one machine-wide `fcntl` lifecycle lock. Gameplay, ADB screenshots, and OCR run concurrently on each engine's distinct ADB port. The second child starts only after the first child has completed creation, configuration, boot, and ADB readiness. No new engine is created while any deletion is pending or retrying. After a successful deletion, keep creation blocked for the configured post-delete settling interval (10 seconds by default) so BlueStacks can release the removed VM's host resources.

BlueStacks Manager can expose an extra same-sized pencil button near the row name and a collapse chevron after the trash control. Deletion therefore retains the exact engine/display-name and single-filtered-row gates, excludes the left-side pencil, and inspects only the exact row's right-side management cluster. In the observed cluster, trash is rightmost when three controls are exposed and second-rightmost when the fourth collapse chevron is exposed. It must still receive BlueStacks' own exact delete-confirmation window before confirming. Never count every compact button across the entire row or use an unbounded positional click.

The Manager readiness probe must accept both the bare Manager executable and the same executable followed by arguments such as `--hidden`. Requiring exact equality with the bare executable causes every headless launch to be misclassified as absent and leaves completed rounds trapped in deletion retry. Creation only needs the hidden Manager process, while deletion must explicitly reveal the app and wait for the accessibility-visible `BlueStacks Air Manager` window before attempting its exact filtered-row UI sequence. If a deletion immediately following another deletion leaves the Manager's search list stale with zero exact rows, restart only the Manager UI once under the lifecycle lock, reapply the exact-name filter, and retain every ownership/unique-row/delete-confirmation safety gate.

BlueStacks 5.21 can also leave a valid stopped engine in `bluestacks.conf` and on disk while omitting that same engine from `Engine/UserData/MimMetaData.json`; the Manager then shows only unrelated protected rows even though its log initializes the missing engine. Before deletion, after exact config/display-name/stopped-instance validation, detect this mismatch, stop only the Manager process, atomically append one metadata row for that exact verified engine, then reopen Manager and continue through the normal exact-filtered UI deletion. Never click an unfiltered sole row and never directly remove the engine directory as a substitute for BlueStacks' own deletion.

Fresh reroll engines disable BlueStacks host-side game controls because all input is sent through ADB. After XAPK installation, also mark the per-app `isGameControlsToogleHintShown` cache flag as complete. As a fallback, a native macOS helper captures only the exact engine-titled BlueStacks window and clicks `繼續` only when Vision OCR simultaneously recognizes `使用鍵盤和滑鼠進行遊戲` and exactly one `繼續` button. This host overlay never appears in an ADB screenshot, so do not try to handle it in `workflow.json`.

A newly booted engine can occasionally report `SGameRealActivity` as focused while its actual ADB framebuffer remains on the portrait BlueStacks Store; the same stale guest may then time out on the next ADB input command. Treat this as an unusable player session rather than a workflow-screen mismatch. Save a launch-recovery screenshot, close and restart only that round's exact owned engine under the lifecycle lock, wait for it to settle and boot again, and retry the already-installed game once. Never restart a peer engine or create a replacement account for this recovery.

With `--parallel 2 --adaptive-parallel`, second-slot admission normally requires at least 26 GiB free, at least 25% memory free, load average below 6, and no macOS thermal/performance warning. The local dual launcher adds `--ignore-load-gate`, so load remains visible in the dashboard but does not by itself block slot two; disk, memory, temperature, ownership, lifecycle, and deletion gates still apply. Critical memory or thermal pressure does not kill active accounts. Consecutive fresh-engine launch/orientation failures use a bounded second-slot cooldown so the batch keeps one healthy worker running without rapidly creating and deleting engines. Parallel values above two are rejected.

Jackpot checks are OR conditions implemented by `scripts/jackpot_detector.py`:

- Screenshot 01: OCR the central reward row. `次元徽章`, `賞金牌`, and `LSR雙倍卡包` are the ordinary result even when all three icons are download placeholders. Additional item names are a keep condition. If OCR is incomplete, use tolerant crop comparison against both loaded and downloading ordinary baselines.
- Screenshot 03: compare the upper-left item under `稀有獎勵包含` against both its ordinary portrait and download-placeholder baselines. Normalize any 16:9 screenshot to the native canvas, correct small brightness differences, and search ±3 pixels before comparing; never require exact pixel equality.

The supervisor persists all owners in `active_rounds.json` before starting their workers. It migrates the old single `active_round.json` format on startup. After a supervisor or machine interruption, the next launch resolves every owned round independently before creating another engine. Screenshots, `decision.json`, per-round logs, and the HTML dashboard remain under the output tree even after the emulator engine is deleted.

The dashboard's compact result map covers the entire recorded history rather than only a recent window. It sorts cells by permanent round number from left to right and then top to bottom, uses green check/red cross/gold star cells for ordinary completion/failure/kept candidates, and lets each cell jump to that round's expandable detail card.

## Confirmed sequence

1. Cancel the optional Google Play Games profile prompt.
2. Expand login providers, choose `Guest`, and tap `開始遊戲`.
3. After `開始遊戲`, enter a dedicated short-timeout MOBA-choice state. Recognize the page-specific `我很熟悉` label (with `你了解MOBA遊戲嗎？` only as a same-page fallback), then tap the fixed lower-right `進入遊戲` target; never use the left `進入引導` branch. Do not rely on a small button crop alone. On the following `進階設置` page, keep the default controls and retry its centered lower `進入遊戲` button until `去戰鬥` proves that the page actually exited; never advance state merely because one tap was sent. If `請選擇一個英雄為你出戰` appears, fail the round immediately instead of waiting through the long battle timeout.
4. Tap `去戰鬥`, then `開始配隊`; accept either the fully downloaded `去戰鬥` screen or its resource-loading/broken-art variant. Recognize the separate ready screen and tap `準備` immediately.
5. Do not select a hero. Wait about 50 seconds for automatic selection, then leave the tutorial battle unattended until either `VICTORY` or `DEFEAT`; both are equivalent terminal outcomes for this workflow. Enter the dedicated outcome-exit state before sending input: while the recognized outcome screen remains, retry the same lower-center tap at ten-second intervals, with a six-input limit. Do not advance to result handling merely because one tap was sent.
6. Advance the variable number of result screens while the recognized `請點擊繼續` label remains, with a bounded six-input limit. On the scoreboard, tap the centered lower `返回大廳`; no separate MVP action is required. After this fresh-account battle, the next state is exclusively the forced `新手福利中心` guide: never treat the lobby's always-visible `活動中心` label as evidence that this guide has already completed. If the lobby guide highlights the `新手福利中心` gift box with a yellow cursor, tap that gift box before continuing the forced reward guide. Hero reward art may still be missing; when the stable `點擊螢幕任意地方繼續` instruction appears, tap the screen even if the rest is blank. Continue the guided detour and return arrows. After the final benefit-page back action, transition to a lobby-only wait state so a lingering `新手福利中心` lobby label can never trigger a second upper-left tap; if a prior run already reached `玩家資料`, recognize that page explicitly and back out once.
7. Open the activity center. Search the scrollable left menu for the actual `7/25至尊次元突破` label, tap the matched label position, use `一鍵領取`, and save the first checkpoint.
8. Prime the 次元突破 poster download. Its artwork can be completely black while resources are missing; after the recognized lobby entry finishes transitioning, tap the exact center of the poster page without waiting for its art or `立即前往`. This first tap only starts the download, so wait two seconds, press upper-left return, continue to the backpack while it downloads, open all three LSR packs, and save the second checkpoint. The opened packs may show `共獲得1個獎勵` or `共獲得2個獎勵`; match the stable `共獲得` prefix instead of assuming the item-type count.
9. Return to 次元突破 and use the same center tap on the poster page. Accept either an `活動教學` overlay or a direct draw-page transition, enable automatic conversion, draw once for free, draw ten three times, accept the automatic extra ten-draw, press confirm, and save the third checkpoint. At any point in these draws, the fixed `這是送給您的禮物` popup may interrupt the sequence while its central prize artwork and name vary. Save a separate engine-and-time-linked event screenshot before tapping its right-side `確定`, then resume the same unfinished draw stage. If the popup races with a paid ten-draw tap, require both the returned `X10抽10次` page and its top token count (`30`, `20`, or `10`) to determine whether that draw committed; retry the same draw when the count did not decrease. These event screenshots are audit evidence only and never change the final jackpot decision.

For a genuinely fresh guest identity, create a brand-new BlueStacks engine and install the user's XAPK with `adb install-multiple`. Do not clone an instance after a guest account has been created; cloning copies application data.

Normal one-command run:

```bash
work/aov-venv/bin/python ~/.codex/skills/automate-aov-reroll/scripts/aov_runner.py fresh-run \
  --xapk '/path/to/Garena.xapk' \
  --output-root '/path/to/aov-reroll-runs'
```

To recover a stopped engine without replaying completed work, combine `--adopt-instance`, `--start-stage`, and `--skip-install`. Recovery stages must recognize both the interrupted screen and any later screen the game may advance to while closing; `benefit_exit_after_signin` therefore accepts a broken hero-reward screen, either benefit header, and both known lobby activity-center appearances. This preserves existing guest data and still closes without deleting the engine when the run ends.

## Runtime requirements

- macOS BlueStacks Air and its multi-instance manager. `fresh-run` starts its own new engine; existing player engines must be stopped.
- `adb` on `PATH`; prefer `/opt/homebrew/bin/adb` when available.
- Xcode Command Line Tools (`xcrun swiftc`) for native macOS Vision OCR.
- Python 3 with `numpy` and `opencv-python-headless`.

Create an isolated environment when dependencies are missing:

```bash
python3 -m venv work/aov-venv
work/aov-venv/bin/python -m pip install numpy opencv-python-headless
```

## Guardrails

- Gate every game-canvas tap on a recognized state: OCR for stable text, or a visual template for graphical/toggle state. Never replay the raw recording's unlabeled clicks.
- Use matched-label centers for items whose list position can move. Use normalized coordinates only for fixed-layout controls after the expected state has matched.
- Treat loading, battle, and unknown states as wait-only.
- A newly launched game may expose a blank `1440x2560` portrait framebuffer before rotating. Wait up to the workflow's orientation timeout for `2560x1440`; do not fail on the first portrait frame.
- During a fresh-engine launch, clear any visible BlueStacks Store surface with Android Home before bringing the game forward, lock the framebuffer to landscape, tolerate a delayed `am start -W`, and require three consecutive `2560x1440` captures before accepting startup.
- Treat hero selection as wait-only; the game auto-selects after roughly 50 seconds.
- Save a diagnostic screenshot and stop on global timeout, battle timeout, repeated-state overflow, or prolonged unknown state.
- After startup rotation settles, keep the BlueStacks framebuffer fixed at `2560x1440` landscape while using the provided coordinates.
- Before `fresh-run`, require at least 20 GiB free by default. Standalone runs preserve every engine and reject every other running player engine. Parallel batch workers allow only the exact peer engines named by their single supervisor. Batch mode may delete only an engine proven absent from that round's pre-run snapshot and owned by that permanent round number; require exact engine/display-name validation, a unique Manager search result, and the Manager's own confirmation dialog. If deletion fails, retry without creating another engine.
- Never embed account identifiers, email addresses, Android IDs, or recorded private data in templates, logs, or the skill.
