消消乐游戏代码
import random, time, pygame, sys, copy
from pygame.locals import *
FPS = 30
WINDOWWIDTH = 600
WINDOWHEIGHT = 600
BOARDWIDTH = 8
BOARDHEIGHT = 8
GEMIMAGESIZE = 64
NUMGEMIMAGES = 7
assert NUMGEMIMAGES >= 5
NUMMATCHSOUNDS = 6
MOVERATE = 25
DEDUCTSPEED = 0.8
PURPLE = (255, 0, 255)
LIGHTBLUE = (170, 190, 255)
BLUE = ( 0, 0, 255)
RED = (255, 100, 100)
BLACK = ( 0, 0, 0)
BROWN = ( 85, 65, 0)
HIGHLIGHTCOLOR = PURPLE
BGCOLOR = LIGHTBLUE
GRIDCOLOR = BLUE
GAMEOVERCOLOR = RED
GAMEOVERBGCOLOR = BLACK
SCORECOLOR = BROWN
XMARGIN = int((WINDOWWIDTH – GEMIMAGESIZE * BOARDWIDTH) / 2)
YMARGIN = int((WINDOWHEIGHT – GEMIMAGESIZE * BOARDHEIGHT) / 2)
UP = 'up'
DOWN = 'down'
LEFT = 'left'
RIGHT = 'right'
EMPTY_SPACE = -1
ROWABOVEBOARD = 'row above board'
def main():
global FPSCLOCK, DISPLAYSURF, GEMIMAGES, GAMESOUNDS, BASICFONT, BOARDRECTS
pygame.init()
FPSCLOCK = pygame.time.Clock()
DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))
pygame.display.set_caption('Gemgem')
BASICFONT = pygame.font.Font('freesansbold.ttf', 36)
GEMIMAGES = []
for i in range(1, NUMGEMIMAGES+1):
gemImage = pygame.image.load('gem%s.png' % i)
if gemImage.get_size() != (GEMIMAGESIZE, GEMIMAGESIZE):
gemImage = pygame.transform.smoothscale(gemImage, (GEMIMAGESIZE, GEMIMAGESIZE))
GEMIMAGES.append(gemImage)
GAMESOUNDS = {}
GAMESOUNDS['bad swap'] = pygame.mixer.Sound('badswap.wav')
GAMESOUNDS['match'] = []
for i in range(NUMMATCHSOUNDS):
GAMESOUNDS['match'].append(pygame.mixer.Sound('match%s.wav' % i))
BOARDRECTS = []
for x in range(BOARDWIDTH):
BOARDRECTS.append([])
for y in range(BOARDHEIGHT):
r = pygame.Rect((XMARGIN + (x * GEMIMAGESIZE),
YMARGIN + (y * GEMIMAGESIZE),
GEMIMAGESIZE,
GEMIMAGESIZE))
BOARDRECTS[x].append(r)
while True:
runGame()
def runGame():
gameBoard = getBlankBoard()
score = 0
fillBoardAndAnimate(gameBoard, [], score)
firstSelectedGem = None
lastMouseDownX = None
lastMouseDownY = None
gameIsOver = False
lastScoreDeduction = time.time()
clickContinueTextSurf = None
while True:
clickedSpace = None
for event in pygame.event.get():
if event.type == QUIT or (event.type == KEYUP and event.key == K_ESCAPE):
pygame.quit()
sys.exit()
elif event.type == KEYUP and event.key == K_BACKSPACE:
return
elif event.type == MOUSEBUTTONUP:
if gameIsOver:
return
if event.pos == (lastMouseDownX, lastMouseDownY):
clickedSpace = checkForGemClick(event.pos)
else:
firstSelectedGem = checkForGemClick((lastMouseDownX, lastMouseDownY))
clickedSpace = checkForGemClick(event.pos)
if not firstSelectedGem or not clickedSpace:
firstSelectedGem = None
clickedSpace = None
elif event.type == MOUSEBUTTONDOWN:
lastMouseDownX, lastMouseDownY = event.pos
if clickedSpace and not firstSelectedGem:
firstSelectedGem = clickedSpace
elif clickedSpace and firstSelectedGem:
firstSwappingGem, secondSwappingGem = getSwappingGems(gameBoard, firstSelectedGem, clickedSpace)
if firstSwappingGem == None and secondSwappingGem == None:
firstSelectedGem = None
continue
boardCopy = getBoardCopyMinusGems(gameBoard, (firstSwappingGem, secondSwappingGem))
animateMovingGems(boardCopy, [firstSwappingGem, secondSwappingGem], [], score)
gameBoard[firstSwappingGem['x']][firstSwappingGem['y']] = secondSwappingGem['imageNum']
gameBoard[secondSwappingGem['x']][secondSwappingGem['y']] = firstSwappingGem['imageNum']
matchedGems = findMatchingGems(gameBoard)
if matchedGems == []:
GAMESOUNDS['bad swap'].play()
animateMovingGems(boardCopy, [firstSwappingGem, secondSwappingGem], [], score)
gameBoard[firstSwappingGem['x']][firstSwappingGem['y']] = firstSwappingGem['imageNum']
gameBoard[secondSwappingGem['x']][secondSwappingGem['y']] = secondSwappingGem['imageNum']
else:
scoreAdd = 0
while matchedGems != []:
points = []
for gemSet in matchedGems:
scoreAdd += (10 + (len(gemSet) – 3) * 10)
for gem in gemSet:
gameBoard[gem[0]][gem[1]] = EMPTY_SPACE
points.append({'points': scoreAdd,
'x': gem[0] * GEMIMAGESIZE + XMARGIN,
'y': gem[1] * GEMIMAGESIZE + YMARGIN})
random.choice(GAMESOUNDS['match']).play()
score += scoreAdd
fillBoardAndAnimate(gameBoard, points, score)
matchedGems = findMatchingGems(gameBoard)
firstSelectedGem = None
if not canMakeMove(gameBoard):
gameIsOver = True
DISPLAYSURF.fill(BGCOLOR)
drawBoard(gameBoard)
if firstSelectedGem != None:
highlightSpace(firstSelectedGem['x'], firstSelectedGem['y'])
if gameIsOver:
if clickContinueTextSurf == None:
clickContinueTextSurf = BASICFONT.render('Final Score: %s (Click to continue)' % (score), 1, GAMEOVERCOLOR, GAMEOVERBGCOLOR)
clickContinueTextRect = clickContinueTextSurf.get_rect()
clickContinueTextRect.center = int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2)
DISPLAYSURF.blit(clickContinueTextSurf, clickContinueTextRect)
elif score > 0 and time.time() – lastScoreDeduction > DEDUCTSPEED:
score -= 1
lastScoreDeduction = time.time()
drawScore(score)
pygame.display.update()
FPSCLOCK.tick(FPS)
def getSwappingGems(board, firstXY, secondXY):
firstGem = {'imageNum': board[firstXY['x']][firstXY['y']],
'x': firstXY['x'],
'y': firstXY['y']}
secondGem = {'imageNum': board[secondXY['x']][secondXY['y']],
'x': secondXY['x'],
'y': secondXY['y']}
highlightedGem = None
if firstGem['x'] == secondGem['x'] + 1 and firstGem['y'] == secondGem['y']:
firstGem['direction'] = LEFT
secondGem['direction'] = RIGHT
elif firstGem['x'] == secondGem['x'] – 1 and firstGem['y'] == secondGem['y']:
firstGem['direction'] = RIGHT
secondGem['direction'] = LEFT
elif firstGem['y'] == secondGem['y'] + 1 and firstGem['x'] == secondGem['x']:
firstGem['direction'] = UP
secondGem['direction'] = DOWN
elif firstGem['y'] == secondGem['y'] – 1 and firstGem['x'] == secondGem['x']:
firstGem['direction'] = DOWN
secondGem['direction'] = UP
else:
return None, None
return firstGem, secondGem
def getBlankBoard():
board = []
for x in range(BOARDWIDTH):
board.append([EMPTY_SPACE] * BOARDHEIGHT)
return board
def canMakeMove(board):
oneOffPatterns = (((0,1), (1,0), (2,0)),
((0,1), (1,1), (2,0)),
((0,0), (1,1), (2,0)),
((0,1), (1,0), (2,1)),
((0,0), (1,0), (2,1)),
((0,0), (1,1), (2,1)),
((0,0), (0,2), (0,3)),
((0,0), (0,1), (0,3)))
for x in range(BOARDWIDTH):
for y in range(BOARDHEIGHT):
for pat in oneOffPatterns:
if (getGemAt(board, x+pat[0][0], y+pat[0][1]) == \\
getGemAt(board, x+pat[1][0], y+pat[1][1]) == \\
getGemAt(board, x+pat[2][0], y+pat[2][1]) != None) or \\
(getGemAt(board, x+pat[0][1], y+pat[0][0]) == \\
getGemAt(board, x+pat[1][1], y+pat[1][0]) == \\
getGemAt(board, x+pat[2][1], y+pat[2][0]) != None):
return True
return False
def drawMovingGem(gem, progress):
movex = 0
movey = 0
progress *= 0.01
if gem['direction'] == UP:
movey = -int(progress * GEMIMAGESIZE)
elif gem['direction'] == DOWN:
movey = int(progress * GEMIMAGESIZE)
elif gem['direction'] == RIGHT:
movex = int(progress * GEMIMAGESIZE)
elif gem['direction'] == LEFT:
movex = -int(progress * GEMIMAGESIZE)
basex = gem['x']
basey = gem['y']
if basey == ROWABOVEBOARD:
basey = -1
pixelx = XMARGIN + (basex * GEMIMAGESIZE)
pixely = YMARGIN + (basey * GEMIMAGESIZE)
r = pygame.Rect( (pixelx + movex, pixely + movey, GEMIMAGESIZE, GEMIMAGESIZE) )
DISPLAYSURF.blit(GEMIMAGES[gem['imageNum']], r)
def pullDownAllGems(board):
for x in range(BOARDWIDTH):
gemsInColumn = []
for y in range(BOARDHEIGHT):
if board[x][y] != EMPTY_SPACE:
gemsInColumn.append(board[x][y])
board[x] = ([EMPTY_SPACE] * (BOARDHEIGHT – len(gemsInColumn))) + gemsInColumn
def getGemAt(board, x, y):
if x < 0 or y < 0 or x >= BOARDWIDTH or y >= BOARDHEIGHT:
return None
else:
return board[x][y]
def getDropSlots(board):
boardCopy = copy.deepcopy(board)
pullDownAllGems(boardCopy)
dropSlots = []
for i in range(BOARDWIDTH):
dropSlots.append([])
for x in range(BOARDWIDTH):
for y in range(BOARDHEIGHT-1, -1, -1):
if boardCopy[x][y] == EMPTY_SPACE:
possibleGems = list(range(len(GEMIMAGES)))
for offsetX, offsetY in ((0, -1), (1, 0), (0, 1), (-1, 0)):
neighborGem = getGemAt(boardCopy, x + offsetX, y + offsetY)
if neighborGem != None and neighborGem in possibleGems:
possibleGems.remove(neighborGem)
newGem = random.choice(possibleGems)
boardCopy[x][y] = newGem
dropSlots[x].append(newGem)
return dropSlots
def findMatchingGems(board):
gemsToRemove = []
boardCopy = copy.deepcopy(board)
for x in range(BOARDWIDTH):
for y in range(BOARDHEIGHT):
if getGemAt(boardCopy, x, y) == getGemAt(boardCopy, x + 1, y) == getGemAt(boardCopy, x + 2, y) and getGemAt(boardCopy, x, y) != EMPTY_SPACE:
targetGem = boardCopy[x][y]
offset = 0
removeSet = []
while getGemAt(boardCopy, x + offset, y) == targetGem:
removeSet.append((x + offset, y))
boardCopy[x + offset][y] = EMPTY_SPACE
offset += 1
gemsToRemove.append(removeSet)
if getGemAt(boardCopy, x, y) == getGemAt(boardCopy, x, y + 1) == getGemAt(boardCopy, x, y + 2) and getGemAt(boardCopy, x, y) != EMPTY_SPACE:
targetGem = boardCopy[x][y]
offset = 0
removeSet = []
while getGemAt(boardCopy, x, y + offset) == targetGem:
removeSet.append((x, y + offset))
boardCopy[x][y + offset] = EMPTY_SPACE
offset += 1
gemsToRemove.append(removeSet)
return gemsToRemove
def highlightSpace(x, y):
pygame.draw.rect(DISPLAYSURF, HIGHLIGHTCOLOR, BOARDRECTS[x][y], 4)
def getDroppingGems(board):
boardCopy = copy.deepcopy(board)
droppingGems = []
for x in range(BOARDWIDTH):
for y in range(BOARDHEIGHT – 2, -1, -1):
if boardCopy[x][y + 1] == EMPTY_SPACE and boardCopy[x][y] != EMPTY_SPACE:
droppingGems.append( {'imageNum': boardCopy[x][y], 'x': x, 'y': y, 'direction': DOWN} )
boardCopy[x][y] = EMPTY_SPACE
return droppingGems
def animateMovingGems(board, gems, pointsText, score):
progress = 0
while progress < 100:
DISPLAYSURF.fill(BGCOLOR)
drawBoard(board)
for gem in gems:
drawMovingGem(gem, progress)
drawScore(score)
for pointText in pointsText:
pointsSurf = BASICFONT.render(str(pointText['points']), 1, SCORECOLOR)
pointsRect = pointsSurf.get_rect()
pointsRect.center = (pointText['x'], pointText['y'])
DISPLAYSURF.blit(pointsSurf, pointsRect)
pygame.display.update()
FPSCLOCK.tick(FPS)
progress += MOVERATE
def moveGems(board, movingGems):
for gem in movingGems:
if gem['y'] != ROWABOVEBOARD:
board[gem['x']][gem['y']] = EMPTY_SPACE
movex = 0
movey = 0
if gem['direction'] == LEFT:
movex = -1
elif gem['direction'] == RIGHT:
movex = 1
elif gem['direction'] == DOWN:
movey = 1
elif gem['direction'] == UP:
movey = -1
board[gem['x'] + movex][gem['y'] + movey] = gem['imageNum']
else:
board[gem['x']][0] = gem['imageNum']
def fillBoardAndAnimate(board, points, score):
dropSlots = getDropSlots(board)
while dropSlots != [[]] * BOARDWIDTH:
movingGems = getDroppingGems(board)
for x in range(len(dropSlots)):
if len(dropSlots[x]) != 0:
# cause the lowest gem in each slot to begin moving in the DOWN direction
movingGems.append({'imageNum': dropSlots[x][0], 'x': x, 'y': ROWABOVEBOARD, 'direction': DOWN})
boardCopy = getBoardCopyMinusGems(board, movingGems)
animateMovingGems(boardCopy, movingGems, points, score)
moveGems(board, movingGems)
for x in range(len(dropSlots)):
if len(dropSlots[x]) == 0:
continue
board[x][0] = dropSlots[x][0]
del dropSlots[x][0]
def checkForGemClick(pos):
for x in range(BOARDWIDTH):
for y in range(BOARDHEIGHT):
if BOARDRECTS[x][y].collidepoint(pos[0], pos[1]):
return {'x': x, 'y': y}
return None
def drawBoard(board):
for x in range(BOARDWIDTH):
for y in range(BOARDHEIGHT):
pygame.draw.rect(DISPLAYSURF, GRIDCOLOR, BOARDRECTS[x][y], 1)
gemToDraw = board[x][y]
if gemToDraw != EMPTY_SPACE:
DISPLAYSURF.blit(GEMIMAGES[gemToDraw], BOARDRECTS[x][y])
def getBoardCopyMinusGems(board, gems):
boardCopy = copy.deepcopy(board)
for gem in gems:
if gem['y'] != ROWABOVEBOARD:
boardCopy[gem['x']][gem['y']] = EMPTY_SPACE
return boardCopy
def drawScore(score):
scoreImg = BASICFONT.render(str(score), 1, SCORECOLOR)
scoreRect = scoreImg.get_rect()
scoreRect.bottomleft = (10, WINDOWHEIGHT – 6)
DISPLAYSURF.blit(scoreImg, scoreRect)
if __name__ == '__main__':
main()
消消乐游戏自动化脚本代码
"""
Gemgem 独立自动化脚本(tkinter 图形界面 + 拖动条 + 可调网格)
"""
import os, json, time, copy, threading
import numpy as np
import cv2
import pyautogui
from PIL import Image, ImageTk
import tkinter as tk
from tkinter import ttk
# ———- 默认常量(可由界面调节) ———-
DEFAULT_WINDOWWIDTH = 600
DEFAULT_WINDOWHEIGHT = 600
DEFAULT_BOARDWIDTH = 8
DEFAULT_BOARDHEIGHT = 8
DEFAULT_GEMIMAGESIZE = 64
NUMGEMIMAGES = 7
EMPTY_SPACE = -1
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
CONFIG_PATH = os.path.join(BASE_DIR, 'auto_config.json')
DEFAULT_CONFIG = {
"window_left": 0,
"window_top": 0,
"capture_width": DEFAULT_WINDOWWIDTH,
"capture_height": DEFAULT_WINDOWHEIGHT,
"board_w": DEFAULT_BOARDWIDTH,
"board_h": DEFAULT_BOARDHEIGHT,
"gem_size": DEFAULT_GEMIMAGESIZE,
"x_margin": -1, # -1 表示自动居中
"y_margin": -1,
"match_threshold": 60,
"click_delay": 0.35,
"loop_delay": 0.30,
}
def load_config():
if os.path.exists(CONFIG_PATH):
try:
with open(CONFIG_PATH, 'r', encoding='utf-8') as f:
c = json.load(f)
m = dict(DEFAULT_CONFIG); m.update(c); return m
except Exception:
pass
return dict(DEFAULT_CONFIG)
def save_config(cfg):
with open(CONFIG_PATH, 'w', encoding='utf-8') as f:
json.dump(cfg, f, indent=2, ensure_ascii=False)
print(f"[信息] 配置已保存到 {CONFIG_PATH}")
def imread_unicode(path, flags=cv2.IMREAD_COLOR):
try:
return cv2.imdecode(np.fromfile(path, dtype=np.uint8), flags)
except Exception:
return None
def imwrite_unicode(path, img):
ext = os.path.splitext(path)[1]
ok, buf = cv2.imencode(ext, img)
if ok:
buf.tofile(path)
return ok
def get_margins(cfg):
"""根据配置算 XMARGIN / YMARGIN"""
bw, bh = cfg['board_w'], cfg['board_h']
gs = cfg['gem_size']
cw, ch = cfg['capture_width'], cfg['capture_height']
xm = cfg['x_margin']
ym = cfg['y_margin']
if xm < 0:
xm = int((cw – gs * bw) / 2)
if ym < 0:
ym = int((ch – gs * bh) / 2)
return xm, ym
def grab_window(cfg):
region = (int(cfg['window_left']), int(cfg['window_top']),
int(cfg['capture_width']), int(cfg['capture_height']))
shot = pyautogui.screenshot(region=region)
return cv2.cvtColor(np.array(shot), cv2.COLOR_RGB2BGR)
def load_gem_templates():
"""
从游戏画面里自动裁 64 个格子,k-means 聚成 7 类作为模板。
这样模板和截图完全同源,匹配分数低、识别准确。
前提:调用前棋盘必须完整可见、没有正在消除/下落的动画。
"""
import numpy as np
# 用默认参数截图一次(如果 cfg 已经存在则用 cfg)
# 这里不依赖 cfg,直接用 window_left/top = 0,0 的默认值
# 若你已在控制台调好窗口位置,请把这里改成从 cfg 读取
cfg_tmp = load_config()
frame = grab_window(cfg_tmp)
xm, ym = get_margins(cfg_tmp)
gs = cfg_tmp['gem_size']
bw, bh = cfg_tmp['board_w'], cfg_tmp['board_h']
# 裁 64 个格子
cells = []
for x in range(bw):
for y in range(bh):
px = xm + x * gs
py = ym + y * gs
cell = frame[py:py + gs, px:px + gs].copy()
if cell.shape[:2] != (gs, gs):
cell = cv2.resize(cell, (gs, gs))
cells.append(cell)
# 特征:缩到 16×16 拉直
feats = []
for c in cells:
small = cv2.resize(c, (16, 16)).astype(np.float32) / 255.0
feats.append(small.flatten())
feats = np.array(feats, dtype=np.float32)
# k-means 聚成 7 类
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 50, 0.5)
_, labels, centers = cv2.kmeans(
feats, NUMGEMIMAGES, None, criteria, 10, cv2.KMEANS_PP_CENTERS)
labels = labels.flatten()
templates = []
for k in range(NUMGEMIMAGES):
idxs = np.where(labels == k)[0]
if len(idxs) == 0:
print(f"[学习] 第 {k} 类无样本,跳过")
continue
dists = np.linalg.norm(feats[idxs] – centers[k], axis=1)
best = idxs[np.argmin(dists)]
templates.append(cells[best].copy())
# 保存到 learned_tpl*.png,方便以后复用(可选)
for i, t in enumerate(templates, 1):
imwrite_unicode(os.path.join(BASE_DIR, f'learned_tpl{i}.png'), t)
print(f"[信息] 已从画面学习 {len(templates)} 个模板,"
f"保存为 learned_tpl1~{len(templates)}.png")
return templates
def read_board(templates, frame, threshold, cfg):
bw, bh = cfg['board_w'], cfg['board_h']
gs = cfg['gem_size']
xm, ym = get_margins(cfg)
board = [[EMPTY_SPACE] * bh for _ in range(bw)]
for x in range(bw):
for y in range(bh):
px = xm + x * gs
py = ym + y * gs
if py + gs > frame.shape[0] or px + gs > frame.shape[1]:
continue
cell = frame[py:py + gs, px:px + gs]
if cell.shape[:2] != (gs, gs):
cell = cv2.resize(cell, (gs, gs))
best_idx, best_score = -1, 1e9
for idx, tpl in enumerate(templates):
diff = cv2.absdiff(cell, tpl)
score = float(np.mean(diff))
if score < best_score:
best_score, best_idx = score, idx
# === 调试输出 ===
print(f"[cell {x},{y}] best={best_idx} score={best_score:.1f}")
if best_score < threshold:
board[x][y] = best_idx
return board
# ———- 游戏逻辑 ———-
def getGemAt(board, x, y, bw, bh):
if x < 0 or y < 0 or x >= bw or y >= bh:
return None
return board[x][y]
def findMatchingGems(board, bw, bh):
out = []
b = copy.deepcopy(board)
for x in range(bw):
for y in range(bh):
if (getGemAt(b, x, y, bw, bh) == getGemAt(b, x+1, y, bw, bh)
== getGemAt(b, x+2, y, bw, bh)
and getGemAt(b, x, y, bw, bh) != EMPTY_SPACE):
t = b[x][y]; off = 0; s = []
while getGemAt(b, x+off, y, bw, bh) == t:
s.append((x+off, y)); b[x+off][y] = EMPTY_SPACE; off += 1
out.append(s)
if (getGemAt(b, x, y, bw, bh) == getGemAt(b, x, y+1, bw, bh)
== getGemAt(b, x, y+2, bw, bh)
and getGemAt(b, x, y, bw, bh) != EMPTY_SPACE):
t = b[x][y]; off = 0; s = []
while getGemAt(b, x, y+off, bw, bh) == t:
s.append((x, y+off)); b[x][y+off] = EMPTY_SPACE; off += 1
out.append(s)
return out
def find_all_moves(board, bw, bh):
moves = []
for x in range(bw):
for y in range(bh):
if x + 1 < bw:
b = copy.deepcopy(board)
b[x][y], b[x+1][y] = b[x+1][y], b[x][y]
m = findMatchingGems(b, bw, bh)
if m:
moves.append((sum(len(s) for s in m), (x, y, x+1, y)))
if y + 1 < bh:
b = copy.deepcopy(board)
b[x][y], b[x][y+1] = b[x][y+1], b[x][y]
m = findMatchingGems(b, bw, bh)
if m:
moves.append((sum(len(s) for s in m), (x, y, x, y+1)))
moves.sort(key=lambda t: -t[0])
return [m for _, m in moves]
def cell_center(cfg, x, y):
xm, ym = get_margins(cfg)
gs = cfg['gem_size']
cx = int(cfg['window_left']) + xm + x * gs + gs // 2
cy = int(cfg['window_top']) + ym + y * gs + gs // 2
return cx, cy
def do_swap(cfg, x1, y1, x2, y2):
p1 = cell_center(cfg, x1, y1)
p2 = cell_center(cfg, x2, y2)
pyautogui.moveTo(p1[0], p1[1], duration=0.05)
pyautogui.mouseDown()
pyautogui.moveTo(p2[0], p2[1], duration=0.12)
pyautogui.mouseUp()
time.sleep(cfg['click_delay'])
# ============================================================
# tkinter 界面
# ============================================================
class AutoGUI:
def __init__(self, root, cfg):
self.root = root
self.cfg = cfg
self.running = False
self.thread = None
self.photo = None
self.templates = None
root.title("Gemgem 自动化控制台")
root.resizable(False, False)
# ——– 左:预览 ——–
left = ttk.Frame(root, padding=8)
left.grid(row=0, column=0, sticky="n")
self.lbl_preview = ttk.Label(left,
text="游戏画面预览(紫色框=棋盘格)",
font=("Microsoft YaHei", 10))
self.lbl_preview.grid(row=0, column=0, pady=(0, 4))
self.canvas = tk.Canvas(left, width=cfg['capture_width'],
height=cfg['capture_height'],
bg="black", highlightthickness=1,
highlightbackground="#888")
self.canvas.grid(row=1, column=0)
# ——– 右:控制 ——–
right = ttk.Frame(root, padding=8)
right.grid(row=0, column=1, sticky="n")
row = 0
# ===== 位置 =====
ttk.Label(right, text="【位置】", font=("Microsoft YaHei", 10, "bold")
).grid(row=row, column=0, columnspan=2, sticky="w")
row += 1
self.var_x = tk.IntVar(value=cfg['window_left'])
self.var_y = tk.IntVar(value=cfg['window_top'])
row = self._add_slider(right, row, "窗口 X 偏移", self.var_x, 0, 2000,
lambda v: cfg.update(window_left=int(float(v))),
fmt="%d")
row = self._add_slider(right, row, "窗口 Y 偏移", self.var_y, 0, 2000,
lambda v: cfg.update(window_top=int(float(v))),
fmt="%d")
# ===== 网格尺寸 =====
ttk.Label(right, text="【网格尺寸】",
font=("Microsoft YaHei", 10, "bold")
).grid(row=row, column=0, columnspan=2, sticky="w", pady=(8, 0))
row += 1
self.var_gem = tk.IntVar(value=cfg['gem_size'])
row = self._add_slider(right, row, "每格像素", self.var_gem, 16, 200,
self._on_gem_size, fmt="%d")
self.var_bw = tk.IntVar(value=cfg['board_w'])
row = self._add_slider(right, row, "棋盘列数", self.var_bw, 4, 20,
self._on_board_size, fmt="%d")
self.var_bh = tk.IntVar(value=cfg['board_h'])
row = self._add_slider(right, row, "棋盘行数", self.var_bh, 4, 20,
self._on_board_size, fmt="%d")
# 边距(-1 自动)
self.var_xm = tk.IntVar(value=cfg['x_margin'])
row = self._add_slider(right, row, "水平边距(-1=自动)", self.var_xm,
-1, 500,
lambda v: cfg.update(x_margin=int(float(v))),
fmt="%d")
self.var_ym = tk.IntVar(value=cfg['y_margin'])
row = self._add_slider(right, row, "垂直边距(-1=自动)", self.var_ym,
-1, 500,
lambda v: cfg.update(y_margin=int(float(v))),
fmt="%d")
# ===== 截图尺寸 =====
ttk.Label(right, text="【截图尺寸】",
font=("Microsoft YaHei", 10, "bold")
).grid(row=row, column=0, columnspan=2, sticky="w", pady=(8, 0))
row += 1
self.var_cw = tk.IntVar(value=cfg['capture_width'])
row = self._add_slider(right, row, "截图宽度", self.var_cw, 200, 2000,
self._on_capture_size, fmt="%d")
self.var_ch = tk.IntVar(value=cfg['capture_height'])
row = self._add_slider(right, row, "截图高度", self.var_ch, 200, 2000,
self._on_capture_size, fmt="%d")
# ===== 识别参数 =====
ttk.Label(right, text="【识别参数】",
font=("Microsoft YaHei", 10, "bold")
).grid(row=row, column=0, columnspan=2, sticky="w", pady=(8, 0))
row += 1
self.var_th = tk.IntVar(value=cfg['match_threshold'])
row = self._add_slider(right, row, "匹配阈值(大=宽松)", self.var_th,
10, 150,
lambda v: cfg.update(match_threshold=int(float(v))),
fmt="%d")
self.var_cd = tk.DoubleVar(value=cfg['click_delay'])
row = self._add_slider(right, row, "点击后等待(秒)", self.var_cd,
0.1, 1.5,
lambda v: cfg.update(click_delay=round(float(v), 2)),
fmt="%.2f", is_float=True)
# ===== 按钮 =====
btns = ttk.Frame(right)
btns.grid(row=row, column=0, columnspan=2, pady=(10, 4), sticky="ew")
row += 1
ttk.Button(btns, text="鼠标校准", command=self.calibrate
).grid(row=0, column=0, padx=2, pady=2, sticky="ew")
ttk.Button(btns, text="保存配置", command=self.save
).grid(row=0, column=1, padx=2, pady=2, sticky="ew")
ttk.Button(btns, text="重置 0,0", command=self.reset_pos
).grid(row=1, column=0, padx=2, pady=2, sticky="ew")
ttk.Button(btns, text="退出", command=self.quit
).grid(row=1, column=1, padx=2, pady=2, sticky="ew")
btns.columnconfigure(0, weight=1)
btns.columnconfigure(1, weight=1)
# ===== 开始 / 停止 =====
self.btn_start = ttk.Button(right, text="▶ 开始自动化",
command=self.toggle_run)
self.btn_start.grid(row=row, column=0, columnspan=2,
pady=(8, 4), sticky="ew")
row += 1
ttk.Button(btns, text="学习模板", command=self.learn_templates_from_screen
).grid(row=2, column=0, columnspan=2, padx=2, pady=2, sticky="ew")
self.lbl_status = ttk.Label(right, text="就绪", foreground="#080",
font=("Microsoft YaHei", 10))
self.lbl_status.grid(row=row, column=0, columnspan=2, sticky="w")
row += 1
# ===== 日志 =====
ttk.Label(right, text="日志").grid(row=row, column=0, sticky="w",
pady=(8, 0))
row += 1
self.txt_log = tk.Text(right, width=40, height=12, font=("Consolas", 9))
self.txt_log.grid(row=row, column=0, columnspan=2, sticky="ew")
self.txt_log.configure(state="disabled")
root.protocol("WM_DELETE_WINDOW", self.quit)
# 启动预览
self.refresh_preview()
# ———- 滑块工具 ———-
def _add_slider(self, parent, row, label, var, from_, to_, cmd,
fmt="%d", is_float=False):
ttk.Label(parent, text=label).grid(row=row, column=0,
columnspan=2, sticky="w")
row += 1
s = ttk.Scale(parent, from_=from_, to=to_, orient="horizontal",
length=280)
s.set(var.get())
lbl = ttk.Label(parent, text=fmt % var.get(), width=8)
s.grid(row=row, column=0, sticky="ew")
lbl.grid(row=row, column=1, sticky="w")
# 把 fmt / label 挂到 Scale 上
s._fmt = fmt
s._label = lbl
s._var = var
s._cmd = cmd
s.configure(command=lambda v, scale=s: self._on_slider(v, scale))
row += 1
return row
def _on_slider(self, v, scale):
fmt = scale._fmt
var = scale._var
if fmt == "%d":
var.set(int(float(v)))
else:
var.set(round(float(v), 2))
scale._label.config(text=fmt % var.get())
if scale._cmd:
scale._cmd(v)
def _on_gem_size(self, v):
self.cfg['gem_size'] = int(float(v))
self.reload_templates()
def _on_board_size(self, v):
self.cfg['board_w'] = int(self.var_bw.get())
self.cfg['board_h'] = int(self.var_bh.get())
def _on_capture_size(self, v):
self.cfg['capture_width'] = int(self.var_cw.get())
self.cfg['capture_height'] = int(self.var_ch.get())
self.canvas.config(width=self.cfg['capture_width'],
height=self.cfg['capture_height'])
def reload_templates(self):
try:
self.templates = load_gem_templates(self.cfg['gem_size'])
except Exception as e:
self.log(f"[模板重载失败] {e}")
# ———- 预览 ———-
def refresh_preview(self):
try:
frame = grab_window(self.cfg)
vis = frame.copy()
xm, ym = get_margins(self.cfg)
gs = self.cfg['gem_size']
bw, bh = self.cfg['board_w'], self.cfg['board_h']
for x in range(bw):
for y in range(bh):
px = xm + x * gs
py = ym + y * gs
cv2.rectangle(vis, (px, py), (px + gs, py + gs),
(255, 0, 255), 2)
cv2.drawMarker(vis, (xm, ym), (0, 0, 255),
cv2.MARKER_CROSS, 16, 2)
rgb = cv2.cvtColor(vis, cv2.COLOR_BGR2RGB)
self.photo = ImageTk.PhotoImage(Image.fromarray(rgb))
self.canvas.delete("all")
self.canvas.create_image(0, 0, anchor="nw", image=self.photo)
except Exception as e:
self.log(f"[预览错误] {e}")
self.root.after(120, self.refresh_preview)
# ———- 校准 ———-
def calibrate(self):
self.log("3 秒内把鼠标移到游戏棋盘左上角…")
for i in range(3, 0, -1):
self.log(f" {i}…")
self.root.update()
time.sleep(1)
pos = pyautogui.position()
self.cfg['window_left'] = pos.x
self.cfg['window_top'] = pos.y
self.var_x.set(pos.x)
self.var_y.set(pos.y)
self.log(f"校准完成 X={pos.x} Y={pos.y}")
def reset_pos(self):
self.var_x.set(0); self.var_y.set(0)
self.cfg['window_left'] = 0
self.cfg['window_top'] = 0
def save(self):
save_config(self.cfg)
self.log("配置已保存")
# ———- 运行 ———-
def toggle_run(self):
if self.running:
self.running = False
self.btn_start.config(text="▶ 开始自动化")
self.lbl_status.config(text="已停止", foreground="#c00")
else:
if self.templates is None:
self.reload_templates()
if self.templates is None:
self.log("模板未加载,无法开始")
return
self.running = True
self.btn_start.config(text="■ 停止")
self.lbl_status.config(text="运行中", foreground="#080")
threading.Thread(target=self.worker, daemon=True).start()
def worker(self):
last, stable = None, 0
while self.running:
try:
frame = grab_window(self.cfg)
board = read_board(self.templates, frame,
self.cfg['match_threshold'], self.cfg)
imwrite_unicode(os.path.join(BASE_DIR, 'debug_window.png'), frame)
unknown = sum(1 for c in board for v in c if v == EMPTY_SPACE)
self.log(f"识别:未识别 {unknown}/{self.cfg['board_w'] * self.cfg['board_h']}")
if board == last:
stable += 1
else:
stable = 0
last = board
if stable >= 1 and unknown <= 8:
moves = find_all_moves(board, self.cfg['board_w'],
self.cfg['board_h'])
if moves:
x1, y1, x2, y2 = moves[0]
self.log(f" 交换 ({x1},{y1})<->({x2},{y2})")
do_swap(self.cfg, x1, y1, x2, y2)
else:
self.log(" 无可行移动")
time.sleep(self.cfg['loop_delay'])
except Exception as e:
self.log(f"[异常] {e}")
time.sleep(0.5)
def learn_templates_from_screen(self):
"""从当前画面裁 64 格聚成 7 类模板"""
import numpy as np
try:
frame = grab_window(self.cfg)
xm, ym = get_margins(self.cfg)
gs = self.cfg['gem_size']
bw, bh = self.cfg['board_w'], self.cfg['board_h']
cells = []
for x in range(bw):
for y in range(bh):
px = xm + x * gs
py = ym + y * gs
cell = frame[py:py + gs, px:px + gs].copy()
if cell.shape[:2] != (gs, gs):
cell = cv2.resize(cell, (gs, gs))
cells.append(cell)
feats = np.array([
cv2.resize(c, (16, 16)).astype(np.float32).flatten() / 255.0
for c in cells
], dtype=np.float32)
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 50, 0.5)
_, labels, centers = cv2.kmeans(
feats, NUMGEMIMAGES, None, criteria, 10, cv2.KMEANS_PP_CENTERS)
labels = labels.flatten()
new_tpls = []
for k in range(NUMGEMIMAGES):
idxs = np.where(labels == k)[0]
if len(idxs) == 0:
continue
dists = np.linalg.norm(feats[idxs] – centers[k], axis=1)
best = idxs[np.argmin(dists)]
new_tpls.append(cells[best].copy())
self.templates = new_tpls
for i, t in enumerate(new_tpls, 1):
imwrite_unicode(os.path.join(BASE_DIR, f'learned_tpl{i}.png'), t)
self.log(f"[学习] 从画面学习 {len(new_tpls)} 个模板")
except Exception as e:
self.log(f"[学习失败] {e}")
# ———- 日志 ———-
def log(self, msg):
print(msg)
self.txt_log.configure(state="normal")
self.txt_log.insert("end", msg + "\\n")
self.txt_log.see("end")
self.txt_log.configure(state="disabled")
def quit(self):
self.running = False
self.root.after(200, self.root.destroy)
# ============================================================
def main():
print("=" * 60)
print("Gemgem 自动化控制台")
print("=" * 60)
cfg = load_config()
root = tk.Tk()
app = AutoGUI(root, cfg) # 不再在启动时 load_gem_templates
root.mainloop()
if __name__ == '__main__':
main()





