简介:这是一份面向Python初学者与中级开发者的连连看小游戏完整源代码,适合想通过实战项目巩固语法、理解GUI编程与游戏逻辑的读者。压缩包共2个文件,包含1个py主程序与1个png图片素材,整体约84KB,轻量易读,下载后可直接运行并对照源码学习。项目以Tkinter构建窗口与画布控件,通过事件绑定实现点击交互,用二维数组表示游戏板布局,并借助DFS或BFS完成相同图片的路径匹配判断,同时涉及定时器驱动的消除动画、异常捕获与日志记录等实用技巧。代码结构清晰,便于在此基础上调整图片数量与布局来改变难度,或扩展双人对战等玩法。目前已有5667人学习下载,是练习Python基础、图形界面与算法思路的合适参考案例。
1. 从一份 Python 连连看小游戏源代码说起:它到底能跑出什么
很多人第一次搜「Python 连连看小游戏源代码」,心里想的其实不是「我要读一份源码」,而是「我想在自己电脑上跑出一个能点、能消、能计时的连连看」。这个诉求很朴素,但真正落地时会发现,一份能跑的连连看源码背后至少压着四件事:图形界面怎么画、两张牌能不能连的判定逻辑怎么写、消除之后怎么补位、计时和关卡怎么收尾。把这四件事拆开,它就不再是一团黑匣子,而是一个可以逐块替换、逐块调试的小工程。
这份源码适合两类人:一类是刚学完 Python 基础语法,想找一个比「猜数字」更有成就感的练手项目;另一类是已经会写脚本,但没碰过 GUI 和状态管理,想借连连看把「界面 + 逻辑 + 状态」串一遍。它不解决高并发,也不涉及复杂算法,但它能让你第一次完整地看到一个小游戏从数据到画面的闭环。下面我按「先跑起来、再拆逻辑、最后调手感」的顺序,把这份源代码里真正值得抄的部分讲清楚。
2. 把 Python 连连看小游戏源代码跑起来:环境、依赖与最小启动命令
2.1 选 tkinter 还是 pygame:先看你的 Python 安装教程走到哪一步
搜「python安装教程」和「python下载安装教程」的人,通常卡在第一步:装完 Python 之后不知道下一步装什么。连连看这类小游戏,常见做法是两条路。第一条是 tkinter,它是 Python 标准库自带的,装完 Python 就有,不需要额外 pip install,适合「python入门」阶段只想先看到窗口的人。第二条是 pygame,需要单独安装,但绘图和事件循环更顺手,适合后面想加音效、动画、粒子效果的人。
我一般会先看源码里 import 了什么。如果开头是import tkinter as tk,那这份源码基本零依赖,直接跑。如果是import pygame,那就先确认 pip 能用。下面这段命令用来确认环境和安装依赖,注意路径和版本号按你自己机器上的来,不要照抄别人的绝对路径。
# 确认 Python 是否可用,以及版本 python --version # 如果源码用 pygame,安装依赖 pip install pygame # 进入源码所在目录后启动,文件名按实际改 python lianliankan.py逻辑说明:第一行只是确认解释器存在,避免出现「命令找不到」这种低级翻车。第二行只在源码依赖 pygame 时才需要,tkinter 版本跳过。第三行是启动入口,很多源码的主文件叫main.py或game.py,不要死记文件名,先看目录里哪个文件包含if __name__ == "__main__":。
参数说明:--version看的是解释器版本,建议 3.8 以上,太低会遇到 f-string 或类型注解不兼容。pip install后面跟的包名必须和源码 import 的一致,pygame 就是 pygame,不要写成 pyGame。启动命令里的文件名区分大小写,Windows 上不敏感,macOS 和 Linux 上写错就报No such file or directory。
提示:如果你用的是 vscode,先按「vscode python环境配置」把解释器选对,再在终端里跑上面的命令。解释器选错时,pip 装到了另一个 Python 里,代码里 import 照样失败,这是最常见的「装了却用不了」。
2.2 最小可运行结构:窗口、画布、事件循环三件套
一份能跑的连连看源码,不管界面多花,骨架都逃不出三件套:创建窗口、在窗口上画格子、进入事件循环等待点击。下面这段是 tkinter 版本的最小骨架,我把它从完整源码里剥出来,方便你先确认「窗口能出来」再往里填逻辑。
import tkinter as tk # 1. 创建主窗口 root = tk.Tk() root.title("连连看") root.geometry("480x560") # 2. 创建画布,所有格子都画在这上面 canvas = tk.Canvas(root, width=480, height=480, bg="#f0f0f0") canvas.pack() # 3. 画一个测试格子,确认坐标系方向 canvas.create_rectangle(10, 10, 70, 70, fill="#4a90d9", outline="white") canvas.create_text(40, 40, text="A", fill="white", font=("Arial", 20, "bold")) # 4. 进入事件循环,窗口才会保持显示 root.mainloop()逻辑说明:Tk()创建根窗口,geometry设定初始大小,宽高要和画布匹配,否则会出现滚动条或者格子被裁掉。Canvas是连连看的核心控件,所有牌面、选中框、连线提示都画在它上面。create_rectangle的四个参数是左上角和右下角坐标,tkinter 的 y 轴向下增长,和数学坐标系相反,第一次写容易把行和列搞反。mainloop()是阻塞调用,放在最后,它之前的所有绘制才会生效。
参数说明:width和height决定画布尺寸,格子大小乘以行列数不能超过它,否则最后一行或一列看不见。bg是背景色,选浅灰是为了让彩色牌面更清楚。font里的字号要跟格子大小匹配,格子 60 像素时字号 20 左右比较合适,太大文字会溢出。fill和outline控制填充色和边框色,边框用白色能做出卡片分隔感。
注意:如果你跑完只看到窗口但里面空白,先检查
pack()有没有调用,再检查绘制语句是不是写在了mainloop()后面。这两个顺序问题占了新手「窗口出不来内容」的一大半。
3. 连连看核心判定逻辑:两张牌能不能连,代码到底在算什么
3.1 连通判定的三种路径:直线、一个拐角、两个拐角
连连看的规则一句话能说完:两张相同图案的牌,如果能用不超过两个拐角的线连起来,且线不穿过其他牌,就能消除。但落到代码里,这句话要拆成三种情况分别判断。第一种是同行或同列且中间全空,直接连。第二种是有一个拐角,拐点必须在两张牌构成的矩形另外两个角上,且拐点到两张牌的路径都空。第三种是两个拐角,需要沿着某一行或某一列扫描,找到一条「两段竖直加一段水平」或「两段水平加一段竖直」的通路。
下面这段判定函数是这类源码里最值得逐行读的部分,我把它写成可独立测试的形式,去掉了界面依赖,方便你单独跑。
def can_link(board, r1, c1, r2, c2): """判断 (r1,c1) 和 (r2,c2) 两张牌能否连通,board 中 0 表示空位""" if board[r1][c1] != board[r2][c2] or board[r1][c1] == 0: return False # 情况一:同行或同列,中间全空 if r1 == r2 and all(board[r1][c] == 0 for c in range(min(c1, c2) + 1, max(c1, c2))): return True if c1 == c2 and all(board[r][c1] == 0 for r in range(min(r1, r2) + 1, max(r1, r2))): return True # 情况二:一个拐角,拐点在 (r1,c2) 或 (r2,c1) if board[r1][c2] == 0 and \ all(board[r1][c] == 0 for c in range(min(c1, c2) + 1, max(c1, c2))) and \ all(board[r][c2] == 0 for r in range(min(r1, r2) + 1, max(r1, r2))): return True if board[r2][c1] == 0 and \ all(board[r][c1] == 0 for r in range(min(r1, r2) + 1, max(r1, r2))) and \ all(board[r2][c] == 0 for c in range(min(c1, c2) + 1, max(c1, c2))): return True # 情况三:两个拐角,沿行或列扫描 for c in range(len(board[0])): if board[r1][c] == 0 and board[r2][c] == 0 and \ all(board[r1][x] == 0 for x in range(min(c1, c) + 1, max(c1, c))) and \ all(board[r2][x] == 0 for x in range(min(c2, c) + 1, max(c2, c))) and \ all(board[r][c] == 0 for r in range(min(r1, r2) + 1, max(r1, r2))): return True for r in range(len(board)): if board[r][c1] == 0 and board[r][c2] == 0 and \ all(board[x][c1] == 0 for x in range(min(r1, r) + 1, max(r1, r))) and \ all(board[x][c2] == 0 for x in range(min(r2, r) + 1, max(r2, r))) and \ all(board[r][x] == 0 for x in range(min(c1, c2) + 1, max(c1, c2))): return True return False逻辑说明:开头先排除「图案不同」和「点到空位」两种情况,这是防御性写法,避免后面索引越界或误判。同行同列的判断用all加range,注意range的起点要加一、终点不加,这样只检查两张牌之间的格子,不包含牌本身。一个拐角的判断里,拐点必须为空,且拐点到两张牌的两条路径都为空,两个条件缺一不可。两个拐角的扫描是外层循环找中间行或中间列,内层再用all验证三段路径,只要找到一条就返回 True。
参数说明:board是二维列表,0表示空位,非零表示牌面编号,相同编号才能消。r1, c1和r2, c2是两张牌的坐标,调用前要保证它们在合法范围内。len(board[0])取列数,len(board)取行数,行列不要写反。这个函数只做判定,不修改board,消除操作要另外写,这样职责分离,调试时可以先单独测判定。
提示:测试判定逻辑时,不要一上来就接界面。先手写几个小
board,比如 3x3 全空只放两张相同牌,用print(can_link(...))看输出。判定对了再接点击事件,能省掉大量「点了没反应但不知道哪错了」的时间。
3.2 消除、补位与重排:状态更新比判定更容易写错
判定通过之后,真正容易翻车的是状态更新。很多源码在这里出问题:消除后没有把board对应位置置零,导致下次判定还能连到已经消掉的牌;或者补位方向写反,牌往上跑还是往下跑和界面显示对不上。常见做法是消除后先置零,再决定是「直接留空」还是「整列下移」。留空实现简单,下移更像传统连连看,但下移会改变坐标,所有已记录的选中状态都要清掉。
def remove_pair(board, r1, c1, r2, c2): """消除两张牌,返回是否成功""" if not can_link(board, r1, c1, r2, c2): return False board[r1][c1] = 0 board[r2][c2] = 0 return True def drop_down(board): """整列下移,空位补到顶部,适合传统连连看手感""" rows, cols = len(board), len(board[0]) for c in range(cols): # 从下往上收集非零牌 column = [board[r][c] for r in range(rows) if board[r][c] != 0] # 顶部补零,底部放牌 new_col = [0] * (rows - len(column)) + column for r in range(rows): board[r][c] = new_col[r]逻辑说明:remove_pair先复用can_link做二次校验,避免外部直接调用时绕过规则。置零只改数据,不改界面,界面刷新由绘制函数统一处理,这样数据和显示不会互相干扰。drop_down按列处理,先取出该列所有非零牌,再在顶部补零,最后写回。这样牌会往下落,空位留在上方,符合多数人对连连看的预期。
参数说明:rows和cols从board实时取,不要写死,否则改行列数时容易漏改。new_col的长度必须等于rows,否则写回时索引对不上。如果源码选择「留空不补位」,那drop_down整个函数可以不要,但判定逻辑里对空位的处理要一致,不能一半补位一半留空。
注意:补位之后,之前选中的第一张牌的坐标可能已经失效。我一般会在补位后强制清空选中状态,宁可让玩家重新点一次,也不要出现「选中框停在空位上」这种玄学现象。
4. 界面绘制与交互:把数据变成能点的格子
4.1 用 Canvas 画牌面:坐标换算与点击命中
数据层用board[r][c]表示,界面层要把它画成一个个矩形。这里的关键是坐标换算:第 r 行第 c 列的格子,左上角 x 是c * cell_size,y 是r * cell_size。点击时反过来,用鼠标坐标除以格子大小取整,得到行列。这个换算写错,就会出现「点 A 消 B」的经典 bug。
CELL = 60 PADDING = 20 def draw_board(canvas, board, selected=None): canvas.delete("all") for r in range(len(board)): for c in range(len(board[0])): if board[r][c] == 0: continue x1 = PADDING + c * CELL y1 = PADDING + r * CELL x2 = x1 + CELL y2 = y1 + CELL color = "#ffd166" if selected == (r, c) else "#4a90d9" canvas.create_rectangle(x1, y1, x2, y2, fill=color, outline="white", width=2) canvas.create_text((x1 + x2) // 2, (y1 + y2) // 2, text=str(board[r][c]), fill="white", font=("Arial", 18, "bold")) def on_click(event, board): c = (event.x - PADDING) // CELL r = (event.y - PADDING) // CELL if 0 <= r < len(board) and 0 <= c < len(board[0]): return r, c return None逻辑说明:draw_board每次先delete("all")清空画布再重画,这是最省心的刷新方式,小游戏规模下性能足够。selected参数用来高亮当前选中的牌,颜色区分开,玩家才知道自己点了哪张。on_click把鼠标坐标减掉边距再整除格子大小,得到行列,最后做边界检查,防止点到画布外导致索引越界。
参数说明:CELL是格子边长,PADDING是画布四周留白,两者要一起调,改一个不改另一个会导致点击偏移。width=2是边框粗细,太粗会吃掉格子内部空间。font字号要小于CELL的三分之一,否则数字会贴边。selected用元组比较,不要用列表,列表不可哈希,后面如果放进集合会报错。
4.2 计时、关卡与重开:小游戏的状态收尾
能消之后,玩家会问「我消完了会怎样」。一份完整的连连看源码通常有倒计时、关卡递增、重开按钮。倒计时用root.after递归调用,不要用time.sleep,后者会卡住事件循环,窗口直接假死。关卡递增可以简单到「消完当前所有牌就加一层」,重开就是把board重新洗牌并重置计时。
import random def shuffle_board(rows, cols, kinds=8): """生成成对的牌面并打乱""" total = rows * cols pairs = total // 2 values = [i % kinds + 1 for i in range(pairs)] * 2 random.shuffle(values) return [values[r * cols:(r + 1) * cols] for r in range(rows)] def tick(root, label, remain): if remain <= 0: label.config(text="时间到") return label.config(text=f"剩余 {remain}s") root.after(1000, tick, root, label, remain - 1)逻辑说明:shuffle_board先生成成对的编号,再整体打乱,最后按行切片成二维列表。kinds控制图案种类,种类越多越难。tick用after每秒调自己一次,remain递减,到零就停。这样计时和界面刷新在同一个线程里,不会出现跨线程改控件的问题。
参数说明:rows * cols必须是偶数,否则pairs取整后会少一张牌,最后剩一张消不掉。kinds不要超过pairs,否则有些编号只出现一次,永远配不成对。after的第一个参数是毫秒,1000 就是 1 秒,改小会让倒计时变快,调试时可以改成 100 先看流程。
提示:重开时记得把
selected清空、把remain重置、把board重新洗牌,三件事少一件都会出现「重开后还能连到上一局的牌」这种后悔药都来不及吃的状态。
5. 避坑与排查:连连看源代码里最容易翻车的 5 个点
5.1 点了没反应,但代码没报错
现象:窗口正常显示,点击格子没有任何高亮或消除。原因通常是事件绑定写在了错误的控件上,比如绑到了root而不是canvas,或者bind的 event 名字写成了<Button-1>以外的形式。解决:确认canvas.bind("<Button-1>", handler),并且canvas有pack()或grid(),没有布局的控件收不到点击。
5.2 两张相同的牌明明能连,判定却返回 False
现象:肉眼看着中间没牌,但can_link就是 False。原因多半是range边界写错,把牌本身也算进了「中间格子」,或者行列参数传反。解决:在can_link里加一行print(r1, c1, r2, c2, board[r1][c1], board[r2][c2]),先确认坐标和牌面值对得上,再检查range(min+1, max)的写法。
5.3 消除后牌面没变,但再点原来的位置还能消
现象:界面看起来牌还在,实际数据已经置零,再点一次又触发一次消除。原因:remove_pair改了board但没有调用draw_board重绘,界面和数据不同步。解决:每次数据变更后立刻重绘,或者用一个统一的refresh()函数包住「改数据 + 重绘」两步,避免漏调。
5.4 倒计时越跑越快,或者窗口卡死
现象:计时器一秒跳好几格,或者点按钮没反应。原因:用了time.sleep放在主线程,或者after被重复注册了多次。解决:计时只用root.after,并且保证同一时间只有一个tick在跑,重开时先取消旧的after,用root.after_cancel把 id 存下来。
5.5 打包或换机器后提示缺少模块
现象:自己机器上能跑,换一台就ModuleNotFoundError。原因:源码用了第三方库但没记录依赖,或者对方 Python 版本太低。解决:在源码目录放一个requirements.txt,把pygame之类的写进去,对方pip install -r requirements.txt即可。标准库 tkinter 不需要写,但要在说明里注明「需要 Python 3.8+」。
6. 进阶技巧:把这份源代码改成你自己的版本
跑通之后,最有价值的动作不是继续读,而是改。我一般会先改三个地方,改完基本就能判断这份源码值不值得继续投入。第一,把牌面从数字换成 emoji 或图片,这一步会逼你理解create_text和create_image的差别,也会暴露坐标换算有没有写死。第二,把固定行列改成从配置文件读,这一步会逼你把所有写死的8和10换成变量,顺便清理掉隐藏的魔法数字。第三,加一个「提示」按钮,自动找一对可连的牌并高亮,这一步会逼你把判定逻辑抽成可复用的函数,而不是散落在点击事件里。
验证改动是否成功,不要靠肉眼扫。写一个简单的自测:生成一个已知可连的board,调用can_link断言为 True;再生成一个中间有阻挡的board,断言为 False。把这两个断言放在if __name__ == "__main__":下面,每次改完先跑测试再开界面,能挡掉大部分回归问题。
if __name__ == "__main__": # 自测:同行无阻挡应可连 b1 = [[1, 0, 1], [0, 0, 0], [0, 0, 0]] assert can_link(b1, 0, 0, 0, 2) is True # 自测:中间有阻挡应不可连 b2 = [[1, 2, 1], [0, 0, 0], [0, 0, 0]] assert can_link(b2, 0, 0, 0, 2) is False print("判定逻辑自测通过")参数说明:b1和b2都是最小可验证用例,行列数小,出错时容易定位。断言里的坐标要和board实际尺寸匹配,越界会直接抛异常而不是返回 False。print只是给你一个正向反馈,真正跑界面时这段可以保留,不影响启动。
我自己的习惯是:任何一份小游戏源代码,先跑通、再自测、最后才改界面。顺序反了,就会在「界面好看但逻辑不对」的状态里反复翻车。这份连连看源码不大,但它把 GUI、状态、判定、计时都串了一遍,认真改完一轮,比看十篇 python教程 都管用。希望帮到你。
本文还有配套的精品资源,点击获取