三周,57 次提交,383 条测试,一次差点把十几网站搞下线的部署。
现在它跑在 https://game.heylai.cyou,三个人手机打开就能玩。
这篇文章记录它是怎么来的,以及一路上踩过的坑。

一、三个人的麻烦
起因很小:我想和两个朋友一起玩点什么。
不是想学技术,也不是想做产品。就是我们三个人各在各家,微信群里聊两句就
没话了,想有个东西能一起玩。
三个人这个数字很尴尬。棋牌游戏几乎都是两人或四人,三个人的选择少得可怜;
想玩个狼人杀吧,我们又不习惯开语音——三个人都是打字聊天的人。
于是某天晚上,我打开了 Claude Code,只说了一句:我脑子里只有一个模糊的
想法,你先别给我方案,你来采访我,一次问一个问题。
二、被采访的四十分钟
这一步比我预想的有用得多。它问的东西我自己根本没想过:
- 你是想真的跟朋友玩,还是想做个给别人玩的产品?
(前者。这个回答砍掉了注册、排行榜、匹配陌生人——一大堆东西) - 三个人是同时在线,还是各自有空走一步?(同时,所以要实时,要处理掉线)
- 一局多长?(十分钟以内,太长不会想开第二局)
- 规则能一句话说完吗?(必须能,不然没人愿意学)
最后它替我说出来的需求,比我自己描述的清楚十倍。那一刻我意识到:
我不是不知道自己想要什么,我是不知道该怎么把它说清楚。
游戏最后长成了这样:6×6 的棋盘,三个人各抽一张只有自己看得见的目标卡,
轮流落子,谁先做成自己那张卡谁赢。六张卡——双线、占角、横贯、围困、散布、
孤岛——各是一幅要凑出来的阵型。你还可以放「禁区」堵别人的关键位置,
可以指控别人猜他的牌(猜错停一手),也可以主动亮牌换资源。
核心乐趣只有一句话:你得一边做自己的,一边从别人落子的位置猜他在做什么。
因为挡住别人和做成自己一样有用。

三、我只提了两个要求
我全程没写过一行这个游戏的代码。但我提了两条规矩:
第一,规则引擎必须先写测试再写实现。
第二,每写完一块,回头把测试故意改坏,确认测试会红。
第二条不是我聪明,是我吃过亏。我见过太多"测试全绿但功能是坏的"——绿色的
进度条最能让人放松警惕。而我完全看不懂这些代码,我唯一能依赖的就是
"这些测试真的在测东西"。
这两条要求后来救了我好几次。
四、它自己抓出来的三个问题
1. 从自己的令牌能算出别人的暗牌
这是最严重的一个。游戏要支持重连——手机切个后台被系统回收,回来还得是
原来那个座位、原来那张牌。实现方式是给每个座位发一个"席位令牌"。
问题是:令牌当时是从游戏的随机数生成器里取的,而那个生成器是种子化的
(为了对局可复现、能存档复盘)。于是:我手里有自己的令牌 → 反推出种子 →
算出这一局完整发牌 → 三个人的目标卡我全知道。
这个漏洞不会有任何报错,游戏照常能玩,只是有人可以作弊,而且是那种
"看起来完全正常"的作弊。修法是把令牌换成密码学安全的随机源,跟游戏种子
彻底分开。现在那个文件的注释里写着一句话:绝不要为了测试方便改回去。
2. 猜对了反而崩
规则是:指控某人,猜对了他要重新抽牌。但卡池空了抽不出新牌怎么办?
当时的实现是抛异常——游戏直接卡死。
更妙的是另一层:一个懂行的人可以故意用它来试探。卡池空不空是可以推算
的,"猜对了会崩"本身就变成了信息渠道——崩了说明你猜对了。这叫预言机攻击,
零成本获取信息。现在改成:卡池空了就保留原牌,但这一手照样消耗掉。
3. 调试接口默认是开着的
有个测试用的内部接口能直接改对局状态,原来的保护是"非生产环境就允许"。
反过来一想:环境变量没设的时候它是开着的,而"忘了设环境变量"恰恰是
最常见的部署失误。改成默认拒绝,明确写了 test 或 development 才放行。
五、中间断了几十次
这件事占了整个过程一半以上的时间,得说一下。
API 连接中断、流超时、响应格式不对、跑了十分钟没输出被判定卡死……各种
失败加起来二十七次以上。有几次它写完了代码、还没来得及写报告就断了。
处理办法是每次断掉之后先查 git status 和 git log,看做完了多少。好几次
发现代码其实是好的,只是没提交。所以后来的做法是**在每个自然接缝处就
提交一次**,别攒着。三周下来,57 次提交。
教训不新鲜,但很实在:长任务要能从中间捡起来。 不管是人做还是 AI 做。
六、数据打脸了我的直觉
游戏能玩了以后,我问它:"这个难度是不是有点大?"
它没有直接回答,而是写了个模拟器,让三个随机走子的玩家跑了 400 局,
把每张卡的达成率摆给我看:
| 目标卡 | 随机走子的达成率 |
|---|---|
| 围困 | 38% |
| 双线 | 33% |
| 散布 | 16% |
| 横贯 | 7.4% |
| 占角 | 2.3% |
| 孤岛 | 1.5% |
整体 48% 的局有人达成——难度其实够,问题是极不均衡。孤岛卡基本是张
废卡,而且换棋盘尺寸、换棋子数都救不回来。
它给的解释我觉得很到位:孤岛的问题不是"难",是"不可控"。 这张卡要求
你的棋子周围八格干净,而你没有任何手段阻止别人往你旁边落子。做不做成
主要看别人怎么下。这是设计缺陷,不是难度。
还有个反直觉的发现:把棋盘做大反而更难——7×7 只有 15% 达成率。子密了
才有机可乘。我原本以为的"棋盘太挤",根本不是问题所在。
它建议把孤岛卡从三颗子降门槛(原来是四颗)。改完再跑:达成率 1.5% →
13.8%,从垫底变成中游。最后它还提醒我:占角那个 2.3% 是**被随机走子严重
低估的**——四颗子挤进一个 2×2 的角,人刻意去做很容易,随机下棋几乎不会
这么走。对这类需要规划的卡,模拟器没有判断力,只能靠真人打几局。
这一趟里我最满意的不是数据本身,而是这个态度:**它没有拿一个"看起来有
数据支撑"的结论糊弄我,而是明确说出了哪部分数据不可信。**
七、部署那天差点把十几个网站搞下线
我那台腾讯云小服务器上跑着博客、几个小工具、一个问卷系统,七八个服务,
都是宝塔面板装的。
AI 写好了部署手册,标准做法:Nginx 配置放进 /etc/nginx/sites-available/,
然后 systemctl reload nginx。
但它上服务器之后先做了一件事:只读地摸一遍现状,没有直接照手册执行。
然后发现了这个:
在跑的 nginx: /www/server/nginx/sbin/nginx ← 宝塔自己那个
PATH 里的 nginx: /usr/sbin/nginx ← 系统自带,没在用
/etc/nginx/sites-enabled/ ← 空的这台机器上有两个 nginx。 真正在服务的是宝塔那个;如果照手册来,配置写
进去没人读(无效),然后 systemctl reload nginx 会去动那个没在用的 nginx,
它会试图绑定 80 和 443 端口——**而那两个端口正被宝塔的 nginx 占着,我那
七八个在线服务全在上面。**
后来的做法是照宝塔的方式来,每次 reload 之后顺手把其他站点 curl 一遍确认
还活着。手册开头也加了一句:"已经部署好的那套请直接看第 10 节,照第 5 节
动那台机器会出事。"
八、上线之后,说明书自己抓出了一个错误
游戏上线那天我提了四条反馈:难度、说明、棋子没有颜色、打完能不能原房
间再来一局。前两条催生了两个我很喜欢的东西。
说明书画错了一步就会被测试抓住。 我要"每张卡配一张达成示意图",
它画完之后做了一件我没想到的事:把示意图的数据喂给真正的判定函数去验证——
每张图必须让 check() 返回真,而且逐颗拿掉己方棋子后必须变成"不达成"
(防止图上多画子)。这条测试当场抓出一张画错的图:散布卡要求"任何方向
都不能有三子连线",而图里藏了一条斜向三连——那是个犯规局面。如果没有
这条测试,这张错图就会印在说明书里,玩家照着摆发现没赢,然后从此不再
相信说明里的任何一句话。



棋子没有颜色那条更有意思。代码逻辑完全正确——把背景色的类名前缀一换
就拼出了文字色的类名——但 Tailwind 是扫源码里的字面量来决定生成哪些 CSS
的,拼出来的类名从没以字面量出现过,那几条 CSS 规则根本没被生成。于是
三家棋子全是灰白色,浏览器没有报警,测试也测不出来(jsdom 不跑 Tailwind)。
修完之后加了一条测试,从两头钉死:完整类名必须以字面量出现在源码里,
并且禁止再用前缀替换这种写法。
"顺手改一下"是危险的操作。我说"打完能不能留在原房间再来一局",听着像
纯界面改动,结果它顺手修了一个我完全不知道的问题:存档的随机种子原来是
房间级的,连开两局会让两条存档写着同一个种子,存档从此在悄悄说谎。
这种东西不会有任何报错,等你想复盘的时候才发现数据是假的。
九、为了这篇文章,我又当了一回玩家
给这篇文章配图的时候,我让两个脚本玩家陪我重新打了一局。过程中还真实
重演了这个项目教过我的事:页面断线回到进场屏,点一下"回到上一局",
座位、暗牌、棋局原样回来了——那是当初为一个 Critical 漏洞换掉整套令牌
机制换来的体验。
这一局我抽到了孤岛——就是那张曾经 1.5% 达成率的废卡。三颗子,每颗周围
八格干净。你看不见别人在做什么,别人也看不见你,只能从落子的位置猜。

最后一颗子落下,摊牌:

三个人的暗牌全部亮出来,完成度摆在一起。输的人能看见自己差多少——
这不是复盘功能,这是这个游戏最诚实的部分。
十、现在
https://game.heylai.cyou,三个人手机打开就能玩。不用注册,不用下载,
房间码念给对面听就行。
代码 361 条服务端测试 + 22 条前端测试,重要的防护都做过变异验证;每次
改动上线前跑一遍验收脚本:三局连打、中途断线重连回原座、原房间重开。
也有几件明知道没做完美的:四位房间码挡不住成批枚举,当熟人局玩就好;
同一人开两个标签页旧的那个会卡住;房间参数还得改配置重启。
技术栈没什么新潮的东西:Fastify + Socket.IO + SQLite,React + Vite +
Tailwind,一台 2 核 2G 的小服务器,Nginx 反代 + systemd,不用 Docker,
一个月零费用。**这些选择全部服务于一个约束:不给一个只给三个朋友玩的
东西增加任何运行成本。**
接下来该做的事很明确:把那两个朋友叫上,真的打几局,然后看还差什么。
毕竟从头到尾,这就是唯一的目的。
评论
抢个沙发还没有评论,来说点什么吧。