diff --git a/README.md b/README.md index 1c2701a..91d8c1d 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,23 @@ 流程为:首页 → 小金猪 → 判断右侧领取按钮。金色时长按领取,确认“恭喜获得”弹框和领取按钮变灰后关闭;灰色时不领取,直接关闭弹框。两种情况都会返回首页。使用 `--dry-run` 可只验证首页而不点击。 +### 已验证的小金猪快速扫荡脚本 + +```bash +.venv/bin/python claim_sweep.py +.venv/bin/python claim_sweep.py --times 5 +``` + +默认扫荡 1 次;`--times 5` 会扫荡 5 次,每次消耗 10 体力。每次扫荡后脚本确认奖励弹框、关闭弹框、等待 2 秒,再进行下一次;全部完成后返回首页。使用 `--dry-run` 可只验证首页和执行计划,不消耗体力。 + +### 已验证的任务奖励脚本 + +```bash +.venv/bin/python claim_tasks.py +``` + +仅在首页右侧任务按钮有红点时进入。脚本先处理当前日常或周常面板中全部可见的“领取”按钮;当前面板领完后,只在另一个标签自身有红点时才切换处理。面板没有领取按钮或任务入口没有红点时,不执行奖励点击并返回首页。使用 `--dry-run` 可只验证首页和执行计划。 + ### 已验证的特惠一键领取脚本 在首页执行: diff --git a/assets/sweep_available.png b/assets/sweep_available.png new file mode 100644 index 0000000..5b11963 Binary files /dev/null and b/assets/sweep_available.png differ diff --git a/assets/sweep_dismiss.png b/assets/sweep_dismiss.png new file mode 100644 index 0000000..6223e21 Binary files /dev/null and b/assets/sweep_dismiss.png differ diff --git a/assets/sweep_reward.png b/assets/sweep_reward.png new file mode 100644 index 0000000..304b091 Binary files /dev/null and b/assets/sweep_reward.png differ diff --git a/assets/tasks_claim.png b/assets/tasks_claim.png new file mode 100644 index 0000000..5d8cb0c Binary files /dev/null and b/assets/tasks_claim.png differ diff --git a/assets/tasks_daily_title.png b/assets/tasks_daily_title.png new file mode 100644 index 0000000..21d2221 Binary files /dev/null and b/assets/tasks_daily_title.png differ diff --git a/assets/tasks_dismiss.png b/assets/tasks_dismiss.png new file mode 100644 index 0000000..0e41de6 Binary files /dev/null and b/assets/tasks_dismiss.png differ diff --git a/assets/tasks_home_dot.png b/assets/tasks_home_dot.png new file mode 100644 index 0000000..a0ece8c Binary files /dev/null and b/assets/tasks_home_dot.png differ diff --git a/assets/tasks_reward.png b/assets/tasks_reward.png new file mode 100644 index 0000000..318dad9 Binary files /dev/null and b/assets/tasks_reward.png differ diff --git a/assets/tasks_tab_dot.png b/assets/tasks_tab_dot.png new file mode 100644 index 0000000..7485347 Binary files /dev/null and b/assets/tasks_tab_dot.png differ diff --git a/assets/tasks_weekly_title.png b/assets/tasks_weekly_title.png new file mode 100644 index 0000000..914fd1a Binary files /dev/null and b/assets/tasks_weekly_title.png differ diff --git a/automation/engine.py b/automation/engine.py index 6e952ee..1d423de 100644 --- a/automation/engine.py +++ b/automation/engine.py @@ -51,6 +51,9 @@ class Matcher: return np.asarray(crop.convert('RGB').resize((562, 1002), Image.Resampling.LANCZOS)) def score(self, frame, spec): + return self.locate(frame, spec)[0] + + def locate(self, frame, spec): name = spec['template'] if name not in self.cache: p = self.assets / name @@ -70,8 +73,10 @@ class Matcher: # for the active home page. Also require similar absolute colors. difference = float(np.abs(candidate.astype(float) - template.astype(float)).mean()) if difference > spec.get('max_color_difference', 28): - return 0.0 - return float(score) if math.isfinite(score) else 0.0 + return 0.0, None + if not math.isfinite(score): + return 0.0, None + return float(score), (x1 + x, y1 + y, w, h) def matches(self, frame, specs): return all(self.score(frame, s) >= s.get('threshold', 0.86) for s in specs) diff --git a/automation/sweep.py b/automation/sweep.py new file mode 100644 index 0000000..e635295 --- /dev/null +++ b/automation/sweep.py @@ -0,0 +1,61 @@ +"""首页小金猪快速扫荡流程。""" +import time + +from .engine import WorkflowError + + +class SweepRunner: + def __init__(self, desktop, matcher, flow, region_provider, log=print, timeout=12): + self.desktop = desktop + self.matcher = matcher + self.flow = flow + self.region_provider = region_provider + self.log = log + self.timeout = timeout + + def wait(self, *names): + deadline = time.monotonic() + self.timeout + previous, stable = None, 0 + while time.monotonic() < deadline: + screenshot = self.desktop.screenshot() + region = self.region_provider(screenshot) + frame = self.matcher.frame(screenshot, region) + found = next((name for name in names + if self.matcher.matches(frame, self.flow['states'][name])), None) + stable = stable + 1 if found and found == previous else 1 if found else 0 + previous = found + if stable >= 2: + return found, screenshot, region + time.sleep(0.25) + raise WorkflowError('未识别到预期页面 ' + '/'.join(names) + ',已停止。') + + def click(self, action, observation, hold=0.2): + _, screenshot, region = observation + if self.region_provider(screenshot) != region: + raise WorkflowError('识别后游戏窗口移动,已停止,请重新运行。') + self.desktop.click(self.flow['clicks'][action], region, screenshot.size, hold=hold) + self.log('已执行:' + action) + + def run(self, times=1, dry_run=False): + if not 1 <= times <= 20: + raise WorkflowError('扫荡次数必须在 1 到 20 之间。') + home = self.wait('home') + if dry_run: + self.log('首页识别通过。计划:小金猪 → 快速扫荡 %s 次 → 每次关闭奖励 → 返回首页。未点击。' % times) + return 'preview' + + self.click('open_pig', home) + for current in range(1, times + 1): + idle = self.wait('sweep_available') + self.click('sweep', idle, hold=0.8) + reward = self.wait('reward') + self.log('第 %s/%s 次扫荡已确认奖励弹框。' % (current, times)) + self.click('dismiss_reward', reward) + if current < times: + self.log('等待 %s 秒后进行下一次扫荡。' % self.flow['interval_seconds']) + time.sleep(self.flow['interval_seconds']) + + self.click('close', self.wait('sweep_available')) + self.wait('home') + self.log('快速扫荡完成,已确认返回首页。') + return 'completed' diff --git a/automation/tasks.py b/automation/tasks.py new file mode 100644 index 0000000..3f02173 --- /dev/null +++ b/automation/tasks.py @@ -0,0 +1,102 @@ +"""首页任务奖励领取流程。""" +import time + +from .engine import WorkflowError + + +class TaskRunner: + def __init__(self, desktop, matcher, flow, region_provider, log=print, timeout=12): + self.desktop = desktop + self.matcher = matcher + self.flow = flow + self.region_provider = region_provider + self.log = log + self.timeout = timeout + + def observe(self): + screenshot = self.desktop.screenshot() + region = self.region_provider(screenshot) + return screenshot, region, self.matcher.frame(screenshot, region) + + def wait(self, *names): + deadline = time.monotonic() + self.timeout + previous, stable = None, 0 + while time.monotonic() < deadline: + screenshot, region, frame = self.observe() + found = next((name for name in names + if self.matcher.matches(frame, self.flow['states'][name])), None) + stable = stable + 1 if found and found == previous else 1 if found else 0 + previous = found + if stable >= 2: + return found, screenshot, region, frame + time.sleep(0.25) + raise WorkflowError('未识别到预期页面 ' + '/'.join(names) + ',已停止。') + + def click(self, point, screenshot, region, hold=0.2): + if self.region_provider(screenshot) != region: + raise WorkflowError('识别后游戏窗口移动,已停止,请重新运行。') + self.desktop.click(point, region, screenshot.size, hold=hold) + + def click_action(self, action, observation): + _, screenshot, region, _ = observation + self.click(self.flow['clicks'][action], screenshot, region) + self.log('已执行:' + action) + + def claim_location(self, frame): + spec = self.flow['claim_button'] + score, location = self.matcher.locate(frame, spec) + if score < spec.get('threshold', 0.86): + return None + x, y, width, height = location + return ((x + width / 2) / 562, (y + height / 2) / 1002) + + def panel_dot(self, frame, panel): + spec = self.flow['tab_dots'][panel] + return self.matcher.score(frame, spec) >= spec.get('threshold', 0.86) + + def claim_panel(self, panel): + count = 0 + while count < self.flow['max_claims_per_panel']: + name, screenshot, region, frame = self.wait(panel) + location = self.claim_location(frame) + if location is None: + return count + self.click(location, screenshot, region, hold=0.8) + count += 1 + self.log('已执行:%s 第 %s 个领取按钮。' % (panel, count)) + reward = self.wait('reward') + self.click_action('dismiss_reward', reward) + raise WorkflowError('单个面板领取次数超过安全上限,已停止。') + + def close_and_home(self): + panel = self.wait('daily', 'weekly') + self.click_action('close', panel) + self.wait('home') + self.log('已确认返回首页。') + + def run(self, dry_run=False): + home = self.wait('home') + if dry_run: + self.log('首页识别通过。计划:检测任务红点 → 日常/周常领取 → 关闭任务 → 返回首页。未点击。') + return 'preview' + + _, screenshot, region, frame = home + if not self.matcher.matches(frame, self.flow['task_dot']): + self.log('任务按钮没有红点,本次不进入任务面板。') + return 'no_task_dot' + self.click_action('open_tasks', home) + panel = self.wait('daily', 'weekly') + current = panel[0] + claimed = self.claim_panel(current) + self.log('%s 面板领取 %s 个奖励。' % (current, claimed)) + + other = 'weekly' if current == 'daily' else 'daily' + _, screenshot, region, frame = self.wait(current) + if self.panel_dot(frame, other): + self.click_action('open_' + other, (current, screenshot, region, frame)) + claimed += self.claim_panel(other) + self.log('%s 面板领取完成。' % other) + else: + self.log('%s 标签没有红点,不切换。' % other) + self.close_and_home() + return 'claimed' if claimed else 'no_claim_button' diff --git a/claim_sweep.py b/claim_sweep.py new file mode 100644 index 0000000..bf77779 --- /dev/null +++ b/claim_sweep.py @@ -0,0 +1,40 @@ +"""Run from the project root: python claim_sweep.py [--times N] [--dry-run].""" +import argparse +import sys +import time +from pathlib import Path + +from automation.engine import Matcher, WorkflowError, load_flow +from automation.offers import MacGameDesktop +from automation.sweep import SweepRunner + +ROOT = Path(__file__).resolve().parent + + +def main(): + parser = argparse.ArgumentParser(description='首页小金猪:快速扫荡并返回首页') + parser.add_argument('--times', type=int, default=1, help='扫荡次数,1 到 20 次;默认 1 次') + parser.add_argument('--dry-run', action='store_true', help='只验证首页并显示计划,不点击') + args = parser.parse_args() + if sys.platform != 'darwin': + print('当前脚本已验证 macOS;Windows 版将在校准入口完成后接入。', file=sys.stderr) + return 2 + try: + desktop = MacGameDesktop() + desktop.activate() + print('3 秒后开始;请保持游戏在前台。鼠标移到主屏幕左上角可紧急停止。', flush=True) + time.sleep(3) + runner = SweepRunner(desktop, Matcher(ROOT / 'assets'), + load_flow(ROOT / 'workflows/sweep_claim.json'), desktop.region_for) + runner.run(args.times, args.dry_run) + return 0 + except KeyboardInterrupt: + print('已停止。', file=sys.stderr) + return 130 + except Exception as exc: + print('流程停止:' + str(exc), file=sys.stderr) + return 1 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/claim_tasks.py b/claim_tasks.py new file mode 100644 index 0000000..f928753 --- /dev/null +++ b/claim_tasks.py @@ -0,0 +1,39 @@ +"""Run from the project root: python claim_tasks.py [--dry-run].""" +import argparse +import sys +import time +from pathlib import Path + +from automation.engine import Matcher, WorkflowError, load_flow +from automation.offers import MacGameDesktop +from automation.tasks import TaskRunner + +ROOT = Path(__file__).resolve().parent + + +def main(): + parser = argparse.ArgumentParser(description='首页任务:按红点领取日常和周常奖励并返回首页') + parser.add_argument('--dry-run', action='store_true', help='只验证首页并显示计划,不点击') + args = parser.parse_args() + if sys.platform != 'darwin': + print('当前脚本已验证 macOS;Windows 版将在校准入口完成后接入。', file=sys.stderr) + return 2 + try: + desktop = MacGameDesktop() + desktop.activate() + print('3 秒后开始;请保持游戏在前台。鼠标移到主屏幕左上角可紧急停止。', flush=True) + time.sleep(3) + runner = TaskRunner(desktop, Matcher(ROOT / 'assets'), + load_flow(ROOT / 'workflows/task_claim.json'), desktop.region_for) + runner.run(args.dry_run) + return 0 + except KeyboardInterrupt: + print('已停止。', file=sys.stderr) + return 130 + except Exception as exc: + print('流程停止:' + str(exc), file=sys.stderr) + return 1 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/docs/261004_1607-首页功能与日常任务梳理.md b/docs/261004_1607-首页功能与日常任务梳理.md index e767284..7a65edb 100644 --- a/docs/261004_1607-首页功能与日常任务梳理.md +++ b/docs/261004_1607-首页功能与日常任务梳理.md @@ -1,7 +1,7 @@ --- created_at: 2026-10-04 16:07:56 CST type: 永远的蔚蓝星球首页、商城、领地与公会入口、日常任务与自动化适配记录 -revision: 第11次 +revision: 第13次 revisions: - time: 2026-10-04 16:07:56 CST version: 第1次 @@ -47,7 +47,15 @@ revisions: version: 第11次 author: gpt-5 content: 补充小金猪挂机奖励的实机领取与灰色跳过分支,新增独立脚本及汇总表条目。 -summary: 记录首页、商城、领地和公会的日常任务入口、待完善流程及已实现脚本汇总。入口红点表示内部有奖励可领取;特惠赏金战令领取、小金猪挂机奖励、狂鼠之家收矿与雇佣、食堂领体力均已实机验证并写成脚本。 + - time: 2026-10-04 18:56:33 CST + version: 第12次 + author: gpt-5 + content: 补充小金猪快速扫荡的单次实机验证、5 次间隔执行规则、脚本和汇总表条目。 + - time: 2026-10-04 19:11:50 CST + version: 第13次 + author: gpt-5 + content: 补充任务红点驱动的日常与周常领奖流程、无红点跳过分支、脚本和汇总表条目。 +summary: 记录首页、商城、领地和公会的日常任务入口、待完善流程及已实现脚本汇总。入口红点表示内部有奖励可领取;特惠赏金战令、小金猪领取与扫荡、任务奖励、狂鼠之家收矿与雇佣、食堂领体力均已实机验证并写成脚本。 --- # 首页、商城、领地与公会功能及日常任务梳理 @@ -105,12 +113,24 @@ summary: 记录首页、商城、领地和公会的日常任务入口、待完 | 区域 | 入口 | 出现规律与用途 | 当前记录状态 | | --- | --- | --- | --- | -| 右侧 | 任务 | 固定入口 | 任务列表、跳转路径、完成条件和领奖逻辑待查看 | +| 右侧 | 任务 | 固定入口 | 已验证脚本:根据首页和日常/周常标签红点领取任务奖励并返回首页;滚动后隐藏任务的领取规则待继续确认 | | 右侧 | 好友 | 固定入口 | 是否包含每日互动或免费奖励待查看 | | 右侧 | 邮件 | 固定入口 | 邮件读取、奖励领取及已领取状态待查看 | | 右侧 | 英雄集结 | 不定期出现的大活动,与金沙游记属于类似类型 | 当前截图有该入口;内部结构及分活动待查看 | | 右上方 | 小游戏、问题反馈、游戏圈、客服 | 用户明确暂时不用处理 | 暂不纳入本轮任务梳理 | +### 任务奖励:日常与周常(已实机验证) + +2026-10-04 实机执行时,首页任务按钮存在红点。进入默认日常面板后发现 3 个“领取”按钮,已逐个领取并确认对应“恭喜获得”弹框。日常面板没有可领奖励后,检查周常面板;周常没有“领取”按钮,因此关闭任务并返回首页。之后重新运行脚本时,首页任务红点已消失,脚本未进入任务面板。 + +对应脚本为项目根目录的 `claim_tasks.py`,配置在 `workflows/task_claim.json`。运行方式: + +```bash +.venv/bin/python claim_tasks.py +``` + +脚本先检查首页任务红点,没有红点时直接结束。进入后处理当前日常或周常面板中全部可见的“领取”按钮,每领取一次都确认奖励弹框并关闭。当前面板没有领取按钮时,脚本仅检查另一个标签自身右上角的红点;有红点才切换并继续领取,没有则关闭任务回到首页。单个面板的领取次数上限为 20 次,达到上限会停止,避免页面异常时重复点击。 + ### 中间区域:小金猪与挂机奖励 用户确认:点击小金猪会打开奖励弹框,里面可以领取奖励。 @@ -139,6 +159,24 @@ summary: 记录首页、商城、领地和公会的日常任务入口、待完 脚本仅在右侧领取按钮识别为金色时执行 0.8 秒长按;必须识别“恭喜获得”弹框和变灰按钮才判定成功。按钮已灰时跳过领取,直接关闭弹框并回到首页。左侧“快速扫荡”消耗体力,不在脚本范围内。 +### 小金猪快速扫荡(已实机验证) + +2026-10-04 已在当前账号完成一次:**首页 → 小金猪 → 快速扫荡 → 恭喜获得弹框 → 关闭弹框 → 关闭挂机奖励 → 首页**。单次快速扫荡按钮显示消耗 10 体力,实机领取结果弹框中包含金币、经验、装备和其他资源。 + +对应脚本为项目根目录的 `claim_sweep.py`,配置在 `workflows/sweep_claim.json`。默认扫荡 1 次: + +```bash +.venv/bin/python claim_sweep.py +``` + +执行 5 次时使用: + +```bash +.venv/bin/python claim_sweep.py --times 5 +``` + +脚本每次扫荡均以 0.8 秒长按点击,确认奖励弹框后关闭;多次执行时在关闭上一次奖励后严格等待 2 秒,再进行下一次。最后关闭挂机奖励弹框并返回首页。当前单次消耗为 10 体力,`--times 5` 预计消耗 50 体力;脚本限制单次运行 1 至 20 次,避免意外的大量消耗。 + ### 主界面其他可见内容 截图还显示关卡列表入口、关卡挑战按钮、宝箱和队伍展示。“开始挑战”标注消耗 10 体力。这些内容尚未由用户展开说明,后续再梳理其是否关联日常任务。 @@ -261,7 +299,8 @@ summary: 记录首页、商城、领地和公会的日常任务入口、待完 | 观影、寻宝 | 先识别活动是否出现,再执行固定流程 | 活动不一定存在,但用户确认出现后的流程稳定 | | 试炼、三类冲刺 | 共用流程,并为不同页面设置识别规则;必要时由 AI 辅助判断 | 基本逻辑一致,页面外观或活动条件会变化 | | 金沙游记等左上角大活动、英雄集结 | 先识别活动名称与分活动,再逐项评估 | 活动名称、数量和内部结构可能变化,当前缺少完整流程 | -| 小金猪挂机奖励 | 已验证脚本:金色领取按钮时领取、验证奖励弹框与灰色按钮、返回首页 | 不执行快速扫荡;领取频率和 Windows 适配待验证 | +| 小金猪挂机奖励 | 已验证脚本:金色领取按钮时领取、验证奖励弹框与灰色按钮、返回首页 | 领取频率和 Windows 适配待验证 | +| 小金猪快速扫荡 | 已验证脚本:单次或指定次数扫荡,每次确认并关闭奖励,间隔 2 秒,返回首页 | 每次消耗 10 体力;体力不足时的按钮状态和 Windows 适配待验证 | | 任务、好友、邮件、返利 | 暂不确定 | 尚未查看内部内容和日常规则 | | 商城:英雄招募、图腾寻宝、每日商店、金币 | 后续确定 | 已记录区域结构,日常目标、执行次数、资源消耗与完成判定尚未明确 | | 商城:关卡礼包、钻石 | 暂不处理 | 用户明确排除 | @@ -295,5 +334,7 @@ summary: 记录首页、商城、领地和公会的日常任务入口、待完 | --- | --- | --- | --- | --- | --- | | 1 | 战令领取(`claim_offer.py`) | 首页 | 首页 → 左侧特惠 → 战令 | 识别“超值月卡”时先切换战令;领取赏金战令奖励,确认奖励弹框和按钮变灰后返回首页;已领取时跳过。 | 2026-10-04 17:45 CST | | 2 | 挂机奖励领取(`claim_idle.py`) | 首页 | 首页 → 小金猪 | 仅在右侧领取按钮为金色时长按领取,确认“恭喜获得”弹框和按钮变灰后关闭;灰色时跳过,最终返回首页。 | 2026-10-04 18:48 CST | -| 3 | 收矿与雇佣(`claim_mining.py`) | 领地 | 首页 → 底部领地 → 狂鼠之家 | 发现“一键收矿”后收矿、关闭奖励弹框、等待 10 秒;按白银罗盘、黄金罗盘、彩虹探测仪顺序一键雇佣,无可用道具时关闭弹框,最后返回首页。 | 2026-10-04 18:17 CST | -| 4 | 领体力(`claim_stamina.py`) | 领地 | 首页 → 底部领地 → 食堂 | 发现“一键领取”时领取体力,确认“恭喜获得”弹框和烤肉库存 0/60 后返回首页;无可领体力时跳过。 | 2026-10-04 18:32 CST | +| 3 | 快速扫荡(`claim_sweep.py`) | 首页 | 首页 → 小金猪 → 左侧快速扫荡 | 默认扫荡 1 次;指定 5 次时每次确认并关闭奖励后等待 2 秒再继续,最后返回首页。每次消耗 10 体力。 | 2026-10-04 18:56 CST | +| 4 | 任务奖励领取(`claim_tasks.py`) | 首页 | 首页 → 右侧任务 → 日常/周常 | 仅在任务红点存在时进入;领取当前面板的可见奖励,另一个标签有自身红点时才切换处理,最后返回首页。 | 2026-10-04 19:11 CST | +| 5 | 收矿与雇佣(`claim_mining.py`) | 领地 | 首页 → 底部领地 → 狂鼠之家 | 发现“一键收矿”后收矿、关闭奖励弹框、等待 10 秒;按白银罗盘、黄金罗盘、彩虹探测仪顺序一键雇佣,无可用道具时关闭弹框,最后返回首页。 | 2026-10-04 18:17 CST | +| 6 | 领体力(`claim_stamina.py`) | 领地 | 首页 → 底部领地 → 食堂 | 发现“一键领取”时领取体力,确认“恭喜获得”弹框和烤肉库存 0/60 后返回首页;无可领体力时跳过。 | 2026-10-04 18:32 CST | diff --git a/tests/test_sweep.py b/tests/test_sweep.py new file mode 100644 index 0000000..634467e --- /dev/null +++ b/tests/test_sweep.py @@ -0,0 +1,70 @@ +import copy +import unittest +from pathlib import Path + +from PIL import Image + +from automation.engine import Matcher, Region, WorkflowError, load_flow +from automation.sweep import SweepRunner + +ROOT = Path(__file__).resolve().parent.parent + + +class SweepDesktop: + def __init__(self, images): + self.images = images + self.index = 0 + self.clicks = [] + + def screenshot(self): + return self.images[min(self.index, len(self.images) - 1)] + + def click(self, point, region, size, hold=0.2): + self.clicks.append((point, hold)) + self.index += 1 + + +class SweepTests(unittest.TestCase): + def setUp(self): + self.flow = load_flow(ROOT / 'workflows/sweep_claim.json') + self.flow['interval_seconds'] = 0 + self.matcher = Matcher(ROOT / 'assets') + self.region = Region(0, 0, 562, 1002) + + def image(self, state): + image = Image.new('RGB', (562, 1002), (42, 58, 73)) + for spec in self.flow['states'][state]: + template = Image.open(ROOT / 'assets' / spec['template']).convert('RGB') + x, y, _, _ = spec['search'] + image.paste(template, (x + 5, y + 5)) + return image + + def runner(self, states): + desktop = SweepDesktop([self.image(state) for state in states]) + flow = copy.deepcopy(self.flow) + return desktop, SweepRunner(desktop, self.matcher, flow, + lambda _: self.region, log=lambda _: None, timeout=0.8) + + def test_five_sweeps_dismisses_each_result_and_returns_home(self): + states = ['home', 'sweep_available'] + for _ in range(5): + states.extend(('reward', 'sweep_available')) + states.append('home') + desktop, runner = self.runner(states) + self.assertEqual(runner.run(5), 'completed') + expected = [(self.flow['clicks']['open_pig'], 0.2)] + for _ in range(5): + expected.extend(((self.flow['clicks']['sweep'], 0.8), + (self.flow['clicks']['dismiss_reward'], 0.2))) + expected.append((self.flow['clicks']['close'], 0.2)) + self.assertEqual(desktop.clicks, expected) + + def test_invalid_count_stops_before_clicking(self): + desktop, runner = self.runner(['home']) + with self.assertRaises(WorkflowError): + runner.run(0) + self.assertEqual(desktop.clicks, []) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_tasks.py b/tests/test_tasks.py new file mode 100644 index 0000000..3f92978 --- /dev/null +++ b/tests/test_tasks.py @@ -0,0 +1,86 @@ +import unittest +from pathlib import Path + +from PIL import Image + +from automation.engine import Matcher, Region, load_flow +from automation.tasks import TaskRunner + +ROOT = Path(__file__).resolve().parent.parent + + +class TaskDesktop: + def __init__(self, images): + self.images = images + self.index = 0 + self.clicks = [] + + def screenshot(self): + return self.images[min(self.index, len(self.images) - 1)] + + def click(self, point, region, size, hold=0.2): + self.clicks.append((point, hold)) + self.index += 1 + + +class TaskTests(unittest.TestCase): + def setUp(self): + self.flow = load_flow(ROOT / 'workflows/task_claim.json') + self.matcher = Matcher(ROOT / 'assets') + self.region = Region(0, 0, 562, 1002) + + def image(self, state, claim=False, dot=None): + image = Image.new('RGB', (562, 1002), (42, 58, 73)) + if state in self.flow['states']: + for spec in self.flow['states'][state]: + template = Image.open(ROOT / 'assets' / spec['template']).convert('RGB') + x, y, _, _ = spec['search'] + image.paste(template, (x + 5, y + 5)) + if claim: + spec = self.flow['claim_button'] + template = Image.open(ROOT / 'assets' / spec['template']).convert('RGB') + x, y, _, _ = spec['search'] + image.paste(template, (x + 5, y + 5)) + if dot == 'home': + spec = self.flow['task_dot'][0] + elif dot: + spec = self.flow['tab_dots'][dot] + else: + spec = None + if spec: + template = Image.open(ROOT / 'assets' / spec['template']).convert('RGB') + x, y, _, _ = spec['search'] + image.paste(template, (x + 5, y + 5)) + return image + + def runner(self, images): + desktop = TaskDesktop(images) + return desktop, TaskRunner(desktop, self.matcher, self.flow, + lambda _: self.region, log=lambda _: None, timeout=0.8) + + def test_claims_current_and_dotted_other_panel(self): + images = [ + self.image('home', dot='home'), + self.image('daily', claim=True), + self.image('reward'), + self.image('daily', dot='weekly'), + self.image('weekly', claim=True), + self.image('reward'), + self.image('weekly'), + self.image('home'), + ] + desktop, runner = self.runner(images) + self.assertEqual(runner.run(), 'claimed') + self.assertEqual(len(desktop.clicks), 7) + self.assertEqual(desktop.clicks[0][0], self.flow['clicks']['open_tasks']) + self.assertEqual(desktop.clicks[3][0], self.flow['clicks']['open_weekly']) + self.assertEqual(desktop.clicks[-1][0], self.flow['clicks']['close']) + + def test_without_home_dot_does_not_open_tasks(self): + desktop, runner = self.runner([self.image('home')]) + self.assertEqual(runner.run(), 'no_task_dot') + self.assertEqual(desktop.clicks, []) + + +if __name__ == '__main__': + unittest.main() diff --git a/workflows/sweep_claim.json b/workflows/sweep_claim.json new file mode 100644 index 0000000..6b9aa65 --- /dev/null +++ b/workflows/sweep_claim.json @@ -0,0 +1,21 @@ +{ + "name": "小金猪快速扫荡", + "interval_seconds": 2, + "states": { + "home": [{"template": "home.png", "search": [150, 730, 410, 865]}], + "sweep_available": [ + {"template": "idle_dialog_title.png", "search": [170, 260, 400, 380]}, + {"template": "sweep_available.png", "search": [20, 645, 330, 825]} + ], + "reward": [ + {"template": "sweep_reward.png", "search": [120, 220, 440, 370], "threshold": 0.8}, + {"template": "sweep_dismiss.png", "search": [145, 610, 420, 730]} + ] + }, + "clicks": { + "open_pig": [0.135, 0.607], + "sweep": [0.32, 0.73], + "dismiss_reward": [0.50, 0.67], + "close": [0.50, 0.94] + } +} diff --git a/workflows/task_claim.json b/workflows/task_claim.json new file mode 100644 index 0000000..02614f0 --- /dev/null +++ b/workflows/task_claim.json @@ -0,0 +1,26 @@ +{ + "name": "日常与周常任务奖励领取", + "max_claims_per_panel": 20, + "states": { + "home": [{"template": "home.png", "search": [150, 730, 410, 865]}], + "daily": [{"template": "tasks_daily_title.png", "search": [130, 75, 430, 190]}], + "weekly": [{"template": "tasks_weekly_title.png", "search": [130, 75, 430, 190]}], + "reward": [ + {"template": "tasks_reward.png", "search": [120, 220, 440, 370], "threshold": 0.8}, + {"template": "tasks_dismiss.png", "search": [145, 610, 420, 730]} + ] + }, + "task_dot": [{"template": "tasks_home_dot.png", "search": [515, 175, 562, 230]}], + "claim_button": {"template": "tasks_claim.png", "search": [345, 300, 535, 875]}, + "tab_dots": { + "daily": {"template": "tasks_tab_dot.png", "search": [360, 830, 430, 910]}, + "weekly": {"template": "tasks_tab_dot.png", "search": [470, 830, 550, 910]} + }, + "clicks": { + "open_tasks": [0.93, 0.21], + "open_daily": [0.62, 0.884], + "open_weekly": [0.80, 0.884], + "dismiss_reward": [0.50, 0.67], + "close": [0.93, 0.14] + } +}