资讯详情

Python+Pygame实战:连连看游戏核心算法与代码实现

📅 2026/9/9 11:35:00 | 华诺云谱 👁 阅读
Python+Pygame实战:连连看游戏核心算法与代码实现
简介QQ连连看单机版含源码与说明是一份面向游戏开发初学者和编程爱好者的完整学习资源解决了想在离线环境体验连连看并研究其实现原理的需求。压缩包共88个文件包含TGA/BMP图像素材、WAV/MID音频文件、多个DLL动态库、EXE可执行程序及说明文档和地图文件覆盖游戏画面、音效、动画特效、菜单交互、关卡地图等模块整体大小约4.44MB。已有323人学习下载。通过这份资源读者可以深入分析C风格的小游戏源代码学习GUI界面设计、动画特效实现、音效播放、多语言支持等关键技术同时丰富的素材与分模块DLL能帮助理解游戏框架搭建和资源整合方法。借助说明文档可迅速上手还能查看地图文件了解关卡设计思路适合作为游戏开发入门参考。 “QQ连连看单机含代码和说明”这个项目是我压箱底的一个练手作品。最近整理硬盘时翻出来发现代码注释还算完整玩法也基本复刻了经典连连看的手感干脆重新梳理一下发出来。游戏本身并不复杂一个矩形棋盘若干对相同图标玩家点击两个相同的图标如果它们之间能用不超过两个拐点的水平或垂直折线连通并且折线没有被其他图标挡住就可以消除全部消完就胜利。整个项目只依赖Python和Pygame非常适合刚学完Python基础、想拿一个小游戏练手的人。我当初写这个项目主要就是想搞清楚“连连看到底是怎么判断能不能连的”后来才发现核心就两个问题一个是地图数据结构怎么设计另一个是路径搜索怎么写。这两个点搞明白了界面反而是体力活。这篇文章我会把项目拆开讲从数据结构、连通判定、界面交互到无解处理最后附上踩坑记录你可以直接照着实现也可以拿它做二次开发。完整代码是用Python写的Pygame负责窗口、绘制和事件算法部分不依赖任何第三方组件单独拎出来也能测试。1. 项目落地前的思路拆解1.1 这个项目到底在解决什么问题连连看这类消除游戏看起来是个小玩意真正写起来会卡住人的地方就一个路径连通判断。很多人拿到的第一版思路是直接在地图上用BFS搜路但BFS找的是最短路径而连连看要求的是折线拐点不超过两个且路径可以绕到棋盘外圈去走。如果一上来就用通用寻路算法很快会碰到“明明能连却判断不能连”的边界问题。所以我在设计时把地图四周扩出来一圈虚拟空地让路径可以沿棋盘的边缘绕行。这一圈空地不显示任何图标只在算法里存在。这样做的好处是直连、一个拐点、两个拐点的情况全部可以用同一种搜索逻辑处理不需要写三套判断。从实现角度看这比用数学方式枚举拐点更省心也更容易扩展成不规则地图规则。1.2 为什么技术栈选Python Pygame我选Python不是因为它性能强而是因为做这种东西开发效率比性能重要得多。Pygame的上手成本非常低窗口初始化不到十行代码绘制图标用blit贴图就行鼠标事件也是现成的。游戏本身跑在2D平面上方块数量撑死几十个每一帧全量重绘也毫无压力。当然如果之后想塞进网页里也可以把核心算法原封不动迁移到JavaScript或C#因为算法只依赖二维数组和队列跟界面完全解耦。这也是我强调“算法单独拎出来”的原因后面给示例代码时你看到的会是一个纯函数不掺Pygame。这一步的决策直接决定后面调试是否顺手所以建议你把逻辑层和显示层分开写。1.3 文件结构与功能拆解整个项目我拆成了三个文件便于阅读和调试main.py程序入口负责初始化Pygame、创建窗口、运行主循环。board.py棋盘数据、生成地图、检测连通、无解重排等纯逻辑代码。resources/存放图标素材的目录也可以改成从assets.py里读字符型图标。如果你只是想看算法直接打开board.py即可。下面各节的代码片段都是从这三个文件里抽出来的核心部分我会在注释里说明功能。这种拆法也方便你后面换UI比如想做Web版只需要把board.py保留把main.py换成前端逻辑。2. 核心算法与数据结构设计2.1 地图建模二维数组与外圈空地我的棋盘默认是10列×8行但真正存数据的二维数组是12列×10行四周各多出一行/列值全部为0。内部有图案的位置填图标编号例如1、2、3……相同编号表示同一种图标。为什么必须留外圈因为连连看的连通路径允许“绕出去”。举个最典型的例子两个相同图标分别贴在棋盘最左边一列和最上边一行如果不扩圈从左上角绕行就越界了扩一圈之后路径就可以安全地从地图外围的0值区域通过。这个设计是后面所有判断的基础千万别省。生成地图时先准备好所有成对的图标编号。比如要放40个图标就先造一个列表让每个编号都出现偶数次然后用random.shuffle打乱再逐个填到内部区域。这样可以保证每种图标都是偶数个避免出现消不完的孤子。如果只是简单地把随机数填进去很容易出现某一种图标只剩一个的无解局面。2.2 连通判定统一用BFS限制拐弯次数连连看的路径判定不需要求最短路径只关心能不能用不超过两次拐弯到达。我直接用BFS扫描队列里记录“当前位置、当前方向、已经拐了几次弯”。初始时从起点的四个相邻方向各走一步方向就是对应方向拐弯次数为0。之后每扩展到下一个格子比较当前方向和新方向是否一致不一致就代表拐了一次累计次数加1。只要累计次数超过2就剪掉不继续扩展。同时用一个三维数组记录每个格子以某个方向进入时的最小拐弯次数避免重复搜索死循环。这样直连、一个拐点、两个拐点共用同一套逻辑不需要分支判断。我写了一个核心函数先贴出来你会更直观from collections import deque def can_connect(grid, a, b): 判断a、b两点是否能在最多两次拐弯内连通。grid中0表示空地非0表示图标。 if a b: return False rows, cols len(grid), len(grid[0]) dirs [(-1, 0), (1, 0), (0, -1), (0, 1)] INF 10 ** 9 # turns[x][y][d] 表示从方向d进入(x,y)时已经使用的最小拐弯次数 turns [[[INF] * 4 for _ in range(cols)] for __ in range(rows)] q deque() for d, (dx, dy) in enumerate(dirs): nx, ny a[0] dx, a[1] dy if 0 nx rows and 0 ny cols and grid[nx][ny] 0: turns[nx][ny][d] 0 q.append((nx, ny, d, 0)) while q: x, y, d, t q.popleft() if (x, y) b: return True if t 2: continue for nd, (dx, dy) in enumerate(dirs): nx, ny x dx, y dy if not (0 nx rows and 0 ny cols): continue # 目标点允许作为终点但不允许作为穿过的中间点 if grid[nx][ny] ! 0 and (nx, ny) ! b: continue nt t (0 if nd d else 1) if nt 2 and nt turns[nx][ny][nd]: turns[nx][ny][nd] nt q.append((nx, ny, nd, nt)) if (nx, ny) b: return True return False这个函数有几点需要注意起点和终点本身不能相同BFS每次从起点的相邻空地出发所以最终能绕过起点自己如果某个邻居正好是终点即使终点位置有图标也被允许进入并立即返回。实际调用前外部要先确认两个格子的值相等且都不为0否则没必要走这个搜索。2.3 消除流程两次点击怎么判断玩家操作流程很简单第一次点击记录选中的格子把图标高亮。第二次点击先判断两个格子是否相同再判断值是否相等。如果都满足且can_connect返回True就把地图上两个位置改成0同时触发消除状态。如果不满足或不能连通就把选中状态清空让玩家重新点。第二步里有个容易忽略的细节第二次点击如果点的是同一个格子不应该触发消除直接忽略就行。另外两个格子值都为0时也不应该进入判定因为空位置没有意义。判断顺序建议先判断坐标相同再判断值非0再判断值相等最后才做路径搜索这样能省下很多无效计算。别小看这个顺序地图快消完时大量格子都是0如果先做路径搜索会很浪费。3. 实操搭建游戏主流程3.1 初始化Pygame窗口和棋盘主程序的第一步是创建窗口、加载图标、生成地图。这里我以字符图标代替图片资源方便没有美术素材的人直接跑。比如用星号、圆形、方形这些符号显示到格子里代码里用一个字典做索引。import pygame import random pygame.init() COLS, ROWS 10, 8 CELL 50 MARGIN 30 WIDTH MARGIN * 2 (COLS 2) * CELL HEIGHT MARGIN * 2 (ROWS 2) * CELL screen pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption(QQ连连看单机版) # 图标编号和显示字符的映射 ICON_CHARS {1: ★, 2: ●, 3: ■, 4: ◆, 5: ▲, 6: ♥, 7: ♦, 8: ♣, 9: ♠, 10: ☆} def create_board(cols, rows, kinds10): # 外面留一圈0 grid [[0] * (cols 2) for _ in range(rows 2)] items [] cell_count cols * rows each cell_count // kinds # 每种图标个数需要是偶数 for kind in range(1, kinds 1): items [kind] * each random.shuffle(items) idx 0 for r in range(1, rows 1): for c in range(1, cols 1): grid[r][c] items[idx] idx 1 return grid这段代码里each cell_count // kinds存在一个问题如果格子数不能被种类数整除最后items数量不等于cell_count填充时会越界。所以要么选择能够整除的棋盘尺寸要么在造列表后统一补成偶数对。我在自己代码里用的方案是先算出需要的总格子数然后按照“每种图标出现偶数次”的原则生成列表再random.shuffle这样无论如何都不会留下落单的图标。3.2 绘制棋盘和选中高亮绘制时把grid里非0的格子按坐标画出来同时要把行列偏移算对。因为棋盘外层还有空地实际绘制区域是从第1行第1列开始的不能直接把行列当像素坐标用。def draw_board(grid, selectedNone): screen.fill((30, 30, 30)) for r in range(1, len(grid) - 1): for c in range(1, len(grid[0]) - 1): val grid[r][c] if val 0: continue rect pygame.Rect(MARGIN c * CELL, MARGIN r * CELL, CELL, CELL) pygame.draw.rect(screen, (60, 60, 60), rect, 2) if selected and (r, c) selected: pygame.draw.rect(screen, (255, 215, 0), rect, 4) font pygame.font.SysFont(simsun, 30) text font.render(ICON_CHARS[val], True, (255, 255, 255)) screen.blit(text, text.get_rect(centerrect.center)) pygame.display.flip()字体这里用了常见的中文字体名不同系统可能不一样。如果直接运行报错找不到字体可以把SysFont的参数改成系统里存在的字体或者直接用Pygame默认字体。图标本身是符号不是图片所以不需要处理资源路径这对第一次接触Pygame的人很友好。3.3 主循环和点击事件主循环的核心就是等Pygame事件拿到鼠标坐标后换算成棋盘行列。这里特别容易遇到坐标算错的问题必须减去MARGIN再除以CELL而且因为棋盘外面有一圈空位行列还要加1。def get_cell_from_pos(pos): x, y pos c (x - MARGIN) // CELL r (y - MARGIN) // CELL if 1 r ROWS and 1 c COLS: return r, c return None selected None grid create_board(COLS, ROWS) running True while running: for event in pygame.event.get(): if event.type pygame.QUIT: running False elif event.type pygame.MOUSEBUTTONDOWN: cell get_cell_from_pos(event.pos) if cell is None: continue if selected is None: selected cell else: r1, c1 selected r2, c2 cell v1, v2 grid[r1][c1], grid[r2][c2] if selected cell: selected None elif v1 ! 0 and v1 v2 and can_connect(grid, selected, cell): grid[r1][c1] 0 grid[r2][c2] 0 selected None draw_board(grid, selected) pygame.quit()这个循环已经能玩起来了但还缺has_solution做无解判断和重排。这也是连连看必须处理的问题不然随机地图很容易卡死。在你做出来的第一版里可以先不加重排但一定要保留后路不然玩家中途一定会遇到无解局面。3.4 无解检测与一键重排随机生成的地图不一定每一步都有解尤其是图标种类多、布局散的时候。我的做法是专门写一个has_solution(grid)函数遍历地图里所有非0的格子对两两调用can_connect只要找到一对能连通就返回True全部找不到才返回False。def has_solution(grid): cells [] for r in range(len(grid)): for c in range(len(grid[0])): if grid[r][c] ! 0: cells.append((r, c)) for i in range(len(cells)): for j in range(i 1, len(cells)): a, b cells[i], cells[j] if grid[a[0]][a[1]] grid[b[0]][b[1]]: if can_connect(grid, a, b): return True return False这个双重循环最多也就几十个格子量级很小不用担心性能。如果没有解就把所有非0图标重新收集、打乱、再填回去然后再次检查如果连续几次依然无解说明图标种类和棋盘比例有问题需要调整。实际项目中我会在每次消除后都调用一次has_solution无解就自动重排并且给一个提示不然玩家会以为程序卡死了。4. 开发中踩过的坑与排查技巧4.1 路径判断里最隐蔽的坑终点被当成障碍我第一次写BFS时把“格子值为0才能走”写成了硬性条件结果发现所有消除都失败。原因是终点位置有图标值不为0BFS在扩展时永远不把终点加入队列自然永远返回False。后来我在进入邻居判断时加了“目标点允许作为终点”的例外并把“到达终点立即返回”放在扩展逻辑里才算解决。这个坑其实很好理解路径搜索时终点虽然占用了一个图标格子但在“是否能连通”的判断里终点是允许被落子的只是不允许被穿过。所以代码里要单独放行而其他非0格子依旧不能走。如果你自己写多拐点判断也要注意这个逻辑否则很容易出现“两个相同图标能连却消不掉”的诡异现象。4.2 外圈空地怎么处理索引越界和地图尺寸如果代码里严格限制0 nx rows实际上把外圈也算进去了所以棋盘grid的尺寸必须是(rows 2) × (cols 2)而不是rows × cols。刚开始我图省事只申请了内部大小坐标换算时非常别扭要么频繁判断边界要么漏掉绕行路线。老老实实把外圈留出来之后算法简单了很多。这里给一个自查方法如果你发现两个贴着边缘的图标连不上十有八九是外圈没有留出来或者绘制时把外圈也算进去了导致坐标错位。你可以打印一下grid的尺寸和坐标看看边缘格子的行列是否落在预期范围内。4.3 字体和图标资源问题用字符画图标省事但跨平台时字体渲染可能有差异。Windows上宋体、黑体一般都有macOS上就未必。建议直接用Pygame自带字体或者把你想要的图标用图片保存到resources目录。我后来改成用数字加颜色区分图标测试时最稳定因为不依赖字体。如果你坚持用字符图标可以在初始化字体时做一个回退先尝试中文字体失败就用默认字体至少保证程序不崩。另外使用非ASCII字符时文件开头最好加# -*- coding: utf-8 -*-避免在某些环境下出现编码报错虽然Python 3默认已经是UTF-8但养成习惯没有坏处。4.4 常见问题速查表现象可能原因解决办法点击两个相同图标不能消除终点被当成障碍路径搜索不到检查can_connect里是否对终点格子放行边缘图标永远连不上地图外圈没有留空地把grid扩成(rows2)×(cols2)随机地图很快无解图标种类/数量配比不合适用has_solution检测无解时重排点击坐标错位换算时没加外圈偏移先减MARGIN再除以CELL行列再加1中文或特殊符号显示为方框系统没有对应字体换成图片图标或系统默认字体窗口关闭后进程不退出没调用pygame.quit()主循环结束后释放Pygame资源这张表是我实际开发时反复踩过的问题对照着检查大多数问题都能在十分钟内定位。尤其是终点放行的问题第一版几乎必踩写的时候就要有意识避开。5. 后续还能怎么扩展5.1 增加计分、计时和关卡现在版本只有消完即胜玩起来没有压力。你可以加一个全局计时器每消除一对按剩余时间和连击数加分全部消除后结算总分。连连看的乐趣在于限时和连击这两点直接决定游戏手感。实现计时很简单主循环里用pygame.time.get_ticks()记录开局时刻剩余时间等于目标时长减去已用时间每消除一对就加分如果两次成功消除之间的间隔小于某个阈值连击数加一分倍率提高。计分面板可以直接绘制在窗口顶部不用额外UI库用font.render画一行文字就行。5.2 最高分保存到本地文件这个项目的评分数据很适合做本地持久化用Python内置的文件读写就够了。比如第一次启动时读score.txt没有文件就创建游戏结束时如果当前分数高于历史最高分就把新分数写回文件。这样每次打开游戏还能看到之前的纪录玩起来更有目标。写文件时注意编码问题建议用utf-8否则在Windows上中文注释可能乱码。如果你想更规范一点可以直接用json模块保存一个字典里面存最高分、通关时间、游戏局数等。这样后续想扩展统计功能也不用再改数据结构。5.3 加动画和音效消除的瞬间如果不做任何反馈会显得很生硬。最简单的方式是淡出效果把被消除的图标从完全不透明逐步变成透明或者用缩放动画让图标逐渐变小消失。Pygame实现这类动画并不难把动画状态放进一个列表每帧更新alpha或大小绘制时先画棋盘再画动画对象。音效可以用Pygame的mixer播放短wav文件比如点击、消除、胜利三个音效素材可以在一些免费音效站找注意版权。音效虽然只是锦上添花但加上游戏完成度会高很多。我第一次加音效时没有初始化mixer结果播放没有声音记得先调用pygame.mixer.init()。5.4 换皮肤、自定义头像和网格大小最后是一个很推荐的方向把数据结构里的图标编号和界面资源解耦做一套皮肤系统。比如同样是编号1在默认皮肤里是星星在卡通皮肤里是猫咪在数字皮肤里就是数字1。这样用户可以通过放不同图片进resources目录来换皮肤。如果你想让游戏支持更多玩法把COLS、ROWS、CELL改成可配置参数即可核心算法完全不用动。我试过把棋盘调到14×10帧率依然很稳瓶颈完全不在地图大小而在图标素材的绘制方式上。一个小建议改成图片图标后最好在加载时统一缩放成CELL × CELL避免因为素材尺寸不一致导致选中框错位。最后说一句个人体会写连连看这种小项目最大的收获不是学会Pygame而是搞懂了“规则”如何转化为“算法”。我一开始画了无数张草稿想用数学方式判断拐点最后发现BFS加次数限制反而最简单。完整代码虽然只有几百行但每部分都值得细读。如果你也照着做建议先只跑通can_connect再用假数据测试各种路径最后再接界面这样定位问题会快很多。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。