四子棋AI实战:极小化极大搜索与Alpha-Beta剪枝实现
简介这份资源是一套重力四子棋人机对弈程序面向对博弈树搜索与人工智能算法感兴趣的学习者尤其是想通过小项目理解α-β剪枝与局面评估实现思路的开发者。压缩包共5个文件以3个.h头文件和2个.cpp源文件为主分别承担棋盘与落子判定、策略搜索、评估函数等模块的声明与实现整体仅约7KB代码精简、结构清晰便于通读与二次修改。资源已有1221人学习下载说明其在同类教学案例中具有一定参考价值。读者可从中获得一套可运行的重力四子棋对抗AI完整实现重点体会α-β剪枝如何压缩搜索空间、估价算法如何权衡棋型与落子位置并据此理解搜索深度与性能之间的取舍适合作为课程设计、算法练习或小型对弈程序的起步参考。1. 一个“看起来简单、写起来翻车”的对抗 AI 项目四子棋Connect Four大概是所有棋类 AI 里最容易被低估的一个规则三句话就能讲完棋盘只有 7×6可一旦加上“重力”——棋子落下必须堆叠到列底——整个搜索空间和评估函数的设计就完全变了味。很多人第一次写对抗 AI都是拿井字棋练手结果换到四子棋上发现 AI 要么秒下臭棋要么思考半天还打不过随机落子。这份「人工智能四子棋对抗 AI」资源就是围绕重力约束下的博弈搜索做的一套完整实现人类执子、AI 应招核心是极小化极大搜索配 Alpha-Beta 剪枝再叠一层启发式局面评估。它适合两类人一类是想跑通一个能玩、能看 AI 思考过程的小项目的新手另一类是已经写过搜索算法、想看看别人怎么在有限深度里把评估函数调得不那么“玄学”的熟手。下面我按“它是什么 → 怎么跑起来 → 核心代码怎么读 → 怎么改”的顺序拆。2. 项目定位与技术选型为什么是搜索而不是神经网络2.1 这个项目到底在解决什么问题先把边界划清楚。四子棋的完整博弈树规模在 10¹³ 量级穷举不现实所以任何能实时对战的实现都必须在“搜索深度”和“评估精度”之间做取舍。这个项目的定位很明确不追求职业级强度而是做一个可解释、可调试、可二次开发的对抗 AI 样板。也就是说你落子之后AI 的每一步决策都能追溯到“它搜了多深、每个候选列的分值是多少、剪枝砍掉了多少节点”而不是丢给你一个黑匣子说“神经网络觉得这里好”。这种定位决定了技术选型。常见做法是纯 Python 实现不引入深度学习框架依赖只有标准库加一个可选的图形界面库比如 tkinter 或 pygame。好处是零环境负担clone 下来就能跑坏处是纯 Python 的搜索速度有限深度上不去。所以项目里 Alpha-Beta 剪枝不是“锦上添花”而是“能不能在 1 秒内给出一步棋”的生死线。我一般会把搜索深度默认设在 57 层配合列顺序优化先搜中间列实测在普通笔记本上单步决策能压到几百毫秒。2.2 技术栈与目录结构从这类项目的通用组织方式看目录大致是这样切的connect4-ai/ ├── main.py # 入口人机对战循环、界面启动 ├── game/ │ ├── board.py # 棋盘状态、落子合法性、胜负判定 │ └── rules.py # 重力落子、四连检测的纯逻辑 ├── ai/ │ ├── minimax.py # 极小化极大 Alpha-Beta 剪枝 │ ├── evaluate.py # 局面评估函数窗口打分 │ └── search.py # 搜索入口、深度控制、候选列排序 ├── ui/ │ └── render.py # 终端或图形界面渲染 └── tests/ └── test_board.py # 落子与胜负判定的单元测试这个切法的核心思想是把“规则”和“智能”彻底分开。game/目录只回答“这步棋合不合法、现在谁赢了”不关心 AI 怎么想ai/目录只吃棋盘状态、吐出一个列号不关心界面长什么样。这种分层在二次开发时特别值钱——你想换评估函数只动evaluate.py想换搜索算法比如换成蒙特卡洛树搜索只动search.py规则层一行不用碰。模块职责二次开发频率game/board.py棋盘数组、落子、四连检测低ai/minimax.py搜索主循环、剪枝中ai/evaluate.py局面打分高ui/render.py渲染与输入中提示如果你打算把这个项目改成“AI 对 AI”的自对弈模式规则层完全复用只需要在main.py里把人类输入换成两次search()调用即可这是这套分层最直接的收益。3. 跑起来从零到人机对战的完整步骤3.1 环境准备与启动这类纯逻辑项目对环境要求很低Python 3.8 以上即可不需要额外装包如果用了 pygame 界面才需要pip install pygame。启动流程一般是# 1. 进入项目根目录 cd connect4-ai # 2. 确认 Python 版本3.8 python --version # 3. 直接运行入口默认终端对战 python main.py # 4. 如果项目带图形界面开关通常这样启动 python main.py --gui跑起来之后终端会打印一个 6 行 7 列的棋盘列号从 0 到 6你输入列号落子AI 随后应招。这里有个血泪经验第一次跑千万别急着调搜索深度先用默认参数下几盘确认“落子位置正确、重力堆叠正确、四连判定正确”这三件事没问题再去碰 AI 参数。因为如果规则层有 bug你调评估函数调到天亮也调不出能赢的 AI——它会一本正经地在一个已经输掉的局面上继续“优化”。3.2 棋盘表示与重力落子棋盘的数据结构是整个项目的地基。常见做法是用二维列表board[row][col]row0表示最底层。重力落子的本质是“找到该列最下面那个空位”# game/board.py ROWS, COLS 6, 7 EMPTY, PLAYER, AI 0, 1, 2 def drop(board, col, piece): 在 col 列落子返回落子后的行号列满返回 -1 for row in range(ROWS - 1, -1, -1): # 从底部往上找第一个空位 if board[row][col] EMPTY: board[row][col] piece return row return -1 # 该列已满非法落子 def valid_moves(board): 返回所有未满的列顺序上优先中间列 order [3, 2, 4, 1, 5, 0, 6] # 中间列优先提升剪枝效率 return [c for c in order if board[0][c] EMPTY]drop里从ROWS-1往上扫是因为重力让棋子总是先填满底部所以从底往上找第一个EMPTY就是合法落点时间复杂度 O(ROWS)比维护一个“每列高度”数组更直观。valid_moves里的order是个容易被忽略的优化中间列优先搜索能显著提高 Alpha-Beta 的剪枝率因为中间列的分支通常更“有希望”好的上界更早出现后面的兄弟节点就更容易被砍掉。这个顺序不影响正确性只影响速度属于典型的“免费午餐”。3.3 胜负判定四连检测怎么写才不漏四连检测是规则层最容易写错的地方。正确做法是只检查“刚落下的那颗棋子”周围四个方向而不是全盘扫描# game/board.py def check_win(board, row, col, piece): 只检查经过 (row, col) 的四条线返回是否四连 directions [(0, 1), (1, 0), (1, 1), (1, -1)] # 横、竖、主对角、副对角 for dr, dc in directions: count 1 for sign in (1, -1): # 正反两个方向延伸 r, c row dr * sign, col dc * sign while 0 r ROWS and 0 c COLS and board[r][c] piece: count 1 r dr * sign c dc * sign if count 4: return True return False这里的关键是“以落子点为中心向两侧延伸”而不是从棋盘一角开始扫。前者每次落子只做 O(1) 量级的检查最多 4 方向 × 7 格后者是 O(ROWS×COLS×4)在搜索树里被调用几十万次差距会被放大到肉眼可见。directions里(1, -1)是副对角线方向很多人只写前三个方向结果副对角线赢了判不出来——这是我在类似项目里踩过的经典坑测试用例一定要覆盖四个方向。4. 核心模块拆解搜索、剪枝与评估函数4.1 极小化极大与 Alpha-Beta 剪枝搜索主循环是整个 AI 的心脏。极小化极大的思路是AI 层取最大值人类层取最小值递归到叶子用评估函数打分。Alpha-Beta 在此基础上维护alpha当前已知的最大下界和beta最小上界一旦alpha beta就剪枝# ai/minimax.py def minimax(board, depth, alpha, beta, maximizing): 返回 (分值, 最佳列)depth 为剩余搜索深度 moves valid_moves(board) if not moves: return 0, None if depth 0: return evaluate(board), None if maximizing: # AI 层取最大 best_score, best_col -float(inf), moves[0] for col in moves: row drop(board, col, AI) score, _ minimax(board, depth - 1, alpha, beta, False) board[row][col] EMPTY # 回溯撤销落子 if score best_score: best_score, best_col score, col alpha max(alpha, best_score) if alpha beta: # 剪枝 break return best_score, best_col else: # 人类层取最小 best_score, best_col float(inf), moves[0] for col in moves: row drop(board, col, PLAYER) score, _ minimax(board, depth - 1, alpha, beta, True) board[row][col] EMPTY if score best_score: best_score, best_col score, col beta min(beta, best_score) if alpha beta: break return best_score, best_col几个参数必须说清楚depth是剩余深度每递归一层减一减到 0 就调评估函数alpha初始为-inf、beta初始为inf随着搜索推进不断收紧maximizing区分当前是 AI 还是人类回合。回溯那行board[row][col] EMPTY绝对不能漏漏了的话棋盘状态会被污染AI 会基于一个“棋子越下越多”的假棋盘做决策表现为越下越离谱。这是递归搜索里最经典的翻车点我建议在drop和回溯之间加断言或者干脆把落子/撤销封装成一对函数强制成对调用。4.2 评估函数窗口打分法搜索到叶子节点时棋盘还没分出胜负得靠评估函数给个“谁更有利”的分数。四子棋里最常用的是窗口打分把棋盘上所有可能的四格窗口横、竖、两个对角扫一遍按窗口内双方棋子数量打分# ai/evaluate.py def evaluate(board): 正分利好 AI负分利好人类 score 0 # 所有长度为 4 的窗口横、竖、主对角、副对角 for r in range(ROWS): for c in range(COLS - 3): score score_window(board[r][c:c4]) for c in range(COLS): for r in range(ROWS - 3): score score_window([board[ri][c] for i in range(4)]) # 对角窗口略逻辑同上只是行列同时偏移 return score def score_window(window): ai_cnt window.count(AI) pl_cnt window.count(PLAYER) if ai_cnt and pl_cnt: # 双方都有子这个窗口废了 return 0 if ai_cnt 3: return 50 # 三连威胁大 if ai_cnt 2: return 10 if pl_cnt 3: return -50 if pl_cnt 2: return -10 return 0score_window的权重设计是评估函数的灵魂。三连给 50、二连给 10是因为三连下一步就能成四威胁远大于二连。但这里有个细节“活三”和“死三”价值不同——如果三连的一端被堵死它其实只能往一个方向成四威胁减半。更精细的做法是判断窗口两端是否还有空位活三给 100、死三给 30。这个调整对 AI 强度的影响非常直接属于调参时最值得花时间的地方。另外评估函数里最好加一条“AI 已有四连直接给极大值、人类已有四连给极小值”否则搜索深度不够时 AI 可能眼睁睁看着对手成四。4.3 搜索入口与深度控制把搜索和评估串起来的入口通常长这样负责把“当前棋盘”翻译成“下一步下哪列”# ai/search.py from .minimax import minimax from .evaluate import evaluate def choose_move(board, depth6): 对外接口返回 AI 选定的列号 score, col minimax(board, depth, -float(inf), float(inf), True) return col if col is not None else valid_moves(board)[0]depth6是个经验值深度每加 1搜索节点数大约翻 7 倍分支因子6 层在纯 Python 下大约几十万节点配合剪枝能压到 1 秒内。如果你的机器慢降到 5如果追求强度且能接受 23 秒等待可以上 7。不要盲目加深度因为纯 Python 的递归开销大深度 8 以上很可能卡到用户以为程序挂了。真要提速方向是加置换表缓存已搜索过的局面或把评估函数向量化而不是硬堆深度。5. 避坑与排查那些让 AI “变傻”的细节5.1 现象AI 明明能赢却下成平局原因搜索深度不够或者评估函数没有对“即将成四”给足权重。深度 4 时 AI 只能看到两步之后的威胁对手的三连它反应不过来。解决先把深度临时调到 6 验证是不是深度问题如果是在score_window里给三连更高权重或者在evaluate开头加“已有四连直接返回 ±10000”的短路判断。5.2 现象AI 思考时间忽长忽短偶尔卡死原因Alpha-Beta 剪枝效率依赖候选列顺序如果valid_moves没做中间列优先某些局面下剪枝几乎失效节点数暴涨。解决确认valid_moves里的order是[3,2,4,1,5,0,6]进一步可以在每层搜索前对候选列按“落子后评估分”做一次浅排序用小的排序开销换大的剪枝收益。5.3 现象落子位置对但胜负判定偶尔失灵原因四连检测漏了副对角线方向或者只从落子点单向延伸导致跨过落子点的四连被漏判。解决检查directions是否包含(1,-1)并确认每个方向都做了sign in (1,-1)的双向延伸补一个覆盖四个方向的单元测试用固定棋盘断言结果。5.4 现象连续对战几局后 AI 行为异常原因搜索回溯时漏了撤销落子棋盘状态在局间没有重置或者drop返回-1列满时没处理导致board[-1][col]越界写。解决在main.py每局开始时重建棋盘在drop调用处判断返回值是否为-1是则跳过该列。这类状态污染问题用断言最容易抓建议在drop后加assert row 0。注意调评估函数权重时一次只改一个参数并记录对战胜率同时改多个权重会让你无法判断到底是哪一项起了作用这是调参最常见的自欺欺人。6. 二次开发把评估函数换成你自己的打分体系这个项目最值得动手的地方是ai/evaluate.py因为搜索框架是通用的评估函数才是决定 AI “性格”的地方。我一般会按这个顺序改先加“活三/死三”区分再引入“中心列加权”中间列的四连窗口更多理应更值钱最后考虑加一层“威胁计数”——统计双方各自有多少个“下一步能成四”的窗口谁多谁占优。改完之后不要凭感觉说“好像变强了”要跑一个固定对局集让新旧两版 AI 各执先手对弈 20 局统计胜率胜率提升超过 60% 才算真的改对了。验证方法上我习惯写一个小脚本做自对弈回归# tests/selfplay.py from game.board import new_board, drop, check_win, valid_moves from ai.search import choose_move def play_one(depth_a6, depth_b4): board new_board() piece 1 while valid_moves(board): depth depth_a if piece 2 else depth_b col choose_move(board, depth) if piece 2 else choose_move(board, depth) row drop(board, col, piece) if check_win(board, row, col, piece): return piece piece 3 - piece # 1 和 2 互换 return 0 # 平局这个脚本的价值在于把“感觉变强了”变成“深度 6 对深度 4 的胜率是 78%”这样的数字。改评估函数、改剪枝顺序、改深度都拿它跑一遍回归问题立刻暴露。表格里可以记录每次改动的胜率对比比拍脑袋靠谱得多。改动对深度 4 胜率平均单步耗时基线三连 5062%0.4s活三/死三区分71%0.4s加中心列加权78%0.5s最后说个习惯从那以后我每次改搜索或评估相关代码都强制先跑一遍自对弈回归再提交因为这类改动太容易“局部看起来对、整体变傻”没有数字兜底根本发现不了。希望这个项目能帮你把四子棋 AI 从“能跑”推到“能打”剩下的就看你自己的评估函数怎么调了。本文还有配套的精品资源点击获取