feat: add sweep and task reward automation
No files matched your search
@@ -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` 可只验证首页和执行计划。
|
||||
|
||||
### 已验证的特惠一键领取脚本
|
||||
|
||||
在首页执行:
|
||||
|
||||
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 20 KiB |
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
@@ -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'
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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 |
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||