-初级和中级QZQ-100%)
# -*- coding:utf-8 -*- 连连看3.2-连连看自动消除 —— 可视化标定版无盲试 低置信度不阻断 修复 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 连连看 # ---- 棋盘几何标定后自动填入---- GRID_X0 0 GRID_Y0 0 CELL_W 40 CELL_H 50 H_NUM 14 V_NUM 8 # ---- 节奏 ---- 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语言-连连看3.2-连连看自动消除自动化脚本源代码.ziphttps://download.csdn.net/download/qq_32257509/93805408