每年毕业设计落到“做网站”这个方向时,十个里有八个会冲进管理系统、商城、预约平台这类经典CRUD赛道。我自己带过几届学生,也看过不少后期回炉的案例,真正让答辩老师眼前一亮的,往往是那种“大家都认识、规则不复杂、但能往里塞很多技术点”的选题。五子棋就是很典型的例子。把一个Next.js + Radix实现的五子棋游戏网站从头做到尾,难度适中,却能同时覆盖组件设计、状态管理、AI算法、实时通信、响应式交互这些模块,既有演示效果,又有论文可写的内容量。
如果你正愁毕业设计题材,或者想拿一个能当作品集的项目练手,这篇内容应该能帮上忙。我会把当时的选题思路、技术选型、AI算法、联机对战、踩坑经过完整拆开讲一遍。项目本身不依赖任何商业服务,核心逻辑都能本地跑起来,代码结构也在最后给了说明。
1. 项目背景与需求拆解
1.1 为什么我盯上了五子棋这个题目
每年选题库里最多的就是“进销存管理系统”“图书馆管理系统”“校园二手交易平台”,这类题目的问题不在难度,在于同质化太严重。答辩现场十组里面八组是相似架构,老师很难提起兴趣,自己写起来也容易变成“谁的表单多谁就赢”。我选五子棋,图的是它有三个其他题目没有的优势。
第一,规则认知成本为零。用户不需要学习任何业务规则,所有人都知道“五个同色棋子连成一线就赢”。这意味着你可以把全部精力放在技术实现上,而不是花大量篇幅给评委解释业务背景。
第二,难度可深可浅。如果只要求能下棋,一个二维数组加点击事件就能搞定;但如果想拿高分,可以加入AI搜索、棋盘形势评估、实时对战、对战数据落库。同一个题目能覆盖到“完成功能”和“深入研究”两个档次。
第三,演示效果好。毕设答辩现场最怕PPT讲得花哨但系统一打开就崩。五子棋是实体应用型项目,点开网页就能开局,AI落子有思考过程,双人联机还能邀请另一台设备加入,评委很吃这一套。
当时我接触过的很多热词都在往这个方向上靠:rapfi五子棋引擎、五子棋C++实现、Android五子棋小程序、类似“开宝五子棋陪练”的交互产品,其实底层棋盘逻辑高度一致。这些东西正好说明五子棋项目具备典型的“小题目、大空间”特征。
1.2 需求拆成一张能落地的功能清单
在设计阶段,我先把原始想法写成了用户故事,再转成需求清单。整个项目最终确定下来四条主线:单人模式下人和搜索AI对战;联机模式下两个玩家通过房间对战;对局过程中支持棋谱状态回显和基本对局操作;界面有基础弹窗、提示和音效反馈。下面是我当时整理的功能清单。
| 功能模块 | 具体需求 | 优先级 |
|---|---|---|
| 对局模式 | 支持AI对战、本地双人、在线联机 | 高 |
| 棋盘基础 | 15×15棋盘、黑白棋轮流落子、胜负判定 | 高 |
| AI能力 | 多个难度档位、思考动画、禁手提示 | 高 |
| 对局控制 | 重新开始、悔棋、认输、返回大厅 | 中 |
| 在线联机 | 创建房间、加入房间、同步落子 | 中 |
| 交互体验 | 弹窗提示、音效开关、键盘可操作 | 低 |
这里有个经验:毕业设计需求一定不能一开始就贪大。我最开始还想过加入排行榜、用户注册、残局挑战、在线Chat,后来评估了一下时间成本,把用户系统砍成了最简的游客模式。对毕设来说,每个高优需求做出完成度,远比十个中优需求只做半吊子要好。
1.3 哪些地方要主动“砍需求”
做项目最容易犯的错是“以开发者的想象去定义用户要什么”。五子棋看着简单,真把“禁手”“三手交换”“五手两打”这种正规规则全塞进去,开发量会陡增。如果做的是毕业设计,我建议把规则控制在标准无禁手规则,也就是黑白双方谁先形成五连谁获胜。
为什么这么决定?无禁手规则下,AI只需要考虑进攻和防守,搜索逻辑相对单纯,便于在论文里讲清楚。正规比赛规则里,黑棋有“三三禁手”“四四禁手”“长连禁手”,这些判断会大量消耗开发时间,但在Web演示场景里普通用户又感知不到。把专业规则砍掉,换来的是AI深度可以做得更高、联机同步可以做得更稳定,这是划算的。
还有一个容易被忽略的需求是“游客进入即玩”。如果题目要求用户注册才能玩,体验会断一层。我最终把身份标识设计成浏览器内生成一个临时客户端ID,需要保存战绩时再关联账号,既保证能快速开局,也没有变成纯本地玩具。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型:为什么最终落在 Next.js + Radix
2.1 Next.js 在这个项目里到底负责什么
如果你只是做一个纯前端小游戏,Vite + React 完全够用。我之所以换到 Next.js,是因为这类毕设往往还需要“网站”属性——你得有首页、对局页、规则说明页,最好还要有几个API接口,让答辩老师看到你做了完整的前后端应用,而不是一个单页canvas。
Next.js 在这里的价值是“多页面应用 + 服务端能力”一起给。首页、规则模块这类内容型页面可以用服务端组件渲染,速度快,SEO也说得过去;棋盘、按钮、对话框这类强交互部分用客户端组件,保证本地状态没问题。落子结果、对局历史等接口可以直接写在Next.js的路由处理程序里,不额外起一个Web服务器。
刚开始我还担心“这种实时性要求高的场景是不是不太适合Next.js”,后来把实时对战单独拆给了WebSocket通道,棋盘页主框架仍由Next.js承载,两者配合得还不错。如果你准备做完全同步的联机游戏,不要期待Next.js的普通HTTP接口去承担高频实时推送,一定要让WebSocket只处理状态同步,页面路由继续交给Next.js。
2.2 Radix“无样式组件”解决了什么问题
选题背景里加Radix,最初很多人不理解,觉得五子棋项目根本不需要组件库。实际上Radix的价值不体现在漂亮外观,而在于它把所有头痛的交互细节都替你处理了。Radix是一套无样式“基础组件集合”,它不负责你长什么样,但负责正确弹窗、正确聚焦、正确绑定键盘事件、正确做溢出管理。
在这套五子棋网站里,我用到了好几个之前没想过会需要的组件:对局开始时用Dialog弹出胜负规则,玩家落子前用Tooltip显示坐标和棋型预测,AI思考过程中用DropdownMenu选择难度,认输或离开时有AlertDialog做二次确认。这些体验细节如果手写,每一处都要踩一遍焦点陷阱和键盘事件坑,用Radix后效果稳定不少。
Radix的另一个好习惯是把逻辑和样式拆得很干净。你可以在Content部分放心写Tailwind类名,不用担心弹层被overflow裁掉,不用担心触发按钮的样式污染。比如我想做一个“悔棋确认弹窗”,Radix的AlertDialog会自己处理遮罩层、Esc关闭、焦点循环,我只需要把自定义样式套到外层。这对需要频繁迭代的毕设项目来说非常友好。
2.3 实时对战架构不是“一言堂”
进入联机模块前,我定了一个原则:页面用Next.js,实时状态用独立的Socket服务。最开始我想过在Next.js的API路由里强行加入WebSocket处理,但带来的问题很直接——开发环境下热更新会不断重启连接,生产环境部署时又需要Node.js长驻进程。与其和这些环境问题较劲,不如把Socket服务抽成一个独立进程。二者的分工关系用一句话概括:Next.js负责你“看到的页面”,Socket服务负责“你和其他玩家之间的状态同步”。
技术选型上,联机部分我用了Socket.IO,它自带房间机制和断线重连,比裸WebSocket少踩很多坑。游戏核心规则函数,比如坐标合法性校验、胜负判断,不放在Socket服务文件里,而是提取成纯函数供两边复用。这样做有个直接好处:AI对战和人机对战共用同一套棋盘规则,不会出现“单机下赢了,联机又说赢不了”的诡异bug。
2.4 状态管理用什么
最初我给棋盘状态用了一个最笨的二维数组,每次落子都更新这一层状态,然后让React重新渲染。由于最多只有225个格子,这个设计在大多数场景下完全可行,不需要引入复杂的棋盘状态管理库。真正容易出问题的是“对局元信息”,比如当前轮次、模式、悔棋历史、房间编号,这些数据较为分散,我把它统一收在Zustand里。
使用Zustand的原因很实际:它足够轻,不需要额外配置Provider;组件可以在任意层级直接读取状态;对局历史可以作为数组推入状态,悔棋时直接从数组末尾弹出。AI搜索的计算过程则没有放在状态管理器里,而是通过Web Worker后台完成,算完再把结果传入store,避免大计算量阻塞棋盘渲染。
3. AI 人机对战算法实现过程
3.1 棋型评估:让AI“看懂”局面的基础
五子棋AI的核心完全不神秘,其实就是回答两个问题:当前局面黑棋占优还是白棋占优?下一步落在哪里最好?前者要依靠棋型评估函数,后者靠搜索算法。评估函数是决定AI强度下限的关键,我用的是基于“棋型打分”的思路。
对每个位置,我会分别检查横、竖、两条斜线共四个方向,统计从这个位置出发连续同色棋子的数量,再看两端是否被对手棋子或边界封死。根据“活四”“冲四”“活三”“眠三”这类形态,给出一个相对分值,然后把AI方的总分减掉对手方的总分作为局面评估值。这里不要简单统计某方数量多少,棋型结构才算关键。比如,同样是三个连子,“两头都空”的活三和“一头被堵”的眠三,后续威胁差别很大,给分必须拉开差距。
我参考了不少开源实现,也包括在社区里很受关注的rapfi五子棋引擎的设计思路。在C++这类项目里如果用暴力扫描全盘会非常快,但在浏览器JavaScript里没有这个条件,所以评估函数必须轻量,于是我对落子候选点做了裁剪,只对“中心区域”“已有棋子附近”“对手最近落子附近”进行精确打分,而不是扫描全部225个空白点。
ts复制function evaluateDirection(board, row, col, dx, dy, player) {
const opponent = player === 1 ? 2 : 1;
let count = 1;
let openEnds = 0;
// 正向
let step = 1;
while (true) {
const nx = row + dx * step;
const ny = col + dy * step;
if (nx < 0 || ny < 0 || nx >= 15 || ny >= 15) break;
if (board[nx][ny] === player) {
count++;
step++;
} else {
if (board[nx][ny] === 0) openEnds++;
break;
}
}
// 反向
step = 1;
while (true) {
const nx = row - dx * step;
const ny = col - dy * step;
if (nx < 0 || ny < 0 || nx >= 15 || ny >= 15) break;
if (board[nx][ny] === player) {
count++;
step++;
} else {
if (board[nx][ny] === 0) openEnds++;
break;
}
}
if (count >= 5) return 100000;
if (count === 4 && openEnds === 2) return 10000;
if (count === 4 && openEnds === 1) return 1000;
if (count === 3 && openEnds === 2) return 1000;
if (count === 3 && openEnds === 1) return 100;
if (count === 2 && openEnds === 2) return 100;
if (count === 2 && openEnds === 1) return 10;
return 0;
}
上面代码只是评估一个方向上的分数。实际调用时,会对当前要落子的位置分别跑四个方向,累加各类棋型分,并依次计算AI方总得分和对手方总得分,最后相减。从数值上可以看出,“活四”远高于“活三”,因为对手下一步不堵就直接赢了;对手的“活三”在AI眼中也必须按接近“冲四”级别的威胁来处理。
3.2 搜索算法:从选一个点变成模拟几步棋
有了评估之后,AI不等于会下棋。它还需要站在未来几步的角度考虑“我下这里,对手会怎么应对,我又怎么继续”。这里我用了经典的极小化极大搜索配合Alpha-Beta剪枝。
搜索树每层代表一方落子,最大化节点是AI选点,最小化节点是模拟对手选最让AI难受的点。一个很常见的误区是让搜索“穷举整盘棋”,这在五子棋里完全不可行,空点太多,搜索树会爆炸。所以我在每一层只从候选点里选出最值得考虑的部分节点,通常控制在8到12个。这样搜索深度能做到4到6层,普通难度下大概在几百毫秒到两秒内返回,体感比较自然。
剪枝逻辑并不复杂:AI层发现某个分支的收益已经超过当前最优值时,这个分支其他节点不用再搜索下去;对手层发现某个分支会让AI收益跌到当前最差值时,也没必要再深入。核心收益不是写在论文里的花哨概念,而是实打实地省掉了大量无效递归。
我做了一个非常直观的难度档位设计:简单难度走随机点加少量防守,中等难度开启3层搜索,困难难度开启5层搜索加更严格的候选点筛选。这样既能让新手正常下完棋,也能给答辩展示“AI能明显堵住你的活三、冲四”。
3.3 启发式候选点:搜索效率的第一道闸门
剪枝能省计算量,但不能把每一层的所有可落点都放进来,否则光是排序本身都会拖累性能。我采用的启发式策略是:以棋盘中心为原点,给每个空位赋一个初始距离分;同时以上一步落子为中心,把周边两格范围内的空位加入高优候选;最后再加上当前局面所有活棋端点的延伸位置,形成一个十几到二十个点的候选列表。
别小看这个设置,它解决了五子棋AI最常见的一个毛病——AI永远不会理远处的棋形。很多初学者会把“离最后一步近”等同于“好棋”,但实际五子棋进攻时常需要隔一个位置做棋形延展。通过“活棋端点”这个条件,AI搜索时能够看到跨越两格的连接点,下出来的棋明显更有连续性。
候选点生成完以后,在进入搜索前会先用评估函数对所有候选点快速排序。高分点排在前面,可以让Alpha-Beta剪枝从一开始就收获一个比较好的界,后续搜索过程中被剪掉的分支比例会显著提高。这个预排序花的时间很少,但对困难的难度档位收益很大。
3.4 让AI计算不卡界面的实现方式
在最早的版本里,我直接在点击事件里同步调用AI搜索。棋下到中盘时候选点变多,搜索一次可能要一两秒,整块页面直接僵住,点击任何按钮都无响应。后来我把它改成Web Worker方案。
落子后的AI计算过程被丢进Worker,由Worker负责生成候选点、调用评估、执行搜索,完成后通过postMessage把最终落子坐标返回给主线程。主线程这边只负责把棋盘状态切换成“AI思考中”,显示一个加载提示,收到消息后再更新store。改造之后,即使用最困难的档位,界面也能保持流畅,棋盘动画、倒计时可以正常操作。
4. 双人联机对战与房间同步机制
4.1 用房间模型组织一场对局
联机模式我最开始想得很简单:建立一条WebSocket连到服务器,谁先点击棋盘就把坐标发给对方。真正做下去发现,得先明确“一场对局”的边界。没有房间,两个不同玩家连到同一个服务器后很难唯一确定对手是谁,也无法处理同时开多局的情况。后来我为每场对局引入了一个8位房间码,作为连接与对局的唯一标识。
创建对局时,Server为发起方生成一个房间,并绑定socket.id;另一位玩家输入房间码后执行加入操作。房间内部保存了当前棋盘、黑棋玩家和白棋玩家、当前轮次、开始时间。房间里最多两人,第三个人尝试加入时直接返回“房间已满”的错误。加入成功后,服务器会把初始棋盘和双方身份一起发给两个玩家,前端根据身份显示自己是黑棋还是白棋。
4.2 服务器验证逻辑的重要性
联机对战最忌讳“客户端说什么服务器就信什么”。我在早期版本把落子合法性判断放在对方客户端上,结果只要有人手动改请求,就能替对手落子,或者在自己不该落子的时候落子。联机功能稳定后,我把状态权威交给了服务器。
现在落子流程是:玩家点击棋盘后,前端先把落子点伪更新到本地预览,但真正把这一步发给服务器的同时,会带一个单调递增的棋步序号。服务器检查当前房间轮到谁、传入序号是否和当前棋步序号一致、目标位置是否为空,校验通过后更新棋谱、切换轮次、判断胜负,再把最终状态广播给双方。整个过程里,客户端不能擅自修改棋盘,服务器消息才是唯一数据源。
4.3 断线重连与异常退出处理
在真实答辩或演示环境里,网络波动几乎一定会发生。最尴尬的场景是笔记本合盖再打开,连接断了,但两个人都在等对方下棋。我设计了一套临时重连机制:玩家创建或加入房间后,除了socket.id,服务器还会生成一个roomToken保存在浏览器本地;断线后重新连接时,客户端带上roomToken请求恢复房间状态。
服务器收到重连请求后会校验这个房间是否存在,以及Token是否匹配,然后重新把当前棋盘发给这个玩家,并把对方标记为在线。如果对局中有玩家直接退出超过90秒,则判定对方获胜,房间关闭。这个机制并不复杂,却让在线对局变得真正可用,而不是演示给评委看时一刷新就全崩。
5. Radix与前端交互的实际落地细节
5.1 棋盘组件的可访问性设计
棋盘是一个强操作区域,如果只用canvas画格子再监听click坐标,视觉上没问题,但键盘用户完全没办法操作。考虑到毕设评分标准里有“工程化程度”和“用户体验”的维度,我选择用CSS Grid渲染15×15个格子,每个交叉点对应一个可以聚焦的按钮。PC端鼠标点击,移动端触摸点击,键盘用户则可以用方向键移动焦点、用空格落子,这一点在答辩演示时能成为加分项。
格子的视觉样式是这样的:按钮本身透明,内部只显示棋子和网格线,棋子是否可见由board[row][col]决定。每个棋格上还加了aria-label,例如“第7行第8列,黑棋”,和相邻棋格之间预留了合适的间距。聚焦时出现高亮轮廓,方便看出当前键盘焦点在哪,避免那种“鼠标移走后就不知道下一步能落哪儿”的情况。
5.2 从“会用组件”到“封装组件”
直接用Radix时最容易犯的错是把原始组件散落在页面里,改样式时到处找选择器。我这里定了几个封装,统一放在components/ui目录里。比如游戏结束弹窗对应GameOverDialog,AI难度选择对应DifficultyMenu,规则说明对应RulePopover。
封装的好处是,当你把Radix的Trigger、Content、Title、Description完整放在同一个组件里时,可以给业务层暴露props。调用方只需要传open、onOpenChange、title、children等属性,完全不用关心弹层内部的层级结构。比如Dialog的Description如果不写,Radix会控制台报错提示无障碍信息缺失,封装之后把规则文案传进去就行,后续改成服务端异步拉取也不影响页面结构。
5.3 几种Radix组件在项目里的使用场景
在下棋页面里,Tooltip是最受欢迎的组件。鼠标悬停在棋盘任意空点时,会提前显示“如果落在这里,是否会形成活三/冲四”的提示。这个功能不参与游戏校验,只是给玩家一个辅助信息,但十分体现交互细节。对了,Tooltip在触摸屏上不会自动触发,所以这个信息同时也在底部状态栏里有文字版本,避免移动端空白。
DropdownMenu主要用于AI难度切换。如果直接在棋盘右上角放三个链接按钮,视觉会很拥挤。用DropdownMenu封装后,点击头像或“难度”按钮,会展开“简单/中等/困难”三个选项。Radix帮你处理了点击外部关闭和Esc关闭,这些基础体验看起来不起眼,但对手写组件来说非常容易漏。
在双人联机加入房间时,我用了Dialog组件作为输入弹窗;玩家开始一局后如果点击返回大厅,则用AlertDialog确认“对局未结束,确认退出吗”。因为这两个组件都遵循WAI-ARIA模式,即使在读屏软件里也能正确播报弹窗标题和内容,这个细节在“毕业设计项目是否有工程意识”的考察点上非常加分。
6. 开发过程中踩过的坑与排查记录
6.1 Next.js水合警告反复出现
项目早期,主页上有一些组件在服务端渲染时判断了window.innerWidth或用localStorage读取了对局偏好,结果客户端首次渲染和服务端渲染结果不一致,React控制台一直报Hydration警告。解决办法不复杂:把依赖浏览器环境的状态读写全部延后到useEffect里,或者用dynamic关闭某些组件的服务端渲染。
在实战中,我的具体做法是写了一个useMounted钩子:在服务器和首次客户端渲染时返回false,等页面挂载后再返回true。只有挂载完成的组件才渲染棋盘和“继续上次对局”按钮。通过这种方式,水合警告彻底消失,也不会出现首屏内容闪一下变掉的问题。
6.2 StrictMode导致WebSocket重复连接
联机模式反复连接断线,问题出在Next.js开发模式下默认启用了React StrictMode,组件在开发环境会执行两次挂载和卸载。如果不清理第一次创建的Socket连接,第二次挂载时就会同时存在两条连接。房间里会出现同一个玩家ID收到两套重复消息,下棋时一个落子被广播两次。
修复方式是在useEffect里创建连接后,return () => socket.disconnect(),确保每次组件卸载都主动清除。排查这个问题时还发现,控制台打印socket.id没变化,容易误以为没有创建新连接,实际上旧连接还在队列里。如果没有把日志打到服务器端,这个问题可能半天都发现不了。
6.3 胜负判断的边界条件
五子棋判断胜负看似简单:检测五个方向上的连续子数是否大于等于5。我在这里踩过一个坑,刚开始只朝一个方向累计,导致部分斜向五连判断不出来。当时写的修正办法是,落子后从当前点同时向正方向和反方向扩展,把两个方向上的连续同色棋子数加起来再减1,如果结果大于等于5就判定胜利。
还有一个容易踩的细节:落子完成后,胜负判断只需要围绕当前落子点检测4个方向,而不需要全盘扫描。因为只有刚落的这颗棋子有可能产生新的五连,其他位置的五连如果存在,在更早落子时就已经被检测出来了。这个判断方式能显著减少计算量,也让代码更清晰。
6.4 评估函数重复扫描带来的性能问题
第一次写完AI,发现中等难度走一步也要等很久,仔细分析后,问题出在评估函数每次都全盘跑一遍。四个方向、225个点,每个点都要跑方向统计,然后搜索树里每个节点都调用一次,计算量被放大到不可接受。
解决方式有两个。第一是缩小评估范围,只评估局部候选点附近区域,而不是全盘。第二是在递归搜索时使用增量评分,不过增量更新的复杂度较高,在毕业设计阶段我没选择这条路。如果你后续想做更强的AI,可以查一下Zobrist哈希配合增量评分的方案,但如果是答辩够用的话,局部评估加候选点裁剪已经能打出不错的棋感。
7. 源码结构、本地运行与后续扩展思路
7.1 项目目录是如何划分的
为了让代码在“展示给人看”时不丢分,我特意把目录按功能域而不是按技术层拆分。前端页面在src/app下,分别有home、game、rules三块;游戏规则和AI逻辑放在src/game下;联机相关放在src/socket下。来看一个简化后的结构。
bash复制src/
├── app/
│ ├── home/
│ │ └── page.tsx # 大厅页:选择单机/联机/难度
│ ├── game/
│ │ └── page.tsx # 对局页:棋盘与操作区
│ └── rules/
│ └── page.tsx # 规则说明页
├── components/
│ ├── board/
│ │ ├── Board.tsx # 15×15棋盘
│ │ ├── Cell.tsx # 单个棋格
│ │ └── BoardProvider.tsx # 棋盘状态封装
│ ├── ui/
│ │ ├── Dialog.tsx
│ │ ├── AlertDialog.tsx
│ │ ├── DropdownMenu.tsx
│ │ └── Tooltip.tsx
├── game/
│ ├── types.ts # 棋盘、玩家、房间类型
│ ├── judge.ts # 胜负判断与合法性判断
│ ├── ai/
│ │ ├── evaluate.ts
│ │ ├── search.ts
│ │ └── worker.ts
├── socket/
│ ├── client.ts # 客户端封装
│ └── server.ts # 独立Socket服务
└── store/
└── gameStore.ts # Zustand状态管理
game目录下放的是纯逻辑代码,不依赖React,这样AI和胜负规则可以被单测覆盖,也能在联机服务端复用。components/ui里封装Radix组件,页面尽量不直接触碰Radix原始层级。
7.2 从仓库到可演示环境需要哪些步骤
先把项目依赖装上,然后跑开发服务。在项目根目录执行npm install,安装完成后执行npm run dev。正常情况下,访问http://localhost:3000就能看到大厅页。
联机功能需要先启动Socket服务。在另一个终端进入src/socket目录或执行项目根目录的脚本,比如npm run socket,等待服务监听在3001端口。页面启动后,如果检测不到Socket服务在线,联机入口会置灰并提示“实时服务离线”,单机AI对战不受影响。
本地部署演示时注意跨域问题。浏览器页面地址是localhost:3000,Socket服务地址是localhost:3001,这两者属于不同端口,一定记得在Socket服务端配置正确的CORS origin,不然前端能连接,但握手阶段会反复失败。如果你用的是局域网演示,后端监听地址不要写死127.0.0.1,要监听0.0.0.0,这样评委手机扫码也能加入房间测试。
7.3 后续方向:从课堂项目到可继续扩展的作品
这个项目做完后,我一直觉得它保留了足够的扩展余地。如果你想继续加深“AI方向”,可以把目前的Alpha-Beta搜索替换成蒙特卡洛树搜索,或者参考开源引擎的启发式评估表;如果想把课题提得更“工程化”,可以加一个战绩存储模块,把对局的过程以标准棋谱格式(比如类似Gomocup的格式)存入数据库,让用户随时复盘;如果想把前端表现力拉满,可以把格子上的Radix Tooltip升级成“最佳落子推荐”,做成教练辅助模式。
另外,很多找Android五子棋相关方案的人,实际上就是想做移动端AI对战。当前这套AI算法并不依赖Next.js,你可以把game/ai下的纯TypeScript逻辑直接迁移到React Native或小程序环境里,只要棋盘组件匹配目标平台的交互习惯即可。这也是当初坚持把核心逻辑和页面组件分离的原因之一。
做这个项目最大的收获,不是“学会了Next.js某个API”,而是明白了怎么把一个看似日常的棋盘游戏拆成可管理的模块:规则逻辑、AI计算、实时通信、React渲染各司其职,改其中一个模块,不会牵连到其他模块。我自己在后期加AI难度档位时,只改了search.ts里的搜索深度和候选点数量,页面上几乎没有动过代码。对于一次毕业设计或者作品集而言,这种模块边界清晰带来的“安全感”,远比堆砌功能数量重要得多。
