一个完整的中国象棋网页版。
不需要构建步骤,克隆下来 npm start 就能跑。手机、平板、电脑都能连。
但真正让我想做这个项目的,不是"做个象棋",而是——
一个规则引擎怎么写才算"写对了"?
规则这东西,看起来简单
七种棋子,每种走法都耳熟能详。写起来好像没什么难的。
真正难的是那些边界条款:
| 规则 | 容易漏在哪 |
|---|---|
| 帅/将 | 九宫内直走一步;任何导致两将同线且中间无子的着法都非法 |
| 仕/士 | 九宫内斜走一步 |
| 相/象 | 走「田」字,象眼被堵不能走,不能过河 |
| 马 | 走「日」字,马腿被堵则该方向不能走 |
| 车 | 直线滑行,遇子即止 |
| 炮 | 不吃子时同车;吃子必须隔且只隔一个炮架 |
| 兵/卒 | 未过河只能向前;过河后可向前或左右,永不后退 |
还有胜负判定层:
- 将死判负 —— 被将军且无合法着法
- 困毙判负 —— 没被将军但也没有任何合法着法(中国象棋特有,国际象棋是判和)
- 长将判负 —— 循环内某方每一步都在将军
- 长捉判负 —— 循环内某方每步都在追捉同一个非兵非将棋子
- 重复局面判和 —— 三次重复且不构成长将/长捉
- 自然限着判和 —— 120 个半回合无吃子
光靠"我觉得应该对了"是写不完的。
perft:一个不给你留面子的检验
国际象棋引擎圈子里有个标准测试叫 perft:从开局开始,逐层统计所有合法着法能生成的节点数。
它有一个残忍的性质:任何一处规则写错,数字立刻对不上,而且不告诉你错在哪。
中国象棋的公认值是这样的。这是我的实测:
| 深度 | 节点数(公认) | 结果 |
|---|---|---|
| 1 | 44 | ✅ |
| 2 | 1,920 | ✅ |
| 3 | 79,666 | ✅ |
| 4 | 3,290,240 | ✅ |
到深度 4,要生成并验证 329 万个局面。
这比"手动走几步看起来没问题"强得多——它逼你把每一个边界条款都写对,因为只要有一处错,329 万会变成 328 万或者别的数。
一个能被数字否证的测试,比一百次"看起来没问题"更有价值。
另外还有 69 项单元测试,逐条覆盖蹩马腿、塞象眼、炮翻山、将帅照面、被牵制棋子、困毙、长将判负、自然限着,以及中文记谱的前后缀消歧。
AI:六档难度,但不许装傻
引擎是纯 JavaScript,跑在 Web Worker 里,所以深度搜索不会卡住界面。
搜索
- Negamax + Alpha-Beta 负极大值搜索
- 迭代加深 + 时间控制(随时可以返回当前最优着法)
- 置换表:Zobrist 校验、深度优先替换、100 万槽位
- 着法排序:置换表着法 > MVV-LVA 吃子 > 杀手着法 > 历史启发
- 静态搜索(Quiescence):消除水平线效应
- 空着裁剪、将军延伸、后期着法削减(LMR)
- 搜索路径内重复局面按和棋处理,避免引擎自己走进循环
评估
子力价值 + 位置价值表(每种棋子一张 10×9 表)+ 结构项:
- 机动性(马/车/炮/兵各自可走到的格数)
- 车的纵线开放性(半开放 / 全开放)
- 缺士怕双车、残局炮贬值、残局兵升值
开局评估为 0 分(完全对称),速度约 30 万节点/秒。
六档难度:不是简单调深度
难度是四层可调的:搜索深度 + 思考时间 + 选点随机窗口 + 失误概率。
| 档位 | 深度 | 时间 | 随机窗口 | 失误率 |
|---|---|---|---|---|
| 入门 | 2 | 0.15s | 150 | 30% |
| 初级 | 3 | 0.3s | 55 | 12% |
| 中级 | 5 | 0.7s | 18 | 3% |
| 高级 | 8 | 1.4s | 5 | 0 |
| 大师 | 12 | 2.8s | 0 | 0 |
| 宗师 | 20 | 6s | 0 | 0 |
「随机窗口」=在「最优分 − 窗口」范围内的着法里随机挑一个——这是低难度"像人但会犯错“的关键。
但这里有一条铁律:
无论哪一档,能一步杀就一定会杀。
低难度可以犯错,但不能在该赢的时候装傻。
因为一旦那种事发生,玩家就不认为自己在跟一个对手下棋,而是在跟一个随机数生成器。
联机:服务端说了算
同一份服务端同时提供 HTTP 静态服务与 WebSocket 信令,端口共用一个。
关键是规则引擎浏览器与 Node 共用同一份代码,所以:
- 服务端为每个房间持有一份
Position - 每一步都用规则引擎重新算一遍
- 非法着法直接回
error,局面不变
改前端无法作弊。
其他细节:
- 房间码 4 位,字符集去掉易混淆的
I/O/0/1 - 座位满了自动成为观战者,可观战、可聊天,不能走棋
- 悔棋需对方同意;对方在你请求后走了子,请求自动作废
- 断线重连:token 存在
localStorage,120 秒内自动回到座位,超时判负 - 心跳 25 秒一次,死连接自动清理
一个手机端的真实坑
移动浏览器的音频策略比桌面严格得多。
页面加载时创建 AudioContext 会被自动播放策略永久挂起。
所以这个项目特意不在加载时建上下文,而是在第一次用户触摸时才创建并解锁。
配套做了个音效自检功能:
| 状态显示 | 含义 |
|---|---|
| 运行中 · 音效已开启,却仍听不到 | 设备侧静音(iPhone 检查侧边静音拨片;Android 调媒体音量不是通话音量) |
| 一直显示「已挂起」 | 浏览器还在拦截,再多点几下棋盘 |
| 显示「尚未开始」 | 还没产生过触摸 |
试听按钮始终会发声——这样就能区分"开关关了"和"设备真没声音”。
自动化验证会在严格自动播放策略 + 手机触屏模拟下跑一条完整链路:加载时不建上下文 → 首次触摸即解锁 → 首声不被吞掉 → 落子/AI 应招都有声。
素材:全部可自由使用
- 棋子汉字字形取自 Wikimedia Commons 的公有领域作品(作者 Inductiveload)
- 原素材是四格精灵图,只取汉字字形,去掉原图圆片,重新合成为木纹实体棋子(径向渐变 + 车削年轮 + 倒角 + 内圈阳文 + 投影)
- 棋盘木纹是 SVG
feTurbulence程序化生成的,无第三方版权 - 音效是 WebAudio 现场合成的,不打包任何音频文件
所以这个项目可以随便分发,没有版权风险。
上面是在线单机版。局域网联机需要跑 Node 服务端,本地
npm start即可。
技术选型
| 选择 | 原因 |
|---|---|
| 原生 JS,无构建步骤 | 克隆下来 npm install && npm start 就能跑 |
| 规则引擎浏览器与 Node 共用 | 联机时服务端复用同一份规则做权威校验,避免两套实现不一致 |
| AI 用 Web Worker | 深度搜索不阻塞界面;拿不到 Worker 时自动退回主线程 |
| 棋子用内联 SVG 精灵表 | 32 枚棋子共用一份渐变与滤镜定义,零额外请求,任意缩放清晰 |
| 音效用 WebAudio 合成 | 不需要打包音频,落子/吃子/将军音色各不相同 |
ws 是唯一运行时依赖 | WebSocket 手写容易在分片、掩码、关闭握手上出错 |
这个项目让我对"做对"这件事的看法变了一点。
规则能凭记忆写出来,但只有 perft 能证明你写对了。
