ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

python语言-Bejeweled 3 / 宝石迷阵3(CHS 中文版)---三消自动挂机脚本(框选区域 + 暴力搜索 + 极速点击)-QZQ-2026-9-20

python语言-Bejeweled 3 / 宝石迷阵3(CHS 中文版)---三消自动挂机脚本(框选区域 + 暴力搜索 + 极速点击)-QZQ-2026-9-20
# -*- coding: utf-8 -*- """ 三消自动挂机脚本(框选区域 + 暴力搜索 + 极速点击) 依赖: pip install numpy opencv-python dxcam keyboard pyautogui pillow """ import os import json import time import random import threading import tkinter as tk from tkinter import ttk import ctypes import numpy as np import cv2 import dxcam import keyboard import pyautogui # 去掉 pyautogui 内置延迟 pyautogui.PAUSE = 0 # ============================================================ # 配置 # ============================================================ REGION_FILE = "region.json" class Config: # 默认值,首次运行会被框选覆盖 GAME_X = 0 GAME_Y = 0 GAME_W = 800 GAME_H = 800 ROWS = 8 COLS = 8 @classmethod def cell_w(cls): return cls.GAME_W // cls.COLS @classmethod def cell_h(cls): return cls.GAME_H // cls.ROWS # 颜色映射:BGR -> 字符 COLOR_MAP = { (0, 0, 255): 'A', # 红 (0, 255, 0 ): 'B', # 绿 (255, 0, 0 ): 'C', # 蓝 (0, 255, 255): 'D', # 黄 (255, 0, 255): 'E', # 紫 (255, 255, 0 ): 'F', # 青 (0, 128, 255): 'G', # 橙 (128, 0, 128): 'H', # 深紫 } COLOR_TOL = 40 # 延迟(全部 0) CLICK_DELAY = 0 STEP_DELAY = 0 # 暴力搜索模式 # "l1" -> 所有相邻交换,试一次消除,选消除数最多 # "l2" -> 所有相邻交换,模拟完整连锁,选总收益最高 BRUTE_MODE = "l1" # L2 模拟连锁时是否补新元素(补元素有随机性,建议 False) BRUTE_REFILL = False # ============================================================ # 区域读写 # ============================================================ def load_region(): if os.path.exists(REGION_FILE): try: with open(REGION_FILE, "r", encoding="utf-8") as f: data = json.load(f) Config.GAME_X = int(data["x"]) Config.GAME_Y = int(data["y"]) Config.GAME_W = int(data["w"]) Config.GAME_H = int(data["h"]) print(f"[区域] 已加载: x={Config.GAME_X}, y={Config.GAME_Y}, " f"w={Config.GAME_W}, h={Config.GAME_H}") return True except Exception as e: print(f"[区域] 读取失败: {e}") return False def save_region(x, y, w, h): data = {"x": int(x), "y": int(y), "w": int(w), "h": int(h)} with open(REGION_FILE, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) Config.GAME_X = int(x) Config.GAME_Y = int(y) Config.GAME_W = int(w) Config.GAME_H = int(h) print(f"[区域] 已保存: {data}") # ============================================================ # 框选工具 # ============================================================ class RegionSelector: def __init__(self, on_done=None): self.on_done = on_done self.start_x = 0 self.start_y = 0 self.rect_id = None self.root = tk.Tk() self.root.attributes("-fullscreen", True) self.root.attributes("-alpha", 0.25) self.root.attributes("-topmost", True) self.root.configure(bg="black") self.root.config(cursor="crosshair") self.canvas = tk.Canvas(self.root, bg="black", highlightthickness=0) self.canvas.pack(fill=tk.BOTH, expand=True) self.canvas.bind("<ButtonPress-1>", self.on_press) self.canvas.bind("<B1-Motion>", self.on_drag) self.canvas.bind("<ButtonRelease-1>", self.on_release) self.root.bind("<Escape>", lambda e: self.root.destroy()) self.canvas.create_text( 200, 30, text="拖拽框选游戏棋盘区域,Esc 取消", fill="white", font=("Microsoft YaHei", 16), anchor="w" ) def on_press(self, event): self.start_x = event.x self.start_y = event.y if self.rect_id: self.canvas.delete(self.rect_id) self.rect_id = self.canvas.create_rectangle( self.start_x, self.start_y, self.start_x, self.start_y, outline="red", width=2 ) def on_drag(self, event): if self.rect_id: self.canvas.coords( self.rect_id, self.start_x, self.start_y, event.x, event.y ) def on_release(self, event): x1, y1 = self.start_x, self.start_y x2, y2 = event.x, event.y x = min(x1, x2) y = min(y1, y2) w = abs(x2 - x1) h = abs(y2 - y1) if w < 20 or h < 20: print("[框选] 区域太小,忽略") self.root.destroy() return save_region(x, y, w, h) self.root.destroy() if self.on_done: self.on_done(x, y, w, h) def run(self): self.root.mainloop() def select_region_interactive(on_done=None): RegionSelector(on_done).run() # ============================================================ # 截屏 # ============================================================ class ScreenCapture: def __init__(self): self.camera = dxcam.create() def grab(self): region = ( Config.GAME_X, Config.GAME_Y, Config.GAME_X + Config.GAME_W, Config.GAME_Y + Config.GAME_H, ) frame = self.camera.grab(region=region) if frame is None: return None return cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) # ============================================================ # 视觉识别 # ============================================================ class Vision: def __init__(self): self.capture = ScreenCapture() @staticmethod def _nearest_color(bgr): best_char = '?' best_dist = 1e9 for (r, g, b), ch in Config.COLOR_MAP.items(): d = (int(bgr[0]) - b) ** 2 + \ (int(bgr[1]) - g) ** 2 + \ (int(bgr[2]) - r) ** 2 if d < best_dist: best_dist = d best_char = ch if best_dist > Config.COLOR_TOL ** 2 * 3: return '?' return best_char def read_map(self): frame = self.capture.grab() if frame is None: return None rows, cols = Config.ROWS, Config.COLS cw, ch = Config.cell_w(), Config.cell_h() map_data = [] for i in range(rows): row_chars = [] for j in range(cols): cx = j * cw + cw // 2 cy = i * ch + ch // 2 patch = frame[cy - 3:cy + 4, cx - 3:cx + 4] if patch.size == 0: row_chars.append('?') continue avg = patch.reshape(-1, 3).mean(axis=0) row_chars.append(self._nearest_color(avg)) map_data.append(row_chars) return map_data # ============================================================ # 消除核心 # ============================================================ def find_matches(map_data, row, col): """找出所有需要消除的坐标集合""" to_remove = set() # 横向 for i in range(row): j = 0 while j < col: if map_data[i][j] == '.': j += 1 continue k = j + 1 while k < col and map_data[i][k] == map_data[i][j]: k += 1 if k - j >= 3: for t in range(j, k): to_remove.add((i, t)) j = k # 纵向 for j in range(col): i = 0 while i < row: if map_data[i][j] == '.': i += 1 continue k = i + 1 while k < row and map_data[k][j] == map_data[i][j]: k += 1 if k - i >= 3: for t in range(i, k): to_remove.add((t, j)) i = k return to_remove def drop_elements(map_data, row, col): """下落:每列非 '.' 压到底部""" for j in range(col): write = row - 1 for i in range(row - 1, -1, -1): if map_data[i][j] != '.': map_data[write][j] = map_data[i][j] write -= 1 for i in range(write, -1, -1): map_data[i][j] = '.' def create_element(map_data, row, col, rng): """补新元素""" for i in range(row): for j in range(col): if map_data[i][j] == '.': map_data[i][j] = chr(ord('A') + rng.randint(0, 25)) # ============================================================ # 暴力搜索 # ============================================================ def _try_swap_and_count(map_data, row, col, x1, y1, x2, y2): """ L1: 只做一次交换 + 一次消除检测。 返回 (是否可消, 单次消除数)。 """ map_data[x1][y1], map_data[x2][y2] = map_data[x2][y2], map_data[x1][y1] matches = find_matches(map_data, row, col) map_data[x1][y1], map_data[x2][y2] = map_data[x2][y2], map_data[x1][y1] return (len(matches) > 0, len(matches)) def _simulate_chain(map_data, row, col, refill=False, rng=None): """ L2: 在副本上模拟完整连锁:消除 -> 下落 -> (可选补元素) -> 再检测。 返回总消除数。 """ test = [r[:] for r in map_data] total = 0 while True: matches = find_matches(test, row, col) if not matches: break total += len(matches) for (i, j) in matches: test[i][j] = '.' drop_elements(test, row, col) if refill and rng is not None: create_element(test, row, col, rng) return total def brute_force_moves(map_data, row, col, mode="l1", refill=False, rng=None): """ 穷举所有相邻交换(只扫右、下,避免重复)。 返回 [((x1,y1), (x2,y2), score), ...] """ moves = [] dirs = [(0, 1), (1, 0)] for i in range(row): for j in range(col): for di, dj in dirs: ni, nj = i + di, j + dj if ni >= row or nj >= col: continue if mode == "l2": # 临时交换后模拟完整连锁 map_data[i][j], map_data[ni][nj] = map_data[ni][nj], map_data[i][j] score = _simulate_chain(map_data, row, col, refill, rng) map_data[i][j], map_data[ni][nj] = map_data[ni][nj], map_data[i][j] else: # L1 ok, score = _try_swap_and_count( map_data, row, col, i, j, ni, nj ) if not ok: continue if score > 0: moves.append(((i, j), (ni, nj), score)) return moves def choose_best_move_brute(map_data, row, col, mode="l1", refill=False, rng=None): """ 暴力搜索并返回最优交换。 返回 ((x1,y1), (x2,y2)) 或 None。 """ moves = brute_force_moves(map_data, row, col, mode=mode, refill=refill, rng=rng) if not moves: return None moves.sort(key=lambda m: m[2], reverse=True) return moves[0][0], moves[0][1] # ============================================================ # 普通搜索(保留作为对照) # ============================================================ def find_all_valid_moves(map_data, row, col): moves = [] for i in range(row): for j in range(col): for di, dj in [(0, 1), (1, 0)]: ni, nj = i + di, j + dj if ni >= row or nj >= col: continue test = [r[:] for r in map_data] test[i][j], test[ni][nj] = test[ni][nj], test[i][j] matches = find_matches(test, row, col) if matches: moves.append(((i, j), (ni, nj), len(matches))) return moves def choose_best_move(map_data, row, col, strategy="brute", rng=None): """ strategy: "max" -> 单次消除最多(普通) "first" -> 第一个可消(普通) "random" -> 随机(普通) "brute" -> 穷举暴力(用 Config.BRUTE_MODE 决定 L1 / L2) """ if strategy == "brute": return choose_best_move_brute( map_data, row, col, mode=Config.BRUTE_MODE, refill=Config.BRUTE_REFILL, rng=rng ) moves = find_all_valid_moves(map_data, row, col) if not moves: return None if strategy == "max": moves.sort(key=lambda m: m[2], reverse=True) return moves[0][0], moves[0][1] elif strategy == "random": rng = rng or random.Random() m = rng.choice(moves) return m[0], m[1] else: return moves[0][0], moves[0][1] # ============================================================ # 控制器:ctypes 极速点击 # ============================================================ class Controller: @staticmethod def cell_to_screen(x, y): cw, ch = Config.cell_w(), Config.cell_h() sx = Config.GAME_X + x * cw + cw // 2 sy = Config.GAME_Y + y * ch + ch // 2 return sx, sy @staticmethod def click_cell(x, y): sx, sy = Controller.cell_to_screen(x, y) ctypes.windll.user32.SetCursorPos(sx, sy) ctypes.windll.user32.mouse_event(2, 0, 0, 0, 0) # 左键按下 ctypes.windll.user32.mouse_event(4, 0, 0, 0, 0) # 左键松开 @staticmethod def swap(x1, y1, x2, y2): Controller.click_cell(x1, y1) Controller.click_cell(x2, y2) # ============================================================ # 自动挂机线程 # ============================================================ class AutoPlayer(threading.Thread): def __init__(self, vision, controller, strategy="brute"): super().__init__(daemon=True) self.vision = vision self.controller = controller self.strategy = strategy self.running = False self.paused = False self.steps = 0 self.rng = random.Random() def run(self): self.running = True while self.running: if self.paused: continue map_data = self.vision.read_map() if map_data is None: continue move = choose_best_move( map_data, Config.ROWS, Config.COLS, strategy=self.strategy, rng=self.rng ) if move is None: print("[挂机] 无可消操作,等待...") time.sleep(0.001) continue (x1, y1), (x2, y2) = move print(f"[挂机] 第 {self.steps+1} 步: " f"({x1},{y1}) <-> ({x2},{y2})") self.controller.swap(x1, y1, x2, y2) self.steps += 1 def stop(self): self.running = False def toggle_pause(self): self.paused = not self.paused return self.paused # ============================================================ # GUI # ============================================================ class App: def __init__(self): self.root = tk.Tk() self.root.title("三消自动挂机(暴力搜索版)") self.root.geometry("360x380") self.root.attributes("-topmost", True) self.vision = Vision() self.controller = Controller() self.player = None self._build_ui() self._bind_hotkeys() def _build_ui(self): frm = ttk.Frame(self.root, padding=10) frm.pack(fill=tk.BOTH, expand=True) ttk.Label(frm, text="策略:").pack(anchor="w") self.strategy_var = tk.StringVar(value="random") ttk.Combobox( frm, textvariable=self.strategy_var, values=["brute", "max", "first", "random"], state="readonly" ).pack(fill=tk.X, pady=4) ttk.Label(frm, text="暴力模式:").pack(anchor="w") self.brute_var = tk.StringVar(value="l1") ttk.Combobox( frm, textvariable=self.brute_var, values=["l1", "l2"], state="readonly" ).pack(fill=tk.X, pady=4) ttk.Button(frm, text="重新框选 (F7)", command=self.reselect)\ .pack(fill=tk.X, pady=4) ttk.Button(frm, text="开始 (F8)", command=self.start)\ .pack(fill=tk.X, pady=4) ttk.Button(frm, text="暂停/继续 (F9)", command=self.toggle_pause)\ .pack(fill=tk.X, pady=4) ttk.Button(frm, text="停止 (F10)", command=self.stop)\ .pack(fill=tk.X, pady=4) self.status_var = tk.StringVar(value="状态: 未运行") ttk.Label(frm, textvariable=self.status_var).pack(anchor="w", pady=6) self.region_var = tk.StringVar() self._update_region_label() ttk.Label(frm, textvariable=self.region_var).pack(anchor="w") def _update_region_label(self): self.region_var.set( f"区域: x={Config.GAME_X}, y={Config.GAME_Y}, " f"w={Config.GAME_W}, h={Config.GAME_H}" ) def _bind_hotkeys(self): keyboard.add_hotkey("f7", self.reselect) keyboard.add_hotkey("f8", self.start) keyboard.add_hotkey("f9", self.toggle_pause) keyboard.add_hotkey("f10", self.stop) keyboard.add_hotkey("f12", self.root.destroy) def reselect(self): self.stop() self.root.withdraw() def done(x, y, w, h): self.root.deiconify() self._update_region_label() self.status_var.set("状态: 区域已更新") select_region_interactive(on_done=done) def start(self): if self.player and self.player.running: return # 把 GUI 选择写回 Config Config.BRUTE_MODE = self.brute_var.get() self.player = AutoPlayer( self.vision, self.controller, strategy=self.strategy_var.get() ) self.player.start() self.status_var.set("状态: 运行中") def toggle_pause(self): if not self.player: return paused = self.player.toggle_pause() self.status_var.set("状态: 暂停" if paused else "状态: 运行中") def stop(self): if self.player: self.player.stop() self.status_var.set("状态: 已停止") def run(self): self.root.mainloop() # ============================================================ # 入口 # ============================================================ if __name__ == "__main__": try: ctypes.windll.shcore.SetProcessDpiAwareness(1) except Exception: pass if not load_region(): print("[区域] 未找到 region.json,启动框选工具...") select_region_interactive() App().run()

python-宝石迷阵3(CHS 中文版)-Bejeweled 3 经典模式游戏自动化脚本源代码.zip
https://download.csdn.net/download/qq_32257509/93805601

RELATED READING

延伸阅读

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