隐秘战线适配新写法
Some checks failed
ci/woodpecker/push/check_format Pipeline failed

This commit is contained in:
zhbaor 2025-02-15 20:31:46 +08:00
parent dd300fd7af
commit 554d54f7b3
13 changed files with 300 additions and 230 deletions

View file

@ -640,46 +640,42 @@
"comment": "保全扫荡确认"
},
"1101": {
"label": "SF_ENTRANCE",
"comment": "隐秘战线入口"
},
"1102": {
"label": "SF_EXIT",
"comment": "暂离行动"
},
"1103": {
"1102": {
"label": "SF_SELECT_TEAM",
"comment": "选择小队"
},
"1104": {
"1103": {
"label": "SF_CONTINUE",
"comment": "继续前进"
},
"1105": {
"1104": {
"label": "SF_SELECT",
"comment": "选择路线"
},
"1106": {
"1105": {
"label": "SF_ACTIONS",
"comment": "行动选项"
},
"1107": {
"1106": {
"label": "SF_RESULT",
"comment": "行动结果"
},
"1108": {
"1107": {
"label": "SF_EVENT",
"comment": "应对危机事件"
},
"1109": {
"1108": {
"label": "SF_TEAM_PASS",
"comment": "小队通过危机事件"
},
"1110": {
"1109": {
"label": "SF_CLICK_ANYWHERE",
"comment": "点击任意处继续"
},
"1111": {
"1110": {
"label": "SF_END",
"comment": "抵达终点"
},

BIN
mower/resources/sf/card_confirm.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
mower/resources/sf/continue_result.png (Stored with Git LFS)

Binary file not shown.

BIN
mower/resources/sf/event_confirm.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
mower/resources/sf/return.png (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -1,26 +1,37 @@
from datetime import datetime, timedelta
from difflib import SequenceMatcher
import cv2
from mower.static import secret_front
from mower.utils import config
from mower.utils import typealias as tp
from mower.utils.csleep import MowerExit
from mower.utils.email import send_message
from mower.utils.email import notify
from mower.utils.graph import SceneGraphSolver
from mower.utils.image import cropimg
from mower.utils.log import logger
from mower.utils.rapidocr import ocr_rec
from mower.utils.scene import Scene
from mower.utils.solver import BaseSolver
from mower.utils.vector import sa, va
CardInfo = tuple[int, int, int, float]
def exp(card):
data = card[:3]
p = card[3]
return [i * p for i in data]
def exp(card: CardInfo) -> list[float]:
"""计算卡片的期望
Args:
card (CardInfo): 卡片的值
Returns:
list[float]: 三项属性的期望
"""
return [i * card[3] for i in card[:3]]
class SecretFront(BaseSolver):
solver_name = "隐秘战线"
target = {
"1A": [20, 20, 20],
"2A": [60, 55, 45],
@ -49,50 +60,77 @@ class SecretFront(BaseSolver):
"结局E": "medicine",
}
def __init__(self):
self.properties: list[int] | None = None
"三项属性值"
self.event: bool = False
"应对危机事件"
self.route_names: list[str] = []
"匹配路线"
self.series: int | None = None
"支援作战平台、游侠、诡影迷踪"
self.actions: dict[int, dict[int, float]] = {}
"记录页面中卡片的期望"
self.send_email: bool = False
"发送邮件提醒"
super().__init__()
@property
def route(self):
"路线"
return self.routes[config.conf.secret_front.target]
@property
def team(self):
"小队"
return self.teams[config.conf.secret_front.target]
def run(self):
self.timeout = timedelta(seconds=config.conf.reclamation_algorithm.timeout)
if self.scheduler_stop_time:
self.deadline = self.scheduler_stop_time - timedelta(minutes=1)
else:
self.deadline = None
self.unknown_time = None
self.properties = None
self.route_matcher = None
self.route_names = []
self.event = False
self.series = None # 支援作战平台、游侠、诡影迷踪
self.series = None
self.send_email = False
self.reset_actions()
super().run()
return False
return super().run()
def reset_actions(self):
self.actions = {}
for page in range(3):
self.actions[page] = {}
"清空缓存的卡片数据"
self.actions = {0: {}, 1: {}, 2: {}}
def number(self, scope: tp.Scope, height: int | None = None):
def number(self, scope: tp.Scope, height: int = 25):
return config.recog.num.number_int("secret_front", scope, height)
def card_pos(self, total, idx):
if total == 3:
return [(301, 466), (830, 466), (1360, 466)][idx]
elif total == 2:
return [(565, 466), (1095, 466)][idx]
else:
return (830, 466)
def card_pos(self, total: int, idx: int) -> tp.Coordinate:
"""总共有total张卡片时下标为idx(从0开始)卡片的左上角坐标
def stage_card(self, total, idx):
Args:
total (int): 卡片总数
idx (int): 下标从0开始
Returns:
tp.Coordinate: 卡片左上角坐标
"""
return {
1: [(830, 466)],
2: [(565, 466), (1095, 466)],
3: [(301, 466), (830, 466), (1360, 466)],
}[total][idx]
def event_card(self, total: int, idx: int) -> tuple[str, bool]:
"""应对危机事件卡片识别
Args:
total (int): 卡片总数
idx (int): 卡片下标从0开始
Returns:
tuple[str, bool]: 危机事件选项名称及是否可选
"""
pos = self.card_pos(total, idx)
scope = sa(((10, 380), (140, 430)), pos)
@ -111,7 +149,16 @@ class SecretFront(BaseSolver):
logger.debug(f"{name=} {hue=}")
return name, hue > 18
def card(self, total, idx):
def card(self, total: int, idx: int) -> CardInfo:
"""卡片识别
Args:
total (int): 总数
idx (int): 下标从0开始
Returns:
CardInfo: 卡片信息
"""
pos = self.card_pos(total, idx)
materiel = sa(((84, 70), (180, 102)), pos)
@ -133,24 +180,32 @@ class SecretFront(BaseSolver):
return materiel, intelligence, medicine, percentage
def page_number(self):
title = cropimg(config.recog.gray, ((1020, 230), (1210, 285)))
if self.route_matcher is None:
def page_number(self) -> int | None:
"""识别当前页数
Returns:
int | None: 页数从0开始
"""
if not self.route_names:
self.tap((125, 840))
return None
score, scope = self.route_matcher.match2d(title)
if scope is None:
return None
pos_x = scope[0][0]
if pos_x < 800:
page_number = 0
elif pos_x < 1330:
page_number = 1
else:
page_number = 2
img = cropimg(config.recog.gray, ((915, 230), (1320, 290)))
name = ocr_rec(img)
max_ratio = 0
page_number = 0
for i, n in enumerate(self.route_names):
if (ratio := SequenceMatcher(None, name, n).ratio()) > max_ratio:
page_number = i
max_ratio = ratio
logger.debug(f"{page_number=}")
return page_number
def card_total(self):
def card_total(self) -> int:
"""识别当前有几张卡片
Returns:
int: 卡片数量
"""
p3 = self.card_pos(3, 0)
p2 = self.card_pos(2, 0)
up_scope = ((0, 0), (473, 120))
@ -159,11 +214,11 @@ class SecretFront(BaseSolver):
s3d = sa(down_scope, p3)
s2u = sa(up_scope, p2)
s2d = sa(down_scope, p2)
if (pos := self.find("sf/card", scope=s3u)) and pos[0][0] < 350:
if self.find("sf/card", scope=s3u):
total = 3
elif self.find("sf/available", scope=s3d):
total = 3
elif (pos := self.find("sf/card", scope=s2u)) and pos[0][0] < 610:
elif self.find("sf/card", scope=s2u):
total = 2
elif self.find("sf/available", scope=s2d):
total = 2
@ -172,14 +227,18 @@ class SecretFront(BaseSolver):
logger.debug(f"{total=}")
return total
def max_card(self):
def max_card(self) -> tuple[int, int]:
"""计算最优选项
Returns:
tuple[int, int]: 返回最优页码及卡片下标
"""
max_page = -1
max_card = -1
max_score = -1
logger.debug(f"{self.properties=}")
# 根据目标计算
for page, page_action in self.actions.items():
for card, action in page_action.items():
for stage in self.route:
@ -201,42 +260,40 @@ class SecretFront(BaseSolver):
logger.debug(f"{max_page=} {max_card=}")
return max_page, max_card
def choose_card(self, total, idx):
self.route_matcher = None
self.properties = None
self.reset_actions()
def choose_card(self, total: int, idx: int):
"""点击卡片
if total == 3:
start = 545
elif total == 2:
start = 805
else:
start = 1075
self.tap((start + idx * 530, 900), interval=0.5)
self.tap((start + idx * 530, 900), interval=2)
Args:
total (int): 卡片总数
idx (int): 卡片下标从0开始
"""
start = {3: 545, 2: 805, 1: 1075}[total]
pos = (start + idx * 530, 900)
self.tap(pos)
def move_forward(self, scene):
# 从首页进入隐秘战线
if scene == Scene.INDEX:
self.tap_index_element("terminal")
elif scene == Scene.TERMINAL_MAIN:
self.tap_terminal_button("main_theme")
elif scene == Scene.TERMINAL_MAIN_THEME:
self.tap("navigation/main/14")
elif scene == Scene.SF_ENTRANCE:
self.tap("sf/entrance")
# 选择小队
def transition(self):
if (scene := self.scene()) == Scene.TERMINAL_MAIN_THEME:
self.main_theme(2, 14)
elif scene == Scene.OPERATOR_CHOOSE_LEVEL:
if pos := self.find("sf/entrance"):
self.tap(pos)
return
if self.animation():
return
SceneGraphSolver().step(Scene.TERMINAL_MAIN_THEME)
elif scene == Scene.SF_SELECT_TEAM:
self.tap(f"sf/team_{self.team}")
if self.animation():
return
if pos := self.find(f"sf/team_{self.team}"):
self.tap(pos)
return
self.tap("sf/select_team_ok")
# 继续前进
elif scene == Scene.SF_CONTINUE:
self.tap("sf/continue")
# 选择路线时识别已有属性值
elif scene == Scene.SF_SELECT:
if self.animation(((0, 0), (1920, 1030))):
return
self.send_email = True
self.event = False
if self.properties is None:
@ -247,8 +304,12 @@ class SecretFront(BaseSolver):
]
logger.debug(f"{self.properties=}")
if self.route_matcher is None:
self.route_matcher = config.recog.matcher
if not self.route_names:
for i in range(3):
pos = self.card_pos(3, i)
scope = sa(((120, 145), (460, 205)), pos)
img = cropimg(config.recog.gray, scope)
self.route_names.append(ocr_rec(img))
self.series = None
if (
@ -267,39 +328,42 @@ class SecretFront(BaseSolver):
self.series = 2
logger.debug(f"{self.series=}")
self.tap((545 + 530 * self.series, 640), interval=1.5)
self.tap((545 + 530 * self.series, 640))
return
self.tap((545, 640), interval=1.5)
# 行动列表
self.tap((545, 640))
elif scene == Scene.SF_ACTIONS:
if self.event and (pos := self.find("sf/event_confirm")):
self.tap(pos)
return
elif pos := self.find("sf/card_confirm"):
self.tap(pos)
return
if self.animation(((0, 0), (1920, 1030))):
return
total = self.card_total()
if self.event:
name_list = [self.stage_card(total, i) for i in range(total)]
for idx, data in enumerate(name_list):
name, available = data
if name in self.route:
if available:
self.choose_card(total, idx)
else:
self.exit = "restart"
self.tap("sf/exit_button")
name_list = [self.event_card(total, i) for i in range(total)]
for idx, (name, available) in enumerate(name_list):
if name not in self.route:
continue
if available:
self.choose_card(total, idx)
return
self.exit = "restart"
break
self.tap("sf/exit_button")
return
if (page_number := self.page_number()) is None:
self.sleep()
return
target = self.target[self.route[-1]]
distance = [max(t - p, 0) for p, t in zip(self.properties, target)]
if sum(distance) == 0:
self.choose_card(total, 0)
self.sleep(3)
return
if self.series is not None:
@ -309,6 +373,7 @@ class SecretFront(BaseSolver):
if all(card[3] < 0.8 for card in card_data):
logger.debug("成功概率太低")
self.series = None
self.solver_update_before_transition = False
return
elif not all(self.actions.values()):
@ -319,6 +384,7 @@ class SecretFront(BaseSolver):
for idx in range(total):
self.actions[page_number][idx] = exp(self.card(total, idx))
logger.debug(f"{self.actions=}")
self.solver_update_before_transition = False
elif (page_number + 1) % 3 == target_number:
self.tap((1785, 225)) # 下一页
else:
@ -329,87 +395,25 @@ class SecretFront(BaseSolver):
if max_page == page_number:
self.choose_card(len(self.actions[max_page]), max_card)
self.sleep(3)
elif (page_number + 1) % 3 == max_page:
self.tap((1785, 225)) # 下一页
else:
self.tap((350, 225)) # 上一页
# 行动结果
elif scene == Scene.SF_RESULT:
if pos := self.find("sf/continue_result"):
self.tap(pos)
else:
self.sleep()
self.route_names = []
self.properties = None
self.reset_actions()
self.tap((1640, 945))
elif scene == Scene.SF_EVENT:
self.event = True
self.tap("sf/continue_event", interval=1.5)
self.tap("sf/continue_event")
elif scene in [Scene.SF_TEAM_PASS, Scene.SF_CLICK_ANYWHERE, Scene.SF_END]:
self.tap((960, 980), interval=2)
if scene == Scene.SF_END:
send_message(
f"隐秘战线成功完成{config.conf.secret_front.target}", level="INFO"
)
# 关闭说明
elif scene == Scene.NOTICE:
self.tap("notice")
self.tap((960, 980))
if scene == Scene.SF_END and self.send_email:
self.send_email = False
notify(f"隐秘战线通关{config.conf.secret_front.target}", level="INFO")
elif scene == Scene.SF_EXIT:
if self.exit == "restart":
self.properties = None
self.route_matcher = None
self.event = False
self.tap("sf/restart")
self.tap("sf/confirm")
elif self.exit == "exit":
self.tap("sf/confirm")
else:
self.tap((480, 590))
self.tap("sf/restart")
else:
self.sleep()
def back_to_index(self, scene):
if scene in [Scene.TERMINAL_MAIN, Scene.TERMINAL_MAIN_THEME, Scene.SF_ENTRANCE]:
self.back()
elif scene == Scene.SF_EXIT:
self.move_forward(scene)
else:
self.exit = "exit"
self.tap("sf/exit_button")
def transition(self):
now = datetime.now()
if (scene := self.sf_scene()) == Scene.UNKNOWN:
if not self.unknown_time:
self.unknown_time = now
elif now - self.unknown_time > self.timeout:
logger.warning("连续识别到未知场景")
try:
self.properties = None
self.route_matcher = None
self.event = False
self.reset_actions()
super().back_to_index()
except MowerExit:
raise
except Exception as e:
logger.exception(e)
config.device.exit()
self.check_current_focus()
else:
self.unknown_time = None
if self.deadline and self.deadline < datetime.now():
if scene == Scene.INDEX:
return True
else:
self.back_to_index(scene)
else:
self.move_forward(scene)
SceneGraphSolver().step(Scene.TERMINAL_MAIN_THEME)

View file

@ -1,3 +1,5 @@
# ruff: noqa: F401
from . import (
extra,
friend,
@ -8,26 +10,9 @@ from . import (
recruit,
riic,
rogue,
secret_front,
shop,
sss,
terminal,
)
from .utils import DG, SceneGraphSolver, edge
__all__ = [
"SceneGraphSolver",
"DG",
"edge",
"extra",
"friend",
"index",
"mission",
"navbar",
"operation",
"recruit",
"riic",
"shop",
"sss",
"terminal",
"rogue",
]
from .utils import SceneGraphSolver

View file

@ -0,0 +1,24 @@
from mower.utils.scene import Scene
from mower.utils.solver import BaseSolver
from .utils import edge
# 隐秘战线
@edge(Scene.SF_EXIT, Scene.OPERATOR_CHOOSE_LEVEL)
def exit_dialog(solver: BaseSolver):
solver.tap("sf/exit_confirm")
@edge(Scene.SF_SELECT_TEAM, Scene.SF_EXIT)
@edge(Scene.SF_CONTINUE, Scene.SF_EXIT)
@edge(Scene.SF_SELECT, Scene.SF_EXIT)
@edge(Scene.SF_ACTIONS, Scene.SF_EXIT)
@edge(Scene.SF_RESULT, Scene.SF_EXIT)
@edge(Scene.SF_EVENT, Scene.SF_EXIT)
@edge(Scene.SF_TEAM_PASS, Scene.SF_EXIT)
@edge(Scene.SF_CLICK_ANYWHERE, Scene.SF_EXIT)
@edge(Scene.SF_END, Scene.SF_EXIT)
def exit_button(solver: BaseSolver):
solver.tap("sf/exit_button")

View file

@ -430,6 +430,34 @@ class Recognizer:
self.scene = Scene.SIGN_IN_ORUNDUM
elif self.find("start_story"):
self.scene = Scene.STORY_STAGE
elif self.find("sf/exit_button"):
if self.find("sf/continue"):
self.scene = Scene.SF_CONTINUE
elif self.find("sf/properties"):
if self.find("sf/select"):
self.scene = Scene.SF_SELECT
elif self.find("sf/return"):
self.scene = Scene.SF_ACTIONS
elif self.find("sf/success"):
self.scene = Scene.SF_RESULT
elif self.find("sf/failure"):
self.scene = Scene.SF_RESULT
else:
self.scene = Scene.UNKNOWN
elif self.find("sf/inheritance"):
self.scene = Scene.SF_SELECT_TEAM
elif self.find("sf/continue_event"):
self.scene = Scene.SF_EVENT
elif self.find("sf/click_anywhere"):
self.scene = Scene.SF_CLICK_ANYWHERE
elif self.find("sf/team_pass"):
self.scene = Scene.SF_TEAM_PASS
elif self.find("sf/end"):
self.scene = Scene.SF_END
else:
self.scene = Scene.UNKNOWN
elif self.find("sf/exit"):
self.scene = Scene.SF_EXIT
# 模板匹配
elif self.detect_index_scene():

View file

@ -163,6 +163,20 @@ color = {
"room_detail": (1291, 33),
"sanity_charge": (1111, 382),
"sanity_charge_dialog": (570, 529),
"sf/continue": (409, 818),
"sf/continue_event": (346, 815),
"sf/entrance": (32, 139),
"sf/exit": (856, 339),
"sf/exit_button": (30, 28),
"sf/exit_confirm": ((1393, 658), (1153, 658)),
"sf/inheritance": (1501, 26),
"sf/properties": (34, 470),
"sf/restart": (869, 657),
"sf/return": (57, 822),
"sf/select_team_ok": (1706, 917),
"sf/team_intelligence": (66, 530),
"sf/team_management": (55, 288),
"sf/team_medicine": (56, 766),
"shop/assist.jpg": (816, 222),
"shop/cart": (1252, 842),
"shop/collect": (1467, 43),
@ -389,6 +403,19 @@ template_matching = {
"rogue/refresh": ((1205, 200), (1256, 350)),
"rogue/team_check_to_do": ((0, 800), (1920, 960)),
"rogue/view_data_back": (147, 46),
"sf/available": None,
"sf/card": None,
"sf/card_confirm": ((720, 830), (1860, 890)),
"sf/click_anywhere": ((750, 940), (1030, 1000)),
"sf/end": ((1060, 825), (1340, 900)),
"sf/event_confirm": ((260, 845), (1920, 890)),
"sf/failure": ((365, 330), (610, 850)),
"sf/lost_in_the_trick": ((300, 600), (1830, 690)),
"sf/ranger": ((300, 600), (1830, 690)),
"sf/select": (274, 181),
"sf/success": ((365, 330), (610, 850)),
"sf/support_battle_platform": ((300, 600), (1830, 690)),
"sf/team_pass": ((960, 825), (1200, 900)),
"sign_in/headhunting/available": (1177, 857),
"sign_in/moon_festival/banner": (704, 92),
"sign_in/shop/0": (1341, 618),
@ -458,6 +485,7 @@ template_matching_score = {
"recruit/stone": 0.7,
"recruit/time": 0.8,
"rogue/node_be_choosed": 0.7,
"sf/card": 0.8,
"sign_in/moon_festival/banner": 0.5,
"sss/add_agent": 0.8,
"sss/drop_EC": 0.8,

View file

@ -319,27 +319,25 @@ class Scene:
"选择定向元件"
SSS_ELIMI_AGENCY = 1019
"保全扫荡确认"
SF_ENTRANCE = 1101
"隐秘战线入口"
SF_EXIT = 1102
SF_EXIT = 1101
"暂离行动"
SF_SELECT_TEAM = 1103
SF_SELECT_TEAM = 1102
"选择小队"
SF_CONTINUE = 1104
SF_CONTINUE = 1103
"继续前进"
SF_SELECT = 1105
SF_SELECT = 1104
"选择路线"
SF_ACTIONS = 1106
SF_ACTIONS = 1105
"行动选项"
SF_RESULT = 1107
SF_RESULT = 1106
"行动结果"
SF_EVENT = 1108
SF_EVENT = 1107
"应对危机事件"
SF_TEAM_PASS = 1109
SF_TEAM_PASS = 1108
"小队通过危机事件"
SF_CLICK_ANYWHERE = 1110
SF_CLICK_ANYWHERE = 1109
"点击任意处继续"
SF_END = 1111
SF_END = 1110
"抵达终点"
HEADHUNTING = 1201
"干员寻访"
@ -606,17 +604,16 @@ SceneComment = {
1017: "应急模式额外元件",
1018: "选择定向元件",
1019: "保全扫荡确认",
1101: "隐秘战线入口",
1102: "暂离行动",
1103: "选择小队",
1104: "继续前进",
1105: "选择路线",
1106: "行动选项",
1107: "行动结果",
1108: "应对危机事件",
1109: "小队通过危机事件",
1110: "点击任意处继续",
1111: "抵达终点",
1101: "暂离行动",
1102: "选择小队",
1103: "继续前进",
1104: "选择路线",
1105: "行动选项",
1106: "行动结果",
1107: "应对危机事件",
1108: "小队通过危机事件",
1109: "点击任意处继续",
1110: "抵达终点",
1201: "干员寻访",
1202: "使用赠送寻访机会确认对话框",
1203: "限定池单抽结果",

View file

@ -522,15 +522,16 @@ Res = Literal[
"sanity_charge_dialog",
"sf/available",
"sf/card",
"sf/card_confirm",
"sf/click_anywhere",
"sf/confirm",
"sf/continue",
"sf/continue_event",
"sf/continue_result",
"sf/end",
"sf/entrance",
"sf/event_confirm",
"sf/exit",
"sf/exit_button",
"sf/exit_confirm",
"sf/failure",
"sf/inheritance",
"sf/lost_in_the_trick",
@ -538,6 +539,7 @@ Res = Literal[
"sf/properties",
"sf/ranger",
"sf/restart",
"sf/return",
"sf/select",
"sf/select_team_ok",
"sf/success",