ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

连连看 4 - 难度Easy - 关卡 无变化-连连看自动消除 —— 可视化标定版(无盲试 + 低置信度不阻断)-初级QZQ-100%

连连看 4 - 难度Easy - 关卡 无变化-连连看自动消除 —— 可视化标定版(无盲试 + 低置信度不阻断)-初级QZQ-100%
# -*- coding:utf-8 -*- """ 连连看 4 - 难度:Easy - 关卡: 无变化-连连看自动消除 —— 可视化标定版(无盲试 + 低置信度不阻断) 修复: 1. 低置信度格子直接当空格(0),不再跳过整帧 2. EMPTY_THRESHOLD 默认 0.75,空格更易识别 3. 消除后空格判定更宽容 快捷键:F1=开始 F2=停止 """ import ctypes, os, threading, time, glob from collections import deque, Counter import tkinter as tk from tkinter import ttk, messagebox import cv2 import numpy as np import win32api, win32gui, win32con from PIL import ImageGrab, Image, ImageTk try: ctypes.windll.shcore.SetProcessDpiAwareness(2) except Exception: try: ctypes.windll.user32.SetProcessDPIAware() except Exception: pass # ==================== 配置 ==================== class Config: WINDOW_TITLE = "连连看 4 - 难度:Easy - 关卡: 无变化" # ---- 棋盘几何(标定后自动填入)---- GRID_X0 = 0 GRID_Y0 = 0 CELL_W = 40 CELL_H = 50 H_NUM = 12 V_NUM = 6 # ---- 节奏 ---- TIME_INTERVAL = 0.05 ANIM_WAIT = 0.25 CONFIRM_WAIT = 0.20 # ---- 匹配 ---- TEMPLATE_DIR = "templates" MATCH_METHOD = cv2.TM_CCOEFF_NORMED MATCH_THRESHOLD = 0.80 # 识别阈值 EMPTY_THRESHOLD = 0.75 # 空格判定阈值(降低,空格更易识别) MERGE_THRESHOLD = 0.88 # ---- 防错 ---- MAX_PAIR_RETRIES = 1 HARD_BLACKLIST_LIMIT = 1 QUIET_WAIT_ROUNDS = 5 QUIET_WAIT_INTERVAL = 0.8 # ---- 预览 ---- PREVIEW_INTERVAL = 0.3 PREVIEW_MAX_W = 900 stop_event = threading.Event() matcher = None # ==================== BFS 路径 ==================== DIR_UP, DIR_DOWN, DIR_LEFT, DIR_RIGHT = 0, 1, 2, 3 DIR_DELTA = {DIR_UP: (-1, 0), DIR_DOWN: (1, 0), DIR_LEFT: (0, -1), DIR_RIGHT: (0, 1)} def bfs_path(matrix, start, end, max_turns=2): if start == end: return None rows, cols = len(matrix), len(matrix[0]) if matrix[start[0]][start[1]] == 0 or matrix[end[0]][end[1]] == 0: return None if matrix[start[0]][start[1]] != matrix[end[0]][end[1]]: return None min_r, max_r, min_c, max_c = -1, rows, -1, cols def in_range(r, c): return min_r <= r <= max_r and min_c <= c <= max_c def is_blocked(r, c): if r < 0 or r >= rows or c < 0 or c >= cols: return False if (r, c) == start or (r, c) == end: return False return matrix[r][c] != 0 visited = {} q = deque() for d in (DIR_UP, DIR_DOWN, DIR_LEFT, DIR_RIGHT): dr, dc = DIR_DELTA[d] nr, nc = start[0] + dr, start[1] + dc if not in_range(nr, nc) or is_blocked(nr, nc): continue visited[(nr, nc, d)] = 0 q.append((nr, nc, d, 0, [(nr, nc)])) while q: r, c, d, turns, path = q.popleft() if (r, c) == end: return [start] + path for nd in (DIR_UP, DIR_DOWN, DIR_LEFT, DIR_RIGHT): dr, dc = DIR_DELTA[nd] nr, nc = r + dr, c + dc if not in_range(nr, nc): continue new_turns = turns if nd == d else turns + 1 if new_turns > max_turns: continue if (nr, nc) != end and is_blocked(nr, nc): continue state = (nr, nc, nd) if state in visited and visited[state] <= new_turns: continue visited[state] = new_turns q.append((nr, nc, nd, new_turns, path + [(nr, nc)])) return None # ==================== 截图 ==================== def get_window_rect(): hwnd = win32gui.FindWindow(None, Config.WINDOW_TITLE) if not hwnd: return None try: win32gui.SetForegroundWindow(hwnd) win32gui.ShowWindow(hwnd, win32con.SW_RESTORE) except Exception: pass return win32gui.GetWindowRect(hwnd) def grab_screen(): return cv2.cvtColor(np.array(ImageGrab.grab()), cv2.COLOR_RGB2BGR) def slice_cell(screen, x_idx, y_idx): x1 = Config.GRID_X0 + x_idx * Config.CELL_W y1 = Config.GRID_Y0 + y_idx * Config.CELL_H x2, y2 = x1 + Config.CELL_W, y1 + Config.CELL_H h, w = screen.shape[:2] x1, y1, x2, y2 = max(0, x1), max(0, y1), min(w, x2), min(h, y2) if x2 - x1 < 5 or y2 - y1 < 5: return None return screen[y1:y2, x1:x2] def grab_all_cells(screen): return [slice_cell(screen, x, y) for y in range(Config.V_NUM) for x in range(Config.H_NUM)] # ==================== 模板匹配器 ==================== class TemplateMatcher: def __init__(self): self.templates = [] self.empty_template = None self._size = (64, 64) os.makedirs(Config.TEMPLATE_DIR, exist_ok=True) def _prep(self, img): if img is None or img.size == 0: return None g = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) if img.ndim == 3 else img.copy() g = cv2.GaussianBlur(g, (3, 3), 0) return cv2.resize(g, self._size) def _crop(self, g): h, w = g.shape m = 0.18 return g[int(h * m):int(h * (1 - m)), int(w * m):int(w * (1 - m))] def _score(self, g1, g2): return float(cv2.matchTemplate(self._crop(g1), self._crop(g2), Config.MATCH_METHOD)[0][0]) def reset(self): self.templates.clear() self.empty_template = None for f in glob.glob(os.path.join(Config.TEMPLATE_DIR, "type_*.png")): try: os.remove(f) except OSError: pass def add(self, cell): tid = len(self.templates) + 1 self.templates.append((tid, self._prep(cell))) cv2.imwrite(os.path.join(Config.TEMPLATE_DIR, f"type_{tid:03d}.png"), cell) return tid def is_empty(self, cell): if cell is None or self.empty_template is None: return False g = self._prep(cell) if g is None: return False return self._score(g, self.empty_template) >= Config.EMPTY_THRESHOLD def recognize(self, cell): """返回 (类型ID, 最高分)。类型ID=0 表示无法识别或空格。""" if cell is None or cell.size == 0: return 0, 0.0 g = self._prep(cell) if g is None: return 0, 0.0 # 先判空(用较低阈值) if self.empty_template is not None: es = self._score(g, self.empty_template) if es >= Config.EMPTY_THRESHOLD: return 0, es # 再匹配所有模板 best_id, best = 0, 0.0 for tid, tpl in self.templates: s = self._score(g, tpl) if s > best: best, best_id = s, tid if best < Config.MATCH_THRESHOLD: return 0, best return best_id, best def build(self, cells, allow_new=True): """根据当前棋盘建库。返回 (matrix, low_conf_list)""" if allow_new: self.reset() # 找方差最小的格子作为空格模板 if allow_new: best_var, best_g = 1e18, None for c in cells: if c is None: continue g = self._prep(c) if g is None: continue v = float(np.var(g)) if v < best_var: best_var, best_g = v, g if best_g is not None and best_var < 300: self.empty_template = best_g print(f"[建库] 空格模板方差={best_var:.1f}") else: print(f"[建库] 未找到明显空格(最小方差={best_var:.1f})") matrix = [] low_conf = [] for y in range(Config.V_NUM): row = [] for x in range(Config.H_NUM): c = cells[y * Config.H_NUM + x] if c is None: row.append(0) continue g = self._prep(c) if g is None: row.append(0) continue # 判空 if self.empty_template is not None: if self._score(g, self.empty_template) >= Config.EMPTY_THRESHOLD: row.append(0) continue # 匹配已有模板 best_id, best = 0, 0.0 for tid, tpl in self.templates: s = self._score(g, tpl) if s > best: best, best_id = s, tid if best >= Config.MATCH_THRESHOLD: row.append(best_id) else: if allow_new: # 建库时:匹配到高相似度的合并,否则新建 if best >= Config.MERGE_THRESHOLD: row.append(best_id) else: row.append(self.add(c)) else: # 运行时不建新模板,视为空格 row.append(0) low_conf.append((y, x, best_id, round(best, 3))) matrix.append(row) return matrix, low_conf # ==================== 点击 ==================== def cell_center(rc): return (Config.GRID_X0 + rc[1] * Config.CELL_W + Config.CELL_W // 2, Config.GRID_Y0 + rc[0] * Config.CELL_H + Config.CELL_H // 2) def click_pair(p1, p2): x1, y1 = cell_center(p1) x2, y2 = cell_center(p2) win32api.SetCursorPos((x1, y1)) time.sleep(0.02) win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, x1, y1, 0, 0) win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, x1, y1, 0, 0) if stop_event.wait(Config.TIME_INTERVAL): return win32api.SetCursorPos((x2, y2)) time.sleep(0.02) win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, x2, y2, 0, 0) win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, x2, y2, 0, 0) # ==================== 热键 ==================== from ctypes import wintypes class HotkeyListener(threading.Thread): ID_F1, ID_F2 = 1, 2 VK_F1, VK_F2 = 0x70, 0x71 WM_HOTKEY = 0x0312 def __init__(self, on_start, on_stop, log): super().__init__(daemon=True) self.on_start, self.on_stop, self.log = on_start, on_stop, log self._stop = False def run(self): u = ctypes.windll.user32 u.RegisterHotKey(None, self.ID_F1, 0, self.VK_F1) u.RegisterHotKey(None, self.ID_F2, 0, self.VK_F2) msg = wintypes.MSG() try: while not self._stop: if u.PeekMessageW(ctypes.byref(msg), None, 0, 0, 1): if msg.message == self.WM_HOTKEY: if msg.wParam == self.ID_F1: self.log("[热键] F1") self.on_start() elif msg.wParam == self.ID_F2: self.log("[热键] F2") self.on_stop() u.TranslateMessage(ctypes.byref(msg)) u.DispatchMessageW(ctypes.byref(msg)) else: time.sleep(0.03) finally: u.UnregisterHotKey(None, self.ID_F1) u.UnregisterHotKey(None, self.ID_F2) def stop(self): self._stop = True # ==================== GUI ==================== class App: def __init__(self, root): global matcher matcher = TemplateMatcher() self.root = root root.title("连连看 — 可视化标定版(无盲试·低置信度不阻断)") root.geometry("1280x860") self.screen = None self.matrix = None self.running = False self._last_preview = 0 # 标定 self.calibrating = False self.calib_scale = 1.0 self.calib_offset = (0, 0) self.calib_rect = None self.calib_start = None self._build_ui() self.hotkey = HotkeyListener( on_start=lambda: root.after(0, self.start), on_stop=lambda: root.after(0, self.stop), log=self.log) self.hotkey.start() self.log("① 截图 ② 标定棋盘(鼠标框选) ③ 建库 ④ F1 开始") self.log(f"当前阈值:MATCH={Config.MATCH_THRESHOLD} EMPTY={Config.EMPTY_THRESHOLD}") root.protocol("WM_DELETE_WINDOW", self.on_close) def _build_ui(self): top = ttk.Frame(self.root, padding=5) top.pack(fill=tk.X) ttk.Button(top, text="① 截图", command=self.snap).pack(side=tk.LEFT, padx=3) ttk.Button(top, text="② 标定棋盘(鼠标框选)", command=self.start_calib).pack(side=tk.LEFT, padx=3) ttk.Button(top, text="③ 建库", command=self.build).pack(side=tk.LEFT, padx=3) ttk.Button(top, text="④ 开始(F1)", command=self.start).pack(side=tk.LEFT, padx=3) ttk.Button(top, text="■ 停止(F2)", command=self.stop).pack(side=tk.LEFT, padx=3) self.status = tk.StringVar(value="请先截图") ttk.Label(top, textvariable=self.status, foreground="blue").pack(side=tk.LEFT, padx=15) mid = ttk.Frame(self.root) mid.pack(fill=tk.BOTH, expand=True) left = ttk.LabelFrame(mid, text="棋盘几何(标定后自动填入)", padding=8) left.pack(side=tk.LEFT, fill=tk.Y, padx=5, pady=5) self.geo_vars = {} for k in ["GRID_X0", "GRID_Y0", "CELL_W", "CELL_H", "H_NUM", "V_NUM"]: row = ttk.Frame(left) row.pack(fill=tk.X, pady=2) ttk.Label(row, text=k, width=10).pack(side=tk.LEFT) v = tk.StringVar(value=str(getattr(Config, k))) self.geo_vars[k] = v e = ttk.Entry(row, textvariable=v, width=10) e.pack(side=tk.LEFT, padx=4) e.bind("<FocusOut>", lambda ev, kk=k: self._apply_geo(kk)) e.bind("<Return>", lambda ev, kk=k: self._apply_geo(kk)) thr = ttk.LabelFrame(left, text="阈值", padding=8) thr.pack(fill=tk.X, pady=8) self.thr_vars = {} for k, mn, mx in [("MATCH_THRESHOLD", 40, 100), ("EMPTY_THRESHOLD", 40, 100)]: row = ttk.Frame(thr) row.pack(fill=tk.X, pady=2) ttk.Label(row, text=k, width=18).pack(side=tk.LEFT) lab = ttk.Label(row, text=f"{int(getattr(Config, k) * 100)}", width=4, foreground="darkgreen") lab.pack(side=tk.RIGHT) v = tk.IntVar(value=int(getattr(Config, k) * 100)) self.thr_vars[k] = (v, lab) s = ttk.Scale(row, from_=mn, to=mx, orient=tk.HORIZONTAL, variable=v, command=lambda val, kk=k: self._apply_thr(kk)) s.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=4) right = ttk.LabelFrame(mid, text="预览(标定时在图上拖拽框选)", padding=5) right.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5) self.canvas = tk.Canvas(right, bg="gray20", cursor="crosshair") self.canvas.pack(fill=tk.BOTH, expand=True) self.canvas.bind("<ButtonPress-1>", self._calib_press) self.canvas.bind("<B1-Motion>", self._calib_drag) self.canvas.bind("<ButtonRelease-1>", self._calib_release) self.canvas.bind("<Configure>", lambda e: self._redraw(force=True)) logf = ttk.LabelFrame(self.root, text="日志", padding=5) logf.pack(side=tk.BOTTOM, fill=tk.X, padx=5, pady=5) self.txt = tk.Text(logf, height=12) self.txt.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) sb = ttk.Scrollbar(logf, command=self.txt.yview) sb.pack(side=tk.RIGHT, fill=tk.Y) self.txt.config(yscrollcommand=sb.set) def log(self, m): try: self.root.after(0, lambda: (self.txt.insert(tk.END, m + "\n"), self.txt.see(tk.END))) except Exception: pass def _apply_geo(self, k): try: setattr(Config, k, int(self.geo_vars[k].get())) except ValueError: pass def _apply_thr(self, k): v, lab = self.thr_vars[k] val = v.get() lab.config(text=str(val)) setattr(Config, k, val / 100.0) # ---------- 截图 ---------- def snap(self): rect = get_window_rect() if not rect: messagebox.showerror("错误", "找不到连连看窗口") return self.screen = grab_screen() self.log(f"截图 {self.screen.shape[1]}x{self.screen.shape[0]},窗口 {rect}") self.status.set("请点『标定棋盘』,在预览图上框选棋盘") self._redraw(force=True) # ---------- 标定 ---------- def start_calib(self): if self.screen is None: messagebox.showwarning("提示", "请先点『截图』") return self.calibrating = True self.calib_rect = None self.status.set("拖拽框选整个棋盘(左上→右下)") def _canvas_to_screen(self, cx, cy): return (int((cx - self.calib_offset[0]) / self.calib_scale), int((cy - self.calib_offset[1]) / self.calib_scale)) def _calib_press(self, e): if not self.calibrating: return self.calib_start = (e.x, e.y) self.calib_rect = (e.x, e.y, e.x, e.y) self._redraw(force=True) def _calib_drag(self, e): if not self.calibrating or self.calib_start is None: return self.calib_rect = (self.calib_start[0], self.calib_start[1], e.x, e.y) self._redraw(force=True) def _calib_release(self, e): if not self.calibrating or self.calib_start is None: return self.calibrating = False sx0, sy0 = self._canvas_to_screen(*self.calib_start) sx1, sy1 = self._canvas_to_screen(e.x, e.y) x0, x1 = sorted([sx0, sx1]) y0, y1 = sorted([sy0, sy1]) if x1 - x0 < 50 or y1 - y0 < 50: self.log("框选太小,取消") self._redraw(force=True) return Config.GRID_X0 = x0 Config.GRID_Y0 = y0 Config.CELL_W = max(1, (x1 - x0) // Config.H_NUM) Config.CELL_H = max(1, (y1 - y0) // Config.V_NUM) for k in ["GRID_X0", "GRID_Y0", "CELL_W", "CELL_H", "H_NUM", "V_NUM"]: self.geo_vars[k].set(str(getattr(Config, k))) self.log(f"✔ 标定完成:GRID=({x0},{y0}) CELL={Config.CELL_W}x{Config.CELL_H} " f"网格={Config.H_NUM}x{Config.V_NUM}") self.status.set("标定完成,点『建库』") self.calib_rect = None self._redraw(force=True) # ---------- 建库 ---------- def build(self): if self.screen is None: messagebox.showwarning("提示", "请先截图") return if Config.GRID_X0 == 0 and Config.GRID_Y0 == 0: messagebox.showwarning("提示", "请先标定棋盘") return cells = grab_all_cells(self.screen) matrix, low_conf = matcher.build(cells, allow_new=True) n_types = len(matcher.templates) self.log(f"✅ 建库完成:{n_types} 种方块") self.log("识别矩阵:") for row in matrix: self.log(" " + " ".join(f"{v:3d}" for v in row)) flat = [v for r in matrix for v in r if v] odd = sum(1 for c in Counter(flat).values() if c % 2) self.log(f"非零格 {len(flat)},奇数类 {odd}") if low_conf: self.log(f"⚠ 有 {len(low_conf)} 个格子置信度不足(前10个):") for r, c, bid, s in low_conf[:10]: self.log(f" ({r+1},{c+1}) 最高候选id={bid} 分={s}") self.matrix = matrix self._redraw(force=True) self.status.set(f"库就绪({n_types}类),按 F1 开始") # ---------- 预览 ---------- def _redraw(self, force=False): if self.screen is None: return now = time.time() if not force and now - self._last_preview < Config.PREVIEW_INTERVAL: return self._last_preview = now h, w = self.screen.shape[:2] cw = self.canvas.winfo_width() ch = self.canvas.winfo_height() if cw <= 1: cw, ch = 800, 700 max_w = min(Config.PREVIEW_MAX_W, cw) scale = min(max_w / w, ch / h, 1.0) nw, nh = int(w * scale), int(h * scale) ox, oy = (cw - nw) // 2, (ch - nh) // 2 self.calib_scale = scale self.calib_offset = (ox, oy) rgb = cv2.cvtColor(self.screen, cv2.COLOR_BGR2RGB) img = cv2.resize(rgb, (nw, nh), interpolation=cv2.INTER_AREA) # 网格 gx0, gy0 = Config.GRID_X0, Config.GRID_Y0 if gx0 or gy0: for j in range(Config.H_NUM + 1): x = int((gx0 + j * Config.CELL_W) * scale) if 0 <= x < nw: cv2.line(img, (x, 0), (x, nh), (0, 255, 0), 1) for i in range(Config.V_NUM + 1): y = int((gy0 + i * Config.CELL_H) * scale) if 0 <= y < nh: cv2.line(img, (0, y), (nw, y), (0, 255, 0), 1) # 识别结果标注 if self.matrix is not None and (gx0 or gy0): for i, row in enumerate(self.matrix): for j, v in enumerate(row): x1 = int((gx0 + j * Config.CELL_W) * scale) y1 = int((gy0 + i * Config.CELL_H) * scale) x2 = int((gx0 + (j + 1) * Config.CELL_W) * scale) y2 = int((gy0 + (i + 1) * Config.CELL_H) * scale) if x2 <= 0 or y2 <= 0 or x1 >= nw or y1 >= nh: continue color = (80, 255, 80) if v != 0 else (255, 80, 80) cv2.rectangle(img, (x1, y1), (x2, y2), color, 1) if v != 0: cv2.putText(img, str(v), (x1 + 2, y1 + 14), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 0), 1, cv2.LINE_AA) # 标定框 if self.calib_rect: x0, y0, x1, y1 = self.calib_rect cv2.rectangle(img, (x0, y0), (x1, y1), (255, 0, 0), 2) im = Image.fromarray(img) photo = ImageTk.PhotoImage(im) self.canvas.delete("all") self.canvas.create_image(ox, oy, anchor=tk.NW, image=photo) self.canvas.image = photo # ---------- 开始/停止 ---------- def start(self): if self.running: return if not matcher.templates: self.log("⚠ 请先建库") return if Config.GRID_X0 == 0 and Config.GRID_Y0 == 0: self.log("⚠ 请先标定棋盘") return self.running = True stop_event.clear() self.log("========== 开始(无盲试) ==========") threading.Thread(target=self._loop, daemon=True).start() def stop(self): if not self.running: return stop_event.set() self.running = False self.log("已停止") # ---------- 主循环 ---------- def _loop(self): dead = set() retry = {} last_nz = None black = set() black_cnt = {} while not stop_event.is_set() and self.running: try: screen = grab_screen() cells = grab_all_cells(screen) # 识别:低置信度格子直接当 0,不阻断 matrix = [] low_cells = [] for y in range(Config.V_NUM): row = [] for x in range(Config.H_NUM): c = cells[y * Config.H_NUM + x] if c is None or c.size == 0: row.append(0) continue if matcher.is_empty(c): row.append(0) continue tid, sc = matcher.recognize(c) if tid == 0: row.append(0) low_cells.append((y, x, round(sc, 3))) else: row.append(tid) matrix.append(row) low = len(low_cells) flat = [v for r in matrix for v in r if v] nz = len(flat) self.screen = screen self.matrix = matrix self._redraw() if nz == 0: self.log("棋盘空,等待…") time.sleep(1.5) dead.clear() retry.clear() black.clear() black_cnt.clear() last_nz = None continue if last_nz is not None and nz < last_nz: self.log(f" ℹ 非零 {last_nz}->{nz},清空临时黑名单") dead.clear() retry.clear() last_nz = nz if low > 0: self.log(f" ℹ {low} 个格子置信度不足,已当空格处理") if low <= 5: for (r, c, s) in low_cells: self.log(f" ({r+1},{c+1}) 最高分={s}") # 找所有可消除对 pairs = [] by_type = {} for i, r in enumerate(matrix): for j, v in enumerate(r): if v: by_type.setdefault(v, []).append((i, j)) for v, cs in by_type.items(): for a in range(len(cs)): for b in range(a + 1, len(cs)): p = bfs_path(matrix, cs[a], cs[b], 2) if p: pairs.append((len(p), cs[a], cs[b], p)) pairs.sort(key=lambda t: t[0]) valid = [p for p in pairs if self._key(p[1], p[2]) not in dead and self._key(p[1], p[2]) not in black and retry.get(self._key(p[1], p[2]), 0) < Config.MAX_PAIR_RETRIES] if not valid: self.log(f"无候选对(非零 {nz}),安静等待…") self._print_matrix(matrix) ok = False for _ in range(Config.QUIET_WAIT_ROUNDS): if stop_event.is_set(): break time.sleep(Config.QUIET_WAIT_INTERVAL) s2 = grab_screen() c2 = grab_all_cells(s2) m2 = [] for y in range(Config.V_NUM): row = [] for x in range(Config.H_NUM): cc = c2[y * Config.H_NUM + x] if matcher.is_empty(cc): row.append(0) else: tid, _ = matcher.recognize(cc) row.append(tid) m2.append(row) nz2 = sum(1 for r in m2 for v in r if v) self.screen = s2 self.matrix = m2 self._redraw() if nz2 < nz: self.log(f" ℹ 非零 {nz}->{nz2},恢复") dead.clear() retry.clear() ok = True break if not ok: self.log(" 仍无解,继续等待…") continue # 选一对 chosen = valid[0] _, p1, p2, path = chosen key = self._key(p1, p2) att = retry.get(key, 0) + 1 self.log(f"消 ({p1[0]+1},{p1[1]+1})<->({p2[0]+1},{p2[1]+1}) " f"路径{len(path)} 第{att}次") # 点击 click_pair(p1, p2) time.sleep(Config.ANIM_WAIT) # 确认 sc = grab_screen() e1 = matcher.is_empty(slice_cell(sc, p1[1], p1[0])) e2 = matcher.is_empty(slice_cell(sc, p2[1], p2[0])) if not (e1 and e2): time.sleep(Config.CONFIRM_WAIT) sc = grab_screen() e1 = matcher.is_empty(slice_cell(sc, p1[1], p1[0])) e2 = matcher.is_empty(slice_cell(sc, p2[1], p2[0])) if e1 and e2: self.log(" ✅ 消除成功") retry.pop(key, None) dead.discard(key) black_cnt.pop(key, None) dead.clear() else: retry[key] = att black_cnt[key] = black_cnt.get(key, 0) + 1 if black_cnt[key] >= Config.HARD_BLACKLIST_LIMIT: black.add(key) self.log(f" ⛔ 连续失败{black_cnt[key]}次,永久拉黑") elif att >= Config.MAX_PAIR_RETRIES: dead.add(key) self.log(f" ⚠ 尝试{att}次未消除,临时拉黑") except Exception as ex: self.log(f"循环异常:{ex}") import traceback self.log(traceback.format_exc()) time.sleep(0.5) self.running = False self.log("========== 结束 ==========") def _key(self, p1, p2): return (p1, p2) if p1 < p2 else (p2, p1) def _print_matrix(self, m): self.log("当前矩阵:") for row in m: self.log(" " + " ".join(f"{v:3d}" for v in row)) def on_close(self): stop_event.set() try: self.hotkey.stop() except Exception: pass self.root.destroy() def main(): root = tk.Tk() App(root) root.mainloop() if __name__ == "__main__": main()

python语言-连连看4.10游戏自动化脚本源代码.zip
https://download.csdn.net/download/qq_32257509/93805125

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进