资讯动态

基于Python与Pygame的五子棋AI实战:从棋盘建模到Alpha-Beta剪枝

发布时间:2026/9/8 17:29:26 来源:尧图企业网站定制
1. 先盘清楚需求再动键盘1.1 一个看似简单但极易失控的小项目五子棋游戏表面上看就是一张棋盘棋子你一颗我一颗谁的五个子连成线谁赢。但你真做起来就会发现这玩意儿的坑一点都不比做一套后台管理系统少。我做这个版本时最初的需求列得很朴素支持人机对战和双人对战能悔棋能重开一盘电脑棋力不能太弱。等真正动手才知道光一条“电脑棋力不能太弱”就把我按在地上摩擦了很久。因为五子棋的规则简单到小学一年级就能学会但它背后的博弈复杂度却足以让一台普通电脑在纯暴力搜索下原地卡死。先说清楚我做的方案选型用 Python Pygame 做大界面逻辑层全部独立成纯 Python 模块AI 用“贪心打分 有限深度极小化极大搜索”的组合策略。为什么要用 Python因为逻辑表达清晰做原型迭代最快Pygame 在 2D 棋盘类游戏里足够轻量不需要引入沉重的引擎。如果你问能不能用其他技术栈当然可以。我见过用纯 JavaScript 在网页里实现的版本也见过用 Electron 套壳的桌面版还见过有人把它做成了小程序。但从个人练习或中小规模项目角度出发Python Pygame 依然是我愿意推荐的首选组合原因后面会详细讲。需要特别提醒的是一旦你决定要做一个“AI 不下低级臭棋”的五子棋重心就会迅速从界面绘制跑到状态评估和搜索决策上。所以这个项目看似没有多少工程量却覆盖了数据结构设计、规则建模、启发式评估、搜索剪枝、UI 事件循环这几个硬核模块完整做下来比做十个 CRUD 增删改查都锻炼人。1.2 哪些人适合拿它练手以及它到底能练什么我做完这套以后回头复盘认为它最适合三类人。第一类是刚学完 Python 基础但不满足于写爬虫和命令行小工具的人。因为你要处理二维数组、类设计、事件循环、图形绘制还要把一个逻辑上的棋盘状态映射到像素坐标上。这能帮你把基础语法真正用起来。第二类是对 AI 算法有兴趣但不想一上来就啃深度学习的人。五子棋的 AI 是典型的传统博弈 AI不依赖 PyTorch 或 TensorFlow用一套评估函数加搜索算法就能做出肉眼可见的智能感。你可以从零开始理解什么叫评估、什么叫深度、什么叫剪枝这些概念将来迁移到其他棋类或者决策类问题上一样成立。第三类是想做个人作品集的人。五子棋有完整交互、有图形界面、有计算机“思考”过程是一个能够清楚展示工程能力和算法思维的微型项目。这里我还要说句容易得罪人的实话如果你只会调 API从来没有独立设计过一个包含状态管理和决策模块的程序那你做这个项目的过程会很难受。但难受恰恰说明踩到了成长点。1.3 项目架构怎么切才顺手我把整个程序拆成了棋盘模型、规则引擎、AI 决策、界面交互四层。它们之间是单向调用关系。棋盘模型负责存储 15×15 格子的状态用二维列表保存空位是 0黑子是 1白子是 2。规则引擎负责判断落子是否合法、检查胜负、统计棋型。AI 决策接手落子建议它读棋盘状态算候选点打分返回一个坐标。界面交互层只做一件事把鼠标点击转换成棋盘坐标调用 AI 或本地玩家逻辑最后把棋盘画出来。四层分离的最大好处是你可以写单元测试不用打开窗口就能验证胜负逻辑和 AI 的计算结果。我做的第一版最吃亏的地方就是把棋盘数组和界面画布耦合在了一起结果每次想测一个边界条件都要手动点几十次鼠标人都要疯了。第二版重构后我把核心算法全部做成无界面依赖的纯函数测试效率一下子提了上来。一个稳定的项目核心逻辑必须跑在无头环境下。2. 棋盘建模和棋型识别是整个项目的地基2.1 二维数组与坐标转换不能图省事棋盘建模我选 15×15 标准规格为什么是 15×15因为这是五子棋最常见的比赛规格太小的棋盘容错性低容易先手必胜太大会拖慢 AI 搜索。初始化不用 NumPy直接用 Python 内置的列表推导式生成二维数组即可。毕竟一个 15×15 的规模内置结构完全够用没必要为此引入重依赖。这里有一个非常容易出错的点二维数组的行列顺序与屏幕坐标的对应。我建议统一把board[x][y]定义为“第 x 行第 y 列”其中 x 代表横向像素坐标换算后的列y 代表纵向行。但这样做在打印棋盘时反而不直观因为控制台输出通常是先行后列。如果你在调试时打印棋盘会看到行列是反的。我的解决方案是逻辑层全部用“先行后列”即board[row][col]在做像素转换时再按行列映射到(x, y)。简单来说就是棋盘的左上角原点的第一维永远留给行号第二维留给列号。这样写 AI 评估时遍历横向、纵向、斜向都顺了不容易绕晕。落子动作的合法性判断很简单两步坐标不能越界格子必须为空。你可能会想这能有什么坑还真有。Pygame 的鼠标事件返回的是像素坐标你在 UI 层要通过除法和取整把它归算到最近的交叉点上。如果棋盘左边距和上边距不一致归算就会偏移常见的表现是“明明点到这一格棋子却跑到隔壁”。所以界面设计时棋盘绘制区域至少要有一个起始偏移margin换算公式是row (mouse_y - margin) // cell_size col (mouse_x - margin) // cell_size千万不要把 margin 遗漏掉这个细节我在测试时踩过一旦棋盘画的不是顶满窗口点击偏差就会出现用户会立刻觉得这个游戏是“坏的”。2.2 胜负有四种方向少一个都不行胜负判断的朴素做法是每次落子后从当前点出发向四个方向检查水平、垂直、正斜、反斜。每个方向分别向两侧延伸数出同色连续棋子数总数大于等于 5 即判胜。这个逻辑听上去毫无障碍但有一个隐蔽问题如果你只是单方向延伸一旦遇到对手的棋子或边界就停那么从当前落子点出发可能把两端棋型拆成了两段。比如当前落点把两段己方棋子连接成了一个长连但你只从落点向一侧数很可能只数出 4 颗明明已经赢了却判不出来。正确做法是把四个方向各写成一个独立函数每次从落子点出发向左和向右同时延伸加起来减去一次当前重复计数的棋子才是完整的连续长度。更稳妥的方案是每下一步棋后直接扫描全棋盘检测是否有任意连续五子而不是只围绕落子点检查。虽然多花一点 CPU 时间但逻辑简单到不可能出错。考虑到棋盘只有 225 个格子全盘扫描的性能代价完全可以忽略。我把规则引擎写成了check_win(board, row, col, player)返回布尔值再额外写一个get_winner(board)用于终局界面的整体判定。比赛过程里每次落子后只调用前者到了残局或者需要展示终局结果时再调用后者做兜底。这样既保证性能又保证准确性。2.3 棋型识别才是 AI 棋力的分水岭如果你只用“某点周围同色子多就下哪里”这种粗暴逻辑那 AI 下的就是幼儿园棋永远不知道防守。要让 AI 有点水平必须先定义棋型。我归纳了五种基础棋型连五、活四、冲四、活三、眠三。所谓活四就是“两头都通畅的四连”你堵了任何一头它依然能连成五冲四则是“只有一头通”的威胁形态活三是“再有一个子能变成活四”的形态眠三是“被堵了一头或者只能变成冲四”的形态。评分上我给连五设成超大分值比如 10000000活四大约 100000冲四 10000活三 5000眠三 800。这样设置的核心原因是一旦棋盘上出现了这一威胁AI 必须立刻做出反应这些分值的相对比例比绝对数字重要。如果活三的分值设定太高超过冲四AI 就可能不去堵对方的冲四反而自己在边上搭活三结果被对手一记连五带走。棋型识别的一个难点在于五子棋不是只看四个方向的局部连续区间还要看当前落点与两侧棋子的距离。我见过不少人用一个滑动的五元组窗口把棋盘扫一遍统计窗口里各种颜色的数量来粗判棋型。好处是快缺点是无法区分活和眠——同样是四个白子加一个空位XXXX_和_XXXX_完全不在一个威胁等级。我的做法是写一个基于方向的连续棋型扫描器。从一个起点出发沿着某个方向把连续的同类棋子和其后的空位抓出来生成一个字符串再返回字符串里的形态描述类型。最后根据形态类型转成分数放进候选点评分里。实现代码比滑动窗口长一点但精准得多识别出来的活三不会和眠三混淆。3. AI 决策引擎让电脑真正会下棋3.1 贪心评分法先跑通一轮再谈优化刚开始做 AI我不建议上来就写 Alpha-Beta 剪枝。很多人容易一头扎进剪枝搜索结果连评估函数都没写好AI 表现依然很笨。我建议分两步走先做一轮纯贪心版本再叠加搜索深度。所谓贪心版本就是在所有空位里对每个位置计算两个分数AI 自己如果下这里的进攻分以及如果对手下这里对 AI 构成威胁的防守分。它最终的分值是“进攻分 防守分 × 防守系数”的加权。我先用一个遍历找出所有进攻分最高的点再找出防守分最高的点然后看两个核心点位的关系做取舍等于先建立一套基础的“有攻有守”逻辑。具体实现时我会维护一个数据结构里面保存候选点的坐标、进攻分、防守分和总分。每次轮到 AI 下棋先把每个空位都遍历一遍。如果你嫌 225 个空位都跑一次棋型扫描太慢也可以在每次落子后只更新受影响的行列斜线上的候选点而不是全局重算。我最初图省事做了全局全量重算依然能跑起来只是后面搜索深度增加到两步时性能明显吃紧。第一版贪心写完后实测效果基本上能打败不思考的新手但会出两种丑态。第一种是不懂得连下后续第二步就断开导致明明有活三的局面最后变成散子。第二种是过于执着堵对手活三一步一步跟着别人走毫无策略布局可言。这是正常的说明你需要引入多层搜索了。3.2 极小化极大与 Alpha-Beta 剪枝正确的复杂度降维真正让 AI 摆脱“一步看一步”的是对未来局面的提前搜索。最基础的模型是极小化极大假设黑方是 AI白方是玩家。轮到 AI 落子时它要挑一个让己方收益最大的点下一层玩家落子时它会认为玩家要挑一个让 AI 收益最小的点。于是落子点收益值的传递是极大、极小、极大交替进行。五子棋的搜索树极为庞大全量搜索根本吃不消。所以要在搜索时限制深度比如只看两步或四步也就是 AI 下一步、对手下一步、AI 再下一步这样棋盘上的连续威胁基本能体现出来。同时必须做剪枝。Alpha-Beta 剪枝的原理可以这么理解你手里已经找到了一个不错的落子候选它至少能带来 80 分收益那这时候你发现某个候选点在下一层模拟中的某个分支会让收益掉到 60 分以下你就不再需要细看这个候选点的其他分支了因为它不可能超过你手里的 80 分。剪枝能把需要展开的节点数从指数级压到大约平方根级别。实际操作中剪枝带来的提速非常可观。同样的单步评估我用朴素极小化极大做两步搜索大概要计算 20000 多个节点加上排序启发后 Alpha-Beta 剪枝只需要计算 3000 多个节点速度差了接近七倍。3.3 候选点生成策略绝不能把全部空位丢进搜索我见过最天真的搜索优化是盲目加大搜索深度然后等电脑算到睡着。为什么不能直接搜深度四因为如果每个节点都要考察 225 个空位哪怕每一步都做一些启发式过滤搜索次数依然是指数爆炸。我做了一组限制搜索时只考虑距离已有棋子两格以内的空位。比如棋子落在 (7,7)我最多搜索范围内 (5,5) 到 (9,9) 这个 5×5 扩展块里的空点。事实上对于大多数局面真正有价值的落子点一定紧贴现有棋子因为离所有棋子都很远的点既形不成攻势也做不了防守当前阶段没必要算。进一步优化是“由评到搜”。我先用贪心评估函数给每个候选点排序把分数最高的前 N 个点挑出来进入深层搜索。N 我通常设为 10 到 12 个。这招非常务实因为绝大多数最佳落子点都存在于前 10 个高分候选点中。这种策略牺牲了极小概率的最优解换取了巨大的性能提升。在 AI 的最终实现里我的落子流程是先用贪心扫描生成所有候选点并排序如果最高分超过必胜阈值比如大于 1000000直接落子这通常是连五或活四这类绝对胜负手否则取前若干个候选点进入两层 Alpha-Beta 搜索搜索完成后谁的总分高选谁。这套组合下来的棋力稳到什么程度我在普通笔记本上做了一次人机对战AI 每一步思考时间在 0.1 到 0.3 秒之间棋力和谨慎的业余玩家有来有回。如果你接着把搜索深度提升到 4 层并继续用候选点收缩仍然可以保持秒级响应但棋力提升的空间边际效应已经变小这时候再回头调评分函数比盲目加大深度更有用。4. 从界面到交互让游戏“像”一个游戏4.1 棋盘绘制与棋子反馈的核心参数用 Pygame 绘制时建议把窗口默认设置成 700×700 像素margin 设为 40 像素。这样棋盘的落子区横跨 (40,40) 到 (660,660)格子边长是 44 像素。棋盘线条用灰色调背景用偏木质的颜色纯黑和白太刺眼。棋子半径可以按格子边长的 42% 计算太大容易和旁边格子粘连太小视觉上又显局促。绘制完成后需要把鼠标位置捕捉到最近交叉点这时需要做像素坐标到棋盘坐标的换算。每次点击时先做宽松判断只要点落在整个棋盘区域附近margin 以外的格子范围内就允许落子。你可以在 UI 层做一个鼠标移动到交叉点附近时显示半透明预览棋子的交互效果这会让整个游戏手感细腻很多。具体做法是在主循环里捕获MOUSEMOTION事件将鼠标坐标换算成行列绘制一个带 alpha 的圆。棋子落下的动画如果不做也行但要有一点即时反馈。我用的方案是播放一个很短的音效并让刚落下的棋子变成高亮色。这样玩家能分出自己刚下的是什么不会盯着满屏黑白点看不出自己走到哪里。4.2 悔棋和重开逻辑上比你想的麻烦悔棋的实现表面看只要把棋盘上最后一颗子清掉。但人机对战时有个特殊逻辑AI 走后如果你悔棋一次应该把玩家刚下的那一手和 AI 的回应一起撤销回到玩家落子前的状态。否则玩家悔一步棋棋盘上却依然多出一个 AI 子没法正常接续。我实现的悔棋栈不单纯保存坐标还保存每一步的行列、棋手身份以及局面的编号。每次落子把本步推进栈里。执行悔棋时如果是双人模式直接弹出最后一步如果是人机模式且轮到玩家走棋要连续弹两步如果轮到 AI 走棋只要弹一步即可。这算是我认为最容易写乱的一段逻辑不建议节省这个“栈”它值得你专门用一个列表来维护。重开一盘相对简单直接清空棋盘、清空历史栈、重置当前执子方。不过这里有一个常被忽略的细节先手权可以轮换或由玩家选择每次点击重开时不要总是硬编码成黑棋先手。我做了两种模式如果本局是玩家执黑获胜重开后玩家继续保持执黑如果玩家是执白获胜可以提示是否交换执子方。这个细节能够让游戏更有变化不至于每局都是黑棋走同样开局。4.3 人机对战模式下轮转状态机别写死人机对战的状态切换强烈建议用状态机来设计不要用一个布尔变量 in_player_turn 一路硬戳下去。因为中途会有 AI 计算、游戏结束、玩家悔棋这些打断性事件布尔变量很容易被搅成一团浆糊。我的状态定义是PLAYER_TURN、AI_THINKING、AI_MOVE_DONE、GAME_OVER、WAITING_REMATCH。AI 不直接在事件循环里同步跑搜索而是用一个启动线程去执行 AI 决策。搜索完成后把结果放到一个队列主循环检测到队列里有结果再更新棋盘。这样 UI 不会在 AI 思考时变成卡死状态棋盘上可以播放“AI 思考中”的提示动画。从双人对战切换到人机模式时尤其要小心开局回合归属。默认配置让玩家执黑先行如果玩家是白棋那第一手必须由电脑先下。不要把它留给玩家手动触发。5. 禁手规则到底做不做我是怎么取舍的5.1 只做民间规则也能跑得很开心在实现过程中有个绕不开的纠结要不要做黑棋禁手规则。所谓禁手是指黑棋在正规比赛规则下禁止下出“三三”“四四”或“长连”等特殊棋形这是为了平衡黑棋先手的巨大优势。如果做禁手你需要在规则引擎里额外判定这些棋形如果不做你实际上用的是民间玩法。对大多数个人项目而言我建议第一版不做禁手原因非常简单不做禁手机的判断逻辑清晰先手优势在普通玩家之间没有大到不可接受大多数人下五子棋图的是休闲而不是竞技。但如果你和我一样想做一个相对严谨的版本就必须考虑禁手导致的另一个连锁问题AI 作为黑棋时它不能主动下出禁手点作为白棋时又要学会“逼”黑棋下到禁手点上。逼禁手比起识别禁手难得多因为需要让 AI 主动往“黑棋看起来最诱人实则违反规则”的位置引导。我第一版没有做逼禁手所以白棋 AI 对抗黑棋玩家时并不会刻意制造禁手陷阱它的打法更偏向“堵死你”。这里我建议先把棋型识别模块做到足够可靠再做禁手判定。禁手判定不是单独判断一个点是不是“双三”而是要看下在这个点后形成的四个方向棋型中同时出现两个或以上活三、或两个或以上冲四、或者是六子以上长连。这些都需要把棋型识别函数拆得更底层允许你从任意位置、任意方向去取棋型。5.2 先手优势需要靠难度策略来稀释如果把黑棋先行优势原样留给人类玩家AI 胜率会受到明显影响。所以我在难度设置里做了一层补偿。简单难度下AI 有 30% 的概率把评分第一位的位置弃掉改选第二位甚至稍弱一点的位置故意放水。中等难度下 AI 依然执行完整搜索但不会使用“必胜点提前返回”的机制因此它偶尔会漏掉必胜手。困难难度则是完整评估开局阶段还会从内置开局库里面调用几段定式争取把先手主动权抢回来。开局库其实很简单就是保存了一些流行开局的前几手坐标序列。程序在棋盘为空或手数很少时直接从库里提取下一步而不是重新搜索。因为刚开始几个子离得太远所有候选点评分都很分散AI 反而容易下出怪局。开局库能避免这一点让 AI 前几步至少下在合理区域后面再交给搜索来接手。6. 开发中容易踩的坑我帮你提前避开6.1 五个方向的细节问题第一是胜负裁判漏判斜向。我在早期测试中发现吃子模式下的对角方向少加了一行坐标增量导致所有从左上到右下正好连成五子的局面永远不判胜。原因是row i和col i的循环里写错了边界。这个问题非常隐蔽因为多数测试都集中在水平和垂直连线上斜向连线的测例如果没特别添加很少会被大家注意到。我的建议是把四种方向都写一个独立测试用例。第二是悔棋时没有正确处理 AI 轮次。玩家悔棋时如果棋盘上只剩玩家一颗子、AI 还没来得及回应我的第一版直接弹掉两个栈帧居然把空棋盘弹出负历史界面瞬间报错。后来我改成先判断当前轮到谁再决定弹出步数这个错就消失了。第三是候选点排序不稳定导致 AI 偶尔“优柔寡断”。评分法因为棋型扫描的顺序固定导致完全对称的局面可能随机选中某一侧视觉上显得摇摆。解决办法很简单对同分的候选点加入一个小小的随机扰动或者按照到棋盘中心的距离做次级排序这样 AI 决策更稳定。第四是鼠标点击的坐标判断用错事件。Pygame 的MOUSEBUTTONDOWN有button属性如果你把右键的点击也算作落子就会出现“左右键一起下子”的诡异结果。记得只对event.button 1做响应。第五是在窗口尺寸变化时忘记重新计算缩放后的格子坐标。如果做了窗口缩放就不能继续用画布固定尺寸的旧参数去换算落子坐标坐标偏到天边去了。如果你不想处理这个复杂度一开始就设置pygame.RESIZABLE但固定内部渲染尺寸再通过缩放变换输出不要让逻辑层感知新的窗口尺寸。6.2 性能监控和调试技巧教你一个笨方法你以为 AI 慢是算法慢其实有一大半时间是赢在 debug 打印上。早期版本我把评估函数里每个候选点的分数都打印到终端225 个点全打印一次每步棋的调试输出几十屏终端渲染速度反而比算法耗时还高。后来我加了一个全局 debug 开关默认关闭只在需要单步分析某个局面时手动打开精确打印前五个候选点的评分明细。另外你需要一个“无头测试”套路。我把棋盘状态序列化成一个字符串比如 225 个字符黑子用 1白子用 2空用 0。测试时可以直接把某个实战残局拼成字符串载入棋盘然后让 AI 决策输出它会选的位置。通过这样的方式我能准确复现一个局面并在不同的评分系数下对照 AI 的选择变化。经过几十个局面案例的反复对比你才会真正理解评分体系里每一个数值的影响。6.3 常见问题排查直接给出对照表下面这份速查表是从我自己项目的调试记录里整理出来的不一定覆盖所有情况但对大多数相似实现都有参考价值现象主要原因解决办法点击位置落子偏移一格像素坐标换算时漏算或算错 margin核对行列换算公式中的边距补偿明明五子连珠却不判胜棋型检查方向不完整或循环边界写错写出四个方向的独立单测覆盖斜向AI 从不防守防守分的权重过低敌方的棋型没参与扫描在候选点评分中加入对手视角的分值扫描AI 越下越慢搜索深度增加后未做候选点过滤只保留有棋子邻近的候选点二次排序后取前 N悔棋后轮到下的人不对没有考虑 AI 是否已经落子修改悔棋逻辑按当前轮次决定弹栈步数程序启动时窗口卡住在主线程里执行长耗时搜索用线程跑 AI 搜索通过队列回传结果双人对战能落子但颜色不变玩家切换身份逻辑被 if 写死检查玩家身份切换是否发生在每次落子后每一行背后都是一个真实的下午我调试到头皮发麻才解决的问题。你看看自己的项目里有没有同样的影子有就立刻去修。6.4 难度调节的数值经验难度调节不只是控制搜索深度。我试过只切换深度发现效果并不理想因为玩家在不同难度下感受到的差异不单纯是“电脑反应速度快了”而是“电脑是不是会突然走出一步让我头疼的棋”。比较有效的做法有三个维度。第一是搜索深度简单模式只搜 0 层即贪心直接用中等模式搜 2 层困难模式搜 4 层。第二是候选点数量简单模式下候选池扩大到 40 个然后在其中随机选一个分数前五的点中等模式前 12 个里选困难模式前 5 个里选并且尽量选分数第一。第三是落子延迟简单模式可以在 AI 决策完成后故意增加 0.5 秒延迟模拟一种“不紧不慢”的感觉但键盘反馈仍然正常。这三个维度最好全部开放做成一个可调的配置字典方便你在游戏设置界面里切换。如果以后想加难度不需要改代码只加一个配置条目即可。7. 后续扩展方向和小技巧我聊几句实在的这个项目本身做完后我最受益的不是最终能下赢一个什么样的 AI而是每一步决策都要面对“如何量化一个棋局好坏”“如何在有限计算时间内做取舍”“如何设计一个能让玩家理解的交互状态机”这些看似抽象的问题。它们会在你以后再写任何带状态、带策略的程序时反复出现。如果你还想继续扩展可以考虑给 AI 加入基于蒙特卡洛树搜索的变体或者把棋盘规格改成标准规则以外的模式。不依赖预定义评分函数蒙特卡洛树搜索通过大量随机模拟来估计每一步的胜率在复杂度上比 Alpha-Beta 更灵活但也更耗费计算资源做出来会是一个完全不同风格的 AI。最后说一个小经验我强烈建议你在一开始就为自己保留一套命令行版本。什么意思就是除了 Pygame 图形界面外把棋盘渲染成一个纯文本的终端图形空格子用点黑子用 X白子用 O。这样你在测试 AI 算法时不需要打开窗口直接用命令行就能复盘每一步的落子。我后来很多 bug 都是在这个纯文本版本下快速重现的窗口版本负责展示终端版本负责调逻辑两个版本共用同一套核心代码。这个项目做完之后你不仅收获了一个能玩的五子棋游戏更重要的是锻炼了“拆分系统”的思维模式这可能是这个项目带给我最值钱的回报。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价