VS五子棋人机对战实现与AI算法拆解:从界面到Alpha-Beta剪枝

发布时间:2026/10/16 6:53:27
VS五子棋人机对战实现与AI算法拆解:从界面到Alpha-Beta剪枝 简介一款基于Visual Studio与EasyX图形库实现的五子棋游戏完整项目面向正在学习C桌面开发、图形界面编程或初级AI算法的开发者也适合课程设计、毕业设计参考。项目围绕人机对战与人人对战双模式展开人机部分采用基于穷举搜索并融合Minimax剪枝思路的AI能模拟不同难度落子人人模式则完整演示了棋盘状态管理、合法落子判断、五子连珠胜负检测、悔棋与重新开始等功能可从中理解Win32窗口与鼠标事件的配合方式。压缩包共49个文件以cpp源文件、h头文件、sln与vcxproj工程配置、jpg/png图片素材、wav音效文件为主同时保留pdb、tlog等构建调试记录便于直接打开工程复现整个项目。整体约107MB已有2657人学习下载。对照源码与界面素材读者可快速掌握在Visual Studio中搭建Win32图形程序、使用EasyX绘制棋盘棋子、实现交互逻辑与简单博弈算法的完整流程是一份结构清晰、可直接编译运行的学习型项目。1. 用 VS 写五子棋人机对战这份资源到底能让你少走多少弯路做过棋类小游戏的人应该都有体会五子棋看起来规则简单真正动手做的时候棋盘坐标、胜负判断、人机策略、界面刷新这些环节一层套一层任何一个地方没理顺都会在后期调试时反复推倒重来。这份 Visual Studio 实现五子棋含人机对战资源核心是给出一套可以在 VS 里直接编译运行的完整工程覆盖双人对战和人机对战两条主线。我拆完以后的结论是它更适合两类人一是刚学完 C# 基础、想做一个完整项目练手的人二是在校作业或简历项目里需要棋类 Demo 的人。它不追求极致的 AI 棋力但把“界面 逻辑 搜索”的骨架搭得清清楚楚照着复现能节省大量从零开始的时间。2. 把棋盘和落子逻辑先做扎实坐标换算与对局状态管理2.1 棋盘控件与数据结构的选择五子棋的棋盘通常用 PictureBox 或自绘 UserControl 承载这个资源用的是自绘控件方式。相比直接用 PictureBox 加背景图自绘的优势在于鼠标交互和格子对齐更精确放大缩小时也不会出现图片模糊导致落子偏移。核心数据结构是二维数组常见的定义是int[,] board new int[15,15]0 表示空位、1 表示黑子、2 表示白子。private int[,] board new int[15, 15]; private bool isBlackTurn true; private bool isPlayerBlack true; public void ResetBoard() { Array.Clear(board, 0, board.Length); isBlackTurn true; // 重绘棋盘 chessBoardControl.Invalidate(); }这段代码里board是整个对局的核心数据源所有落子、判断、悔棋操作都围绕它展开。isBlackTurn控制当前轮到谁落子isPlayerBlack则是人机模式下玩家执黑还是执白。这里有个设计决策值得注意把棋盘数据和控件分离UI 只负责渲染和捕获鼠标逻辑层只读写数组。这样后期如果要加存档、回放或者网络对战不需要改动渲染代码。2.2 鼠标坐标到棋盘格子的映射鼠标点击坐标换算成棋盘行列号是所有棋类程序里最容易出暗坑的地方。边界情况下差一两个像素落子就会偏到格子线上看起来非常别扭。private void ChessBoard_MouseClick(object sender, MouseEventArgs e) { // 假设每个格子大小为 cellSize棋盘左上角留出 margin 像素空白 int col (e.X - margin) / cellSize; int row (e.Y - margin) / cellSize; if (col 0 || col 15 || row 0 || row 15) return; if (board[row, col] ! 0) return; // 执行落子逻辑 PlacePiece(row, col); }这里margin是棋盘外边框的留白cellSize是每个格子占用的像素数。换算公式的本质是取整运算但问题是(e.X - margin) / cellSize在鼠标点击到格子边缘附近时取整结果可能和玩家视觉判断不一致。常见解决做法是在取整前先加上半个格子大小让判定范围以格子中心为准。我一般会写成int col (e.X - margin cellSize / 2) / cellSize;这样鼠标点在格子左半边时归到当前列右半边时归到下一列手感更符合预期。2.3 对局状态流转人机切换与回合控制双人对战和人机对战的区别本质上是“白方落子权”是交给另一个玩家还是交给算法。资源里用枚举或布尔值控制模式切换落子后检查当前模式如果是人机模式且轮到 AI就启动后台计算并延时落子。private void PlacePiece(int row, int col) { board[row, col] isBlackTurn ? 1 : 2; chessBoardControl.Invalidate(); if (CheckWin(row, col)) { // 弹出胜利提示锁定棋盘 return; } if (isVsComputer !isBlackTurn) { // 交给 AI 落子这里用异步避免界面卡死 Task.Run(() AIThinkAndMove()); } else { isBlackTurn !isBlackTurn; } }注意这里人机模式下黑色方落完以后直接把 AI 调用放到异步线程里这是避免 AI 搜索耗时导致窗口假死。很多人机对战小项目翻车就是卡在这一步AI 的搜索耗时超过几百毫秒后界面会变成白屏状态。资源里用的 Task.Run 配合 Invoke 做 UI 更新在第五章我会详细说这个坑的完整表现和解决办法。3. 胜负判断与棋型评估从规则判定到 AI 落子依据3.1 四方向扫描式胜负判定五子棋的胜负判断常规思路是落子后从当前位置出发沿着水平、垂直、两条对角线共四个方向分别向两边延伸统计相同棋子的连续数目。这里有个效率细节不需要每次扫全盘只需要围绕刚落下的那颗棋子检测即可。private bool CheckWin(int row, int col) { int[] dx { 1, 1, 0, -1 }; int[] dy { 0, 1, 1, 1 }; for (int i 0; i 4; i) { int count 1; // 正方向统计 for (int step 1; step 5; step) { int nr row dx[i] * step; int nc col dy[i] * step; if (nr 0 || nr 15 || nc 0 || nc 15) break; if (board[nr, nc] ! board[row, col]) break; count; } // 反方向统计 for (int step 1; step 5; step) { int nr row - dx[i] * step; int nc col - dy[i] * step; if (nr 0 || nr 15 || nc 0 || nc 15) break; if (board[nr, nc] ! board[row, col]) break; count; } if (count 5) return true; } return false; }四个方向的方向向量(dx, dy)只用了四个组合这是因为水平和垂直加上两条对角线总共就是这四个方向。每条方向只需要向两端延伸最多 4 格就能判断是否达到五连因为超过五格已经没有意义。步长控制为 5配合越界判断能避免数组访问越界。这个实现的好处在于它不需要维护额外的连子状态每次落子后独立计算逻辑简单不容易出错。3.2 棋型编码活三、冲四、眠三到底代表什么人机对战的 AI 部分棋力高低核心取决于棋型评估函数。资源里的做法是扫描棋盘上所有可能的连子线段对每种棋型打一个分数。常见棋型包括五连、活四、冲四、活三、眠三、活二、眠二每种的威胁程度依次递减。private int EvaluateLine(int[] line) { // line 表示以某个点为中心某一方向上的连续棋子片段 int score 0; // 判断黑白双方在片段中的分布情况 bool hasBlack false, hasWhite false; int blackCount 0, whiteCount 0; int emptyCount 0; foreach (int cell in line) { if (cell 1) { hasBlack true; blackCount; } else if (cell 2) { hasWhite true; whiteCount; } else emptyCount; } if (hasBlack hasWhite) return 0; // 双方都有子无威胁 if (blackCount 5 || whiteCount 5) return 100000; // 活四四个己方子 两个空位且两边没有被堵 if (blackCount 4 emptyCount 2) score 10000; else if (blackCount 3 emptyCount 3) score 2000; // 白子同理但权重略低 if (whiteCount 4 emptyCount 2) score 9000; else if (whiteCount 3 emptyCount 3) score 1500; return score; }这个评估逻辑是简化版严格来说棋型判断需要结合具体位置关系例如活三和眠三的区别在于两端是否都被对方棋子堵住。资源里的处理方式是先扫描一整条线统计空位数和双方棋子数粗略判断威胁程度。这种方法在工程上够用但因为没考虑“断点”形态对跳三、跳四这类棋型识别不精确。如果你想让 AI 更强后续可以在这里扩展模式匹配把连续两子空一位再一子的跳棋型单独编码。3.3 评估函数如何决定 AI 落子评估函数在 AI 中的用法不是对单点打分而是对“如果 AI 在这里落一颗子”之后的局面打分。常见做法是对每个空位做假设落子然后重新扫描整个棋盘计算所有四方向线段的总分。资源的 AI 逻辑把这个过程封装成了GetBestMove遍历空位并返回得分最高的位置。private Point GetBestMove() { int bestScore -1; Point bestMove Point.Empty; for (int r 0; r 15; r) { for (int c 0; c 15; c) { if (board[r, c] ! 0) continue; // 假设 AI 在 (r, c) 落子计算局面总分 board[r, c] aiColor; int score EvaluateBoard(); board[r, c] 0; if (score bestScore) { bestScore score; bestMove new Point(c, r); } } } return bestMove; }这里EvaluateBoard会对整个棋盘所有方向的线段评分求和复杂度不算高15 乘 15 的棋盘遍历所有空位并重新评分耗时在一百毫秒以内所以落后在可接受范围。如果你要提速可以只扫描距离最近落子点两步以内的区域因为远处棋型在单步评估中不会改变。资源里没做这个优化但结构已经留好了接口改起来不算麻烦。4. 搜索层与剪枝Alpha-Beta 参数调优和性能边界4.1 为什么单层评估不够需要多层搜索单层评估只考虑了“我下一步走哪最好”没有考虑“我走了以后对手会怎么回应”。对付新手够用但想要棋力看起来像一个正常人类玩家至少需要两层到四层的搜索。资源里给的是 Alpha-Beta 剪枝搜索这是绝大多数棋类 AI 的基础框架。它的核心思想是维护两个边界值Alpha 代表当前玩家能保证的最低分Beta 代表对方能保证的最高分一旦某个子树的分值落在这个区间之外就剪掉不再搜索。private int AlphaBetaSearch(int depth, int alpha, int beta, bool isMaximizing) { if (depth 0) return EvaluateBoard(); ListPoint moves GenerateCandidateMoves(); if (isMaximizing) { int maxEval int.MinValue; foreach (Point move in moves) { board[move.Y, move.X] aiColor; int eval AlphaBetaSearch(depth - 1, alpha, beta, false); board[move.Y, move.X] 0; maxEval Math.Max(maxEval, eval); alpha Math.Max(alpha, eval); if (beta alpha) break; // 剪枝 } return maxEval; } else { // 对手回合取最小分值逻辑对称 } }纸上推导 Alpha-Beta 的时候觉得不难实际写的时候最容易出问题的点是返回值是在某一层的视角下是“我自己的收益”还是“从根节点视角的收益”。这个资源里的实现每一层都以当前局面的视角返回分值然后在回溯时做正负号转换代码简洁但容易看晕。建议调试时在返回语句处打印深度和分值确认每一层的正负方向一致。4.2 候选点生成全部空位搜索还是局部搜索老实说15 乘 15 全空位加四层深度的 Alpha-Beta在普通机器上大概率会卡到两三秒以上这对桌面小游戏来说已经明显影响交互体验了。所以资源里做了一层优化只搜索已有棋子周围一定范围内的空位。private ListPoint GenerateCandidateMoves() { ListPoint candidates new ListPoint(); bool[,] visited new bool[15, 15]; for (int r 0; r 15; r) { for (int c 0; c 15; c) { if (board[r, c] 0) continue; // 围绕已有棋子的棋盘空位扩展两格 for (int dr -2; dr 2; dr) { for (int dc -2; dc 2; dc) { int nr r dr; int nc c dc; if (nr 0 nr 15 nc 0 nc 15) { if (board[nr, nc] 0 !visited[nr, nc]) { visited[nr, nc] true; candidates.Add(new Point(nc, nr)); } } } } } } // 如果棋盘为空直接落中心点 if (candidates.Count 0) candidates.Add(new Point(7, 7)); return candidates; }扩展半径 2 的逻辑是假设影响范围不会超出两格这个假设在大多数局面下成立但开局阶段存在特例。实际测试中如果第一手没有落在中心附近候选点可能覆盖不到棋盘的另半边导致 AI 漏掉关键位置。我一般会在候选点生成后再补上几个固定点比如棋盘的四个角和四条边的中点确保搜索覆盖完整。4.3 搜索深度、节点数和响应时间的权衡资源默认的搜索深度是 4 层实测单步耗时在一秒到三秒之间波动。这个波动主要取决于候选点的数量和剪枝效果。如果对手下出较多散落的棋子候选点会变多搜索时间会明显上升。可以观察的直观指标是候选点数量超出二十个以后四层搜索的耗时基本就突破两秒了。要提速的话除了缩小扩展半径还可以用置换表缓存已经计算过的局面或者把评估函数里的重复计算优化掉。资源自带的评估函数每次重新扫描全盘这在搜索内层会被反复执行开销很大。一个更高效的替代方案是增量更新在递归过程中只重算受影响的行列对角线。// 增量评估的关键思路落子和撤销时只更新当前行、列、两条对角线 private void UpdateLines(int row, int col, int delta) { // delta 为 1 表示落子-1 表示撤销 // 仅重新计算包含 (row, col) 的所有线段而不是全盘扫描 }资源里用的还是全量扫描但你在复现后可以自己加这个增量逻辑棋力不变的情况下速度至少翻倍。不过要注意增量更新在悔棋和撤销时要同时维护额外的计分缓存忘记清理缓存会导致评估分越算越偏是这类优化最常见的翻车点。5. 避坑指南VS 五子棋实现中常见的五个翻车现场5.1 AI 搜索导致界面卡死现象点击落子后窗口立即进入“无响应”状态标题栏出现“未响应”持续 1 到 3 秒后恢复。原因AI 的搜索逻辑直接写在 UI 线程的事件处理函数里for循环遍历空位递归调用UI 线程被阻塞无法处理重绘消息。解决把 AI 计算移出 UI 线程用Task.Run或BackgroundWorker完成后通过Invoke回到 UI 线程更新棋盘。private void AIThinkAndMove() { Point bestMove AlphaBetaSearch(4, int.MinValue, int.MaxValue, true); this.Invoke(new Action(() { PlacePiece(bestMove.Y, bestMove.X); })); }这里Invoke的作用是确保棋盘控件只能在 UI 线程上更新跨线程直接修改board数组虽然不一定崩但控件重绘方法Invalidate会抛异常。加入延迟落子的话可以在Task.Run里先Thread.Sleep(200)模拟思考时间否则 AI 落子过快看起来不太自然。注意不要在Task.Run里直接访问chessBoardControl的属性所有 UI 操作必须回到主线程。5.2 棋型评估分方向不一致导致 AI 出现“盲区”现象AI 在某些局面下明明能四连获胜却去堵对手的三连或者不挡对手的冲四导致直接输棋。原因评估函数对四个方向的扫描方式不对称某个方向的线段漏掉了跳子形态导致该方向上的威胁评分偏低。解决把四个方向的扫描代码提取成同一个方法只传入方向向量强制保证四条路径走完全相同的判断逻辑。更彻底的方案是构造单测用例针对活三、冲四、跳三各摆几个棋形分别打印EvaluateLine的输出比对是否符合预期。5.3 悔棋功能把棋子删错位置现象连续悔棋两次后棋盘出现“幽灵子”或者 AI 认为的棋局和显示不一致。原因悔棋时只改board数组没有同步更新 AI 搜索时依赖的历史记录栈或者棋盘数组与 UI 重绘的顺序不对数组已经回退但控件还是旧画面。解决用一个StackMoveInfo保存每步的落子方和坐标悔棋时先弹栈再根据栈里的数据恢复棋盘。StackMoveInfo history new StackMoveInfo(); private void Undo() { if (history.Count 2) return; // 先撤销玩家的一步 MoveInfo playerMove history.Pop(); board[playerMove.Row, playerMove.Col] 0; // 如果当前是人机模式再撤销 AI 的一步 if (isVsComputer history.Count 0) { MoveInfo aiMove history.Pop(); board[aiMove.Row, aiMove.Col] 0; } chessBoardControl.Invalidate(); }悔棋逻辑的复杂度在于人机模式需要一次性回退两步否则回合制就乱了。另外悔棋后 AI 的搜索缓存如果没有失效AI 会基于旧局面做计算导致落子逻辑错乱。最简单的处理是在悔棋后强制清空所有缓存标志位或者干脆每一次搜索都不复用历史结果多花几十毫秒换稳定性。5.4 窗口缩放后棋盘格子对不上现象最大化窗口后鼠标落点偏移点击的位置和棋子实际出现的位置隔了一格。原因绘制的cellSize用固定像素但窗口拉伸后棋盘区域的实际宽度变了鼠标坐标换算时还用旧的cellSize。解决每次重绘时根据控件实际尺寸重新计算cellSize换算鼠标坐标时也使用实时值。private int GetCellSize() { return Math.Min(chessBoardControl.Width, chessBoardControl.Height) / 15; }注意GetCellSize要在MouseClick事件里调用不能缓存到字段里然后在Resize时更新因为Resize事件和实际重绘之间有时间差快速拖拽窗口时缓存值很容易过期。我一般连背景棋盘线的绘制也用这个实时值保证棋子、格线、鼠标换算三个层面用的是同一套计算。5.5 胜负判断出现“斜线赢了却不认”的情况现象横线或竖线五连能正常判定但斜线五连没有反应。原因方向向量写错了四条方向中有一条斜率算反导致斜线方向扫到的不是对角相邻的格子。解决把四个方向向量统一列在表格里逐一对地排查重点确认dx和dy的符号组合覆盖了两条对角线。检查方向向量组合(1,0)水平向右、(0,1)垂直向下、(1,1)主对角线、(1,-1)副对角线。注意副对角线的dy是-1如果错写成1扫描到的位置就是另外一条斜线。这个错在单步调试里很难看出来因为只有副对角线上的五连才触发平时下棋很少会注意到。快速排查办法是写一个硬编码棋局在副对角线上摆五颗同色子调用CheckWin看返回结果即可。6. 把 AI 调强一点评估函数可视化与搜索调参的实操技巧很多人在拿到资源后试着跑了几盘赢了菜鸟水平就放下了。但如果你想把这个作品当成简历项目或者课程设计来交AI 的棋力表现是区分“套模板”和“理解了实现”的关键。这里分享一个我调 AI 时必做的土办法把每一步 AI 的九个候选位置及其评分打印出来。private void DebugScores(ListPoint candidates) { StringBuilder sb new StringBuilder(); foreach (Point p in candidates) { board[p.Y, p.X] aiColor; int attackScore EvaluateBoard(); board[p.Y, p.X] 0; board[p.Y, p.X] playerColor; int defendScore EvaluateBoard(); board[p.Y, p.X] 0; sb.AppendLine($({p.X},{p.Y}) 攻:{attackScore} 守:{defendScore}); } System.Diagnostics.Debug.WriteLine(sb.ToString()); }这段代码的作用是把每个空位“我方落子后”和“对方落子后”两个维度的评分并列输出。排错时你会发现很多看起来不合理的落子原因都是防守分算得太高或者进攻分的某种棋型被重复计分。如果发现 AI 老是去堵对手的二连而不顾自己已经形成的活三多半是EvaluateLine里活三分支返回值设置过低或者连续棋型判断里把活三错算成了眠三。此时不需要改搜索层参数只需要回到棋型函数调整分值权重。调分值的时候建议先把五连设为十万活四一万冲四八千活三两千眠三五百活二一百然后跑几局观察 AI 是否出现以下行为能四连时果断下赢棋位置、对方有冲四时必须立刻堵点、对方有活三时优先在活三两端落子而不是下无关位置。如果这些行为都正常就可以逐级拉高活三和冲四的相对分值观察 AI 是否更喜欢通过做活三来制造攻势。另外一个实用技巧是控制搜索深度在 4 和 6 之间切换测试。同一个棋盘状态下跑一次深度 4记录落子位置和耗时再跑一次深度 6对比两者选的位置是否一致。如果深度 6 选的位置和深度 4 差得很远说明评估函数的棋型权重存在局部盲区如果两个深度选的位置一致但耗时差了三倍以上说明剪枝效率不高候选点生成器可以进一步缩小范围。资源里的 AI 模块没有做开局库和残局库这是它的边界。如果你想要 AI 在开局阶段更像人类棋手最简单的做法是维护一个固定的前五步落子表比如第一步中心、第二步根据玩家落子选择角位或侧翼五步以后才进入搜索。这样实现成本很低但观感上会明显比纯搜索更自然。我后来在某个跨平台系统里做棋类模块时沿用了一模一样的思路先用规则表垫底再让搜索层接管中后盘效果好过单纯堆搜索深度。UI 线程和搜索线程的时间配合也算一个进阶技巧。棋力提升后单层搜索耗时可能到两三秒这期间玩家感觉游戏“卡住了”。与其让玩家干等不如在到达搜索深度上限前先快速跑一个深度 2 的搜索结果垫底然后后台慢慢加深搜索等结果算完再覆盖落子。这个“先快后准”的策略我用在多个游戏 Demo 里效果比加一个进度条更好。实现方式也不复杂第一次搜索完成后如果还没到限制时间就自动在当前局面基础上多搜两层。这套五子棋资源整体来说不算那种开箱即用到惊艳的成品但作为学习和复现的底子代码结构清晰扩展点够多适合在拿到后做二次开发。我每回给团队成员交接类似资源时都会强调先原封不动跑通一遍再做至少一处自己和需求相关的改动无论是加一个禁手规则、加一档 AI 难度还是换成 19 路棋盘。跑通再改能保证你踩坑时知道是自己引入的还是框架本身的问题。改完以后记得把调试用的评分输出代码注释掉或者做成全局开关不然正式演示时输出窗口刷屏会显得很业余。希望这份拆解能帮你在复现五子棋人机对战时少走几步弯路也祝你调出来的 AI 在复盘时能让你自己眼前一亮。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询