刚开始做这个项目时,我本来只想把大学时写的一个五子棋控制台程序加个界面,结果一路迭代到了“五子棋3.0”。从能用键盘敲坐标下棋的版本,到鼠标点一点就能联机对战,这中间踩过的坑、重构过的代码、调出来的经验,我觉得比项目本身更有价值。这篇文章不聊虚的,就从“五子棋3.0”这个版本切入,说说我为什么做这个项目、核心模块怎么选型、AI评分系统怎么一步步调出来,以及联机、悔棋、判定这些功能背后的实现思路。如果你正在学前端、想找一个能串起 UI、算法、网络通信的完整练手项目,或者单纯想知道一个“小游戏”能做成什么深度,这篇应该能给你点实在的东西。
1. 从 1.0 到 3.0:这个项目到底想解决什么问题
1.1 三个版本背后的需求变化
“五子棋3.0”不是一拍脑袋想出来的版本号。它对应的是一条从“能跑”到“好用”再到“有可玩性”的演进路径。
1.0 版本是纯命令行程序。棋盘用二维数组在终端里打印,白子黑子用字符表示,玩家轮流输入行列坐标。它的痛点非常明显:操作不直观、没有任何交互反馈、代码也基本没有模块化,规则判断和主循环全部堆在 main 方法里。这个版本的意义在于让我把“五子棋的基本规则”从业务逻辑上理顺了:15x15 棋盘、黑先白后、五连获胜、平局判定。
2.0 版本加上了图形界面,用的是最基本的表格布局。每一格是一个单元格,点击时把棋子画上去。这个版本比 1.0 好用太多,但代码上还是“界面和业务逻辑混在一起”,棋盘状态、胜负判断、界面刷新全部耦合。到后期想加 AI 对战,我明显感觉到改不动了,每次因为 AI 要落子,就得反向去更新界面组件,数据流非常混乱。
3.0 的目标就是彻底解决这些问题:把界面、规则引擎、AI 决策、网络通信分成独立模块;游戏模式从“双人同屏”扩展到“人机对战”和“局域网联机”;同时把棋局数据记录下来,支持悔棋和复盘。
有人说,一个五子棋做个 3.0 是不是小题大做。我的体会是,正是这种“看起来简单”的项目,才能把架构问题暴露得干干净净。如果你一上来就去写一个复杂的后台管理系统,大概率会被业务细节淹没,根本分不清哪些设计是为了解决真实问题。五子棋规则固定、功能边界清晰,反而适合拿来打磨代码结构。
1.2 3.0 版本的功能需求拆解
我先把 3.0 的功能列表列出来,然后按优先级排了一下:
| 优先级 | 功能点 | 说明 |
|---|---|---|
| P0 | 人机对战 | AI 至少能下出有章法的棋,不是随便乱走 |
| P0 | 双人对战 | 同一设备上黑白双方轮流落子 |
| P0 | 胜负自动判定 | 落子后立即检测是否五连,并高亮获胜棋子 |
| P1 | 悔棋 | 支持回退一步或两步 |
| P1 | 局域网联机 | 两台设备在同一个网络内可以创建/加入房间对战 |
| P1 | 难度选择 | 简单/普通/困难,对应 AI 的策略深度 |
| P2 | 棋局导出与复盘 | 记录每一步落子坐标,支持回放 |
| P2 | 音效与动效 | 落子音效、获胜动画 |
P0 是核心,没有这些这个项目根本不叫“五子棋”;P1 是 3.0 相对 2.0 的明显增量,尤其是联机功能,直接决定了项目的天花板;P2 属于锦上添花。
关于 P1,说句实在话,联机功能在开发量和调错成本上,比前面所有功能加一起还要多。光是网络同步的边界情况就够喝一壶的:一方断线怎么办、双方同时落子怎么办、房间里的“先后手”怎么分配。我建议如果时间不够,P1 里的“复盘和棋局导出”可以先砍掉,联机保留,因为联机带来的架构提升是复盘比不了的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心模块设计与关键技术选型
2.1 棋盘渲染方案:Canvas 还是 DOM 网格
2.0 版本用表格(table)绘制的棋盘,有一个很现实的问题:当棋子数量变多,频繁操作 DOM 会导致明显的卡顿,而且对高分屏适配不友好。这次我直接考虑两个方案:Canvas 和 SVG。
SVG 的优点是每个元素都是独立节点,方便监听点击事件和做动画;缺点是元素数量多时性能一般,15x15 的棋盘加上棋子不过两百多个节点,其实也还好。但我最终还是选了 Canvas,原因是这个项目后续要录制对局回放,Canvas 可以直接把整一帧渲染成图片或视频帧,不需要额外处理 DOM 结构。另一个原因是我对画笔类 API 更熟,绘制线条、圆、渐变都比较顺手。
网格坐标换算是 Canvas 版本最容易出错的地方。我用的参数是:棋盘边距 padding = 40,棋盘格间距 cellSize = 50,棋盘实际像素宽度就是 padding * 2 + cellSize * 14 = 780。这里注意是乘 14 而不是 15,因为 15 条线只有 14 个间隔。
javascript复制const BOARD_SIZE = 15;
const CELL_SIZE = 50;
const PADDING = 40;
const CANVAS_SIZE = PADDING * 2 + CELL_SIZE * (BOARD_SIZE - 1);
const baseX = (col) => PADDING + col * CELL_SIZE;
const baseY = (row)
