前六轮都是「design → 代码」方向。本轮反过来:从代码出发,把每个规则决策与 常量拉出来追问 design 有无明文依据。没有发现与 design 冲突的行为,但列出 6 项 「design 未明文规定、由代码自行决定」的项(倒计时秒数、§5.4.4 降级阶梯是否 最长优先、甩牌分解的最大化合并约定、同级副7对不成连对、call 入参宽松而 cards 严格、get_chongguan 的 <28 隐式兜底),已写进 compliance 待规则设计者拍板。 同时补上一块此前完全没有测试的红线:**下发面按可见性下发**。design §4 暗牌 只有庄家可见、§9 查牌模式、§11 结束亮底牌,以及 server 红线「发全 ≠ 发多」, 此前各处门控只有逐条手工核对,从未系统验证「有没有哪个包把不该看的牌送到了 某个座位」。 新增 test/test_leak.js:跑 6 局(可查牌/不查牌 × 叫 65/70/5),对每一个 「服务器 → 某座位」的下发面(各 RPC 逐座位包 + 各阶段重连快照 + 明牌应答) 深度扫描出所有牌 id,逐个判定该座位此刻是否有权知道。结果 1814 个下发面 / 18706 次可见性判定,0 泄露。 变异检验:非 70 分把暗牌发给闲家(第三轮修过的历史缺陷)、重连把底牌发给 闲家、出牌包把剩余手牌发给所有人 —— 三条全部被抓住。 全套单测 520 → 523 项全绿。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
7.8 KiB
二七王服务端单元测试
运行
node server/games/erqiwang/test/run.js # 跑全部单测
node server/games/erqiwang/test/test_arith.js # 单独跑某一组
退出码 0 表示全过,非 0 表示有失败;run.js 逐个以独立进程 spawn 各 test_*.js 并汇总。
原理:一套代码,两个运行时(dev-guide 01 §1)
正式代码(class.*.js)遵循平台的双运行时规范:
- 友乐 / 浏览器:无
require,由mod.js用min_loadJsFile加载为全局对象;文件顶部的if (typeof require !== 'undefined')守卫被跳过,跨模块按同名全局引用。 - Node(本地 / 单元测试):有
require,守卫块var X = require('./class.X.js')取到依赖;文件底部if (typeof module !== 'undefined') module.exports = ...导出。
因此这两处守卫是纯增量:友乐运行时行为完全不变,只在 Node 下额外启用 require/exports。
_shim.js:平台全局垫片(仅测试用,不进正式代码)
正式代码按全局名引用少量平台工具(min_ary_include/min_ary_deduct/min_random/min_now/min_ontimeout)与模块入口 youle_erqiwang.import。友乐由平台提供,Node 单测由 test/_shim.js 提供等价实现(数组/随机工具忠实复刻 server/minhttp.js,youle_erqiwang.import 用 mock)。测试文件顶部先 require('./_shim') 再 require('../class.*.js')。
测试计划
完整的"规则 → 用例"覆盖矩阵、缺口清单与优先级见 ../docs/compliance/02-测试计划.md。
覆盖
test_arith.js:主牌排序/相邻链(§3)、扣底倍数(§6.3)、算子常规+爬坡逐档+分界+升3级(§7)、算奖 get_chongguan(§8.1)、甩牌分解/最大性/合法性(§5.4)、跟甩牌强制分量拆解(§5.4.4)。test_follow.js:正常跟牌/毙牌/垫牌,单张/对子/拖拉机的必出与缺门毙牌数量对应(§5.1/5.2)。test_callgrade.js:叫分坐庄 do_callgrade(叫5立即/两家不叫/后叫更低)(§4.2)。test_config.js:roomtype 位串解析、局数/扣卡映射(§10.1)。test_paiju.js:亮牌 get_liangpai(§8.2,含「埋牌后静态快照、出牌后重连不缩水」回归)、结算 get_paiju_account 集成(§7/§8,大光/过庄/升级/爬坡/傍王/投降)、扣底触发 get_bottom_account(§6.3)。test_deal.js:发牌构成——92张/去3-4/三家28+底8/分值(§2/§6.1)。test_desk.js:坐庄轮换 do_prepare(庄赢连庄/闲赢·投降下家)(§4.6)。test_endgame.js(L4 端到端,用真实发牌跑完整一局):叫分→上庄→选主→埋牌→28 轮出牌→小局结算的流程闭合(§12.1),以及与牌面无关的不变量(牌张守恒 84/8/16、捡分=闲家赢得分牌+扣底分、X分配庄±2X/闲∓X、算奖X×(2Ni−Nj−Nk)、结算零和、result与判定一致);另覆盖 §4.4/§7.1 投降局与 §12.2 中途解散两条支线。test_rpc.js(L3,用_rpc.js装配 mod.js + 捕获下发包):明牌 mingpai、出牌 chupai 的 seatlist/baozhu 查牌门控(§9,含「seatlist 整表带三家」)、投降 touxiang 条件、jiaofen 入参校验、70分上庄暗牌亮牌下发(§4)。
两组差分测试(按 design 原文另写参考实现,与代码对拍)
下面两组与上面的手写用例互补:它们不枚举"我想到的场景",而是把 design 的规则整条转写成参考实现,再和代码大面积比对,专治手写用例覆盖不到的欠约束分支。
test_calc.js:§7 算子全量对拍。参考实现逐字转写自 §7.1/§7.2/§7.3 的判定表,与get_base_bycall/get_qvalue/get_upgrade比对 2 种算子模式 × 14 个叫分档 × grade 0~260 = 7308 格(每格比 base/Q/判定倍率/最终子数四项),另按 design 的 17 张表逐格抽查 141 个"写死的最终子数"。断言按(模式,叫分档)与表名分组,失败能直接定位到档位。test_followdiff.js:§5.2 跟牌强制层级穷举差分。参考裁定按 §5.2 原文写(缺门/不足/够牌 × 单张/对子/N 连对 + 最长拖拉机优先的档案比较),对每手牌枚举全部 C 张出牌组合逐个比对"参考允许 vscan_followcard允许",任何一侧多放行或多拒绝都会被抓到(约 11 万组)。test_leak.js:下发面泄露审计(server 红线「发全 ≠ 发多,按可见性下发」+ design §4/§9/§11)。跑 6 局(可查牌/不查牌 × 叫 65/70/5),对每一个「服务器 → 某座位」的下发面——含各 RPC 的逐座位包、各阶段重连快照、明牌应答——深度扫描出其中所有牌 id,逐个判定该座位此刻是否有权知道这张牌(自己的牌 / 已打出 / 庄家的暗牌与底牌 / 70 分亮 3 秒 / 结束亮底牌 / §9 明牌)。约 1800 个下发面、1.8 万次判定。此前各处门控只有逐条手工核对,没有系统验证。test_fuzz.js:整局模糊 + 每墩胜者差分。test_endgame.js的驱动器只会出单张,所以对子/拖拉机/甩牌/甩错在「整局链路」里从未跑过——单测逐个覆盖了can_playcard/can_followcard,但它们与do_playcard/mod.chupai/结算的集成是空白。本文件用带牌型的对局补上(20 局约 480 墩,四种房间选项组合轮换),并对每一墩用独立参考实现重算"谁最大"与服务端比对(不是抽查,每墩都比),另独立重算闲家捡分、扣底倍数、算奖与捡分子数的分配。提交时还会把牌按最坏顺序摆([K,K,Q,Q]→[K,Q,K,Q],把每个对子拆开),压服务端判定的入参顺序无关性。
三组都做过变异检验(在副本上注入缺陷确认会转红):关掉 follow_tractor_cover_ok → test_followdiff 三种拖拉机首出全红;大光倍率改 3→4 / 常规 55 档 base 改 4→5 / 爬坡 40·35 档 Q 改 20→40 → test_calc 精确指出档位与 grade;牌的归属写给非胜者、单张毙牌不再压过副牌、算奖分配去掉 ×X、跟牌牌面值改回未排序入参 → test_fuzz 全部转红。
改这三个文件时别破坏的写法约定:
- 随机源必须是固定种子的 PRNG,不用
Math.random——同一份代码每次跑的用例集完全相同。test_fuzz.js连发牌也接到同一个种子上:它末尾的覆盖下限(甩牌/毙牌/扣底各出现多少次)是发牌相关的,用随机发牌时稀有事件会在某些运行里一次都不出现而让断言随机转红(实测 60 次里有 1 次 0 次扣底)。随机发牌的整局覆盖由test_endgame.js承担,两者互补。 - 关键结构要用确定性手牌钉死,不能指望随机造牌覆盖到。例如「同花色三组互不相邻的两连对」(12 张)是唯一能区分"只出零散对子"与"先凑最长拖拉机"的形状(少于 3 组时任取 3 对必含相邻对,测不出差别);纯随机造牌造不出它,关掉覆盖度校验也全绿,差分就形同虚设。
structHands()就是为此存在的,另注意别让掺入的其他花色把手牌撑过枚举上限而被静默跳过。 - 不变量不能拿被测字段自证。
test_fuzz里踩过两次:① 闲家捡分若按playowner累加再与aset.grade比,playowner写错时两边一起错、断言照样通过——改成按独立算出的墩胜者累计;② 扣底倍数若再调get_bottom_multiple去比,倍数表被改成恒返回 2 也通过——改成按 design §6.3 独立重算。
约定(dev-guide §10 测试纪律)
- 测试代码可用现代 JS(不受严格 ES5 限制);正式代码不得为测试放宽/加后门。
- 正反面 + 边界都要覆盖;失败先裁根因(业务缺陷改业务、脚本缺陷改脚本),不软化断言。