首页/新闻资讯/正文详情

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

发布时间:2026/11/8 7:46:37 来源:云帆数科 栏目:资讯中心
连连看 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_turns2): 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_okTrue) 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, ftype_{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, 最高分)。类型ID0 表示无法识别或空格。 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_newTrue): 根据当前棋盘建库。返回 (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__(daemonTrue) 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_startlambda: root.after(0, self.start), on_stoplambda: root.after(0, self.stop), logself.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, padding5) top.pack(filltk.X) ttk.Button(top, text① 截图, commandself.snap).pack(sidetk.LEFT, padx3) ttk.Button(top, text② 标定棋盘(鼠标框选), commandself.start_calib).pack(sidetk.LEFT, padx3) ttk.Button(top, text③ 建库, commandself.build).pack(sidetk.LEFT, padx3) ttk.Button(top, text④ 开始(F1), commandself.start).pack(sidetk.LEFT, padx3) ttk.Button(top, text■ 停止(F2), commandself.stop).pack(sidetk.LEFT, padx3) self.status tk.StringVar(value请先截图) ttk.Label(top, textvariableself.status, foregroundblue).pack(sidetk.LEFT, padx15) mid ttk.Frame(self.root) mid.pack(filltk.BOTH, expandTrue) left ttk.LabelFrame(mid, text棋盘几何标定后自动填入, padding8) left.pack(sidetk.LEFT, filltk.Y, padx5, pady5) self.geo_vars {} for k in [GRID_X0, GRID_Y0, CELL_W, CELL_H, H_NUM, V_NUM]: row ttk.Frame(left) row.pack(filltk.X, pady2) ttk.Label(row, textk, width10).pack(sidetk.LEFT) v tk.StringVar(valuestr(getattr(Config, k))) self.geo_vars[k] v e ttk.Entry(row, textvariablev, width10) e.pack(sidetk.LEFT, padx4) e.bind(FocusOut, lambda ev, kkk: self._apply_geo(kk)) e.bind(Return, lambda ev, kkk: self._apply_geo(kk)) thr ttk.LabelFrame(left, text阈值, padding8) thr.pack(filltk.X, pady8) self.thr_vars {} for k, mn, mx in [(MATCH_THRESHOLD, 40, 100), (EMPTY_THRESHOLD, 40, 100)]: row ttk.Frame(thr) row.pack(filltk.X, pady2) ttk.Label(row, textk, width18).pack(sidetk.LEFT) lab ttk.Label(row, textf{int(getattr(Config, k) * 100)}, width4, foregrounddarkgreen) lab.pack(sidetk.RIGHT) v tk.IntVar(valueint(getattr(Config, k) * 100)) self.thr_vars[k] (v, lab) s ttk.Scale(row, from_mn, tomx, orienttk.HORIZONTAL, variablev, commandlambda val, kkk: self._apply_thr(kk)) s.pack(sidetk.LEFT, filltk.X, expandTrue, padx4) right ttk.LabelFrame(mid, text预览标定时在图上拖拽框选, padding5) right.pack(sidetk.LEFT, filltk.BOTH, expandTrue, padx5, pady5) self.canvas tk.Canvas(right, bggray20, cursorcrosshair) self.canvas.pack(filltk.BOTH, expandTrue) 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(forceTrue)) logf ttk.LabelFrame(self.root, text日志, padding5) logf.pack(sidetk.BOTTOM, filltk.X, padx5, pady5) self.txt tk.Text(logf, height12) self.txt.pack(sidetk.LEFT, filltk.BOTH, expandTrue) sb ttk.Scrollbar(logf, commandself.txt.yview) sb.pack(sidetk.RIGHT, filltk.Y) self.txt.config(yscrollcommandsb.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(textstr(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(forceTrue) # ---------- 标定 ---------- 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(forceTrue) 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(forceTrue) 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(forceTrue) 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(forceTrue) # ---------- 建库 ---------- 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_newTrue) 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 ({r1},{c1}) 最高候选id{bid} 分{s}) self.matrix matrix self._redraw(forceTrue) self.status.set(f库就绪({n_types}类)按 F1 开始) # ---------- 预览 ---------- def _redraw(self, forceFalse): 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), interpolationcv2.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, anchortk.NW, imagephoto) 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(targetself._loop, daemonTrue).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 ({r1},{c1}) 最高分{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(keylambda 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游戏自动化脚本源代码.ziphttps://download.csdn.net/download/qq_32257509/93805125

相关推荐

LinkSwift 网盘直链解析:8 大网盘 3 分钟接入 IDM,把 KB/s 拉回 MB/s
LinkSwift 网盘直链解析:8 大网盘 3 分钟接入 IDM,把 KB/s 拉回 MB/s

LinkSwift 网盘直链解析:8 大网盘 3 分钟接入 IDM,把 KB/s 拉回 MB/s 【免费下载链接】Online-disk-direct-link-download-assistant 一个基于 JavaScript 的网盘文件下载地址获取工具。基于【网盘直链下载助手】修改 ,支持 百度网盘 / 阿里云… · 2026/11/6 3:29:19

DevilutionX 完整安装指南:5 分钟让《暗黑破坏神》在现代系统上跑起来
DevilutionX 完整安装指南:5 分钟让《暗黑破坏神》在现代系统上跑起来

DevilutionX 完整安装指南:5 分钟让《暗黑破坏神》在现代系统上跑起来 【免费下载链接】DevilutionX Diablo build for modern operating systems 项目地址: https://gitcode.com/gh_mirrors/de/DevilutionX DevilutionX 是一个开源项目,用现代 C… · 2026/11/6 3:29:19

MAA 下载安装全平台教程:Windows、macOS 与 maa-cli 从零跑通自动化日常
MAA 下载安装全平台教程:Windows、macOS 与 maa-cli 从零跑通自动化日常

MAA 下载安装全平台教程:Windows、macOS 与 maa-cli 从零跑通自动化日常 【免费下载链接】MaaAssistantArknights 《明日方舟》小助手,全日常一键长草!| A one-click tool for the daily tasks of Arknights, supporting all clients. 项目… · 2026/11/6 3:29:19

电工证过期了不知道怎么复审延期?湛江电工证收费真实经历告诉你怎么搞
电工证过期了不知道怎么复审延期?湛江电工证收费真实经历告诉你怎么搞

电工证过期了不知道怎么复审延期?湛江电工证收费真实经历告诉你怎么搞 证书过期了不知道怎么复审延期?我身边有好几个朋友因为这事吃了亏,最后花了冤枉钱还耽误了工作。今天我就以真实经历为基础,带大家一步步了解湛江电工证收费、复审流程和注意事项,避… · 2026/11/8 7:45:56

证书过期了不知道怎么复审延期?2026河南最新政策告诉你1800的电工证贵不
证书过期了不知道怎么复审延期?2026河南最新政策告诉你1800的电工证贵不

证书过期了不知道怎么复审延期?2026河南最新政策告诉你1800的电工证贵不 你是不是也遇到过这种情况:手里拿着电工证,结果一不小心就过期了,找来找去也不知道怎么复审?2026年河南最新政策对特种作业证的管理更加严格,很多老电工都开始着急了… · 2026/11/8 7:45:56

浙江杭州特种工考试通过率揭秘:怕考不过白交培训费?一文说清
浙江杭州特种工考试通过率揭秘:怕考不过白交培训费?一文说清

浙江杭州特种工考试通过率揭秘:怕考不过白交培训费?一文说清 怕考不过白交培训费?浙江杭州特种工考试通过率揭秘,很多人在报名前心里都在打鼓。尤其是像我这样从三门峡转岗过来的,第一次接触特种作业证,不知道从哪里下手,怕花了几千块培训费,最后还是… · 2026/11/8 7:45:56

电工证哪里查询保姆级教程:外省证能转吗?薪资前景全解析
电工证哪里查询保姆级教程:外省证能转吗?薪资前景全解析

电工证哪里查询保姆级教程:外省证能转吗?薪资前景全解析 之前考的证在外省能不能转过来?这个问题是很多已经持证或者准备报考电工证的小伙伴最关心的。特别是在信阳浉河区,很多应届毕业生刚步入职场,面对电工证报考、考试、复审等流程一头雾水,更别说外… · 2026/11/8 7:45:56

浙江杭州特种工考试多少钱?三门峡建筑工人避坑指南
浙江杭州特种工考试多少钱?三门峡建筑工人避坑指南

浙江杭州特种工考试多少钱?三门峡建筑工人避坑指南 你是不是也这样, 不知道去哪报名怕被中介坑 ?尤其是像你这样在三门峡做建筑工的,想要考个 浙江杭州特种工考试 ,但又担心被坑钱、被坑证?别急,今天我就来跟你唠唠,从 多少钱… · 2026/11/8 7:45:56

电工作业带什么保护品 怎么报名不被中介坑
电工作业带什么保护品 怎么报名不被中介坑

电工作业带什么保护品 怎么报名不被中介坑 你是不是正为不知道去哪里报名电工作业证发愁?怕被中间人坑钱、怕走弯路、怕浪费时间?其实,只要掌握了正确的方式, 怎么报名… · 2026/11/8 7:44:56

电工证丢失还能补吗?千万别踩坑,丢了证等于失业
电工证丢失还能补吗?千万别踩坑,丢了证等于失业

电工证丢失还能补吗?千万别踩坑,丢了证等于失业 你正在找工作,但手里的电工证突然丢了,这下怎么办?别急,先别慌, 电工证丢失还能补吗 ?答案是肯定的,但 千万别踩坑 ,补办流程如果不了解,可能会耽误你上岗、甚至影响到你的收入。… · 2026/11/8 0:00:20

电力电缆作业证直播报名入口与办理流程良心建议
电力电缆作业证直播报名入口与办理流程良心建议

电力电缆作业证直播报名入口与办理流程良心建议 你是不是也担心学历不高报不了名?别急,今天就给你讲讲 电力电缆作业证直播 的报名流程和那些你必须知道的干货, 良心建议 在这里,别错过。 电力电缆作业证直播报名需要什么条件?… · 2026/11/8 0:00:20

英德电工证报考费多少钱,怕考不过白交培训费?看完这篇别再踩坑
英德电工证报考费多少钱,怕考不过白交培训费?看完这篇别再踩坑

英德电工证报考费多少钱,怕考不过白交培训费?看完这篇别再踩坑 怕考不过白交培训费?英德电工证报考费多少钱,你真的了解吗?现在市面上培训机构五花八门,价格参差不齐,很多人一不小心就花了冤枉钱,结果还考不过。别急,今天就来给你说清楚英德电工证的… · 2026/11/8 0:00:20

电工证丢失还能补吗?千万别踩坑,丢了证等于失业
电工证丢失还能补吗?千万别踩坑,丢了证等于失业

电工证丢失还能补吗?千万别踩坑,丢了证等于失业 你正在找工作,但手里的电工证突然丢了,这下怎么办?别急,先别慌, 电工证丢失还能补吗 ?答案是肯定的,但 千万别踩坑 ,补办流程如果不了解,可能会耽误你上岗、甚至影响到你的收入。… · 2026/11/8 0:00:20

电力电缆作业证直播报名入口与办理流程良心建议
电力电缆作业证直播报名入口与办理流程良心建议

电力电缆作业证直播报名入口与办理流程良心建议 你是不是也担心学历不高报不了名?别急,今天就给你讲讲 电力电缆作业证直播 的报名流程和那些你必须知道的干货, 良心建议 在这里,别错过。 电力电缆作业证直播报名需要什么条件?… · 2026/11/8 0:00:20

英德电工证报考费多少钱,怕考不过白交培训费?看完这篇别再踩坑
英德电工证报考费多少钱,怕考不过白交培训费?看完这篇别再踩坑

英德电工证报考费多少钱,怕考不过白交培训费?看完这篇别再踩坑 怕考不过白交培训费?英德电工证报考费多少钱,你真的了解吗?现在市面上培训机构五花八门,价格参差不齐,很多人一不小心就花了冤枉钱,结果还考不过。别急,今天就来给你说清楚英德电工证的… · 2026/11/8 0:00:20

双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/7 11:25:08

STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/7 12:02:18

DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/8 1:18:25

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码