King's Gambit 开发回顾:6种3D棋类游戏从零到开源
King's Gambit 开发回顾:6种3D棋类游戏从零到开源 · 柳飞舟 · 2026-08-10
载具化的中国象棋阵容。红方金盔赤旗,黑方玄甲蓝焰,光看轮廓就分得清阵营。 开头 开头没什么戏剧性。我在浏览器里下网棋,棋子是贴在格子上的图片,吃子就是图片消失,整个画面像一张会动的表格。我当时想,网页 3D 早就不新鲜了,怎么像样的 3D 棋类游戏这么少。 于是建了个目录叫 chess。一开始真的只打算做一个国际象棋。Vite 5 加 React 18 加 TypeScript,Three.js 0.166,chess.js 管规则。第一版跑起来的那个晚上,看着一个车从 e1 滑到 e4,身后带起一点尘土,我觉得这事能做,而且能多做一点。 后来就多做了一点,又多了一点。最后变成六个棋:国际象棋、中国象棋、围棋、将棋、跳棋、黑白棋。源码大概 12900 行,58 局名局棋谱,二十多个验证脚本,还有两段从游戏里录出来的视频。说好的规划呢,没有,就是一路加上去的。 什么都自己写 早期我给自己立了条规矩:能自己写的,不拿现成的。 引擎最明显。六棋里只有国际象棋用了 chess.js,那个轮子太成熟了,重造没意义。其余五个全是手写:xiangqi.ts 277 行管九宫河界,go.ts 223 行管提子、劫争、自杀禁手和数子终局,shogi.ts 336 行管升级、打入、二歩和打歩诘,跳棋和黑白棋各有各的一份。每个引擎的大小都刚好够它那个棋用,多一行抽象都没有。 AI 也是手写的。骨架都一样:评估函数、alpha-beta、开局库、时间盒降级。围棋是启发式评分加蒙特卡洛兜底。水平很菜,陪练级,这个我认。但每一行我都能说出它为什么在那。 自研围棋引擎的终局画面:19 路棋盘、星位、最后落子标记,提子劫争自杀禁手全在 223 行里。 美术更彻底。没引一个外部模型和贴图,棋子全是程序化建模:将棋駒是挤出来的五角形,駒面汉字用 Canvas 现画,成駒改赤字。音效也是,WebAudio 现场合成,项目里一个音频文件都没有。 代价是慢。好处是踏实:整个仓库没有一个许可证含糊的东西,推到 GitHub 开源的时候我一秒都没犹豫。 象棋棋子重做 要说我最喜欢讲的一段,是象棋棋子的 3D 重做。 最早就是圆饼上写汉字,能用,但没意思。我决定推翻重来,并且定了条死规矩:象棋棋子必须是中式造型,国际象棋的元素一个都不许进来。把"车"做成城堡塔楼,我不干。 最后的方案是载具化。傌做成骑兵,战马加持兵器的骑士。俥做成秦式战车,双轮牵引,车上武士持戟。炮做成野战火炮,带轮炮架,旁边站个拿点火杆的炮手。 近景:红方持戟武士、戴笠炮手、双轮战车,黑方在河对岸列阵。 红黑两方也花了心思。不是换个色,是头盔形制不同、旗帜不同、武器姿态不同、载具细节不同。光看轮廓就该认得出是哪一方的队伍。 这个标准后来管住了整个项目。将棋的五角駒、跳棋的双层圆饼加王冠、黑白棋的双凸圆盘,都是照着这个来的。阵营光晕也统一了:金色给象牙方,蓝色给黑曜方,棋子翻面、升变的时候光晕跟着换。六个棋放一起像一个产品,靠的就是这个。 炮的一发炮弹 fx.ts 只有 202 行,是玩得最开心的一部分。 底层都是常规的:吃子火花、冲击环、尘土、跳跃压扁拉伸、将军红圈、相机震屏。真正得意的是炮的专属动效。象棋炮隔着一个炮架吃子,这个机制本身就有画面感。于是我做了一整套:炮身后坐,一枚带引信的深色弹体沿弧线飞 0.34 秒,落地强闪光、双色爆裂、大金环扩到半径 2.2,被吃的棋子碎成 12 块残片飞散,相机跟着震,残骸溶解消失。 炮吃子:后坐、抛弹、引信、落地爆裂、残片飞散,一套 0.34 秒加余波。 普通吃子也不敷衍,火花冲击环尘土一样不少: 共享特效系统下的普通吃子:受击弹飞加爆裂粒子。 第一次跑通之后,我让炮反复吃子玩了十来遍。挺幼稚的,但这就是一个人开发的乐趣。 把古谱喂给引擎 名局棋谱系统是工程量最密的一块。 象棋数据来自三部古谱:橘中秘、梅花谱、自出洞来无敌手。古谱写的是"炮二平五"这种传统记谱,我写了 _classic_gen.ts 把它解析成坐标着法,再逐着过引擎校验合法性,歧义就回溯消歧。古谱常常停在胜势而不是绝杀,就让 AI 续着补杀:强攻方 level 3,弱方 level 1 加随机抖动,补出来的着法才像古谱的味道。最后产出 45 局,引擎全量重放,45 局全过。 围棋走 SGF 管线:_build_go_sgf.mjs 解析 SGF,在浏览器里用项目自己的 GoGame 引擎逐手验证,再吐成数据。收进来 5 局,跨度 170 年:1846 年秀策的耳赤之局、1933 年吴清源对秀哉的三三星天元、1939 年镰仓十番棋第一局,还有 2016 年 AlphaGo 对李世石第二局和第四局。国象收了 8 局,PGN 全部验证过。 打谱中的"三三·星·天元"之局。右侧着法浏览器,点击跳转,当前着高亮,可导出。 功能做成了研究工具:着法浏览器、自动演示,最要紧的是自由落子记变着,在名局任何一步拐出去自己试一手,系统自动生成分支,随时切回主线。加上记谱导入导出和摆盘研究,大致就是个棋院研究室。 这里摔过整个项目最大的一跤。最早我把引擎落子挂在棋子动画的 tween 回调里,动画播完才真正走子。无头浏览器里渲染被节流,动画超时,自动演示双重落子,棋盘直接散了。查了很久,结论很简单:棋谱播放必须同步落子,引擎立刻执行,动画只管看。又加了道保险:每个 tick 拿新引擎实例重演一遍,对不上就强制重同步。这两条现在写在交接文档里,标着"务必遵守"。 一天一棋 2026 年 8 月 8 日和 9 日,我上了三个新棋:8 号将棋和跳棋,9 号黑白棋。每个棋都是引擎、渲染、AI、UI 加全套验证。能这么快,是因为架构已经把新增棋种变成填空题:引擎管规则,Xxx3D.ts 管渲染,AI 复用骨架,主菜单加个 tab。 将棋:五角駒、汉字駒面、駒台持駒,升级弹成不成的选择钮。 快,但没糊弄。将棋最难:五角駒用 ExtrudeGeometry 挤出来,汉字駒面 Canvas 画,吃掉的子进駒台叠堆显示,打入走抛物线,升级弹成不成的选择钮。规则一条不少:二歩、香步桂底线禁打、打歩诘。isAttacked 用反向定向扫描,随机 200 手局面下 legalMoves 大概 9 毫秒,这个写法就是冲着性能去的。 跳棋的强制吃子连跳链用 DFS 枚举。3D 侧做成逐段 hop,沿途吃子依次出局,兵升王那一下王冠当场 pop 上去。 黑白棋看着最简单,翻车最多。测 pass 链的时候,局面死活进不了 pass 分支,后来才明白要同时满足"对方无着且我还有子可下",不然终局判定先置位了。还有 undo 翻回色写错,翻回来的子该恢复成敌方色。这种 bug 写的时候毫无感觉,一测一个准。 不点一下我不信 这个项目最不像个人玩具的地方是验证。 根目录躺着二十多个 _verify 脚本,分两层。第一层纯逻辑单测,不开浏览器:引擎的走法生成、特殊规则、undo 还原,AI 各档合法性和耗时。光黑白棋就 27 条断言。第二层 Playwright 真浏览器,真实指针点击完成走子、吃子、升变、undo、AI 应答,每个棋 16 项左右。 跳棋:双层圆饼棋子,连跳逐段 hop,升王加冠。点击测试曾在这里被代理圆柱挡住射线。 为什么坚持真点击,不直接调内部 API?因为 3D 拾取本身会出错,后来真让我逮着了。跳棋测试怎么点都点不中格子,查了半天是瞄准点的问题:脚本取的是格子上空 y=0.3 做投影,低视角下射线先撞上近端棋子的隐形拾取代理圆柱。修法是把瞄准点降到棋盘面 y=0.14。产品代码一行没改,改的是测试对三维空间的理解。 类似的教训还有:无头环境 rAF 被节流,动画断言要轮询引擎状态,别傻等;tween 的 dt 钳制从 0.05 放到 0.25,不然低帧率下动画直接卡死。都记进文档了。 每个新棋上线,单测、AI 验证、浏览器点击三层全绿,再截图存档。都讲慢工出细活,我这套验证才是快得起来的原因。 顺手录了部电影 自动演示做完,录视频就是顺手的事。第一部是象棋名局,114 秒。认真做的一部是围棋镰仓十番棋第一局:276 手,6 分 16 秒,44 MB。Playwright 录 webm,ffmpeg 拼接。中间踩了个 macOS 的坑:homebrew 装的 ffmpeg 不带 drawtext 滤镜,字幕上不去。办法很土但有效:字幕卡片用 Playwright 渲染成 PNG,再当素材合进去。 后来这部片子又升级成多机位:9 个镜头,开局高空缓推,布局阶段给双方各一个侧面中景,中盘开打切低角度特写,终局缓缓拉远。段间 0.3 秒淡入淡出,落子节奏跟着战局走。 多机位版的一个镜头。录制脚本和镜头剧本都留在仓库里,下一局名局随时能复用。 完整成片在仓库根目录,六分多钟,泡杯茶看: <video controls width="800" src="go_kamakura_final.mp4">你的播放器不支持内嵌视频,直接打开 go_kamakura_final.mp4 即可。</video> 《镰仓十番棋 · 第一局》完整对局视频,1280×720,276 手自动演示。 还学了一件事:自动演示的时候相机得稳住,不许晃。晃的棋盘看起来像地震。 收尾 最后干的活没什么意思但必要:git init,写 .gitignore,把根目录堆的 66 张调试截图清掉,设置持久化到 localStorage,修 45 局列表撑破菜单的布局,给六个棋都加上返回主页按钮和玩法说明弹窗。 还有 HANDOFF.md,200 行,项目概况、代码结构、设计铁律、验证命令、踩过的坑都在里面。预设读者是下一个接手的人,虽然到目前为止,那个人一直是下一次对话里的我自己。每次新开对话,都是从读它开始的。 现在 六个棋,58 局名局,三套记谱的导入导出,打谱研究工具,AI 三档,MIT 开源。 短板也摆在那:AI 还是陪练水平,将棋和黑白棋没有名局库,移动端和部署都没做。接着做的话,先给围棋上蒙特卡洛,其他棋补开局库和置换表,然后才是部署和死活题库。 不过回头看,我记住的不是功能清单。是炮弹第一次沿弧线飞出去的那一下,是 45 局古谱重放 45 局全过的那一下,是镰仓十番棋第 276 手落下、镜头慢慢拉远的那一下。 接下来打算先把 AI 弄强一点,至少让它别每盘都输给我。