All checks were successful
ci/woodpecker/push/check_format Pipeline was successful
248 lines
8.2 KiB
Python
248 lines
8.2 KiB
Python
"""
|
|
Copyright (c) 2025 zhbaor <zhbaor@zhaozuohong.vip>
|
|
|
|
This file is part of mower-ng (https://git.zhaozuohong.vip/mower-ng/mower-ng).
|
|
|
|
Mower-ng is free software: you may copy, redistribute and/or modify it
|
|
under the terms of the GNU General Public License as published by the
|
|
Free Software Foundation, version 3 or later.
|
|
|
|
This file is distributed in the hope that it will be useful, but
|
|
WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
"""
|
|
|
|
from enum import Enum, auto
|
|
from typing import NamedTuple
|
|
|
|
import cv2
|
|
import numpy as np
|
|
|
|
from mower.utils import config
|
|
from mower.utils import typealias as tp
|
|
from mower.utils.image import cropimg, loadres, thres2
|
|
from mower.utils.log import logger
|
|
from mower.utils.scene import Scene
|
|
from mower.utils.solver import BaseSolver, TransitionOn
|
|
from mower.utils.vector import in_scope, va, vs
|
|
|
|
|
|
class Map:
|
|
class NodeStatus(Enum):
|
|
OUT_OF_VIEW = auto()
|
|
IN_VIEW_UNAVAILABLE = auto()
|
|
IN_VIEW_AVAILABLE = auto()
|
|
|
|
def __init__(self, img: tp.Image):
|
|
self.location = {
|
|
"base": ((1938, 340), (2243, 642)),
|
|
"聚羽之地": ((1350, 502), (1605, 631)),
|
|
"林中寻宝": ((1200, 773), (1453, 901)),
|
|
"丰饶灌木林": ((1390, 160), (1667, 288)),
|
|
"直奔深渊": ((930, 460), (1180, 591)),
|
|
"参差林木": ((388, 725), (639, 854)),
|
|
"原始奔腾": ((480, 992), (732, 1120)),
|
|
"砾沙平原": ((914, 1060), (1164, 1142)),
|
|
"饮水为饵": ((236, 1140), (493, 1268)),
|
|
}
|
|
self.src_pts = np.float32([[0, 0], [1920, 0], [-450, 1080], [2370, 1080]])
|
|
self.dst_pts = np.float32([[0, 0], [1920, 0], [0, 1080], [1920, 1080]])
|
|
self.trans_mat = cv2.getPerspectiveTransform(self.src_pts, self.dst_pts)
|
|
self.rev_mat = np.linalg.inv(self.trans_mat)
|
|
self.img = img
|
|
self.map = cv2.warpPerspective(img, self.trans_mat, (1920, 1080))
|
|
self.map = cropimg(self.map, ((280, 137), (1640, 993)))
|
|
|
|
class NodeInfo(NamedTuple):
|
|
status: "Map.NodeStatus"
|
|
skewed_scope: tp.Scope
|
|
|
|
self.nodes: dict[str, NodeInfo] = {}
|
|
results_dict = {}
|
|
for key in self.location:
|
|
template = loadres(f"ra/map/{key}")
|
|
res = cv2.matchTemplate(self.map, template, cv2.TM_CCOEFF_NORMED)
|
|
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
|
|
results_dict[key] = (max_val, max_loc)
|
|
|
|
name, (score, pos) = max(results_dict.items(), key=lambda item: item[1][0])
|
|
|
|
if score <= 0.9:
|
|
self.scope = None
|
|
return
|
|
map_scope_loc = self.location[name]
|
|
offset = vs(map_scope_loc[0], pos)
|
|
h, w, _ = self.map.shape
|
|
self.scope = (offset, va(offset, (w, h)))
|
|
|
|
crop_offset = (280, 137)
|
|
for node_name, node_scope in self.location.items():
|
|
local_p1 = vs(node_scope[0], self.scope[0])
|
|
local_p2 = vs(node_scope[1], self.scope[0])
|
|
p1_before = va(local_p1, crop_offset)
|
|
p2_before = va(local_p2, crop_offset)
|
|
pts = np.float32([p1_before, p2_before]).reshape(-1, 1, 2)
|
|
transformed_pts = cv2.perspectiveTransform(pts, self.rev_mat)
|
|
skewed_scope = (
|
|
tuple(map(int, transformed_pts[0][0])),
|
|
tuple(map(int, transformed_pts[1][0])),
|
|
)
|
|
|
|
p1_in = in_scope(self.scope, node_scope[0])
|
|
p2_in = in_scope(self.scope, node_scope[1])
|
|
if not (p1_in and p2_in):
|
|
status = self.NodeStatus.OUT_OF_VIEW
|
|
elif results_dict[node_name][0] > 0.9:
|
|
status = self.NodeStatus.IN_VIEW_AVAILABLE
|
|
else:
|
|
status = self.NodeStatus.IN_VIEW_UNAVAILABLE
|
|
|
|
self.nodes[node_name] = NodeInfo(status, skewed_scope)
|
|
|
|
|
|
class ReclamationAlgorithm(BaseSolver):
|
|
solver_name = "生息演算"
|
|
solver_default_scene = Scene.RA_MAIN
|
|
|
|
@TransitionOn()
|
|
def _(self):
|
|
if self.find("ra/easy_mode"):
|
|
self.tap((1729, 970))
|
|
return
|
|
if self.animation():
|
|
return
|
|
self.tap((1324, 971))
|
|
|
|
@TransitionOn(Scene.RA_SWITCH_MODE)
|
|
def _(self):
|
|
if pos := self.find("ra/easy_mode_select"):
|
|
self.tap(pos)
|
|
return
|
|
self.scene_graph_step(Scene.RA_MAIN)
|
|
|
|
def number(self, scope, height, thres):
|
|
result = config.recog.num.number_int("secret_front", scope, height, thres=thres)
|
|
logger.debug(result)
|
|
return result
|
|
|
|
def number_day(self):
|
|
return self.number(((1722, 117), (1809, 164)), 44, 150)
|
|
|
|
def tap_save_menu(self):
|
|
self.ctap((1540, 1009), 3)
|
|
|
|
@TransitionOn(Scene.RA_MAP)
|
|
def _(self):
|
|
if pos := self.find("ra/forward"):
|
|
self.tap(pos)
|
|
return
|
|
if self.animation(interval=1):
|
|
return
|
|
if (day_number := self.number_day()) > 10:
|
|
if not self.find("ra/delete_save"):
|
|
self.tap_save_menu()
|
|
return
|
|
self.tap("ra/delete_save")
|
|
return
|
|
if day_number == 10:
|
|
if not self.find("ra/load_save"):
|
|
self.tap_save_menu()
|
|
return
|
|
self.tap("ra/load_save")
|
|
return
|
|
if day_number < 7:
|
|
self.tap((1770, 136))
|
|
return
|
|
if self.find("ra/delete_save") or self.find("ra/load_save"):
|
|
self.tap_save_menu()
|
|
return
|
|
ra_map = Map(config.recog.img)
|
|
if ra_map.scope is None:
|
|
logger.error("地图识别失败")
|
|
self.back()
|
|
return
|
|
|
|
img = cropimg(config.recog.gray, ((1765, 170), (1785, 195)))
|
|
img = thres2(img, 127)
|
|
index = day_number * 2 + int(cv2.countNonZero(img) < 200) - 13
|
|
node = list(ra_map.location.keys())[index]
|
|
node_scope = ra_map.nodes[node].skewed_scope
|
|
|
|
if ra_map.nodes[node].status == Map.NodeStatus.IN_VIEW_AVAILABLE:
|
|
self.ctap(node_scope, 3)
|
|
return
|
|
|
|
(x1, y1), (x2, y2) = node_scope
|
|
screen_center = 960, 540
|
|
drag_x, drag_y = vs(screen_center, ((x1 + x2) // 2, (y1 + y2) // 2))
|
|
drag_x = max(min(drag_x, 1280), -1280) // 2
|
|
drag_y = max(min(drag_y, 720), -720) // 2
|
|
drag_start = vs(screen_center, (drag_x, drag_y))
|
|
drag_stop = va(screen_center, (drag_x, drag_y))
|
|
self.swipe_ext([drag_start, drag_stop], [600], 300, 0.1)
|
|
|
|
@TransitionOn(Scene.RA_WASTE_TIME)
|
|
def _(self):
|
|
self.tap("ra/waste_time")
|
|
|
|
@TransitionOn(
|
|
[
|
|
Scene.RA_DIALOG_SKIP_DAY,
|
|
Scene.RA_DIALOG_ENTER_BATTLE,
|
|
Scene.RA_DIALOG_LEAVE_CURRENT,
|
|
Scene.RA_DIALOG_NO_DRINK,
|
|
Scene.RA_DIALOG_NO_OPERATOR,
|
|
Scene.RA_DIALOG_DELETE_SAVE,
|
|
]
|
|
)
|
|
def _(self):
|
|
self.tap((1441, 726))
|
|
|
|
@TransitionOn(Scene.RA_CYCLE_COMPLETE)
|
|
def _(self):
|
|
self.tap((960, 1024))
|
|
|
|
@TransitionOn(Scene.RA_DAY_COMPLETE)
|
|
def _(self):
|
|
self.tap((960, 905))
|
|
|
|
@TransitionOn(Scene.RA_EUNECTES)
|
|
def _(self):
|
|
self.tap("ra/eunectes")
|
|
|
|
@TransitionOn(Scene.RA_TEAM)
|
|
def _(self):
|
|
self.tap((1789, 1017))
|
|
|
|
@TransitionOn(Scene.RA_START_ACTION)
|
|
def _(self):
|
|
self.tap("ra/start_action")
|
|
|
|
def number_kitchen_ready(self):
|
|
return self.number(((627, 840), (691, 881)), 37, 180)
|
|
|
|
def number_kitchen_done(self):
|
|
return self.number(((997, 156), (1047, 191)), 25, 220)
|
|
|
|
@TransitionOn(Scene.RA_KITCHEN)
|
|
def _(self):
|
|
ready = self.number_kitchen_ready()
|
|
done = self.number_kitchen_done()
|
|
if done >= 8:
|
|
self.back()
|
|
return
|
|
if ready + done >= 8:
|
|
self.tap((1707, 861))
|
|
return
|
|
self.tap((1414, 863))
|
|
|
|
@TransitionOn(Scene.RA_LOAD_SAVE)
|
|
def _(self):
|
|
if self.find("ra/load_save_choose"):
|
|
self.tap((736, 707))
|
|
return
|
|
self.tap((960, 555))
|