
简介一份基于Unity引擎实现的五子棋人机对战小游戏项目资源面向Unity游戏开发、C#编程以及AI算法感兴趣的初中级学习者适合作为课程设计或技术入门参考。资源以zip压缩包形式提供整体大小约20.43MB便于快速下载与本地运行。项目围绕一个完整可玩的小型游戏展开既包含Unity场景与交互界面的搭建思路也涉及C#脚本对游戏规则、胜负判断和落子响应的实现核心亮点是AI模块对博弈树搜索的应用通过评估函数对棋盘局势打分并利用Minimax算法与Alpha-Beta剪枝来优化决策路径减少无效搜索。跟随该项目实践可以较直观地理解状态空间搜索在游戏AI中的落地方式同时掌握将算法转化为实际游戏功能的方法。当前已有522人学习对希望结合游戏体验来学习AI原理的开发者来说是一个轻量而完整的示例。 如果你让我给刚接触编程的朋友推荐一个练手项目我的答案大概率是五子棋人机对战。它看上去不过是一个15×15的棋盘但要把“电脑会下棋”这件事真正做出来你得同时啃掉数据结构、算法设计、状态管理、事件驱动这几块硬骨头。我从最早用控制台打印棋盘到后来用Canvas做网页版再给AI逐步加上评估、搜索和剪枝前后迭代了好几版。这篇文章就按我实际开发的顺序把从零到一的完整过程和踩过的坑一起写出来。想找个完整项目练手、又打算搞懂AI决策原理的朋友应该能从里面拿走不少东西。1. 动手之前拆解一次人机对弈到底要做什么很多人拿到这个需求第一反应是打开编辑器画棋盘画完就开始写点击落子写着写着发现逻辑乱成一团。我自己的习惯正相反先不动代码把“一次完整对弈”从头到尾在脑子里过一遍棋盘初始化玩家落子判断是否五连AI思考AI落子再判断切换回合……把这个流程一拆项目的边界就清楚了。1.1 四个必须分开的模块五子棋人机对战看着简单但至少包含四块独立的东西棋盘状态记录15×15个交叉点上的棋子状态是整个对弈的共享数据模型。落子规则坐标换算、合法性校验、落子后的胜负判定属于业务逻辑层。AI决策输入当前局面和执棋颜色输出落子坐标是整场博弈的大脑。界面表现画棋盘、画棋子、处理鼠标事件、展示胜负结果。这四块可以分别独立开发和测试。我的顺序是先把棋盘状态和落子规则写完让玩家能手动交替下棋再用最简单的随机落子把AI占位跑通整局最后才把真正的AI算法换进去。分模块还有一个隐性好处调试AI时不需要每次从零开局直接在代码里构造一个指定的棋盘局面丢给它就能验证某个具体情形下的决策是否正确。1.2 技术选型为什么我建议先用Web前端这篇文章里的示例用HTML JavaScript Canvas实现。选它不是因为性能多强而是因为一个文件就能在浏览器里双击运行零环境依赖对新手最友好。JavaScript的数组和对象操作非常直白写AI算法时不用操心内存管理调试也方便。如果你更想练Python整体思路完全一致把Canvas换成Pygame逻辑层代码基本可以平移。至于要不要上Vue/React这类框架我的看法是这个项目规模还用不上框架硬上反而多一层心智负担等做到联机对战、房间管理再引入也不迟。1.3 用状态机管理回合流转整局对弈可以看成一个非常简单的状态机轮到玩家、AI思考中、轮到AI、回到玩家每一步落完都要检查游戏是否结束。我代码里用一个turn变量表示当前执棋方用isThinking标志锁住玩家输入一旦胜负判定通过就把状态切到GAME_OVER。很多初学者项目出现“玩家连下两子”“AI还没想好玩家又点了一下”这类bug本质都是回合状态没有管理清楚。提前把这个状态流转想明白后面写事件处理会顺畅很多。2. 棋盘数据与胜负判定地基没打好后面全是补丁棋盘的表示方式看似简单但它是整个项目的地基。AI评估函数要在棋盘上做大量方向扫描如果这个数据结构选得不顺手后面写起来会很难受。2.1 棋盘用二维数组还是其他结构棋盘数据我推荐最直接的二维数组const board Array.from({ length: 15 }, () new Array(15).fill(0));用0表示空、1表示黑子、2表示白子。有人会用一维数组甚至位运算来压缩存储性能确实更好但在这个项目里完全没必要。二维数组board[row][col]的索引方式最贴近人脑后续写AI评估函数时要频繁沿着四个方向扫描相邻棋子这种可读性带来的收益远大于那一点点性能提升。2.2 坐标换算像素坐标与格子坐标的边界Canvas绘制用的是像素坐标业务逻辑关心的是格子坐标。棋盘左上角留出边距margin格子间距为cellSize那么鼠标点击位置可以这样算const col Math.round((e.offsetX - margin) / cellSize); const row Math.round((e.offsetY - margin) / cellSize);计算完再判断row和col是否落在0到14范围内。这里要特别提醒一定要先四舍五入再判断越界否则点在棋盘边缘很容易算出负数坐标一个看着很正常的落子动作就变成数组越界了。计算格子索引后绘制时用margin col * cellSize来定位交叉点棋子和格线才能严格对齐。2.3 胜利判定以小见大只扫四根线最简单也最不容易出bug的胜利判定是每次落子后只从当前落子点向四个方向扫描水平、垂直、主对角线、副对角线。每个方向向两端延伸统计同色连续棋子数只要有一个方向连续数大于等于5就结束。function checkWin(row, col, player) { const dirs [[1, 0], [0, 1], [1, 1], [1, -1]]; for (const [dx, dy] of dirs) { let count 1; for (let step 1; step 5; step) { const nr row dx * step, nc col dy * step; if (board[nr]?.[nc] ! player) break; count; } for (let step 1; step 5; step) { const nr row - dx * step, nc col - dy * step; if (board[nr]?.[nc] ! player) break; count; } if (count 5) return true; } return false; }为什么只检查当前落子点因为新增的一颗子只可能让包含它的新棋形获胜。如果此前已经五连对局早就结束了根本轮不到这一步。这里用可选链board[nr]?.[nc]来规避数组越界判断如果你用Python写成if 0 nr 15 and 0 nc 15也是一样的效果。四方向扫描的复杂度是常数级对每步棋来说几乎不耗时间。2.4 悔棋与重开历史栈带来的状态回退悔棋在逻辑层就是弹栈。维护一个moves数组每次落子都push进{row, col, player}。悔棋时从栈里弹出一步把board对应位置重置为0再重绘。这个功能看着容易其实藏着一个新手很容易踩的坑玩家悔棋悔的往往是自己刚走的那一手的正对面也就是AI刚下的那一手。如果只弹一步棋盘状态退回到AI落子前但当前回合又是玩家回合AI没有重新思考的入口整个局面就僵住了。我处理的方案是玩家回合悔棋时连续弹出两步把玩家和AI的各一步一起退掉然后重新轮到玩家。另外重开一局时要把board、moves、turn、winner全部重置干净。我早期只重置了board结果点“再来一局”之后悔棋按钮还能弹出上一局的历史记录整个状态全乱了——这种脏状态问题排查起来很费劲最好在一开始就做好完整reset函数。3. AI决策核心评分、搜索和剪枝的一次完整配合AI是五子棋人机对战项目的灵魂。很多教程讲到AI就直接上Minimax但我觉得先理解评分逻辑更重要因为搜索算法是在评分基础上做推演的基础不打牢后面全是空中楼阁。3.1 AI到底在思考什么AI下棋的本质是对每一个可选落子点打分然后选分数最高的那个。但“分数高”必须用棋手的方式定义。我们要扫描一个点四方向形成的棋形连续几个子、两端是否被封死根据棋形给出不同档位的分数。我用的是一套常见的参考分数棋型分数说明五连100000直接获胜活四50000两端都开放下一手必胜冲四10000只有一端开放对手必须堵活三5000再下一手可成四眠三1000威胁有限但需要警惕活二500开局布局的基础眠二100影响力较小这套分数的绝对值不是关键级差才是。五连必须大得离谱活四和冲四的差距要严格拉开这样AI才明白该赢的时候直接赢该冲四的时候别去贪一个活三。分数设置不合理的话AI经常会出现“优势局面不走制胜手”的诡异行为这种问题查起来很让人头疼。3.2 判定棋型把一条线切成片段要算一个点在某个方向上的棋型分数最直接的做法是以这个点为中心向两端各延伸若干格统计连续的同色棋子数以及两端的状态。我把这部分抽成evaluateDirection函数返回四个方向的分数之和。function evaluateDirection(row, col, player, dx, dy) { let count 1; let openEnds 0; // 沿正方向延伸 let nr row dx, nc col dy; while (inBoard(nr, nc) board[nr][nc] player) { count; nr dx; nc dy; } if (inBoard(nr, nc) board[nr][nc] 0) openEnds; // 沿反方向延伸 nr row - dx; nc col - dy; while (inBoard(nr, nc) board[nr][nc] player) { count; nr - dx; nc - dy; } if (inBoard(nr, nc) board[nr][nc] 0) openEnds; return shapeScore(count, openEnds); }这里最关键的是区分“两端都开放”和“一端被封死”。同样是连续三个子活三和眠三的威胁完全不是一个量级。扫描时还要小心己方棋子被对手隔断的情况例如“空、黑、黑、白、黑”两个黑的片段绝不能合并算一个活二。评估函数是整个AI的基石它准不准直接决定AI的棋力上限。我调试时经常把某个候选点的每个方向分数打印出来一旦发现AI走了明显不合理的棋多半就是某个方向的棋型判断漏了边界情况。3.3 进攻分与防守分会进攻也得会堵只计算“我落子后的价值”会出大问题AI只顾发展自己的活三完全无视对手已经冲四的事实最后被一手反杀。我第一次跑到这个bug时还挺惊讶的因为单看代码逻辑感觉没什么问题。解决方案是每个候选点同时算两遍分进攻分我下在这里的价值和防守分如果让对手下在这里的价值。最终得分可以用进攻分 * 1.1 防守分来算。为什么要乘1.1只是让AI在势均力敌时略微激进一点优先展开自己的攻势。一旦对手出现高威胁棋型防守分本身会大到压倒一切AI自然知道该去堵。系数用1.1还是1.2可以自己调我的经验是系数太高会让AI过于激进明明该防守的时候还在拼命进攻反而容易崩盘。3.4 从贪心到极小化极大一步与多步的差距贪心选点实现简单但短视。经典的双三局面里AI只看一步时永远只能堵住一个活三对手下一手在另一侧形成连接就赢了。要处理这种组合杀需要把后续回合也纳入考虑。最经典的做法是极小化极大搜索AI作为Max方对手作为Min方双方交替模拟落子若干步每层都选择能让自己评分最大、让对手评分最小的分支。这个思路跟下棋的直觉完全一致——我不能只考虑自己这一步爽不爽还要想想对手可能的回应。配上Alpha-Beta剪枝可以把大量没必要的分支直接剪掉搜索效率会高很多。3.5 搜索优化的两个杀手锏五子棋的搜索宽度其实不小15×15共225个空位如果全量枚举再强的剪枝也撑不住。我实际应用了两个非常有效的优化。第一是候选点裁剪只搜索已有棋子周围两格半径内的空位。远离棋子的位置几乎不可能是合理落点候选点能从200多个降到20个左右搜索规模直接小一个量级。第二是启发式排序递归之前先按评估分数把候选点排个序让高分走法先被搜索。Alpha-Beta剪枝的效率极度依赖节点访问顺序如果先搜的是差分支剪枝规则根本触发不了排序之后剪枝率会大幅提升。实测在JavaScript里深度4、候选点20个左右的情况下单步搜索稳定在几百毫秒配合异步任务处理完全不会卡住界面。4. 界面交互与视觉反馈让游戏至少拿得出手算法再强界面一塌糊涂也没人愿意玩。我在这个项目里最大的体会是交互细节带来的体验提升往往比多写一百行算法更直观。4.1 画棋盘细节决定观感棋盘我是按600×600像素画的15×15格、格子间距40像素、周围边距20像素。画布背景用接近木纹的浅黄色格线用深棕色整体颜色偏暖看着比较舒服。棋子用fillStyle加arc圆来画但别用纯黑纯白黑子用深灰径向渐变白子加一圈浅灰描边否则画面会显得很死板。另外我强烈建议落子前用mousemove画一个半透明预览子让玩家清楚看到当前悬停在哪个交叉点。这个小细节对体验的提升非常明显玩家不需要自己数格子。4.2 鼠标事件与回合锁点击事件主要做三件事换算格子坐标、校验空位、执行落子。但AI思考期间必须锁住输入否则玩家快速点击会造成棋盘状态错乱。我用isThinking标志位true时click事件直接return。玩家落子后立即把isThinking置为trueAI落子完成后再置回false。棋盘上方加一条状态栏显示当前是谁的回合玩家就不需要猜测自己该不该点。状态栏文案也很简单轮到玩家显示“你的回合”AI思考时显示“电脑思考中……”。4.3 胜负呈现别让体验停在alert判断到五连之后我会用一层半透明遮罩盖住棋盘中间显示“黑子胜”或“白子胜”附一个“再来一局”按钮。很多入门教程直接用alert弹窗体验相当差而且alert会阻塞JavaScript主线程让整个页面像崩溃了一样。弹结果前用setTimeout给落子动画留一点播放时间否则玩家可能根本看不清最后一手落在哪里结果就糊脸上了。这些看起来是小事但一个游戏“能不能拿得出手”很大程度上就差在这些细节上。5. 实测中的坑与调优记录任何项目写到能跑只是第一步把体验和棋力调到位才是真正花时间的地方。这里记几个我在实测阶段真实踩过、也真实解决掉的坑。5.1 深度上去后AI变慢原因不是计算量大第一次把搜索深度从2提到4浏览器明显卡顿。我一开始以为是候选点太多但降到10个候选点之后还是慢。实际排查发现剪枝几乎没生效——因为我没对候选点做任何排序。Alpha-Beta剪枝非常依赖节点访问顺序如果先搜到一堆差分支剪枝规则根本触发不了搜索树就退化成全量枚举。加上启发式排序之后同样的深度下耗时反而下降。这个坑让我意识到算法优化的顺序往往不是加算力而是先把数据的访问顺序处理好。5.2 评估函数的局部视野漏掉了组合杀单点棋型评价天然漏掉一种情况双威胁。比如某个点落子后能同时形成两个活三这种棋形在单点评分里可能只被算成一个活三的分数但实战里它就是必胜的组合杀。我给AI加了一个后处理对得分最高的前N个候选点额外检查落子后是否形成双三或双四是的话直接加一个加权分。这个改动成本很低棋力提升却很显著。如果你想让AI更强还可以在这个基础上再考虑跳活三、跳冲四等特殊棋型方向是一样的。5.3 用自对弈来验证AI强度写完AI怎么知道它到底行不行我的土办法是让AI自己跟自己下把落子顺序和关键点的评估分打印出来复盘。黑棋和白棋都要测执黑时进攻顺畅说明进攻端没问题执白时如果总是被压着打说明防守端评估或搜索深度还有欠缺。自对弈还能用来微调参数我把进攻系数从1.0调到1.1再调到1.2对比AI在同一组局面下的表现最后选了一个比较平衡的值。这个办法虽然土但没有它调参基本靠蒙。5.4 特别提醒不要在第一版就追求完美回头看这个项目第一版只要能完成玩家落子、AI随机或贪心落子、五连判定就已经是一个完整可玩的游戏了。AI强度可以无限优化但项目从0到1、能跑起来才是最关键的里程碑。很多朋友写这种小游戏容易陷入“永远在重构”的循环我建议先把最笨AI的整条链路打通再一步步替换成复杂算法。每轮迭代都保留一个可运行的版本你的开发体验会好很多也更容易坚持下去。五子棋人机对战这个项目我从控制台版一路做到网页版最大收获不是哪个算法记得多熟而是学会把一个模糊的“做一个游戏”的需求拆成棋盘、逻辑、AI、交互四个清晰模块。特别是AI部分让我真正理解了评分、搜索和剪枝在实践里是怎么协作的——很多算法书上轻描淡写的概念自己从零写一遍才会撞上各种各样的边界情况。如果你想拿这个项目练手照着我说的顺序来第一版打通流程第二版换成评分AI第三版加上搜索和剪枝。到第三版跑通的时候你对整个项目、对算法落地的理解都会有一个很实在的跃迁。本文还有配套的精品资源点击获取