ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

用Python实现黑白棋AI:从规则到Alpha-Beta剪枝

2026/9/1 9:03:12 拓冰建站 浏览量
用Python实现黑白棋AI:从规则到Alpha-Beta剪枝 简介一份基于Qt与C的黑白棋游戏完整源码面向具备基础C语法、希望进阶桌面应用开发或棋类算法设计的开发者。项目在Qt平台上实现完整对弈流程包含界面搭建、落子交互、合法位置判定、翻转棋子、自动计分与胜负检测并额外提供网络对战模式可通过TCP或UDP协议完成双人实时通信涉及GUI设计、事件驱动、游戏逻辑、网络编程等多个知识模块。压缩包共15个文件以cpp、h源代码为主配有一份ui界面定义、pro工程文件以及jpg和png图片素材、一个规则说明文件整体仅1.12MB。各文件职责清晰从主程序入口到棋盘界面实现、核心游戏逻辑、网络通信均能对应查看适合直接导入Qt工程编译调试。通过这份代码读者可以掌握Qt框架下UI与业务逻辑分离的开发思路理解棋盘状态如何用二维数组或类似结构表示并学习如何将复杂的翻转规则拆分为可测试的函数网络对战部分还能帮助了解Socket编程与简单同步机制。目前已有348人学习下载对于想系统梳理Qt项目组织方式和游戏算法实现的开发者这份代码具有不错的参考与复用价值。1. 黑白棋规则核心与代码设计思路黑白棋也叫奥赛罗棋是我个人觉得最适合用来练手棋类AI的入门项目没有之一。它不像围棋那样状态空间爆炸到没法穷举又比井字棋多了足够多的策略深度。一个8x8的棋盘、64个格子加上“夹住就翻转”的简单规则组合出来的变化量恰好够你折腾各种算法贪心、极大极小搜索、Alpha-Beta剪枝、蒙特卡洛树搜索都能在这套代码上找到落地的感觉。先说清楚黑白棋的核心规则因为代码本质上就是规则的翻译。黑白双方轮流落子黑棋先手。落子的合法性取决于一个核心动作你下的这个位置必须在水平、垂直或对角线八个方向上能“夹住”至少一颗对方棋子。所谓夹住就是从你下的位置出发沿某个方向连续经过至少一颗对方棋子最终遇到一颗你自己的棋子。满足这个条件后中间被夹住的对方棋子全部翻面变成你的颜色。如果一方没有合法落子位置就跳过回合双方都没有合法位置或者棋盘下满游戏结束棋子多的一方获胜。这个规则在代码里拆解下来其实就三个关键函数合法性判断、执行落子并翻转、判定终局。很多初学者会犯一个错误就是先把棋盘UI画出来再想着写逻辑。我的建议恰恰相反逻辑层和UI层彻底分离。你先用纯Python的数据结构把游戏规则跑通用命令行输入坐标来对弈等规则完全正确了再套一层PyQt、Tkinter或者pygame界面都不迟。这样出bug的时候你不需要在界面和逻辑之间来回排查。另外一个值得注意的设计点是坐标系的约定。常见做法是用二维列表board[row][col]row表示行0到7col表示列0到7。也可以用一维列表加位运算那是追求极致性能的写法性能竞赛和做AI对战平台时有用但日常学习和演示二维列表的可读性碾压一切。我做的时候选的就是二维列表用1表示黑棋、-1表示白棋、0表示空格这样翻转棋子只需要乘一个-1省去了一堆if-else判断。2. 棋盘初始化与底层工具函数2.1 初始盘面的搭建黑白棋初始状态是固定的棋盘正中央四格黑棋在(3,3)和(4,4)白棋在(3,4)和(4,3)。代码里初始化的时候有一个容易忽略的细节——如果你用[[0] * 8 for _ in range(8)]来创建棋盘每行都是独立的列表没问题但如果图省事写成[[0] * 8] * 8那就是个天坑因为这会创建8个指向同一列表的引用你改一行其他行全跟着变。这个坑我当年踩过排查了半天才反应过来是引用共享的问题。def init_board(): board [[0 for _ in range(8)] for _ in range(8)] board[3][3] 1 # 黑棋 board[4][4] 1 board[3][4] -1 # 白棋 board[4][3] -1 return board2.2 方向向量的定义与使用黑白棋判断夹子最核心的代码思路就是方向遍历。八个方向定义成方向向量比八个单独的if分支不知道清爽多少倍。实际写代码的时候用两个列表或者二元组列表都可以重点是穷举方向的时候别漏掉斜向DIRECTIONS [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]注意这里没有(0,0)因为原地不动没有意义。我在调试的时候踩过一个边界问题当落子在棋盘角落比如(0,0)的时候往左上方向走一步就越界了所以必须在每次沿方向步进之前检查坐标是否在合法范围内。边界检查是所有棋盘类项目最容易出bug的地方建议把is_on_board这个判断抽成一个单独函数后续复用率极高。2.3 合法性判断理解了规则就懂了代码合法性判断是黑白棋代码的命门。很多人一开始按直觉写认为只要八个方向上存在一颗对方棋子且再往后有己方棋子即可但漏了一种关键情况——连续多颗对方棋子的情况。中间有2颗、3颗甚至5颗对方棋子只要末尾是自己的棋子都是合法的。def is_valid_move(board, row, col, player): if board[row][col] ! 0: return False opponent -player for dr, dc in DIRECTIONS: r, c row dr, col dc if not is_on_board(r, c) or board[r][c] ! opponent: continue r dr c dc while is_on_board(r, c): if board[r][c] player: return True if board[r][c] 0: break r dr c dc return False这段逻辑的核心在于先走一步看是不是对手棋子不是就换个方向是的话继续沿着方向走遇到己方棋子就说明合法遇到空格或越界就说明这个方向夹不住。这里有一个性能优化的小技巧遍历合法位置时只需要检查那些周围八格有棋子的空位不需要扫描整个棋盘虽然64格的全扫描在现代电脑上毫无压力但如果你想给AI做更快的局面评估这种微优化积少成多还是有意义的。2.4 执行落子与翻转执行落子比判断更复杂一些因为你要真的去翻棋子。我建议在这个函数里不要简单地返回None而是返回一个被翻转棋子的位置列表这样在GUI里可以做翻转动画在AI里可以做撤销操作在测试里可以验证正确性一举三得。def apply_move(board, row, col, player): if not is_valid_move(board, row, col, player): return [] board[row][col] player flipped [] opponent -player for dr, dc in DIRECTIONS: r, c row dr, col dc candidates [] while is_on_board(r, c) and board[r][c] opponent: candidates.append((r, c)) r dr c dc if is_on_board(r, c) and board[r][c] player: for fr, fc in candidates: board[fr][fc] player flipped.append((fr, fc)) return flipped这里的关键点是先把当前方向上的候选翻转位置存到临时列表里等确认这条路径末端确实是自己的棋子再去翻转。很多新手写到这里会边走边翻结果把对手棋子翻成自己的之后继续沿方向走就遇到了刚翻出来的己方棋子错误判定终点导致多翻或者少翻。先收集候选再统一翻转这是黑白棋实现中最重要的一个原则。3. 核心游戏循环与胜负判定3.1 获取全部合法落子位置游戏循环里你需要频繁获取当前玩家所有可落子的位置用来判断是否有棋可下、是否跳过回合、以及给AI做决策。这个函数写起来简单就是遍历棋盘所有空格逐个调用is_valid_move。def get_valid_moves(board, player): moves [] for r in range(8): for c in range(8): if is_valid_move(board, r, c, player): moves.append((r, c)) return moves实际使用中有一个经验如果后续要做AI这里可以在返回坐标列表的同时把每次apply_move会翻转的棋子数量也缓存下来能省掉AI搜索时重复计算翻转数量的开销。不过初版代码不用想这么远先把功能跑通。3.2 终局判定不是只有棋盘满了才结束终局判定有一个初学者特别容易忽略的情况当双方各剩一堆合法位置但都不下的时候游戏也会结束。更准确地说当连续两个回合双方都无棋可下游戏才算结束。因为黑白棋允许跳过回合所以代码里需要在循环中记录连续跳过的次数。def count_pieces(board): black sum(row.count(1) for row in board) white sum(row.count(-1) for row in board) return black, white还有个细节终局时如果双方棋子数相同算平局。你可以在循环结束后输出结果时处理平局分支。我自己写游戏循环的时候习惯把状态机抽象成PLAYING、PASS、GAME_OVER三种状态。这样写GUI或者做自动对战时主循环的逻辑会非常清晰不会出现状态判断混乱的问题。4. AI对战从贪心到Alpha-Beta剪枝4.1 第一步一个能用的贪心AI黑白棋代码最有意思的部分就是AI。最简单的AI就是贪心策略每次选择能翻转对方棋子数量最多的位置落子。这个策略虽然战力不高但作为初学者第一个AI已经足够有成就感了。def ai_move_greedy(board, player): valid_moves get_valid_moves(board, player) if not valid_moves: return None best_move None best_score -1 for move in valid_moves: temp_board [row[:] for row in board] flipped apply_move(temp_board, move[0], move[1], player) if len(flipped) best_score: best_score len(flipped) best_move move return best_move注意这里复制棋盘的方式[row[:] for row in board]这是深拷贝避免AI模拟落子时污染实际棋盘状态。这个细节必须重视用temp_board board的话AI每模拟一步真实棋盘就被改得面目全非了。4.2 第二步基于位置价值表的评估函数贪心AI很快会遇到瓶颈因为它只看眼前利益。翻子数量多不代表后续优势大尤其是边角和边缘位置的价值远高于中间区域。黑白棋里有个公认的经验占据角位几乎等于锁死这个角因为角永远不会被翻转而角旁边的格子比如(0,1)、(1,0)反而是送给对手的陷阱位置新手一贪心就会往那里下结果帮对手创造占角的机会。可以定义一张8x8的位置价值表给角赋值100角旁边赋负值其他位置给一个基础正值。AI评估局面时就按棋手每个棋子的位置权重求和。这张表需要反复调整我最初从网上抄了一套经典表效果还行后来自己加了边角惩罚项实战提升非常明显。POSITION_WEIGHTS [ [100, -20, 10, 5, 5, 10, -20, 100], [-20, -50, -2, -2, -2, -2, -50, -20], [10, -2, 1, 1, 1, 1, -2, 10], [5, -2, 1, 0, 0, 1, -2, 5], [5, -2, 1, 0, 0, 1, -2, 5], [10, -2, 1, 1, 1, 1, -2, 10], [-20, -50, -2, -2, -2, -2, -50, -20], [100, -20, 10, 5, 5, 10, -20, 100], ] def evaluate(board, player): score 0 for r in range(8): for c in range(8): if board[r][c] player: score POSITION_WEIGHTS[r][c] elif board[r][c] -player: score - POSITION_WEIGHTS[r][c] return score4.3 第三步Minimax Alpha-Beta剪枝如果你想让AI再聪明一点就得用搜索树。黑白棋的分支因子不大前期大概10-20个合法走法后期更少所以配合Alpha-Beta剪枝后搜索深度到6或者8是完全可行的普通笔记本电脑上也能在几秒内完成决策。Minimax的思想很简单假设你走一步对手会在所有合法走法中选择对你最不利的那一步也就是他的评估函数最大化你再在对手的响应中选对自己最有利的。如此反复递归深度越深看到的未来越远。伪代码结构大致是这样实际实现时把Alpha和Beta作为参数传递进递归函数def minimax(board, depth, alpha, beta, maximizing_player, ai_player): if depth 0: return evaluate(board, ai_player), None valid_moves get_valid_moves(board, ai_player if maximizing_player else -ai_player) if not valid_moves: valid_moves [None] # 允许跳过回合 ...这里有个实现细节容易出问题轮到某一方时它无棋可下搜索不能直接返回而是应该让回合跳过继续搜索另一方的走法。否则AI在残局阶段会因为找不到合法走法而误判局面导致错过正确的走子路径。Alpha-Beta剪枝的优化点在于当某个分支的评估值已经比已知最优路径更差时直接剪掉不再搜索。剪枝后的搜索耗时能减少到原来的三分之一甚至更少。我实测下来深度4的搜索从约3秒缩短到不到1秒体感差别非常明显。4.4 评估函数的高级优化如果想再进阶可以在评估函数中增加几个维度行动力当前玩家合法走法的数量、稳定子永远不会被翻转的棋子数量、边缘控制度。参考一些开源黑白棋引擎最终的评估函数往往是加权组合总分 位置权重分 行动力权重 * (我方合法走法数 - 对方合法走法数) 稳定子权重 * (我方稳定子 - 对方稳定子)这套权重需要大量对局测试来调整最简单的办法是让两个AI互相下几百局根据结果手动调参。我调试AI时用的框架是主AI vs 基线AI贪心位置表记录200局胜负比例权重调优后胜率从55%提到接近75%。这种“AI打AI”的思路后来成了我做任何棋类AI的标准流程强烈建议你也在代码里留一个自动对战的入口手工对战调试效率太低了。5. 交互层设计命令行起步、GUI进阶5.1 命令行模式命令行模式是最快的验证方式也是我推荐的起点。你不需要任何图形库代码量少逻辑清晰非常适合测试规则函数是否正确。def print_board(board): print( .join(str(i) for i in range(8))) for r in range(8): row_display [] for c in range(8): if board[r][c] 1: row_display.append(●) elif board[r][c] -1: row_display.append(○) else: row_display.append(·) print(str(r) .join(row_display))主循环的逻辑是显示当前玩家、打印所有合法走法、等待输入坐标、执行落子、检查跳过状态、切换玩家。循环跳出的条件是双方均无合法走法。5.2 可视化GUI的选择命令行跑通后可以给代码配一个界面。我试过几条路Tkinter最轻量适合快速出原型pygame功能强大但需要自己处理各种事件PyQt5效果最专业但依赖稍重。因为黑白棋的UI相对简单用Tkinter就够了核心代码不超过100行就能实现点击落子。如果你做GUI建议把棋盘面板定义成一个自定义组件重写paint事件来绘制棋盘和棋子。鼠标点击事件转换坐标的逻辑也很简单用点击位置除以每个格子的像素宽度向下取整就得到行列号。翻转动画可以用after方法定时重绘在flipped列表上做渐变色过渡视觉体验会好很多。我的一个实操建议是不要急着把GUI和AI深度耦合。界面只负责展示和接收输入AI决策在后台线程里跑。否则AI思考时界面会卡死体验很糟糕。用Python的threading模块AI算完通过队列把结果返回给界面线程更新这样就能保持流畅交互。6. 常见问题与排查技巧实录6.1 棋子翻转异常少翻或者多翻这个问题的根源几乎都是“边走边翻”。我的排查方法是在apply_move函数里加临时打印把每个方向的candidates列表打出来对照棋盘手工推演一遍。如果候选列表最后一位不是己方棋子说明路径检查的逻辑有漏洞。加上前面提到的“先收集再翻转”原则这类bug基本能根治。6.2 跳过回合逻辑错误黑白棋允许玩家无棋可下时自动跳过但很多人写的版本会在玩家有棋可下时也误判断跳过。排查时关注连续跳过计数器只有双方都跳过才算游戏结束。一个简单有效的测试用例是手动构造一个几乎下满的棋盘残局然后用代码跑一遍终局判定。6.3 AI思考时间过长AI卡顿最常见的原因是评估函数里做深拷贝太频繁每搜索一个节点就复制整个8x8棋盘。优化方向是落子/悔棋的增量更新在递归里记录当前走法翻转的棋子位置列表回溯时用列表逆序恢复棋盘这样能省掉大量复制开销。我的Alpha-Beta搜索从深度4到深度6关键优化就是这个增量更新。6.4 边界位置判断出错比如在(0,0)落子往左上方向检查时索引变成负数。这类问题最好的防御手段是统一封装边界检查函数然后所有方向遍历都先过一遍这个函数。我在代码中把is_on_board放在第一个判断条件里靠Python的短路机制避免索引越界同时保证代码可读性。6.5 平局与胜负判定不准确建议在游戏结束时输出黑白双方棋子数手工比对一下。如果棋子总数不等于64个说明某个apply_move丢了棋子或者翻转逻辑没有覆盖到某些方向。出现这种问题时最快的定位方法就是写一个自动随机对战的脚本让它跑1000局统计每局结束时的棋子总数一旦出现总数不等于64的局就说明隐藏bug在某个随机路径上被触发了这种压力测试比手工测试高效得多。7. 如果你想继续扩展黑白棋代码的价值远不止于一个游戏。往AI方向深挖你可以实现蒙特卡洛树搜索让AI具备更强的中局战力往工程方向走你可以封装成Web服务前端用Canvas画棋盘实现一个多人在线黑白棋平台往教学方向走这个项目非常适合作为算法课程的课程设计规则简单但能承载搜索、剪枝、评估函数设计等多个经典主题。就我个人经验来说把黑白棋的每个模块写干净、测试完备比盲目追求AI棋力更有收获。因为你在写代码过程中积累的边界处理意识、搜索树优化技巧、逻辑与UI解耦的架构思维是可以迁移到任何其他项目的通用能力。如果你也是第一次接触棋类AI项目我建议你按照文中的顺序先规则后AI、先命令行后GUI、先贪心后搜索每一步都把代码结构理清楚完成后的成就感绝对值得你付出的时间。本文还有配套的精品资源点击获取