二七王合规文档:修正"全部符合"结论并补第三轮核对

01 的「二次全量复核结论」写的是「服务端全部符合 design.md」,但该
轮核对完全按 design 章节组织,平台接入、客户端入参校验、下发面一
致性、协议红线这几类问题整类不在范围内——所以 270 项断言全绿的同
时,漏掉了 1 项阻断级(子游戏从未在 app.js 注册,运行时不可达)、
3 项严重(重复牌id 可伪造牌型、入参无类型校验、下发面 multiple 忽
略爬坡)与 data.success 红线缺失。

本次:
- 01 把结论限定为「就 design.md 的玩法规则维度而言符合」,并加警示
  「符合 design.md ≠ 实现完整正确」。
- 01 新增「第三轮核对」章节:A1/B1/B2/B3/C1/D1~D7 共 11 项的严重
  度、位置、实测现象与处置,并单列两项需外部输入的待办(D3 超时托
  管无定时器、D6 不查牌模式是否屏蔽出牌历史的规则歧义)。
- 02 测试计划更新断言总数 270→357,新增「第三轮补充:design 之外
  的可验证维度」覆盖表(入参校验/重复id/报无主刷新/success 成功包
  与失败回包/multiple 同源/甩牌下发面/pushlist 结构/相邻链花色),
  并写明两项仍无自动化覆盖的原因。
- 00 加一段说明前两轮的核对范围局限,指向 01 第三轮。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-12 04:25:15 +08:00
co-authored by Claude Opus 5
parent a049498f2c
commit c4256c51db
3 changed files with 55 additions and 2 deletions
@@ -49,3 +49,5 @@
---
以上为初步清单。经逐节精查后,另发现若干**运行期代码缺陷**(如 `can_followcard` 引用未定义变量 `tuolaji_list` 会抛异常、`can_playcard` 的 14 张上限 `do_returnfalse` 漏写括号导致失效等)、以及**甩牌合法性 / 强制跟牌 / 局数扣卡 / 查牌模式**等更多不一致,详见 [`01-design合规逐节核对.md`](./01-design合规逐节核对.md)。
> **本文与 01 的前两轮都只沿 design.md 逐节核对玩法规则**,因此平台接入、客户端入参校验、下发面一致性、协议红线这几类问题整类不在核对范围内。第三轮补上这些维度后又查出 1 项阻断级(子游戏从未在 `server/youle/app.js` 注册,运行时完全不可达)、3 项严重(重复牌id 可伪造牌型、入参无类型校验、下发面 `multiple` 忽略爬坡开关)与协议红线缺失(全仓无 `data.success`),见 [`01`「第三轮核对」](./01-design合规逐节核对.md#第三轮核对平台接入--入参--下发面--协议红线)。
@@ -17,8 +17,10 @@
> 本轮已按 design.md 修正的项(均已用 Node 脚本对 design 判定表逐格验证,见"验证"列)。下方"结论摘要"与各节详评仍保留**原始不一致**描述以留痕;本节记录当前状态。
> **二次全量复核结论**:对整改后代码做了一次独立对抗性复核(§7/§8 算子算奖、§5 出牌甩牌、§4 开局投降暗牌阶段机、§2/§3/§9/§10 编码查牌房间,逐条比对 + Node 实测)。结果:**服务端全部符合 design.md**,且未发现整改引入的符号/下标/边界错、无 step4 死代码残留、投降 36 张快照与 X×N 算奖均正确。
> **二次全量复核结论(限定范围)**:对整改后代码做了一次独立对抗性复核(§7/§8 算子算奖、§5 出牌甩牌、§4 开局投降暗牌阶段机、§2/§3/§9/§10 编码查牌房间,逐条比对 + Node 实测)。结果:**就 design.md 的玩法规则维度而言,服务端符合**,且未发现整改引入的符号/下标/边界错、无 step4 死代码残留、投降 36 张快照与 X×N 算奖均正确。
> - 复核时发现的唯一未落地项 **§5.4.4 强制跟牌分量拆解已于本轮修复**:合法甩牌在 `can_playcard` 记录分量需求 `shuai_demand`(拖拉机长度/对子/单张),存入 `playproc`;跟牌时 `do_playcard` 调 `arith.flush_follow_ok` 在"同花色凑张数、主牌最大化"之上追加"拖拉机→对子→单张"的强制分量匹配(能凑同长主拖必凑、能凑主对必凑,不得拆散去垫)。已用 Node 正反面/边界用例验证。
>
> ⚠️ **该结论只覆盖"玩法规则是否符合 design.md"这一个维度**。第三轮核对(见下方「第三轮核对」)在**平台接入、客户端入参校验、下发面一致性、协议红线**四个维度上又发现 1 项阻断级、3 项严重、若干中等缺陷——它们不属于 design.md 的玩法条款,所以逐节核对没有覆盖到,但同样会让服务端跑不起来或被利用。**"符合 design.md" ≠ "实现完整正确"**,后续复核不要再把前者当成后者。
| 项 | 位置 | 整改内容 | 验证 |
| --- | --- | --- | --- |
@@ -64,6 +66,32 @@
---
## 第三轮核对(平台接入 / 入参 / 下发面 / 协议红线)
> 前两轮只沿 design.md 逐节核对玩法规则,因此**整类问题在核对范围之外**。本轮补上这些维度,结论如下(均已修复并有单测回归)。
| # | 严重度 | 位置 | 问题 | 处置 |
| --- | --- | --- | --- | --- |
| A1 | 🟥 阻断 | `server/youle/app.js` | 只加载 `games2/jinxianmahjong/mod.js`,**从未加载 `games/erqiwang/mod.js`**,全仓平台侧对 erqiwang 零引用 → `youle_erqiwang` 模块从不创建、`pack.route="erqiwang"` 无法命中三层路由,**整个子游戏运行时不可达** | 已在 app.js 补注册(server dev-guide README 允许触碰的唯一平台文件) |
| B1 | 🟥 严重 | `class.paiju.js` `check_cards_inhand` | 不查重复牌id:`do_playcard([方块K,方块K])` 实测 `result=true, cardtype=201`——同一张牌被判成一对,可伪造拖拉机/甩牌分量,且该分牌 score 被重复累加;`maipai` 传 8 个相同 id 实测**只埋下 1 张**,庄家带 35 张进入出牌阶段 | 新增 `check_cards_valid`(非空数组 / 0~107 整数 / 无重复)作为唯一入参校验入口 |
| B2 | 🟥 严重 | `mod.js` `maipai`/`chupai` | 越界 id 或非数组入参在解引用处抛异常,中断 `DoPack`;且 `cards.length` 读在校验之前 | 校验前置于 length 判断之前 |
| B3 | 🟥 严重 | `arith.get_multiple_bycall` + 6 个调用点 | 写死 `climb=false`:**爬坡房对局全程界面显示常规算子子数,与结算 `aset.multiple` 不符**(叫 45 分:界面 6、结算 7) | 删除该兼容包装,全部改调 `get_base_bycall(call, cfg.climb)`,与结算同源 |
| C1 | 🟥 红线 | 全部下发包 + 所有失败分支 | **全仓无 `data.success`**;失败一律裸 `return`、不回任何包,前端只能干等倒计时。违反 server dev-guide「成败唯一是 data.success、主动推送必须自带」与 client dev-guide「发包只请求、收包才表现」 | 成功包统一补 `success:true`;新增 `ERR` 码表 + `do_sendfail`,每条失败分支回 `success:false + errcode` 给请求者 |
| D1 | 🟧 中等 | `arith.is_continuous` | `is_8`/`is_6` 只判 `code % 100`、不校验花色:`is_continuous(♥8=308, ♣6=206)` 实测为 `true`。`get_chongguan` 是唯一扫描整手牌(含各花色副牌)的调用点,连对链走到主 8 后会被任意花色的一对 6 错误接续、多算奖(design §8.1 要求 A~5 段必须是主花色) | 追加「编码相差 2」约束(同花色 8/6 恒差 2) |
| D2 | 🟧 中等 | `export.get_deskinfo` pushlist | ① 内层循环上界误用轮次数当座位数,实测 5 轮时内层被撑成长度 5 且含 `undefined`;② 排序传的是上个循环泄漏的 `pai.flower`(=最后一张牌大王的花色 5),等于按「无主牌」排序 | 内层固定 3、改用 `paiju.flower`,并对缺失轮次加守卫 |
| D3 | 🟧 中等 | `class.desk.js` | 下发了 `countdown`,但服务端**无任何定时器**做超时兜底/托管,玩家掉线不操作则牌局永久卡死(server dev-guide「服务端自动操作复用真人链路」) | **未修**,见下方「待办」 |
| D4 | 🟨 轻微 | `paiju.do_playcard` | 报无主后只更新出牌者自己那一格,B/C 要等各自轮到出牌才补上,界面最多滞后两次出牌(design §9 要求「为全体三人显示另外两家」) | 改为一旦 `have_baofu()` 成立即按实际手牌刷新三个座位 |
| D5 | 🟨 轻微 | `can_playcard` / `chupai1` | `cardtype` 只能表达单一牌型,甩牌被压平成 1xx/2xx(「主K对+主5」→103),客户端连"这是一次甩牌"都看不出来 | `chupai1` 增发 `shuai` 分量构成(与 `shuai_demand` 同源) |
| D6 | 🟨 待确认 | `export.get_deskinfo` | 不查牌模式下 `pushlist`(已打出牌历史)仍照发。design §9「不可查牌模式:不提供以上任何查看功能」的"以上"是否含第一条(查看已打出的牌),**规则本身有歧义** | **未修**,需规则设计者确认 |
| D7 | 🟨 轻微 | `check_cards_inhand` | 牌已出/已埋时 `return;`(undefined)而非 `false` | 已改为 `return false` |
### 尚未处置(需外部输入)
- **D3 超时托管**:服务端对四个阶段只下发 `countdown` 数值,没有对应的定时器与超时后的默认动作(自动叫分/选主/埋牌/出牌)。design.md 未规定托管行为,且默认动作与前端表现强相关,需与前端和规则设计者一起定义后再实现。当前状态下**任一玩家掉线不操作,该局会永久停住**。
- **D6 不查牌模式的范围**:见上表,等规则设计者确认后再决定是否屏蔽 `pushlist`。
---
## 结论摘要
> **注**:下表是**首轮核对**的初始发现快照(保留以记录起点)。其中 §4.4、§5.4、§6.3、§7、§8.1-8.4、§9、§10、§11 等标 🟥/🟧/🟨 的项**后续均已修复并验证**——当前逐节状态以本文档后面的分节详情及全套单测(270 项全绿)为准。
@@ -147,7 +147,7 @@
## 5. 当前状态小结 —— ✅ DoD 达成,P0/P1/P2 全部完成
**共 270 项断言全绿**(`test_arith` 101 / `test_callgrade` 6 / `test_config` 19 / `test_deal` 12 / `test_desk` 6 / `test_follow` 50 / `test_paiju` 31 / `test_rpc` 45)。
**共 357 项断言全绿**(`test_arith` 106 / `test_callgrade` 6 / `test_config` 19 / `test_deal` 12 / `test_desk` 6 / `test_follow` 50 / `test_input` 34 / `test_paiju` 31 / `test_rpc` 64 / `test_success` 29)。
design 全部服务端可验证章节均有正/反/边界单测:
- **§2 构成**:`test_deal` 92张/去3-4/三家28+底8/分值。
@@ -162,3 +162,26 @@ design 全部服务端可验证章节均有正/反/边界单测:
- **§12**:端到端(叫分→上庄→选主→埋牌→出牌ready,真实 RPC 驱动)。
> 说明:§12 覆盖到"出牌准备就绪"(各阶段串接正确);完整 28 墩逐墩对打需一个合法出牌选择器,属后续增强,非规则合规缺口(逐墩规则已由 §5/§6 单测覆盖)。
---
## 6. 第三轮补充:design 之外的可验证维度
> 前两轮的覆盖矩阵完全按 design 章节组织,因此**平台接入、客户端入参、下发面一致性、协议红线**这几类根本不在矩阵里——270 项全绿却漏掉了 1 项阻断级与 3 项严重缺陷(见 `01-design合规逐节核对.md`「第三轮核对」)。本节把这些维度补进测试计划,后续复核必须一并跑。
| 维度 | 被测 | 用例(正/反/边界) | 文件 | 状态 |
| --- | --- | --- | --- | --- |
| 客户端入参合法性 | `check_cards_valid` / `check_cards_inhand` | 正:单张/多张/边界 id 0 与 107;反:非数组(undefined/null/字符串/伪数组)、空数组、重复 id、越界、负数、小数、数字字符串;反:不在手上/已出/已埋 | `test_input` | ✅ 21 |
| 重复 id 不得伪造牌型 | `mod.maipai` / `mod.chupai` | 反:8 个重复 id 埋牌被拒且一张未埋;反:重复 id 出牌被拒;正:8 张不同牌埋牌成功且实埋 8 张;反:越界/非数组入参不抛异常且不产生出牌 | `test_input` | ✅ 7 |
| §9 报无主即时刷新 | `do_playcard` | 正:打空瞬间三家统计与主花色标志同时刷新;边界:报无主前三家均为 `[-1,-1]` | `test_input` | ✅ 6 |
| 红线 `data.success`(成功包) | `class.desk`/`mod.js`/`get_paiju_account` | 正:fapai/zhunbei/jiaofen/shangzhuang/xuanzhu/maipai/chupai1/mingpai/tishi 均带 `success:true`;正:jiesuan 的正常/投降/解散三种 type 都带 | `test_success` | ✅ 12 |
| 红线 `data.success`(失败回包) | 各 handler 失败分支 | 反:每个 handler 取代表性失败路径,断言"恰好回 1 包 + `success:false` + errcode 正确 + 回到请求者 fromid",覆盖 STEP/SEAT/PARAM/RULE/PLAYER 五类 | `test_success` | ✅ 17 |
| §7.3 下发面 multiple 与结算同源 | `mod.jiaofen` / `get_deskinfo` / `get_paiju_account` | 正:叫 45 分在常规房/爬坡房分别得 6/7;正:上庄包与重连 ChooseMain 一致;边界:与结算包 `aset.multiple` 逐一对齐 | `test_rpc` | ✅ 7 |
| §5.4 甩牌下发面 | `chupai1` | 正:合法甩牌带 `shuai` 分量;反:非甩牌不带;反:甩错只打最小一张且不带 `shuai` | `test_rpc` | ✅ 8 |
| 重连 pushlist 结构 | `get_deskinfo` | 正:轮数正确、内层恒 3 个座位、无 undefined;正:按本局主牌花色排序(主5 排在副A 前) | `test_rpc` | ✅ 4 |
| §3 相邻链花色 | `is_continuous` | 正:同花色 8-6 连;反:三组跨花色 8-6 不连;反:差 2 但非 8/6 不连 | `test_arith` | ✅ 5 |
**仍无自动化覆盖(需外部输入后才能定义期望)**:
- **超时托管**(`01` 表 D3):服务端只下发 `countdown`、无定时器与默认动作,玩家不操作则牌局永久停住。默认动作需与前端/规则设计者共同定义,定义前无法写期望。
- **不查牌模式是否屏蔽出牌历史**(`01` 表 D6):design §9 表述有歧义,待规则设计者确认。