Visual Studio五子棋人机对战:评分表算法与C#实现

发布时间:2026/10/14 17:18:19
Visual Studio五子棋人机对战:评分表算法与C#实现 简介一套基于Visual Studio编写的五子棋游戏完整工程采用C结合EasyX图形库实现覆盖人机对战与人人对战两种模式。项目围绕棋盘状态管理、合法落子判断、五子连珠检测、AI落子策略等核心逻辑展开面向正在学习Windows窗体程序、游戏算法或C实战项目的开发者适合作为课程设计或入门练手参考。压缩包内共49个文件主体包括cpp源文件与h头文件、用于界面及棋子的jpg/png图像素材、wav背景音乐、以及解决方案工程文件与调试符号文件整体约107MB解压后可直接用Visual Studio打开构建。目前已有2655人学习下载。借助附带的素材与工程结构使用者可以完整跟踪棋盘绘制、事件响应、AI决策及胜负判定等实现细节还能基于现有框架自行扩展难度或增加悔棋等功能对理解桌面游戏开发流程很有帮助。1. Visual Studio 写五子棋人机对战的核心不是界面而是棋盘上的那个大脑想用 Visual Studio 做一套带人机对战的五子棋很多人第一步就去折腾棋盘的绘制、鼠标落子的响应结果界面做得挺好看AI 却只会堵在对手旁边乱下一气。真正让五子棋从「能玩」变成「能对战」的是棋盘背后那套评分算法——它决定电脑往哪儿下、为什么往那儿下。这套工程最适合正在找课程设计题目、或者想用 C#/C 把数据结构与算法真正落地的开发者。把棋盘建模、胜负判定、AI 评分和界面事件串起来就是一个完整且能演示的桌面应用而且每一步都能单独验证难度曲线比想象中平缓。2. 棋盘建模与落子逻辑先让数据结构和坐标索引对齐2.1 棋盘数据结构用二维数组还是位棋盘五子棋的棋盘标准是 15×15落子状态只有空、黑、白三种。最直接的做法是int[15,15]0 表示空、1 表示黑棋、2 表示白棋。选这个结构的原因很朴素人机 AI 每下一步都要反复扫描棋盘评分二维数组的随机访问是 O(1)遍历也直观而位棋盘用 64 位整数压缩行状态虽然省内存但在评分时要频繁做位运算转换增加了理解成本。对于 Visual Studio 里的这类桌面项目二维数组的可读性优势明显而且调试时可以直接看数组内容不用做进制转换。棋子坐标和界面像素坐标的映射是另一个容易埋坑的地方。棋盘画出来之后鼠标点击的位置是像素坐标比如(137, 223)但落子要落在格子上需要换算成数组索引。换算公式要同时处理边界和取整private Point PixelToGrid(Point p, int margin, int cellSize) { int gx (int)Math.Round((p.X - margin) / (double)cellSize); int gy (int)Math.Round((p.Y - margin) / (double)cellSize); if (gx 0 || gx 15 || gy 0 || gy 15) return Point.Empty; // 越界不落子 return new Point(gx, gy); }margin是棋盘边缘留白像素cellSize是相邻网格线的间距。Math.Round而不是直接int强转是因为鼠标在格子中间附近点击时浮点除法会得到 7.6、8.4 这样的值直接截断会落到错误的格子上。越界判断必须在取整之后做否则点击棋盘外的空白区域会得到一个负数索引数组访问直接抛异常。2.2 落子、悔棋与提子用栈保存对局历史落子操作不只是往数组里写一个数字还要让界面能跟着刷新、让 AI 知道「该我了」。一个常规的做法是维护一个StackPoint保存落子历史黑棋白棋交替落子悔棋时弹出最后一次落子并把数组对应位置清零。private StackPoint moveHistory new StackPoint(); private int currentPlayer 1; // 1黑 2白 public bool PlaceStone(int gridX, int gridY) { if (board[gridY, gridX] ! 0) return false; // 已有棋子 board[gridY, gridX] currentPlayer; moveHistory.Push(new Point(gridX, gridY)); lblStatus.Text (currentPlayer 1) ? 白棋回合 : 黑棋回合; currentPlayer (currentPlayer 1) ? 2 : 1; Invalidate(); // 触发重绘 return true; } public bool UndoLastMove() { if (moveHistory.Count 0) return false; Point last moveHistory.Pop(); board[last.Y, last.X] 0; currentPlayer (currentPlayer 1) ? 2 : 1; Invalidate(); return true; }用栈这里有个细节容易被忽略悔棋后currentPlayer要翻转回来。如果黑棋刚下完、当前轮到白棋悔棋撤掉的是黑棋那当前玩家应该是黑棋。翻转逻辑只适用于「悔一步」如果支持连续悔棋得在每次Pop后都翻转一次或者干脆保存一个完整的状态快照。栈的形式直观但要注意它只保存了坐标没有保存每一步的棋盘状态所以 AI 在悔棋后的下一手计算必须从当前棋盘数组重新扫描不能依赖任何缓存。3. 人机 AI 的核心评分表算法与落子决策3.1 为什么不是极大极小搜索做过棋类 AI 的人第一反应可能是极大极小搜索加 Alpha-Beta 剪枝。但在五子棋上这个方案的性价比不高——15×15 棋盘有 225 个落子点搜索深度到 4 层时即使有剪枝评估节点的数量也在万级到十万级而每次评估都要对棋盘做多方向扫描在 WinForms 界面里直接同步执行界面会卡死数秒体验极差。另一个问题是五子棋的棋型评价活三、冲四、双活三远比「局势好坏」这种抽象评估更有效。与其搜索几十步之后的局面不如直接对当前每个空点打分看谁最能形成威胁。评分表算法的思路是对每个空位假设黑棋落在那里计算这个点对黑棋的价值再假设白棋落在那里计算对白棋的价值两者加权求和就是该点的最终得分。这个算法不需要递归只需要一次全棋盘扫描计算量在几百次棋型识别以内界面完全扛得住。视觉上 AI 的每一步都「看得懂」——它会优先堵你的活三能成五连就走五连行为接近真人下棋的逻辑。3.2 棋型打分表把棋型翻译成数字五子棋的棋型可以拆成「连子数 两端状态」的组合。两端状态只有两种一端被堵有对方棋子或墙、两端都开放。一组常用分值如下。棋型含义分值五连五个连续同色棋子1000000活四四个连续且两端开放100000冲四四个连续但一端被堵10000活三三个连续且两端开放8000眠三三个连续但一端被堵1000活二两个连续且两端开放500眠二两个连续但一端被堵100活一单子且两端开放50分值差距设得很大是为了让 AI 在任何情况下都优先考虑成五连和堵对方的活四。实战里冲四的分值可以再调因为冲四往往能逼对方应一手从而腾出另一个进攻点。工程上建议把这些分值做成一个Dictionarystring, int或者常量数组方便后面实验参数。3.3 落子决策遍历空点、攻守合并、随机打破平局AI 每次决策时做三件事遍历所有空点对每个空点分别按黑棋和白棋角度计算分数进攻分和防守分加权取总分最高的点落子。public Point GetBestMove(int[,] board, int aiPlayer) { int opponent (aiPlayer 1) ? 2 : 1; int bestScore -1; ListPoint candidates new ListPoint(); for (int y 0; y 15; y) { for (int x 0; x 15; x) { if (board[y, x] ! 0) continue; int attack EvaluatePoint(board, x, y, aiPlayer); int defend EvaluatePoint(board, x, y, opponent); int total attack (int)(defend * 1.1); // 防守权重略高 if (total bestScore) { bestScore total; candidates.Clear(); candidates.Add(new Point(x, y)); } else if (total bestScore) { candidates.Add(new Point(x, y)); // 同分候选 } } } Random rand new Random(); return candidates[rand.Next(candidates.Count)]; }得分相同的多个点随机选一个这一步很关键。如果不做随机化AI 面对对称局面会一直走同一个方向对局看起来死板加了随机后每一局的走势会有变化。防守分乘 1.1 是我调试下来的习惯值因为五子棋「先防后攻」的容错率更高——漏堵一个活三下一手可能就是死局。EvaluatePoint是 AI 的核心函数它临时把某个点涂成指定玩家的棋子然后从该点出发沿四个方向数棋型private int EvaluatePoint(int[,] board, int x, int y, int player) { board[y, x] player; // 临时落子 int score 0; int[,] dirs { { 1, 0 }, { 0, 1 }, { 1, 1 }, { 1, -1 } }; for (int i 0; i 4; i) { int dx dirs[i, 0], dy dirs[i, 1]; int count 1, openEnds 0; // 正方向延伸 int nx x dx, ny y dy; while (nx 0 nx 15 ny 0 ny 15 board[ny, nx] player) { count; nx dx; ny dy; } if (nx 0 nx 15 ny 0 ny 15 board[ny, nx] 0) openEnds; // 反方向延伸 nx x - dx; ny y - dy; while (nx 0 nx 15 ny 0 ny 15 board[ny, nx] player) { count; nx - dx; ny - dy; } if (nx 0 nx 15 ny 0 ny 15 board[ny, nx] 0) openEnds; score GetPatternScore(count, openEnds); } board[y, x] 0; // 还原棋盘 return score; }这里count是包含当前点的连续同色棋子数openEnds记录这个连续序列两端有多少个空位最多为 2。GetPatternScore查表返回对应分值。注意函数末尾的board[y, x] 0必须执行——临时落子后不还原整个评分就废了。我见过有开发者把还原逻辑写在某个 return 之后导致棋盘状态被污染后续评估全错。4. 工程实施界面绘制、事件响应与 AI 异步计算4.1 工程选型C# WinForms 还是 C GDIVisual Studio 里做五子棋C# WinForms 的工程效率最高。事件驱动模型直接对应鼠标点击、定时器刷新Graphics对象画直线和椭圆都是现成的 APIC 用 GDI 也能实现但消息循环和窗口过程要自己维护项目时间会多花 30% 以上。如果你的目标是把精力放在 AI 算法上选 WinForms如果想练底层窗口机制再考虑 C。实际调试中 WinForms 的PaintEventArgs可以直接拿来画棋盘和棋子不需要额外引入第三方控件库。4.2 棋盘绘制双缓冲与坐标换算绘制逻辑统一放在Form_Paint事件里画的内容包括棋盘网格线、星位标记、已落棋子。最容易出问题的环节是棋子错位——网格线坐标和数组索引不一致导致画出来的棋子在两条线中间。private void Form_Paint(object sender, PaintEventArgs e) { Graphics g e.Graphics; int margin 30, cellSize 40; // 画网格线 for (int i 0; i 15; i) { g.DrawLine(Pens.Black, margin i * cellSize, margin, margin i * cellSize, margin 14 * cellSize); g.DrawLine(Pens.Black, margin, margin i * cellSize, margin 14 * cellSize, margin i * cellSize); } // 画棋子 for (int y 0; y 15; y) { for (int x 0; x 15; x) { if (board[y, x] 0) continue; int px margin x * cellSize; int py margin y * cellSize; if (board[y, x] 1) g.FillEllipse(Brushes.Black, px - 14, py - 14, 28, 28); else g.FillEllipse(Brushes.White, px - 14, py - 14, 28, 28); } } }margin i * cellSize是画线和画棋盘格的统一基准。PixelToGrid换算时用的也是这套公式只要两处保持一致棋子永远不会画偏。还有一步不能省窗体构造函数里开启双缓冲否则每落一子整个棋盘会闪烁。SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer, true);OptimizedDoubleBuffer让所有绘制先写进内存位图、再一次刷到屏幕。没有这行代码快速落子或拖动窗口时棋盘会出现明显的残影。4.3 AI 计算不卡界面异步落子与状态保护AI 的GetBestMove在普通局面下耗时在几十毫秒到几百毫秒之间取决于评分函数实现但如果直接在 UI 线程里同步跑落子后界面会短暂无响应。工程上的标准做法是Task.Run把 AI 计算丢到后台线程计算完成后用Invoke回到 UI 线程更新棋盘。private void AITurn() { if (isAIThinking) return; isAIThinking true; int[,] snapshot (int[,])board.Clone(); // 防止棋盘被悔棋修改 Task.Run(() { Point move GetBestMove(snapshot, aiPlayer); BeginInvoke(new Action(() { PlaceStone(move.X, move.Y); isAIThinking false; CheckGameOver(move.X, move.Y); })); }); }board.Clone()这一步容易被忽略用户如果在 AI 思考时点了悔棋后台线程还在读旧的棋盘数组就会出现「AI 计算了半天落子落在了一个已经被悔掉的位置」。快照保证 AI 计算的是发起时刻的局面。isAIThinking作为标志位避免 AI 还没算完玩家又触发第二次思考。注意Invoke和BeginInvoke的差别前者会阻塞后台线程直到 UI 处理完后者异步排队不会卡 AI 的后续逻辑。5. 避坑与排查五子棋工程里最常见的四个翻车点5.1 棋子画在线中间和鼠标点击位置对不上现象 鼠标点下去在格子里棋子却出现在两条网格线之间或者格子边缘点击时落子落到隔壁。原因PixelToGrid的取整逻辑不对像素坐标到数组索引的换算用了int强转而不是Math.Round另一处是网格线绘制时的起点和margin不一致绘制基准就偏了。解决 统一用一个函数生成坐标比如GridToPixel和PixelToGrid都基于margin index * cellSize计算PixelToGrid用Math.Round并对x margin - cellSize/2这类边界做提前过滤。改完后再用鼠标在左上角、右下角各落一子验证映射是否精确。5.2 AI 不堵活三专下没用的位置现象 AI 分数算出来很高但实际走出来的棋完全不防守坚持下在离战局很远的地方。原因 评分函数里EvaluatePoint只计算了进攻角度的棋型没有把防守分合进来或者防守分的权重设成了 0。另一种情况是临时落子后没有还原棋盘后续所有空点的评估都建立在被污染的棋盘上分数乱掉。解决 检查GetBestMove里是否同时调用了两个EvaluatePoint一个传aiPlayer一个传opponent确认EvaluatePoint里board[y, x] 0在返回前一定执行。调试时可以打印每个候选点的进攻分、防守分和总分看 AI 选的点是否在数值上合理。5.3 白棋赢棋判定失败五连不弹胜利现象 黑棋五个连成一线能判定胜利白棋同样五连却没有任何反应。原因 胜负判定用board[y, x] 1硬编码了黑棋或者CheckGameOver里只接收黑棋落子的触发白棋落子后没有调用判定函数。解决 落子后统一调用CheckGameOver(x, y)函数内部取出board[y, x]的值作为当前玩家再沿四个方向数连子。不要在判定函数里写死 1 或 2用参数传进去。同一套判定逻辑要覆盖黑棋白棋。5.4 AI 思考时界面卡死点哪儿都没反应现象 AI 落子前整个窗体变成「未响应」状态过好几秒才恢复。原因GetBestMove直接跑在 UI 线程。15×15 棋盘虽然只有 225 个点但如果棋型识别写得低效——比如每个点都对全盘做四方向全扫描——计算量会膨胀到几十万次循环UI 线程被占满。解决 把 AI 决策放进Task.Run用快照棋盘计算完再BeginInvoke回 UI 线程。同时可以优化EvaluatePoint只扫描以落子点为中心、半径 4 的窗口五子连珠最长就是五子超过半径 4 的棋子不可能参与当前点的棋型组合。窗口扫描能把评估耗时降到原来的十分之一左右。6. 进阶技巧让 AI 更难缠的三招基础评分表能让 AI 达到「能下、能防」的水平但它的走法太机械同一局面下每次的选择都是固定的容易被摸透。我在调一个模拟项目 X 的 AI 时用了三招效果明显。第一招是给评分加随机扰动total rand.Next(-5, 6)。扰动值远小于棋型分值的数量级差距不会让 AI 放弃一个活四去走边角但能让几个分数接近的候选点之间有随机变化对局不再千篇一律。第二招是候选点剪枝。不要全盘遍历所有空点只评估「距离已有棋子不超过 2 格」的空点。实战中没人会把第五手打在离战场八格远的地方剪枝后 AI 的思考时间从几百毫秒降到十几毫秒这为后面加搜索深度留出了余量。第三招是验算习惯。每次改完评分表或防守权重我会固定跑一个自测先手动布一个对方活三的局面让 AI 落子看它是不是第一时间堵住再布一个自己能冲四的局面看 AI 是不是直接走冲四而不是去围边角。这两步通过再进入正常对局。某次我把防守权重从 1.1 调到了 1.5AI 变得过于保守明明能直接赢的冲四不走了反而去堵一个无关紧要的眠二——全靠这套验算流程拦下来。从那以后我每次动 AI 参数都强制先跑一遍活三拦截和冲四进攻两个场景再谈后续调优。这套东西做到最后一个 Visual Studio 工程文件、几百行核心代码就能换来一套能演示、能对战、能改进的五子棋人机对战程序。你拿到手后先把评分表的分值改一版、跑几局看看 AI 风格变化再按「验算两场景」的流程过一遍基本就能摸清 AI 的脾气了。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

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

尧图内容编辑团队

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

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

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

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

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

网站改版的5个关键决策

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

获取专属建站方案

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

立即免费咨询