mower-ng/mower/solvers/reclamation_algorithm.py
zhbaor b0a908d68b
All checks were successful
ci/woodpecker/push/check_format Pipeline was successful
生息演算
2025-07-09 00:31:14 +08:00

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))