feat: integrate platform room entry, UI migration and erqiwang documentation

This commit is contained in:
2026-09-07 21:01:08 +08:00
parent 9c4510697c
commit 30a1b4114c
259 changed files with 84995 additions and 30201 deletions
@@ -0,0 +1,53 @@
# 二七王 服务端实现 vs 设计文档:初步不一致清单
> 本文是首轮通读 `server/games/erqiwang/*.js` 与 `docs/design/design.md` 后记录的**初步**疑点清单,作为逐条深入核对的线索。完整、逐节的核对结论见同目录 [`01-design合规逐节核对.md`](./01-design合规逐节核对.md)。
>
> 方向约定(与根 `CLAUDE.md` 一致):`design.md` 是玩法规则的唯一权威,代码与其冲突时**应改代码去符合规则**(除非规则标「待确认」);本清单只做记录,不代表已修复。
---
## 1. 算子(基础子数)与设计表格完全对不上
- 设计 §7.1 常规算子基础子数:65→2、60→3、55→4、50 及以下→6;70 投降→1、70 打牌→2。
- 代码 `arith.get_multiple_bycall` 只返回 1/2/4/0(`call>60→1`、`call>40→2`、`call>0→4`),与设计四档数值、结构均不符。
## 2. 爬坡 / 傍王 房间选项未做成开关
- `arith.get_upgrade` 用 `halfcall = ceil(call/2)` 作小光/过庄分界与升级级距;设计常规算子该分界**固定 40**、级距**固定 40**。
- 代码里**没有任何读取房间「爬坡 / 傍王」勾选的分支**:`grade_w`(傍王)无条件计算、`halfcall` 恒用。设计 §7.3 / §8.3 / §10.3 中这两项都是可选规则,需要开关。
## 3. 扣底倍数不符
- 设计 §6.3:单张主 ×1、主对子 ×2、两连对 ×4、三连对 ×6、N 连对 ×2N(线性)。
- 代码 `arith.get_bottom_multiple`:单张 →2、对子 →4、两连对 →8、三连对 →16、四连对 →32、五连对 →64(近似指数),且单张即翻倍;六连对落空返回 `undefined`。
## 4. 「固定主牌 ≥10 张」给了算奖
- 代码 `arith.get_chongguan` 对 `王+2+7≥10` 追加 `total−9` 奖(「10 个老主判断」)。
- 设计 §8.2 明确举例:固定主牌 ≥10 张只触发**亮牌**、**不触发任何算奖**。
## 5. 算奖归属模型与结算方式不同
- 设计 §8.1:常规算奖**只计庄家**;§8.3 傍王才庄闲都算(且需勾选);§8.4:算奖额外支付额 = `X × N`(`X` 为第 7 节算出的基础输赢子数)。
- 代码:对**三家**都算「冲关」并两两结算,且叠加「冲关必叫 / 四王必踢」的**作废**规则(设计中无此机制);`grade_cg`、`grade_w` 是**扁平奖数差**(每奖记 1 子),**未乘 `X`**。
## 6. 叫分缺上限校验
- 设计 §4:叫分范围 5 ~ 70。
- 代码 `mod.jiaofen` 只校验 `call%5==0` 与「后叫必须更低」,**缺 `call<=70` 上限**,首家可叫出 75、100 等非法分值。
## 7. 投降资格范围过宽
- 设计 §4.4 / §7.1:投降是 **70 分坐庄专属**分支。
- 代码 `mod.touxiang` 用 `call < 65 → 拒绝`,即 **65 分也能投降**。
## 8. 70 分暗牌亮 3 秒等交互细节未实现
- 设计 §4.4 / §7.1:70 分坐庄,8 张暗牌需向两个闲家亮 3 秒(投降 / 打牌都要)。
- 代码未见相关下发逻辑。
---
以上为初步清单。经逐节精查后,另发现若干**运行期代码缺陷**(如 `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#第三轮核对平台接入--入参--下发面--协议红线)。
@@ -0,0 +1,531 @@
# 二七王 服务端实现 vs `design.md` 逐节合规核对
> 权威方向(同根 `CLAUDE.md`):`design.md` 是玩法规则的**唯一权威**。凡代码与规则冲突,除非规则项标注「待确认」,一律视为**代码缺陷**,应改代码去符合规则,**不得改规则迁就代码**。
>
> 核对对象:`server/games/erqiwang/` 下 `mod.js`、`class.pai.js`、`class.paiju.js`、`class.arith.js`、`class.desk.js`、`class.export.js`、`class.import.js`。
>
> 严重度标记:
> - 🟥 **严重**:影响发牌 / 计分 / 输赢 / 核心玩法正确性。
> - 🟧 **中等**:可选规则缺失、边界校验缺失、局部规则偏差。
> - 🟨 **轻微**:交互提示 / 展示细节 / 死代码。
> - 🐞 **代码缺陷**:运行期异常或明显写错(无论是否直接违背规则)。
> - ✅ **符合**。
---
## 整改进度
> 本轮已按 design.md 修正的项(均已用 Node 脚本对 design 判定表逐格验证,见"验证"列)。下方"结论摘要"与各节详评仍保留**原始不一致**描述以留痕;本节记录当前状态。
> **二次全量复核结论(限定范围)**:对整改后代码做了一次独立对抗性复核(§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" ≠ "实现完整正确"**,后续复核不要再把前者当成后者。
| 项 | 位置 | 整改内容 | 验证 |
| --- | --- | --- | --- |
| D1 崩溃 | `class.arith.js` `can_followcard` | 用 `get_pairlist`+`get_tuolaji_list(_followcards)` 校验拖拉机牌型,消除未定义 `tuolaji_list` | 语法通过 |
| D3 | `can_playcard` | `do_returnfalse;` → `do_returnfalse()`,恢复 14 张上限 | 语法通过 |
| D4 | `do_choiceflower` | 删除空循环死代码 | 语法通过 |
| §6.3 | `get_bottom_multiple` | 改为线性:单张 1、主对 2、N 连对 2N | ✅ 10 用例 |
| §7 算子 | `get_base_bycall`/`get_qvalue`/`get_upgrade`(新)+ `get_paiju_account` | 实现常规算子(大光×3、Q=40) 与爬坡(§7.3 分段) 两套,按爬坡位(roomtype 位3)切换;`X=multiple×|upgrade|` | ✅ 74 格逐档 |
| §8.1 | `get_chongguan` + `get_paiju_account` | 常规算奖只计庄家;连对链修复(传实际主花色);移除 ≥10 老主奖;去掉设计外的"冲关必叫/四王必踢"作废 | ✅ get_chongguan 用例 |
| §8 快照 | `get_seat_cards_award`(新) | 庄家用埋牌后 28 张(排除已埋)、闲家用发牌后 28 张 | 语法通过 |
| §8.3 傍王 | `get_paiju_account` | 改为按傍王位(roomtype 位2)开关,庄闲每王 1 奖,去作废 | ✅ 公式用例 |
| §8.4 | `get_paiju_account` | 算奖并入结算改为 `X×(2Ni−Nj−Nk)`(乘 X) | ✅ §8.4 举例 |
| §4.2 | `mod.jiaofen` | 增加叫分上限 `call<=70` | — |
| §4.4 投降 | `mod.touxiang` + `do_burycard` + `get_deskinfo` | 投降仅 70 分、在**选主阶段 step2** 与选主互斥、不选主不埋牌;结算基础 1、庄输 1 子/闲;算奖用庄家 **36 张**(发牌+暗牌,无主花色→无连对链);埋牌后 step 直接进 5、删除已废弃的投降 step4 与重连 Surrender 视图 | — |
| §10.1 | `get_asetcount`/`get_needroomcard` | 局数 6/12;房主扣卡 2/4 | — |
| §5.4 甩牌 | `arith`(新增 `trump_rank`/`decompose_trump`/`opp_can_beat_flush`/`flush_follow_ok`/`remove_pairs`)+ `can_playcard` + `do_playcard` + `mod.chupai` | 副牌绝对禁甩;最大性改为**按对手全部主牌**逐分量判定;甩错惩罚(收回、只打最小一张、下发 `shuaicuo`);**§5.4.4 强制跟牌分量拆解**(拖拉机→对子→单张优先级,`shuai_demand`+`flush_follow_ok`) | ✅ 分解/最大性/各路径/跟牌分量用例 |
| roomtype | `class.config.js`(新)+ `export`/`paiju` | 数组下标改为**位串**(`"01011"`),`class.config.js` 唯一解析(SSOT) | ✅ parse/扣卡/局数用例 |
| §4 暗牌亮牌 | `shangzhuang` + `get_deskinfo` | 70 分坐庄把 8 张暗牌下发给所有玩家 + `ancard3s=1`(供摸牌前亮 3 秒);修正**非 70 分暗牌泄露给闲家**(现暗牌只发庄家,符合 §4"只有庄家可见");重连的暗牌一律仅庄家可见 | — |
| §8.2 亮牌 | `get_liangpai`(新)+ `mod.maipai` + `get_deskinfo` | 庄家埋牌后 28 张按阈值(固定主≥10/王≥3/7≥6/2≥6)统计,出牌开始向闲家亮(只亮数量/结构);可查牌模式才下发 | ✅ 阈值/统计用例 |
| §9 查牌 | `class.config.js`(nocheck) + `mod.chupai` + `get_deskinfo` + `mod.mingpai`(新) | 报无主的座位统计 `info`/`baozhu`/`PushCards.seatlist` 与亮牌均按 `cfg.nocheck` 门控;新增 `mingpai` RPC(可查牌+已报无主时查看他家全部主牌) | — |
**尚未整改(多需与客户端/协议协同,或为较大的独立子系统)**:
- ✅ **§5.4 甩牌(已全部完成)**:副牌绝对禁甩、最大性按对手全部主牌逐分量判定、甩错惩罚、以及 §5.4.4 强制跟牌分量拆解(`flush_follow_ok`:拖拉机→对子→单张优先级)均已实现并用 Node 验证。
- 🟩 **§9 查牌 / §8.2 亮牌(服务端已完成)**:亮牌 `get_liangpai`(庄家埋牌后 28 张按阈值统计)在 `maipai`/重连下发给闲家;报无主的座位统计(`info`/`baozhu`/`PushCards.seatlist`)与亮牌均按查牌位 `cfg.nocheck` 门控(不查牌一律不下发);新增 `mingpai` RPC(可查牌 + 已报无主时查看他家全部主牌)。剩客户端展示(面板/明牌按钮)。
- 🟩 **§4.4 70 分暗牌亮 3 秒(服务端已就绪)**:`shangzhuang` 对 70 分坐庄把 8 张暗牌下发给所有玩家并带 `ancard3s=1`,供客户端在庄家摸暗牌前亮 3 秒;顺带修正了**非 70 分把暗牌泄露给闲家**的问题(现非 70 分暗牌只发庄家,符合 §4"只有庄家可见")。剩下的 3 秒动画由客户端完成。
- ✅ **§11 闲家 3 提示(踩/没分/有分)**:`mod.tishi` 服务端转发给对家已实现;剩客户端展示。
- 🟧 **客户端建房配置**:`roomtype` 已改为位串(见下表:位0局数/位1扣卡/位2傍王/位3爬坡/位4查牌),需在 erqiwang 客户端建房界面按位勾选拼串;服务端已按该格式读取(`class.config.js`)。
### roomtype 位串约定(服务端已按此读取;解析入口 `class.config.js`)
`roomtype` 为**定长数字位串(字符串)**,每一位(`charAt`)一个开关 `'0'/'1'`,便于前端逐项勾选。缺省 `"00000"` = 6局/房主扣卡/无傍王/常规算子/可查牌。解析对缺失/过短字符串按 `'0'` 兜底。
| 位 | 含义 | `'0'` | `'1'` |
| --- | --- | --- | --- |
| 0 | 局数 | 6局(缺省)| 12局 |
| 1 | 扣卡方式 | 房主扣卡(缺省)| AA每人扣卡 |
| 2 | 傍王 | 关(缺省)| 开 |
| 3 | 爬坡 | 常规算子(缺省)| 爬坡 |
| 4 | 查牌模式 | 可查牌(缺省)| 不查牌 |
示例 `"10100"` = 12局 / 房主扣卡 / 傍王开 / 常规算子 / 可查牌。
---
## 第三轮核对(平台接入 / 入参 / 下发面 / 协议红线)
> 前两轮只沿 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`,但服务端无任何定时器做超时托管,玩家不操作则牌局停住等待 | **规则设计者确认:维持现状、不做超时动作**。已写入 design §11「倒计时只作展示,不触发任何自动操作」并注明不是缺口;牌局停住由玩家走房间解散流程收场(按当前累计分结算,design §12.2)。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`(已打出牌历史)仍照发。规则设计者已确认:**不可查牌模式必须屏蔽出牌历史,只有可查牌模式才能查看** | 已按确认修复:`pushlist` 按 `cfg.nocheck` 门控;design §9 同步改写为无歧义表述(四项功能整体开关 + 「当前轮桌面牌不属于查牌、两种模式都可见」的边界) |
| D7 | 🟨 轻微 | `check_cards_inhand` | 牌已出/已埋时 `return;`(undefined)而非 `false` | 已改为 `return false` |
### 尚未处置
**无。** 本轮发现的 11 项已全部收口:
- A1/B1/B2/B3/C1/D1/D2/D4/D5/D7 共 10 项为缺陷,已改代码 + 补单测;
- D6(不查牌是否屏蔽出牌历史)原为规则歧义,已由规则设计者拍板「屏蔽」,改 design §9 + 代码 + 单测;
- D3(超时托管)原列为待外部输入,已由规则设计者拍板 **维持现状、不做超时动作**,属**规则如此、并非缺陷**,已写入 design §11 固化,避免后续核对反复把它当缺口重提。
---
## 第四轮核对(全量重核 design.md:先流程、后规则细节)
> 从 design.md §1~§12 重新逐条对照当前代码(含流程阶段机与全部规则细则),**流程层无缺环**(发牌→叫分→摸暗牌/70分亮3秒→选主/投降互斥→先选主后埋牌→出牌→末轮扣底→算子+算奖→轮庄→大局/解散结算,`step` 迁移 1→2→3→5→6 闭合)。规则层新发现 3 项不一致,已全部修复并补回归单测。
| # | 严重度 | 位置 | 问题 | 处置 |
| --- | --- | --- | --- | --- |
| E1 | 🟧 中等 | `class.paiju.js` `get_liangpai` | 亮牌统计取 `get_seat_cards`(只含未出的牌),而 §8.2 的依据是**庄家埋牌后的手牌**。埋牌那一刻算对,但 `get_deskinfo` 在 step5 重连时会按庄家**当前剩余手牌**重算:实测「4王+6个7」的庄家打掉 2 大王 + 1 对♦7 后,重连再取从 `{zhu:10,zhupair:5,zhutuo:1,wang:4,qi:6}` 变成 `null`,闲家重连即丢失亮牌 | 改用已有的静态快照 `get_seat_cards_award(o_paiju, banker)`(埋牌后 28 张、含之后已打出的),并在函数注释里固化"不得用 get_seat_cards"的原因;补 3 条回归用例(快照 / 打出后重连不缩水 / 已埋牌不计入) |
| E2 | 🟨 轻微 | `mod.js` `chupai` | 第三轮 D4 只修了服务端状态(`do_playcard` 里已按 `have_baofu()` 同时刷新三个座位),**下发面没跟上**:出牌包仍只带 `info = seatlist[seat]` 一家,另两家的主牌数/对子数要等它们各自出牌才送达,仍最多滞后两次出牌,与 §9「一旦有人报无主,立即为全体三人显示另外两家」不符 | 出牌包改发整表 `seatlist`(三家),字段名/结构与重连包 `PushCards.seatlist` 统一;协议文档三处 `info` 条目同步改写;补「整表带三家 + 含非出牌者统计」用例 |
| E3 | 🟨 轻微 | `class.arith.js` `can_playcard` | 开头 `if (cards.length > 14)` 是"规则外的约定"硬上限,与 §5.4.3「甩牌无组合数量限制」冲突:手持 15 张以上顶级主牌时的合法甩牌会被整个拒掉(如 4王+8个7+正2对+副2对 共 16 张) | 上限改为结构性上界 28(出牌阶段单人最多持 28 张,且 28 张内牌型编码不溢出 1xx/2xx/3xx 区间),不再设规则性限制;补 16 张合法甩牌用例 |
**非缺陷的观察(本轮确认,勿再重提)**:
- 70 分「暗牌亮 3 秒」:服务端在上庄包里一次性下发 8 张暗牌 + `ancard3s=1`,"摸入前亮 3 秒"的先后由前端表现;重连不重放该一次性事件。design 未要求服务端表达先后,**按现状即合规**。
- §4/§11「选主按钮显示各花色对子数」:design 标注为**前端表现**,服务端已把庄家全部手牌下发,前端可自算,**不是下发面缺口**。
### 尚未处置
**无。** E1/E2/E3 已全部改代码 + 补单测,协议文档同步。
### 第四轮·复验(E1~E3 修复后的独立取证复核)
对修复后的实现又做了一次**取证式**复核:不再靠通读,而是把 design 的判定语句直接做成可执行探针打在代码上。**未发现新的不一致**,全部符合。取证覆盖:
| 维度 | 取证方式 | 结果 |
| --- | --- | --- |
| §5.1/§5.2 跟牌 | 层级 2/3/4(放着更长拖拉机不出→拒、拆对出散张→拒、留着对子只出散张→拒)、同花色不足全出+缺口任意、混合出牌牌面判 0、毙牌须完全缺门且牌型对应、首家出主必须跟主 | 15/15 符合 |
| §8.1 算奖 | 三/四王、链从正 7 起、无正 7 链断、正 7-副 7-正 2-副 2-主 A-主 K 链 5、副 K 对(非主花色)不入链、六/七/八个 7 与 2、固定主 ≥10 不给奖 | 13/13 符合 |
| §7 算子 | 常规与爬坡逐档抽查 18 格(含大光/小光/过庄/升 N 级、爬坡 45 独有档、10 分与 5 分无小光档) | 18/18 符合 |
| §5.4 甩牌 | 单一牌型不算甩牌、分量被更大同长拖拉机压→甩错、对手仅持更大散张压不了对子、甩错只打最小一张、"持有即算被压" | 6/6 符合 |
| §6.3 扣底 | 混合甩牌取最高规格倍数、末轮用副牌赢不扣底 | 符合 |
| §8.4 结算 | **闲家也持奖时的三条独立支付线**(此前未覆盖):`N=[4,1,0]`→`aw=[28,-8,-20]`,与捡分子数叠加后零和 | 符合 |
| §12 全流程 | 端到端跑通整局 + 18 局随机模糊(含甩牌轮),校验牌张守恒/捡分一致/结算零和/分配公式/扣底触发条件 | 无异常 |
| §4/§9/§10/§11 | 叫分 6 种序列(含跳过已"不叫"者)、70 分暗牌下发与投降资格、重连各阶段门控(暗牌仅庄家、pushlist/seatlist/亮牌按 `nocheck`、当前轮桌面牌两种模式都有)、爬坡房 `multiple` 三处同源、提示只发对家、明牌返回另两家全部主牌、扣卡 4 组合 | 全部符合 |
固化产物:新增 `test/test_endgame.js`(L4 端到端,21 项,断言均与具体牌面无关、连跑 10 次无抖动),全套单测 **389 项**全绿。
---
## 第五轮核对(design 复验 + 首次以 packet_protocol.md 为核对对象)
> 本轮做两件事:① 再次独立复核 design.md ↔ 代码;② **首次把 `docs/protocol/packet_protocol.md` 本身当作核对对象**(前四轮只在改代码时顺手同步文档,从未反向验证「文档写的 = 客户端实际会收到的」)。
>
> **design 维度**:§1~§12 逐条重核,含自行重算 §7 边界档(爬坡 10 分/5 分无小光、70 分打牌、常规 40 分无过庄)与 §8.1 连对链(正 7 起链、副 7 跨花色不成对、非主花色断链)——**规则层未发现新的不一致**,前四轮整改均真实落地。
>
> **新发现 4 项**(1 项严重缺陷 + 2 项协议文档不准确/不完整 + 1 项健壮性),已全部处置。
| # | 严重度 | 位置 | 问题 | 处置 |
| --- | --- | --- | --- | --- |
| F1 | 🟥 严重 | `class.arith.js` `can_followcard` 尾段 | 计算 `cardvalue` / `noflower` / `nopair` 时用的是**入参原始数组 `followcards`**(客户端提交顺序),而不是上面已排好序的副本;`get_pairlist` / `get_tuolaji_list` 都按**降序相邻**取对。端到端实测:庄家出红心 KKQQ、闲家用黑桃 KKQQ 主拖拉机毙牌且为末轮——降序提交 `[51,105,50,104]` → `maxseat=1`、闲家捡分 40、扣底 ×4;打乱成 `[51,50,105,104]` → 对子漏判、`cardvalue=0`、`maxseat=0`(庄家赢)、捡分 0(大光)、不扣底。**同一手合法牌,客户端只靠数组顺序就能翻转本轮胜负、捡分归属、扣底与最终结算**;同源问题还能抹掉 `noflower` 缺门标志、污染 §9 下发给全场的牌况表。踩 server 红线「前端不是数据源」「服务器权威」 | 在 `min_ary_deduct` 削减 `_followcards` **之前**另存完整排序快照 `_sortfollow`,尾段 8 处全部改用它(`_followcards` 会被削减、不能复用);函数内注释固化"绝不能用入参 followcards"的原因。补 6 条回归(`test_follow.js` 入参顺序无关性 5 条 + `test_endgame.js` 端到端 1 条),已验证**回退修复后这 6 条全红** |
| F2 | 🟧 中等 | `packet_protocol.md` §14 | 「解散结算」被写成 `route=erqiwang / rpc=jiesuan` 的独立包。实际平台(`server_room/rpc.js`、`server_room/class.room.js`)是把 `get_disbandRoom` 的**返回值整个对象**塞进**房间路由**的解散包:`route=room / rpc=free_room`,`data.deskfree = { rpc:"jiesuan", data:{ success, aset, account } }`——**多一层 `data` 包装**,真实路径是 `data.deskfree.data.aset`。前端照原文档写会取空。另外该包外层 `data.success` 由平台填写,子游戏的 `success` 在 `deskfree.data.success`,与 §0.1 的读法不同 | 新增 §14.1「解散结算的投递方式」:给出真实包结构 JSON、三条取值要点、两层 `success` 的归属,并说明 `deskfree` 可能缺失(见 F4);§14 开头标注原表头只适用于正常出牌结算与投降结算 |
| F3 | 🟨 轻微 | `packet_protocol.md` 全篇 | 文档在 `multiple` / `bangwang` / `climb` / `baozhu` / `seatlist` / `liangpai` / `pushlist` 等多处引用「roomtype 位2 / 位3」,但**全文从未给出 roomtype 位串定义**——它只存在于本 compliance 文档里。前端据协议文档无法拼出建房串 | 新增 §0.5「`roomtype` 房间选项位串」:位表(位0局数/位1扣卡/位2傍王/位3爬坡/位4查牌)+ 缺省与示例 + 缺失兜底规则 + 局数/扣卡 4 组合对照,并指明 `class.config.js` `parse()` 是唯一解析入口(SSOT) |
| F4 | 🟨 轻微 | `class.export.js` `get_disbandRoom` | 未像 `get_deskinfo` 那样判 `o_room.o_desk` / `curr_paiju()` 为空。平台在 `rpc.js` 开战处 `makewar` 后**立刻**置 `battlestate=1`,而本游戏首局是 `makewar` 里 `min_ontimeout(...,1000)` 延迟创建的——这段窗口内解散会在 `curr_paiju()` 的 `undefined` 上解引用抛异常,打断平台整条解散链路 | 两级空守卫,返回 `null`(平台对 `null` 的 `_deskfree` 已有「照常发解散包、不带 deskfree」分支);补 2 条回归用例 |
**本轮同时复核并确认无误(勿再重提)**:
- **协议文档字段面**:跑完整局(含 `mingpai`/`tishi`/失败包/投降局)与重连五个阶段,把**实际下发的 `data` 键集**与文档逐包对齐——`fapai / jiaofen / shangzhuang / xuanzhu / maipai / chupai1,2,3 / jiesuan / mingpai / tishi / zhunbei` 及 `deskinfo` 的 `CallRun / ChooseMain / BuryCards / PushCards / Balance`,**字段名、出现条件、按座位裁剪(暗牌仅庄家、`gradecards` 仅闲家、`liangpai` 仅闲家+可查牌、`cardsinhand` 仅出牌者)全部一致**;`errcode` 码表、§0.1 `success` 约定、§0.3「`countdown` 无定时器」说明亦准确。
- **`can_followcard` 首家/跟家牌面值的比较基准不一致(首家取拖拉机最小对、跟家取最大牌)**:因同花色同长拖拉机不可能共用点数(两副牌已被首家占满),两段区间必然不重叠,比较结果恒正确,**属既有写法差异、非缺陷**。
- **`get_chongguan` 的 `cards.length < 28` 早退**:属隐式兜底写法,但三个调用点的快照恒为 28(闲家)或 36(70 分投降的庄家),**当前不可触发**。
### 尚未处置
**无。** F1/F4 已改代码 + 补回归单测(并已用「回退修复→用例转红」反验有效性),F2/F3 已改协议文档。全套单测 **401 项**全绿。
### 第五轮·复验(design 维度的穷举/差分取证)
F1~F4 修复后,对 **design.md 维度**又做了一次独立取证复核。方法上比前几轮更进一步:不再逐条读代码,而是**按 design 原文另写一份参考实现**,再和代码**穷举对拍**——参考实现只依据 design 的文字与表格,不看 `class.arith.js`。**未发现任何不一致。**
| design 章节 | 取证方式 | 规模 / 结果 |
| --- | --- | --- |
| §2 牌局构成 · §6.1 分牌 | 直接清点 `init_cards` 产物 | 108 张牌表 / 去 3-4 共 16 张 / 发 92 张 = 三家 84 + 暗牌 8 / 11 种点数各 8 张 + 大小王各 2 张 / 分值 5-10-K / 全场总分 200 ✅ |
| §3 主牌顺序 · 对子判定 | 取全部主牌排序后归并成「等级序列」,与 §3 表逐级比对 | 大王-小王-正7-副7-正2-副2-主A…主5 共 15 级完全一致;副牌 A…5 顺序一致;♥7+♣7 / ♥2+♣2 / 大王+小王 均不成对,同花色两副成对 ✅ |
| §5.3 拖拉机相邻 | 15 级相邻链逐段验证 + 反例 | 链全通;7 不与 6/8 相连、6 与 8 同花色相连、♥8+♣6 跨花色不相连、副2 只接主A ✅ |
| §5.1 / §5.2 跟牌强制层级 | **穷举差分**:按 §5.2 原文写参考裁定(缺门/不足/够牌 × 单张/对子/N 连对 + 最长拖拉机优先的档案比较),随机手牌枚举**全部** C 张出牌组合逐个对拍 | 4000 手牌 / **86474 个出牌组合,失配 0** ✅ |
| §5.4.1-3、§5.4.5 甩牌 | **随机差分**:按 §5.4.2 原文写「对手是否持有能压过任一分量的主牌/主对/同长主拖」的参考判定 | **6000 例,失配 0**;副牌禁甩 4 种组合全拒、副牌单一牌型全放行 ✅<br>(首轮出现的 170 例「最小张不符」经查是**同一等级的两副牌选了不同那张**,非分歧——改按牌面等级比对即归零) |
| §5.4.4 强制跟牌分量拆解 | 按 §5.4.4 五条逐条造正反用例 | 同长主拖必出 / 无同长退化为主对 / 主对不够全出+主单补 / 主牌不够全出+垫副 / 无主牌随意垫 / 多组不同长拖拉机逐组匹配,12 条全符合 ✅ |
| §5.4.5 甩错惩罚落地 | 走 `do_playcard` 真实链路 | 甩错后 `shuaicuo=true`、实际只打出最小一张、其余收回手牌、本轮牌型退化为 101、不再带 `shuai_demand` ✅ |
| §6.2 捡分归属 | 造庄赢/闲赢两轮 | 庄家赢 → 台面分牌作废(闲家捡分 0);闲家赢 → **台面全部分牌(含庄家自己打出的)** 计入闲家 ✅ |
| §6.3 扣底倍数 | §6.3 表逐格 | 单张 1 / 主对 2 / 2-6 连对 4·6·8·10·12;混合甩牌取最高规格;末轮用副牌或混合牌赢 → 0 不扣底 ✅ |
| §7 算子(全部) | **全量对拍**:把 §7.2.1 / §7.3.3 的判定表转写成参考实现,两种算子模式 × 14 个叫分档 × grade 0~260 逐格比 base / Q / 判定 / 最终子数 | **14672 格失配 0**,另抽查 design 表格里写死的最终子数 **113 格失配 0** ✅ |
| §8.1 常规算奖 | 逐组合造手牌 | 三王 1 / 四王 3 / 六·七·八个 7 → 1·2·3 / 六·八个 2 → 1·3;连对链 正7→副7→正2→副2→主A→主K 逐级 2·3·4·5·6·7 奖;链必起于正7(无正7 断链)、缺副7 断于正7、副A 对(非主花色)不入链、无三王则无链奖、固定主 ≥10 不给奖 ✅ |
| §8.2 亮牌 | 四档阈值边界(含刚好差 1) | 王≥3 / 7≥6 / 2≥6 / 固定主≥10 分别触发,各自差 1 即不触发;输出只含数量与结构、无具体牌面 ✅ |
| §8 快照时间点 | 走真实埋牌/出牌/投降链路 | 庄家=埋牌后 28 张(已埋 8 张不在内、出牌后不缩水)、闲家=发牌后 28 张、投降局庄家 36 张且 `flower=-1` ✅ |
| §8.4 算奖并入结算 | §8.4 举例数值 | `X=6, N=[1,2,0]` → `aw=[0,18,-18]`,三条支付线零和;只庄家持奖 → 另两家各付 X ✅ |
| §4.2 叫分 | **穷举全部叫分序列**(DFS,叫分集 {70,50,25,10,5}+不叫) | **80 条终局路径**:受理判定、待叫者永不落在已「不叫」者身上、上庄者恒为最低叫分者、上庄条件恒为「叫 5」或「另两家都不叫」,**异常 0**;首家不能不叫、75/71/67/3/-5 全按 PARAM 拒 ✅ |
| §4.3 暗牌可见性 · §4.4 投降 | 非 70 分 / 70 分两条链路 + 重连 | 非 70 分暗牌只给庄家(闲家无 `bottomcards`、无 `ancard3s`、无 `cards`);70 分三家都收到 8 张 + `ancard3s=1`;重连一律只给庄家(3 秒亮牌不重放);`touxiang` 仅 70 分为 1;投降 base=1、庄 -2 闲各 +1 ✅ |
| §4.6 轮庄 | 三种 result | 庄赢连庄 / 闲赢顺延下家 / 投降顺延下家;首局暂定庄家为 0 号座位 ✅ |
| §9 查牌模式 · §10 房间设置 · §11 提示 | 可查牌与不查牌两套房间跑同一段对局 | 可查牌下 `chupai.seatlist` 整表三家、`baozhu` 有值、重连带 `pushlist`/`seatlist`;不查牌下三者全无、`baozhu` 恒 0、明牌回 RULE;**当前轮桌面牌(`playproc`)两种模式都下发**;局数 6/12、房主 2/4、AA 1/2 与加入者扣卡 4 组合正确;提示只转发给对家、庄家发 → RULE、非法 tip → PARAM;不操作则 `step`/`playproc` 不变(无超时托管)✅ |
> 一句话结论:**就 design.md 而言,服务端实现完整且准确**。本轮两次「疑似失配」(甩错最小张 170 例、闲家赢轮捡分 15 vs 10)经查**都是探针写错**,代码是对的;已在上表注明,后续核对不要再把它们当缺陷重提。
>
> **已固化两组差分测试入库**(其余取证为一次性探针):`test/test_calc.js`(§7 全量对拍,7308 格 + design 表 141 格)与 `test/test_followdiff.js`(§5.2 穷举差分,约 11 万组),全套单测由 401 项增至 **462 项**,总耗时仍 < 1.5s。
>
> 两组都做过**变异检验**:关掉 `follow_tractor_cover_ok` → `test_followdiff` 三种拖拉机首出全红;大光倍率 3→4 / 常规 55 档 base 4→5 / 爬坡 40·35 档 Q 20→40 → `test_calc` 精确指出档位与 grade。
>
> ⚠️ 固化过程中的一个教训(改这两个文件时别踩回去):**差分测试的强度取决于造牌器,不取决于对拍逻辑**。第一版用纯随机抽牌,跑 7.8 万组全绿,但关掉 `follow_tractor_cover_ok` **依然全绿**——因为「同花色三组互不相邻的两连对」(12 张)这种唯一能区分"只出零散对子"与"先凑最长拖拉机"的结构,随机抽根本抽不出来(少于 3 组时任取 3 对必含相邻对,测不出差别)。补上确定性的 `structHands()` 后才转红。同理,掺入的其他花色一度把这手撑到 15 张、超过枚举上限被**静默跳过**,表面照常全绿。**"跑了很多组且全绿"不等于"测到了",新增差分测试必须做变异检验。**
---
## 第六轮核对(多局大局 + 整局带牌型模糊)
> 前五轮的端到端**都只跑单局、且只出单张**。本轮补两个从未被驱动过的维度:① `design §12.1 步骤10 → §12.2` 的**多局大局**(跨局轮庄、累计分、大局结算、战绩);② **带牌型的整局链路**(对子/拖拉机/甩牌/甩错/毙牌/扣底与 `do_playcard`→`mod.chupai`→结算的集成)。**未发现任何不一致。**
| 维度 | 取证方式 | 结果 |
| --- | --- | --- |
| §12.1 步骤10 · §4.6 跨局轮庄 | 真实驱动 6 局(RPC 全链路,含每局 `zhunbei` 开新局) | 每局 `firstseat` 与「上局 banker + result」严格对应;另单独构造出 `result=0` 的**庄赢局**验证**连庄**(此前只有 `do_prepare` 的桩单测,端到端从未走过)✅ |
| §7 累计分 | 6 局 | 每局零和;`desk.seatlist[i][0]` == 逐局累加;`aset.seatlist[i].score` == 累计分 ✅ |
| §12.2 大局结算 | 6 局房与 12 局房各跑满 | `account` **只在末局**出现(6 局房第 6 局、12 局房第 12 局);打满后再 `zhunbei` 回 RULE 拒 ✅ |
| §10.1 扣房卡 | 6 局 | 全程只扣一次(第一局结算),`save_grade` 一次 ✅ |
| 战绩 | `save_grade` 载荷 | `gameinfo1.asetcount`/`players.score` 与累计分一致;`gameinfo2` 每局 `seatlist`/`banker`/`call`/`flower`/`result` 齐全 ✅ |
| §12.2 中途解散 | 打完 2 局后在第 3 局解散 | 解散局 `multiple/upgrade` 均 0、三家得分 `[0,0,0]`、`result=3`;`account` 累计分 == 解散前累计分("按当前累计分结算")✅ |
| 牌局隔离 | 每局开新 paiju | 新局重新发满 92 张;上一局的 `tmp_jiesuan_aset` 已清理 ✅ |
| §5/§6/§7/§8 整局带牌型 | 160 局 / 约 3800 墩(4 个种子 × 40 局),**每一墩**用独立参考实现重算胜者与服务端比对 | 多张首出、合法甩牌、甩错、毙牌墩、扣底均大量出现;牌张守恒 84/8/16、捡分守恒、结算零和、扣底触发条件、算奖分配公式**全部相符,0 异常** ✅ |
**本轮两次"疑似缺陷"经查都不是**(勿再重提):
- 某局结算 `[0,0,0]`:是 `jf=[4,-8,4]` 与 `aw=[-4,8,-4]` 恰好抵消,不是漏算。
- 首轮 40 次尝试跑不出庄赢局:是**用例设计反了**——叫 5 分是闲家最容易达标的档(`grade ≥ 5` 即赢),应叫 70 分才易守。改后立刻出现。
**固化产物**:新增 `test/test_fuzz.js`(整局模糊 + 每墩胜者差分,20 局约 480 墩,8 项聚合断言,0.3s)。多局大局部分为一次性探针未入库(跑满 12 局较慢,且其不变量已由 `test_desk`/`test_endgame`/本轮探针共同覆盖)。全套单测 **511 → 519 项**全绿。
---
## 第七轮核对(反方向:代码 → design)
> 前六轮都是「design → 代码」方向:拿规则去找代码有没有实现。本轮换**反方向**——从代码出发,把服务端每一个规则决策与常量拉出来,逐个追问「design 里有没有明文依据」。找的是两类此前查不到的东西:**代码做了 design 没授权的事**,以及 **design 有歧义时代码替规则做的选择**。
>
> **结论:没有发现与 design 冲突的行为。** 但列出 6 项「design 未明文规定、由代码自行决定」的项——它们不是缺陷,但也从未被规则设计者确认过,建议逐条拍板后写回 design。
| # | 代码的决定 | design 依据 | 性质 |
| --- | --- | --- | --- |
| R1 | 四个阶段的倒计时秒数 **15 / 20 / 25 / 30**(`class.desk.js`) | §11 只说「四个阶段都会给出一个倒计时秒数」,**未规定具体值** | 实现选择,需确认 |
| R2 | **§5.4.4 拖拉机分量的降级阶梯不要求「最长优先」**:甩牌里有 3 连对分量而闲家只有 2 连对时,代码允许出任意 3 对,不强制先凑出那个 2 连对 | §5.4.4 第 1 条明文写「没有同等连对数的拖拉机,退而求其次用**同等张数的主对子**顶替」——支持代码;但该节开头又说「沿用 §5.2 强制层级同一原则」,而 §5.2 层级 2 要求「能凑多长就必须先凑多长」。**两处措辞可作两种解读** | 歧义,代码按显式的第 1 条实现 |
| R3 | 服务端把提交的一组主牌按「**连续对子最大化合并成拖拉机**」分解(`decompose_trump`) | §5.4.3 只说甩牌可自由混搭,**未规定服务端如何分解**。该选择同时影响最大性判定、跟牌分量需求、扣底倍数(合并对甩牌方更有利:`AAKKQQ` 记 3 连对 ×6 而非 2 连对 ×4) | 实现选择,需确认 |
| R4 | 两个**同级**的副 7 对(♥7对 + ♣7对)**不构成连对** | §5.3 的链只列一次「副7」,同级即非相邻——代码按此。但 design 未直说 | 解读,合理 |
| R5 | `call` 用 `parseInt` 宽松解析:`"65"`(字符串)与 `65.5`(小数截断为 65)都会被接受;而 `cards` 是严格校验(字符串数字/小数一律拒) | design 不管入参类型;协议 §0.2 只对 `cards` 定了严格约束。**两者标准不一致**,但截断后语义正确、不改变规则结果 | 一致性瑕疵,非规则违反 |
| R6 | `get_chongguan` 对 `cards.length < 28` 静默返回 0 奖 | 隐式兜底(违反工程总则「显式失败优于隐式兜底」)。三个调用点的快照恒为 28 或 36,**当前不可触发** | 代码风格,非缺陷 |
### 本轮新增取证:下发面泄露审计
design §4(暗牌只有庄家可见 / 70 分亮 3 秒)、§9(查牌模式)、§11(结束亮底牌)与 server 红线「**发全 ≠ 发多,按可见性下发**」此前**没有任何测试**——各处门控是逐条手工核对的,从未系统验证过「有没有哪个包把不该看的牌送到了某个座位」。
本轮把它做成可执行审计(`test/test_leak.js`):跑完整局(可查牌 / 不查牌 × 叫 65 / 70 / 5 共 6 局),对每一个「服务器 → 某座位」的下发面(含 6 个 RPC 的逐座位包 + 各阶段重连快照 + 明牌应答)做深度扫描,取出其中出现的**所有牌 id**,逐个判定该座位此刻是否有权知道这张牌。
**结果:1814 个下发面 / 18706 次「座位×牌」可见性判定,0 泄露。**
变异检验(注入真实泄露确认审计有效):
| 注入的泄露 | 结果 |
| --- | --- |
| 非 70 分也把 8 张暗牌发给闲家(这正是第三轮修过的历史缺陷) | ✅ 抓住 |
| 重连时把庄家埋的底牌发给闲家 | ✅ 抓住 |
| 出牌包把出牌者剩余手牌发给所有人 | ✅ 抓住 |
---
## 第八轮核对(按 流程 / 牌型 / 玩法 / 算分 四个维度复核)
> 规则方复述了一条叫分规则:「开局庄家先叫分,不允许不叫分;第一局在没有庄家的情况下,默认座位号 0 玩家开始叫分」。据此按四个维度重新取证,**未发现不一致**。
**新增取证(此前从未做过):阶段机迁移矩阵。** 前几轮每个 handler 只测了 1~2 条代表性失败路径,从没系统验证过「某个请求在**别的**阶段会不会被误放行、会不会静默丢弃」。本轮把 8 个 RPC × 5 个阶段做成 **40 格矩阵**逐格驱动:
| | step1 叫分 | step2 选主/投降 | step3 埋牌 | step5 出牌 | step6 结算 |
| --- | --- | --- | --- | --- | --- |
| jiaofen | ✓ 受理 | × STEP | × STEP | × STEP | × STEP |
| xuanzhu | × STEP | ✓ 受理 | × STEP | × STEP | × STEP |
| touxiang | × STEP | ✓ 受理 | × STEP | × STEP | × STEP |
| maipai | × STEP | × STEP | ✓ 受理 | × STEP | × STEP |
| chupai | × STEP | × STEP | × STEP | ✓ 受理 | × STEP |
| mingpai | × STEP | × STEP | × STEP | ✓ 受理 | × STEP |
| tishi | × STEP | × STEP | × STEP | ✓ 受理 | × STEP |
| zhunbei | × STEP | × STEP | × STEP | × STEP | ✓ 受理 |
**40 格全部符合**,无误放行、无静默丢弃。(`mingpai` 在 step5 还需「已有人报无主」这一前置,首轮矩阵曾把它标红——是期望表漏了前置条件,不是缺陷。)
**叫分起始者规则逐条取证**(`design §4.2 + §4.6`,与规则方复述一致):
| 规则 | 取证 | 结果 |
| --- | --- | --- |
| 第一局默认座位 0 开始叫分 | `makewar → do_new_paiju(0)` → `firstseat=0`、待叫者=0 | ✅ |
| 之后每局由**暂定庄家**先叫 | 连打 6 局,每局 `firstseat` 与「上局 banker + result」逐局比对 | ✅ |
| 庄赢 → **连庄**(起始仍是该庄家) | 随机对局几乎打不出庄赢,用「叫 70 分 + 首家出最大牌」专门构造出 `result=0` 局 | ✅ |
| 庄输 / 投降 → 起始顺延到下家 | 6 局中逐局验证 | ✅ |
| **首家不允许"不叫"** | 第一局与其后每一局(含连庄局)的首家发 `call=0`,一律回 `RULE` 且叫分过程不变 | ✅ |
四个维度的现有取证汇总:
| 维度 | 取证手段 | 规模 |
| --- | --- | --- |
| **流程** | 阶段机 40 格矩阵、叫分序列 DFS 80 条终局、多局大局(6 局/12 局)、轮庄含连庄、中途解散、端到端整局 | 本轮新增矩阵 |
| **牌型** | §3 等级序列逐级、§5.3 相邻链(副牌同花色 / 主牌不限花色 / 同档位不相邻)、§5.2 跟牌穷举差分 11 万组 | `test_arith`/`test_followdiff` |
| **玩法** | §5.4 甩牌最大性 6000 例差分、甩错惩罚、§5.4.4 分量拆解、整局带牌型模糊每墩胜者差分、§9 查牌门控、下发面泄露审计 1.8 万次判定 | `test_fuzz`/`test_leak` |
| **算分** | §7 算子全量对拍 7308 格 + design 表 141 格、§6.3 扣底、§8.1 算奖、§8.4 分配公式、结算零和 | `test_calc`/`test_paiju` |
**固化产物**:新增 `test/test_flow.js`(7 项聚合断言,0.3s)。变异检验:去掉 `chupai`/`maipai` 的阶段校验、把 `jiaofen` 的阶段校验改成静默丢弃——三条全部转红。全套单测 **540 → 547 项**全绿。
---
## 结论摘要
> **注**:下表是**首轮核对**的初始发现快照(保留以记录起点)。其中 §4.4、§5.4、§6.3、§7、§8.1-8.4、§9、§10、§11 等标 🟥/🟧/🟨 的项**后续均已修复并验证**——当前逐节状态以本文档后面的分节详情及全套单测(270 项全绿)为准。
| 设计章节 | 主题 | 结论(首轮快照,非当前状态) |
| --- | --- | --- |
| §2 | 牌局构成(92 张、去 3/4) | ✅ 符合 |
| §3 | 主牌顺序(牌编码) | ✅ 符合(副 7/副 2 跨花色成对存疑,见下) |
| §4.1-3 | 发牌 / 叫分 / 埋牌流程 | 🟧 缺叫分上限、投降资格过宽 |
| §4.4 | 70 分投降 / 打牌分支 | 🟥 投降资格 65 分即可、暗牌亮 3 秒未实现、投降结算数值错 |
| §4.6 | 坐庄轮换 | ✅ 符合 |
| §5.1-5.3 | 跟牌 / 毙牌 / 垫牌 / 拖拉机 | ✅ 基本符合(1 处运行期崩溃缺陷) |
| §5.4 | 甩牌 | 🟥 合法性判定模型、副牌禁甩、强制跟牌拆解、甩错惩罚均不符 |
| §6.1-6.2 | 分牌 / 捡分 | ✅ 符合 |
| §6.3 | 扣底倍数 | 🟥 倍数表完全不同 |
| §7 | 算子(基础子数 / 大光小光过庄 / 升级 / 爬坡) | 🟥 系统性不符,且无爬坡开关 |
| §8.1 | 常规算奖(只庄家 + 连对链) | 🟥 三家都算、含额外作废规则、连对链因传参失效、≥10 老主误给奖、庄家快照含底/埋牌牌 |
| §8.2 | 亮牌 | 🟧 未实现 |
| §8.3 | 傍王 | 🟥 无开关(恒开)、含设计外作废、且未乘 X |
| §8.4 | 算奖并入结算 | 🟥 扁平奖数(每奖 1 子),未按 `X × N` |
| §9 | 查牌 / 不查牌模式 | 🟧 无模式开关、明牌功能无服务端支持 |
| §10.1 | 扣卡 / 局数 | 🟥 局数 2/4(应 6/12)、房主扣卡数不符 |
| §10.2-10.3 | 查牌 / 傍王 / 爬坡选项 | 🟥 均无开关 |
| §11 | 交互提示 | 🟨 亮底牌 ✅;甩牌见 §5.4;闲家 3 提示无服务端支持 |
---
## §2 牌局构成 — ✅ 符合
`class.paiju.js` `init_cards`:两副牌,点数 3、4 置 `dealowner=-1` 且不发(`if (k != 3 && k != 4) do_dealpai`);`tmpdeal` 共 `28×3 + 8 = 92` 个槽位。发出 92 张(11 种点数 ×4 ×2 + 4 王),去掉 16 张 3/4,与设计一致。分值 `5→5、10→10、K(13)→10` 亦一致(§6.1)。
---
## §3 主牌顺序 — ✅ 符合(1 处存疑)
`arith.id_to_code` 以 `flower*100+number` 为底,`A(+13)`、`2(+2000)`、`7(+7000)`、主花色 `(+1000)`、王固定 `9553/9554`,得到 code 降序即设计主牌顺序:
大王 9554 > 小王 9553 > 正 7(8xxx)> 副 7(7xxx)> 正 2(3xxx)> 副 2(2xxx)> 主花色普通牌 A…5(1xxx)> 副牌 A…5(<1000)。
`is_continuous` 正确实现全部相邻段(含 `小王↔正7`、`副2↔主A`、`8↔6`,以及「7 不与 6/8 连」——7 恒为主、不在普通链内)。
**存疑(待规则确认)**:不同花色的副 7(code 7107/7207/7307)、副 2(2107/2207/2307)编码互不相等。因此:
- 排序上三家副 7 被强行分出大小;比大小时 `can_followcard` 又把落在 7xxx/2xxx 段的牌**归一为 7000/2000**(视作同级),二者存在不对称,但因跟牌只能跟同花色、毙牌只能用更大主牌,实测不产生错误赢家。
- 成对判定 `get_pairlist` 要求 `code 相等`,故「副 7 对 / 副 2 对」只认**同花色两张**,跨花色的两张副 7 不算一对。设计 §3 把副 7 列为同一等级,但未明确「两张不同花色副 7 是否成对」——**需与规则设计者确认**。若规则要求跨花色副牌可成对,则此处需改。
---
## §4 开局与坐庄
### §4.1 发牌(每人 28 + 8 暗牌)— ✅ 符合
### §4.2 叫分 — 🟧 缺上限校验
- ✅ 步进 5:`mod.jiaofen` 校验 `call % 5 != 0` 拒绝。
- ✅ 暂定庄家必须叫:首家 `currcall==0 && call==0` 被拒。
- ✅ 后叫更低:`curr_call != 0 && call >= curr_call` 被拒。
- ✅ 叫 5 立即上庄、两家不叫则上庄:`paiju.do_callgrade` 实现。
- 🟧 **缺 `call <= 70` 上限**:`mod.jiaofen` 未限制上界,首家可叫 75、100 等(设计 §4 范围 5~70)。
### §4.3 埋牌 — ✅ 符合(选主/埋牌顺序已确认解决)
庄家得暗牌(`get_seat_cards` 对庄家含 `dealowner==0`),`mod.maipai` 校验埋 8 张且都在庄家手上,`do_burycard` 置 `playround=0` 后由庄家开首轮。
✅ **顺序问题已解决**:复查时曾发现 design §4 列举顺序是「埋牌→选主」、而代码是「选主(step2)→埋牌(step3)」,二者相反。经与规则设计者确认,正确顺序为**先选主、后埋牌**(庄家知道主副后才好决定埋哪 8 张)——即**代码本就正确**,是 design §4 的列举笔误。已修正 design §4 的步骤顺序,并新增 design §12「完整牌局游玩流程」明确整局先后。代码无需改动。
### §4.4 70 分投降 / 打牌 — 🟥 多处不符
- 🟥 **投降资格过宽**:设计仅 **70 分**可投降;代码 `mod.touxiang` 判 `o_paiju.call < 65` 才拒绝,即 **65 分也能投降**。应改为「仅 `call == 70` 允许」。同样地 `shangzhuang.touxiang`、`get_deskinfo` 各阶段的 `touxiang` 字段都用 `call < 65` 判定,需一并改。
- 🟥 **投降结算数值错**:设计投降为「基础子数 1、庄家每闲家输 1 子」。代码 `get_paiju_account(type=1)`:`upgrade=-99→-2`、`multiple=get_multiple_bycall(70)=1`,庄家 `grade_jf = -2 × 1 × 2 = -4`(每闲家 2 子)。金额翻倍且模型不对。
- 🟨 **暗牌亮 3 秒未实现**:设计要求 70 分坐庄把 8 张暗牌向两闲家亮 3 秒(投降 / 打牌都要),代码无此下发。
### §4.6 坐庄轮换 — ✅ 符合
`class.export.js` `makewar` → `do_new_paiju(0)`:首局暂定庄家为 0 号座位(设计 §4.6)。`class.desk.js` `do_prepare`:`result==1(闲赢)||result==2(投降)` 时下一局 `firstseat=(banker+1)%3`(下家),否则连庄。两点均与设计一致。
---
## §5 出牌规则
### §5.1 / §5.2 跟牌 / 毙牌 / 垫牌 — ✅ 基本符合(含 1 处崩溃缺陷)
- 每轮由上轮最大者先出、每局首轮庄家先出:`new_playround(maxseat)` / `do_burycard→new_playround(1, banker)`。✅
- 跟同花色、缺门可毙(主牌压过)或垫(任意副牌不争):`arith.get_followcard`/`can_followcard` 按「同花色是否够 / 有无对子 / 有无拖拉机」给必出牌与可出牌,毙牌能否赢由 `cardvalue`(副 7/副 2 归一、跨花色垫牌记 0、主牌对/拖拉机才 >0)裁定。✅ 与 §5.2「毙牌必须用对应牌型」一致。
- 🐞 **`can_followcard` 引用未定义变量 `tuolaji_list` 会抛异常**(`class.arith.js` 约 801 行,`get.cantype` 为拖拉机型 3xx 分支内 `if (tuolaji_list.length == 0)`,该变量在此作用域未赋值)。当跟牌方手中存在**多个**符合长度的候选拖拉机(`get_followcard` 返回 `cantype=startcardtype`)时命中此分支,`DoPack` 抛错、该次出牌链中断。需修复。
### §5.3 拖拉机定义 — ✅ 符合(见 §3 的 `is_continuous`)
### §5.4 甩牌 — 🟥 原合法性 / 禁令 / 跟牌 / 惩罚多处不符(🟩 第 1/2/4 点已整改,见「整改进度」)
> **整改状态**:下述第 1(副牌禁甩)、第 2(最大性按全手牌判定)、第 3(强制跟牌分量优先级,`flush_follow_ok`)、第 4(甩错惩罚)**均已实现并验证**。以下保留原始不一致描述留痕。
1. 🟥 **副牌禁甩未落实**:设计 §5.4.1「所有副牌完全禁止甩牌,无论外面是否剩余该花色副牌」。代码 `can_playcard` 对副牌甩牌只要求「其他两家没有主牌 **且** 没有该副花色(`other_noflower`)」即**放行**(约 415-419、427-438 行),等于「对手缺门时允许甩副牌」,与绝对禁令冲突。
2. 🟥 **甩牌合法性判定模型不符**:设计 §5.4.2 要求服务端**按全部手牌**判断「对手是否持有能压过甩牌任一分量的更大主牌 / 主对 / 更长主拖拉机」。代码改用出牌过程累积的 `seatlist[i][flower-1][0/1]`、`[4]` 等**报副标志**(`other_noflower`/`other_noflowerpair`),语义是「对手是否已被记录为该花色 / 主牌 / 对子出空」,既非「按全部手牌」也非「更大」而是「有没有」,与设计判定完全不同。
3. 🟥 **强制跟牌拆解未按分量**:设计 §5.4.4 要求闲家把甩牌拆成各分量(拖拉机优先跟拖拉机、对子优先跟对子…)逐个匹配。代码把甩牌压成单一 `cardtype`(如 `102 两张单张甩`、`203 三对甩`),`get_followcard` 对甩牌型只要求「同花色、张数相等」,不校验对子 / 拖拉机结构(如多对甩分支 `cantype=100+startcount` 只按单张计数)。
4. 🟥 **甩错惩罚未实现**:设计 §5.4.5 规定甩错要「收回甩牌、本轮只强制打出最小一张、失去甩牌资格」。代码把非法甩牌当作无效输入(`can_playcard` 返回 `result=false`→`mod.chupai` 直接 `return`),无任何惩罚流程。
5. ✅ **报无主**:`do_playcard` 出牌后自动检测主牌出空并置 `seatlist[..][4][0]=0`,属自动判定(设计允许由服务端精确计算)。
---
## §6 捡分与扣底
### §6.1 分牌 / §6.2 捡分 — ✅ 符合
`do_playcard` 仅在 `maxseat != banker` 时累计本轮分;`get_jian_grade` 汇总 `playowner != banker` 的分牌。庄家赢的轮分牌作废、不计入。与设计一致。
### §6.3 扣底 — 🟥 倍数表完全不同
- ✅ 触发条件「闲家用主牌赢下最后一轮」:`get_bottom_account` 仅在 `maxseat != banker` 时调 `get_bottom_multiple`,后者首末张非主(code<1000)即返回 0(不扣底),与「非主牌赢末轮不算扣底」一致。
- 🟥 **倍数不符**:
| 闲家扣底牌型 | 设计倍数 | 代码 `get_bottom_multiple` |
| --- | --- | --- |
| 单张主 | 1 | 2 |
| 主对子 | 2 | 4 |
| 两连对 | 4 | 8 |
| 三连对 | 6 | 16 |
| 四连对 | 8 | 32 |
| 五连对 | 10 | 64 |
| 六连对 | 12 | `undefined`(🐞 落空返回未定义) |
代码为近似 `2^k`、且单张即翻倍;设计为线性 `2N`(单张不翻倍)。
---
## §7 结算:子数与升级 — 🟥 系统性不符
设计的四层模型(§7.0):`基础子数(由叫分定) × 判定倍率(大光×3/小光×2/过庄×1/升N级×N) = X`(每「庄–闲」对子的基础金额)。常规算子分界 `Q` 固定 40、升级级距固定 40;勾选爬坡才改用 §7.3 的梯度子数与分段 `Q`。
代码实际实现(`get_paiju_account` + `get_multiple_bycall` + `get_upgrade`):
- `multiple = get_multiple_bycall(call)`:`call>60→1`、`>40→2`、`>0→4`、`≤0→0`。
- `upgrade = get_upgrade(call, grade)`:大光→4、小光→2、过庄→1、升级→`-(floor((grade-call)/halfcall)+1)`;分界与级距用 `halfcall = ceil(call/2)`。
- 每对子金额 = `multiple × upgrade`,庄家收两家(×2)、闲家各付(×-1)。
**问题**:
1. 🟥 **基础子数不符**:代码 `multiple∈{1,2,4}` 与设计常规算子 `{65:2, 60:3, 55:4, 50↓:6}` 数值、档位都不同。
2. 🟥 **大光倍率错**:代码大光 `=4`,设计大光 `×3`。(小光 ×2、过庄 ×1 与设计一致。)
3. 🟥 **小光 / 过庄分界与升级级距错**:代码用 `halfcall=ceil(call/2)`;设计常规算子固定 40。例如叫 65:代码分界 35(35~64 记过庄),设计分界 40(40~64 才过庄,35~39 仍是小光)。
4. 🟥 **无爬坡开关**:设计 §7.3 是「勾选爬坡才生效」的可选梯度;代码无任何房间选项分支,只有一套公式。巧合的是 `halfcall` 对低档(40/35→20、30/25→15、20/15→10、10/5→5)恰等于爬坡的 `Q`,但对高档(65→35≠40、60→30≠40、50→25≠40)不等,且基础子数 `{1,2,4}` 与常规 `{2,3,4,6}`、爬坡 `{2,3,4,6,7,8,…}` 都不符——即代码既不是常规算子、也不是爬坡,而是第三套数值。
**常规算子每对子最终子数对照(代码 vs 设计)**:
| 叫分 | 判定 | 设计 | 代码 |
| --- | --- | --- | --- |
| 65 | 大光 / 小光 / 过庄 / 升1 / 升2 | 6 / 4 / 2 / 2 / 4 | 4 / 2 / 1 / 1 / 2 |
| 60 | 同上 | 9 / 6 / 3 / 3 / 6 | 8 / 4 / 2 / 2 / 4 |
| 55 | 同上 | 12 / 8 / 4 / 4 / 8 | 8 / 4 / 2 / 2 / 4 |
| 50 | 同上 | 18 / 12 / 6 / 6 / 12 | 8 / 4 / 2 / 2 / 4 |
| 40 | 大光 / 小光 /(无过庄)/ 升1 / 升2 | 18 / 12 / — / 6 / 12 | 16 / 8 /(误记过庄 4)/ 4 / 8 |
(叫 40 一行还暴露分界问题:设计 `call≤40` 无过庄档,代码却在 grade 20~39 记出「过庄」。代码升级子数 = `multiple(=4) × 级数`,故升 1 级 4 子、升 2 级 8 子。)
> **70 分打牌**同样落在本套公式里且同样不符:代码 `multiple = get_multiple_bycall(70) = 1`(设计打牌基础子数应为 2),`halfcall = 35`(设计 `Q = 40`);大光 4×1=4、小光 2、过庄 1,而设计应为大光 6、小光 4、过庄 2。
---
## §8 算奖规则 — 🟥 归属、开关、数值均不符
### §8.1 常规算奖(只庄家) — 🟥
- ✅ 基础组合计数正确:`get_chongguan` 中 `3 王→1、4 王→3`、`6/7/8 个 7→1/2/3`、`6/7/8 个 2→1/2/3`。
- 🟥 **只应计庄家,代码却算三家**:`get_paiju_account` 对 0/1/2 号位都调 `get_chongguan` 并两两结算(`grade_cg = cg×2 − 另两家`)。设计 §8.1「常规算奖只认庄家身份,不看闲家手牌」。
- 🟥 **设计外的作废规则**:`do_obsolete_with_call` 引入「冲关必叫」「四王必踢(`call>60`)」,设计中不存在。
- 🟥 **连对链算奖失效(传参 bug)**:设计 §8.1「三 / 四王在有正 7 及以后连续对子时每对 +1 奖」。但 `get_chongguan` 在正常 / 投降结算被以 `mainflower = -1` 调用(`get_paiju_account` 中 `if (type != 2) _flower = -1;`)。此时 `id_to_code` 不加 `+1000`,正 7 与副 7 都落到 7xxx、主花色普通对子不被识别为主,`is_continuous(小王, 正7)` 因 `is_zheng7` 只认 8xxx 而返回假——连对链一对也加不上。即该奖项**实质未生效**。
- 🟥 **≥10 老主误给奖**:`if (_total >= 10) re.count += _total - 9`(「10 个老主判断」)。设计 §8.2 明示固定主牌 ≥10 只亮牌、**不算奖**。
- 🟥 **庄家算奖手牌快照错(含底牌与已埋牌)**:设计 §8「算奖依据静态初始手牌——庄家是**埋牌完成后的那 28 张**,闲家是发牌后的 28 张」。代码 `get_paiju_account` 用 `get_seat_cards_owner(seat)` 取牌喂给 `get_chongguan`,而该函数按 `dealowner ∈ {seat+1, 0}` 取牌、**不排除 `playround==0`(已埋)**:庄家因此拿到「28 张发牌 + 8 张底牌 = 36 张」,把**已埋进底牌的 8 张也计入算奖**,闲家则是正确的 28 张。庄家的常规算奖与傍王都因此可能虚高(例如把埋掉的王/7/2 仍算进去)。应改为庄家取「埋牌后保留的 28 张」(排除 `playround==0`)。
### §8.2 亮牌 — 🟧 未实现
设计 §8.2 的四档亮牌门槛(固定主 ≥10 / 王 ≥3 / 7 ≥6 / 2 ≥6,向闲家亮统计信息,受「不查牌」模式抑制)在服务端无对应计算与下发。
### §8.3 傍王(可选) — 🟥
- 🟥 **无开关(恒开)**:`grade_w`(傍王得分)在 `get_paiju_account` 中无条件计算。设计 §8.3 / §10.3 傍王需勾选才生效。
- 🟥 **含设计外作废**:`grade_w` 复用 `obsolete` 置 0 逻辑;设计傍王「每张王算一奖、庄闲都算」,无作废概念。
### §8.4 算奖并入结算 — 🟥 未乘 X
- 设计 §8.4:某玩家 `N` 奖,则另外两人各额外付 `X × N`(`X` 为 §7 第三层的每对子子数)。
- 代码:`grade_cg`、`grade_w` 是**扁平奖数两两差**(`cg×2 − 另两家`),每奖等价 1 子,**未乘 `X`**。举例设计(X=6、庄家 1 奖)应从每闲家收 6 子;代码只收 1 子/家。
- ✅ 结构上「所有两两之间(含闲–闲)都结算」这一点与设计一致,仅倍率错。
---
## §9 牌局查看模式 — 🟧
- 报无主后,服务端确有记录他家剩余主牌数 / 对子数(`seatlist[..][4]` 与花色标志),并通过 `chupai*`/`PushCards` 的 `info`、`seatlist`、`baozhu` 下发,供客户端展示——方向正确。
- 🟧 **无「可查牌 / 不查牌」模式开关**:`baozhu` 恒发、`seatlist` 恒带,未按房间设置抑制(设计 §9 / §10.2 的不查牌模式应屏蔽这些信息)。
- 🟧 **「明牌」(查看他家具体主牌)无服务端支持**:服务端不会向闲家下发他家的具体主牌牌面。
---
## §10 房间设置选项 — 🟥
### §10.1 扣卡方式 / 局数 — ✅ 已整改(下述为原始不一致留痕)
> **现状(已改)**:`get_asetcount` 按位串返回 6/12;`get_needroomcard` 房主 6局2张/12局4张、AA 6局1张/12局2张,均符合 §10.1。以下为原始(数组下标版)不一致记录。
- 🟥(原始)**局数不符**:旧 `export.get_asetcount` 返回 `roomtype[0]==1→2`、`==2→4`。设计 §10.1 为 **6 局 / 12 局**。
- 🟥(原始)**房主扣卡数不符**:旧 `get_needroomcard` 房主档返回 1/2;设计房主扣卡为 6 局 2 张 / 12 局 4 张。
### §10.2 查牌模式 — 🟥 无开关(见 §9)
### §10.3 附加规则(傍王 / 爬坡,可同时勾选)— 🟥 均无开关(见 §7.4、§8.3)
---
## §11 牌局交互提示 — 🟩
- ✅ 牌局结束亮底牌:`get_bottom_account` 结算恒带 `bottom.cards`。
- ✅ 选主对子数 / 70 分投降按钮门控:随 `shangzhuang`/`xuanzhu` 阶段包下发(见 §4)。
- ✅ 甩牌相关提示:见 §5.4(甩牌实现已核对一致)。
- ✅ **闲家 3 提示(踩 / 没分 / 有分)**:`mod.tishi`(design §11)——闲家在出牌阶段(step5)发 踩/没分/有分,服务端不校验真实性、只 `SendPack` 转发给对家(另一闲家 `3-banker-seat`);庄家不参与。协议见 packet_protocol §13.7/13.8,测试见 `test_rpc`。
---
## 附录:运行期代码缺陷汇总(与规则合规相对独立,但都需修)
| # | 位置 | 现象 | 影响 |
| --- | --- | --- | --- |
| D1 | `class.arith.js` `can_followcard`(约 801 行) | 引用未赋值的 `tuolaji_list.length` | 跟牌方有多个候选拖拉机时抛异常、出牌中断 🐞 |
| D2 | `class.arith.js` `get_bottom_multiple`(六连对分支) | 六连对时无返回值 | 返回 `undefined`,扣底倍数计算异常 🐞 |
| D3 | `class.arith.js` `can_playcard`(约 276 行) | `do_returnfalse;` 漏写 `()`,语句空转 | 「一次最多 14 张」上限失效,超量出牌被当作合法 🐞 |
| D4 | `class.paiju.js` `do_choiceflower`(约 417-421 行) | `for` 循环体为空、取出 `o_card` 后无操作 | 死代码(主牌重编码实际在 `id_to_code` 动态完成),无害但应清理 🟨 |
> D1~D3 均为真实缺陷;虽多数属「代码写错」而非「规则理解错」,但会导致对应规则(跟拖拉机校验、扣底、出牌张数上限)无法正确执行,需按缺陷修复处理。
---
## 总体判断
- **流程骨架**(发牌、叫分推进、选主埋牌、逐轮出牌收束、断线重连快照、战绩存储)实现完整且大体正确。
- **牌型与大小体系**(编码、排序、相邻、跟牌 / 毙牌 / 垫牌的必出可出计算)设计良好、基本符合规则,主要缺陷是 D1 崩溃与甩牌子系统。
- **不符合集中在「算分 / 算奖 / 房间选项」三块**:§6.3 扣底倍数、§7 算子全套、§8 算奖归属与并入方式、§10 局数 / 扣卡 / 各类开关——这几处需按 `design.md` 重做,是后续整改的重点。
- 结合数值特征(`multiple` 只有 1/2/4、局数 2/4、扣底 2^k、含「冲关必叫 / 四王必踢」等设计外机制),**服务端很可能是在早期或另一套规则版本上实现的,与当前 `design.md` 已系统性脱节**,而非个别笔误。
@@ -0,0 +1,236 @@
# 二七王服务端 · 合规测试计划
> 目标:用 Node 单测/集成测**系统覆盖 `design.md` 的全部服务端可验证规则**,做到"规则 → 用例"可追溯,作为"代码是否符合手册"的**自动化证据**(与 `01-design合规逐节核对.md` 的人工审计互补)。
>
> 运行:`node server/games/erqiwang/test/run.js`。测试原理与双运行时见 `test/README.md`。测试纪律(正/反/边界、不软化断言、失败先裁根因)见 `docs/server/development-guide/04` §10。
---
## 0. 分层与测试类型
| 层 | 含义 | 依赖 | 现状 |
| --- | --- | --- | --- |
| **L1 单元** | 纯函数直接调用(arith/config/paiju 静态计算) | 仅 `_shim` 的平台工具 | 已有 `test_arith`/`test_config`/`test_paiju`(部分) |
| **L2 局内集成** | `cls_youle_erqiwang_paiju.new(o_desk,firstseat)` 造牌局,驱动 `do_callgrade→do_choiceflower→do_burycard→do_playcard…→get_paiju_account` 验证状态/结算 | mock `o_desk`/`o_room` + shim;**不经 mod.js** | 仅 `get_paiju_account` 造了极简对象,**无完整一局驱动** |
| **L3 RPC/广播** | `mod.js` 收包入口 → 校验 → 广播 | mock `check_player`/`SendPack`/`sendpack_toother`,捕获下发包 | **完全未覆盖** |
**发牌随机性处理**:`do_dealpai` 用 `min_random`。L2 需要**可控发牌**——测试 `_shim` 提供可注入的 `min_random`(预置序列或指定发牌结果),使一局可复现。这是 L2 的前置基建。
---
## 1. 覆盖矩阵(按 design 章节)
状态:✅ 已覆盖 · 🟡 部分 · ❌ 待补。用例列标注 正/反/边界 三类(`docs/server/development-guide/04` §10 要求三类齐全)。
### §2 牌局构成
| 规则 | 被测 | 用例(正/反/边界) | 层 | 状态 |
| --- | --- | --- | --- | --- |
| 92 张、去两副 3/4 | `init_cards`/`do_dealpai` | 正:发完 3 家各 28 + 底 8 = 92;反:牌堆不含 number 3/4;边界:两副各花色计数 | L2 | ❌ |
| 分值 5→5、10→10、K→10、余 0 | `init_cards` | 正:各面值 score;边界:非分牌 score=0 | L1/L2 | 🟡(扣底/结算间接) |
### §3 主牌顺序 / 编码
| 规则 | 被测 | 用例 | 层 | 状态 |
| --- | --- | --- | --- | --- |
| 主牌 code 降序=设计顺序(大王>小王>正7>副7>正2>副2>主A…主5>副牌) | `id_to_code`/`order_cards` | 正:排序结果逐位;边界:正/副 7、正/副 2 的相对大小;主A vs 副2 | L1 | ❌(间接用到,无专测) |
| 相邻链 + 6/8 可连、7 不与 6/8 连 | `is_continuous` | 正:全相邻段逐对;反:7-6/8-7/9-7 不连;边界:8-6 连、大王-小王连 | L1 | 🟡(甩牌/算奖间接) |
| 副7/副2 跨花色比大小归一 | `trump_rank`/`can_followcard` | 正:副7 vs 副7 不可压;反:正7 压副7 | L1 | 🟡(甩牌间接) |
### §4 开局与坐庄
| 规则 | 被测 | 用例 | 层 | 状态 |
| --- | --- | --- | --- | --- |
| 叫分 5~70、步进5、暂定庄必叫、后叫更低/不叫 | `mod.jiaofen` 校验 + `do_callgrade` | 正:合法叫分推进;反:>70/非5倍/首家0/叫≥当前;边界:叫70、叫5 | L3(校验)+L1(do_callgrade) | ❌ |
| 叫5立即上庄、两家不叫上庄 | `do_callgrade` | 正:叫5→banker;正:一家叫另两家不叫→banker;边界:三家叫分序列 | L1 | ❌ |
| 坐庄轮换:庄赢连庄、庄输/投降下家、首局庄=0号 | `desk.do_prepare`/`makewar` | 正:result=0 连庄;正:result=1/2 顺延;边界:首局 firstseat=0 | L2/L3 | ❌ |
| 阶段机 1→2→3→5→6,无 step4 | `do_up_banker`/`do_choiceflower`/`do_burycard` | 正:各 handler 后 step 值;反:错误 step 调用被拒 | L2/L3 | ❌ |
| 投降仅70分/step2/庄家/不埋牌 | `mod.touxiang` | 正:70分 step2 投降→结算;反:≠70/step≠2/非庄;边界:投降后 step=6 | L3 | ❌ |
| 70分暗牌向所有玩家亮3秒(+ancard3s)、非70只发庄 | `mod.jiaofen`(shangzhuang) | 正:70→闲家有 bottomcards+ancard3s;反:非70 闲家无 bottomcards | L3 | ❌ |
| 选主后先选主后埋牌 | `do_choiceflower`/`do_burycard` | 正:flower 记录、step 推进;边界:埋牌须8张且在手 | L2 | 🟡(check_cards_inhand 未测) |
### §5 出牌规则
| 规则 | 被测 | 用例 | 层 | 状态 |
| --- | --- | --- | --- | --- |
| **§5.1/5.2 跟牌/毙牌/垫牌/混合出牌/拖拉机覆盖度** | `get_followcard`/`can_followcard` | 正:同花色跟、有对必出对、有拖必出拖、两对不连必出、尽量长拖优先;反:缺牌型被拒、放长拖只出散对被拒;毙牌(须完全缺门):副单→主单、副对→主对、副N连→主N连;垫牌/混合出牌:cardvalue=0 不争权 | L1 | ✅(`test_follow` 50 例,含 else 欠约束修复回归) |
| §5.3 拖拉机相邻 | `is_continuous`/`get_tuolaji_list` | 见 §3 | L1 | 🟡 |
| §5.4 甩牌:副禁甩/最大性/甩错 | `can_playcard`/`opp_can_beat_flush`/`decompose_trump` | 已覆盖(合法/甩错含smallest/副禁甩/单张/对子/拖拉机) | L1 | ✅ |
| §5.4.4 跟甩牌强制分量拆解 | `flush_follow_ok` | 已覆盖(有对必打对/有拖必打拖/退化/主牌最大化/无主全垫) | L1 | ✅ |
### §6 捡分与扣底
| 规则 | 被测 | 用例 | 层 | 状态 |
| --- | --- | --- | --- | --- |
| §6.1 分值 | `init_cards` | 见 §2 | L1 | 🟡 |
| §6.2 闲家赢归闲、庄赢作废、两闲谁赢都算闲 | `do_playcard`/`get_jian_grade` | 正:闲赢累计分;反:庄赢不计;边界:两闲各赢 | L2 | 🟡(1 集成例) |
| §6.3 扣底倍数 单1/对2/N连2N、全主牌守卫、取最高规格 | `get_bottom_multiple` | 已覆盖 | L1 | ✅ |
| §6.3 触发:闲家用主牌赢末轮才扣底 | `get_bottom_account` | 正:闲家主牌赢末轮翻倍;反:非主牌赢不扣、庄家赢不扣 | L2 | ❌ |
### §7 结算:子数与升级
| 规则 | 被测 | 用例 | 层 | 状态 |
| --- | --- | --- | --- | --- |
| 常规算子 基础子数 + 大光×3/小光×2/过庄×1/升N级、Q=40 | `get_base_bycall`/`get_qvalue`/`get_upgrade` | 已覆盖各档代表值 | L1 | ✅(可补每档过庄/小光分界与升3级) |
| 爬坡 基础子数梯度 + 分段Q | 同上(climb=true) | 已覆盖各档 | L1 | ✅ |
| X=基础×倍率、庄赢/闲赢符号、按房间爬坡切换 | `get_paiju_account` | 正:大光/过庄/升级/爬坡局 seatlist;边界:banker=-1 | L2 | 🟡(仅65小光+投降) |
### §8 算奖
| 规则 | 被测 | 用例 | 层 | 状态 |
| --- | --- | --- | --- | --- |
| §8.1 常规算奖只庄家、三/四王+连对链、6-8个7/2、无≥10老主/无作废 | `get_chongguan` | 已覆盖 | L1 | ✅ |
| §8.2 亮牌阈值/统计、只亮结构 | `get_liangpai` | 已覆盖 4 例 | L1 | 🟡(补 王≥3/7≥6 组合、边界9/10) |
| §8.3 傍王按位开关、庄闲每王1奖 | `get_paiju_account`(bangwang) | 正:傍王局 grade_aw 含王奖;反:未勾选不计 | L2 | ❌ |
| §8.4 算奖并入 X×(2Ni−Nj−Nk)、含闲-闲、投降X=1 | `get_paiju_account` | 正:庄1奖/闲傍王2奖的三家净额;边界:三家同时有奖 | L2 | ❌(公式单测有,集成未测) |
| §8 快照:庄埋后28/闲发后28;投降庄36无主 | `get_seat_cards_award` | 正:庄排除已埋;正:投降含底36;边界:无主花色无连对链 | L2 | 🟡(投降 count 间接) |
### §9 查牌 / 明牌
| 规则 | 被测 | 用例 | 层 | 状态 |
| --- | --- | --- | --- | --- |
| 报无主统计 info/baozhu 门控 | `mod.chupai` | 正:可查牌下发 info/baozhu;反:不查牌 info 缺失、baozhu=0 | L3 | ❌ |
| 亮牌/PushCards.seatlist 门控 | `mod.maipai`/`get_deskinfo` | 正:可查牌闲家有 liangpai;反:不查牌无 | L3 | ❌ |
| 明牌 mingpai:可查牌+已报无主+出牌阶段 | `mod.mingpai` | 正:条件满足返回他家主牌;反:不查牌/未报无主/非出牌阶段被拒 | L3 | ❌ |
### §10 房间设置
| 规则 | 被测 | 用例 | 层 | 状态 |
| --- | --- | --- | --- | --- |
| 位串解析、局数6/12、扣卡房主2·4/AA1·2 | `config.parse`/`export.get_asetcount`/`get_needroomcard` | 已覆盖(config 复刻映射) | L1 | ✅(可改为直接调 export 函数) |
| 傍王/爬坡/查牌三开关生效 | `get_paiju_account`/`mod.*` | 傍王/爬坡见 §7/§8;查牌见 §9 | L2/L3 | 🟡 |
### §12 完整流程
| 规则 | 被测 | 用例 | 层 | 状态 |
| --- | --- | --- | --- | --- |
| 一局端到端:发牌→叫分坐庄→选主→埋牌→逐轮出牌→末轮扣底→结算→轮庄 | 全链路 | 正:一局可控发牌打到结算,各家总分正确;含连庄/下家轮转 | L2 | ❌ |
> §11 牌局交互提示中,选主对子数/亮底牌等随对应阶段包下发;闲家三提示(踩/没分/有分)为服务端转发项(`mod.tishi`,见 P1)。
---
## 2. 待补用例清单(按优先级)
### P0 · 规则/金额关键 —— ✅ 已完成
1. ✅ **§5.1/5.2 正常跟牌/毙牌/垫牌/混合出牌/拖拉机覆盖度**(`can_followcard`,`test_follow.js` 50 例)——单张/对子/拖拉机的必出、毙牌数量对应(副单→主单/副对→主对/副N连→主N连、且毙牌须完全缺门)、垫牌与"混合出牌"(有该花色不够+补主牌→`cardvalue=0` 不争权)、缺门/同花色不足/对子不够各分支、多候选拖拉机(D1 崩溃回归)、**降级递归 else 欠约束回归**(三组等长 2 连对时出孤立对→拒),正反边界齐全。
2. ✅ **§7/§8 结算集成扩充**(`get_paiju_account`,`test_paiju.js`)——大光/过庄/升2级、爬坡局、傍王局(庄3王,N=4)、不傍王对照、投降,逐档验证 seatlist 各家 grade。
3. ✅ **§4.2 叫分坐庄**(`do_callgrade`,`test_callgrade.js` 6 例)——叫5立即上庄、两家不叫上庄、后叫更低→最低者上庄、不同首家。
4. ✅ **§6.3 扣底触发**(`get_bottom_account`,`test_paiju.js`)——闲家主对/两连对赢末轮才扣底(×2/×4);副牌赢、庄家赢均不扣。
> 注:§4.2 中 `mod.jiaofen` 的**入参校验**(>70/非5倍/首家必叫/后叫更低)属 L3,随投降/暗牌/查牌门控一起在 P1 补。
### P1 · 规则关键、需 RPC 脚手架 —— ✅ 已完成(`_rpc.js` + `test_rpc.js` / `test_desk.js`)
5. ✅ **§9 查牌门控 + mingpai**——chupai 的 info/baozhu 按 `nocheck` 门控(可查有/不查无);mingpai 合法下发他家主牌,不查牌/未报无主/非出牌阶段均拒。
6. ✅ **§4 投降 + 暗牌亮牌**——touxiang 仅 70分/step2/庄家;真实 jiaofen 驱动到 70分上庄,验证 shangzhuang 闲家有 bottomcards+ancard3s、叫5上庄闲家无。
7. ✅ **§4.2 `mod.jiaofen` 入参校验**——>70/非5倍/首家必叫/后叫更低 全部拒,合法接受。
8. ✅ **§4.6 坐庄轮换 `do_prepare`**——庄赢连庄、闲赢/投降下家。
### P2 · 补强既有 —— ✅ 已完成
9. ✅ §2 牌局构成专测(`test_deal`)、§3 编码/排序专测(`test_arith`:order_cards + is_continuous 链)。
10. ✅ §9 `mod.maipai` 亮牌门控 / `get_deskinfo` PushCards 门控(`test_rpc` 端到端驱动后验证)。
11. ✅ §8.2 亮牌更多阈值(6个7/固定主9-10边界)、§7 算子每档分界+升3级(`test_arith`)。
12. ✅ §12 端到端(`test_rpc` 真实驱动到出牌就绪)。
13. ✅ §10 直接调 `export.get_asetcount`/`get_needroomcard`/`get_needroomcard_joinroom`(`test_config`)。
14. ✅ **§11 闲家三提示 `mod.tishi`**——正:闲家 step5 发 踩/没分/有分,只转发给对家(`3-banker-seat`,验证 fromid/seat/tip);反:庄家发/非出牌阶段/非法 tip/缺 tip 均拒(`test_rpc` 11 例)。
---
## 3. 集成脚手架需求(实现 L2/L3 的前置)
- **L2 局内驱动器 `test/_harness.js`**:
- 可注入发牌的 `min_random`(或直接构造 `o_paiju.cards` 指定各家手牌),使牌局可复现。
- mock `o_desk`(seatlist 累积、`get_desk_account`、`o_room.roomtype/asetcount`)与 `o_room`。
- 提供"驱动一局到某阶段"的辅助:叫分序列 → 选主 → 埋牌 → 出牌序列。
- **L3 RPC 捕获器**:mock `youle_erqiwang.import.check_player`(返回构造的 `o_room`)、`o_room.method.sendpack_toother`/`youle_erqiwang.app.SendPack`(把下发包收集进数组),断言包字段(含 §9 门控、暗牌、投降、chupai1/2/3、jiesuan)。
---
## 4. 完成定义(DoD)—— ✅ 已达成
- ✅ 覆盖矩阵中所有 ❌/🟡 项补到 ✅(详见 §5 现状),每条规则至少含正/反/边界;
- `node test/run.js` 全绿、退出码 0;
- 新增用例遵守测试纪律:不改正式代码去迁就测试、不软化断言、失败先用证据裁根因;
- 本计划与 `01-design合规逐节核对.md` 的结论一致(自动化测试成为人工审计的可回归证据)。
---
## 5. 当前状态小结 —— ✅ DoD 达成,P0/P1/P2 全部完成
**共 511 项断言全绿**(`test_arith` 122 / `test_calc` 49 / `test_callgrade` 6 / `test_config` 19 / `test_deal` 12 / `test_desk` 6 / `test_endgame` 25 / `test_follow` 58 / `test_followdiff` 12 / `test_input` 34 / `test_paiju` 50 / `test_rpc` 89 / `test_success` 29)。其中 `test_calc`/`test_followdiff` 见 §7、G1~G9 见 §8。
design 全部服务端可验证章节均有正/反/边界单测:
- **§2 构成**:`test_deal` 92张/去3-4/三家28+底8/分值。
- **§3 编码/排序**:`test_arith` order_cards 降序=设计顺序、is_continuous 相邻链逐段+反例。
- **§4**:叫分坐庄 do_callgrade、`mod.jiaofen` 入参校验、投降 touxiang 条件、70分上庄暗牌亮牌下发、坐庄轮换 do_prepare。
- **§5**:**正常跟牌全面覆盖(§5.1/5.2)**——按跟牌方手牌各分支穷举:主牌首出与副牌首出、单张/对子/拖拉机各牌型、有同花必出对应牌型、同花不足则出仅有的+补、缺门毙牌(数量对应)与垫牌(不压)、拖拉机的对子不够/同花无对/**多候选拖拉机(D1崩溃回归)**/3连对/富余对子必出拖、手牌数==首出张数全部必出;**甩牌全面覆盖(§5.4)**——合法(纯单/纯对/单+拖/单+对+拖三混)、甩错(单张/主对/拖拉机分量各自被压、双对之一被压)、副牌禁甩(对手缺门与非缺门)、甩错惩罚**实际执行**(do_playcard 只打最小一张、其余留手);**跟甩牌全面覆盖(§5.4.4)**——对子必出、拖拉机必出/退化对子/退化到单张、多组拖拉机、混合demand逐分量对位、主牌最大化、无主全垫,以及"首出甩牌→跟牌违规→do_playcard 拒"的集成。
- **§6**:捡分(结算集成间接)、扣底倍数+触发。
- **§7**:算子常规+爬坡逐档、每档分界+升3级、结算集成(大光/过庄/升级/爬坡/傍王/投降)。
- **§8**:算奖 get_chongguan、亮牌 get_liangpai(含 6个7/固定主9-10边界)、算奖并入 X×N(傍王对照)。
- **§9**:查牌门控(chupai info/baozhu、maipai liangpai、deskinfo PushCards seatlist)、明牌 mingpai。
- **§10**:位串解析、局数/扣卡(直接调 export 接口)。
- **§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 |
| §9 出牌历史按查牌模式门控 | `get_deskinfo` | 反:不查牌房无 `pushlist`;正:可查牌房有 `pushlist`;边界:两种模式下当前轮桌面牌 `playproc.cards` 都必须恢复 | `test_rpc` | ✅ 4 |
| §3 相邻链花色 | `is_continuous` | 正:同花色 8-6 连;反:三组跨花色 8-6 不连;反:差 2 但非 8/6 不连 | `test_arith` | ✅ 5 |
**关于超时**:`countdown` 只作展示、服务端不做任何超时动作,这是 design §11 确认的规则(并非缺口),因此**没有也不需要「超时自动操作」的用例**;反过来,若将来有人给服务端加了超时代打,那才是违反 design §11——现已由 `test_rpc` 的**守卫用例**拦下(见 §8 G9)。
---
## 7. 第五轮:按 design 原文另写参考实现的差分测试
> 前六节的用例都是**手写的场景枚举**——覆盖的是"我想到的情况"。第五轮补两组**差分测试**:把 design 的规则整条转写成参考实现,与代码大面积对拍,覆盖手写用例想不到的组合。
| 文件 | 覆盖 | 规模 |
| --- | --- | --- |
| `test_calc.js` | §7 算子全量对拍:参考实现逐字转写自 §7.1/§7.2/§7.3 判定表,比 base / Q / 判定倍率 / 最终子数 | 2 模式 × 14 档 × grade 0~260 = **7308 格**,另按 design 的 17 张表逐格抽查 **141 个写死的最终子数** |
| `test_followdiff.js` | §5.2 跟牌强制层级穷举差分:参考裁定按原文写,对每手牌**枚举全部 C 张出牌组合**逐个对拍 | 约 **11 万组** |
**差分测试的强度取决于造牌器,不取决于对拍逻辑。** 第一版 `test_followdiff` 用纯随机抽牌跑 7.8 万组全绿,但把 `follow_tractor_cover_ok` 整个关掉**依然全绿**——因为唯一能区分"只出零散对子"与"先凑最长拖拉机"的形状是「同花色三组互不相邻的两连对」(12 张),随机抽根本抽不出来。故:
- 随机源必须是**固定种子 PRNG**(不用 `Math.random`),保证用例集可复现;
- 关键结构必须用**确定性手牌**钉死(`structHands()`),不能指望随机覆盖到;
- 注意别让掺入的其他花色把手牌撑过枚举上限而被**静默跳过**(踩过一次)。
---
## 8. 第五轮:逐规则的正/反/边界补全
> 对 design §1~§12 重新做了一次"每条规则是否三类用例齐全"的审计。覆盖面本来就广,但仍有 9 处 design 明文规则缺用例(多是**规则本身**而非入参校验,前几轮的矩阵按"被测函数"组织,容易漏掉这类"跨函数才体现"的规则)。
| # | design | 规则 | 补的用例(正/反/边界) | 文件 |
| --- | --- | --- | --- | --- |
| G1 | §4.2 | 暂定庄家必须叫、其后才可"更低或不叫" | 边界:下限 5 接受;正:非首家 `call=0` 接受;反:负数 / 缺 `call`(NaN) / 步进外小值 1 / 与当前叫分相同 均拒 | `test_rpc` |
| G2 | §4.2 | **"不叫"即退出本局叫分,轮次跳过他** | 正:首家叫 65 后轮到 seat1;正:seat1 不叫后跳到 seat2;反:seat1 反悔再叫 → SEAT 拒、`callproc` 不变、仍轮到 seat2、未产生庄家 | `test_rpc` |
| G3 | §4.5 | 埋牌仅庄家、仅埋牌阶段、恰 8 张 | 反:非庄家 → SEAT;反:step2/step5 埋 → STEP;边界:7 张 / 9 张 / 空数组 → PARAM | `test_rpc` |
| G4 | §5.1 | **每轮由上一轮牌面最大的一方先出** | 正:闲 1 赢 → 下一轮 `start/currseat` 都是 1;边界:闲 2 赢 → 改由闲 2 先出 | `test_paiju` |
| G5 | §6.2 | 闲家赢 → 台面**全部**分牌计入;庄家赢 → 分牌作废 | 反:庄赢 → 下发面无 `grade`、闲家捡分 0;正:闲 1 赢 → 15 分(**含庄家自己打出的 ♥5**);边界:闲 2 赢结果与闲 1 赢一致("两个闲家谁大不影响") | `test_paiju` |
| G6 | §8 | 算奖快照的时间点与静态性 | 正:庄家 = 埋牌后 28 张、已埋 8 张不在内;正:闲家 = 发牌后 28 张;边界:投降局庄家 = 发牌+暗牌 36 张;边界:之后打出 10 张,快照仍是同样的 28 张 | `test_paiju` |
| G7 | §3 | **对子只认同花色同点数的两副** | 正:同花色两副 ♥7/♥2/大王成对;反:♥7+♣7、♥2+♣2、正7+副7、正2+副2、大王+小王 均不成对;边界:四张跨花色 7/2 组不出拖拉机,而正7对+副7对可以 | `test_arith` |
| G8 | §6.3 | 赢末轮的牌不全是主牌就不扣底 | 反:副牌两连对赢 → 0;反:混合出牌(主K+副K)→ 0;边界:主副提交顺序颠倒仍判 0 | `test_arith` |
| G9 | §11 | **无超时托管**(守卫) | 守卫:叫分→选主→埋牌→出牌全程 `min_ontimeout` 调用次数为 0。将来若有人加"超时自动出牌/代打",必然要注册定时器,这条即转红 | `test_rpc` |
**每条都做了变异检验**(在副本上注入违反该规则的改动,确认对应断言转红):
| 注入的缺陷 | 转红的断言 |
| --- | --- |
| 去掉 `mod.jiaofen` 的"非当前叫分位"校验 | G2 三条 |
| 埋牌张数 `!= 8` 放宽为 `< 1` | G3 两条边界 |
| §6.2 改成庄家赢也累计闲家捡分 | G5 一条 |
| 下一轮改为固定庄家先出 | G4 两条 |
| 算奖快照不排除已埋的牌 | G6 两条 + 亮牌快照一条 |
| `get_pairlist` 放宽为"只看点数" | G7 五条 |
| 在 `mod.chupai` 里注册一个定时器 | G9 一条 |
> 过程中的一个教训:M2(埋牌张数)第一次跑出来是**整个文件崩溃**而不是干净的 FAIL——因为 `mkBury` 桩缺 `do_burycard`,校验一被改松就走到成功路径抛异常,后面的断言全不执行。**桩要完整到能走完成功路径**,否则"校验被改松"这类变异只会表现为崩溃,定位与信号都变差。已修。
> 原列在此处的两项「待外部输入」均已由规则设计者拍板:「不查牌模式是否屏蔽出牌历史」确认屏蔽,已改 design §9 + 代码 + 用例(见上表);「超时托管默认动作」确认维持现状、不做超时动作,已写入 design §11。至此测试计划无待外部输入项。
+788
View File
@@ -0,0 +1,788 @@
# 二七王 玩法设计文档
> 本文档是二七王玩法的唯一权威规则说明。内容随讨论持续与规则设计者逐项确认、修订;文中标注"待确认"的地方是当前仍未有定论的开放问题,其余内容均已确认生效。
## 目录
1. [术语约定](#1-术语约定)
2. [牌局构成](#2-牌局构成)
3. [主牌顺序](#3-主牌顺序)
4. [开局与坐庄流程](#4-开局与坐庄流程)
5. [出牌规则](#5-出牌规则)
- [5.1 跟牌基本原则](#51-跟牌基本原则)
- [5.2 按牌型跟牌](#52-按牌型跟牌)
- [5.3 拖拉机定义与相邻关系](#53-拖拉机定义与相邻关系)
- [5.4 甩牌详细规则](#54-甩牌详细规则)
6. [捡分与扣底](#6-捡分与扣底)
- [6.1 分牌](#61-分牌)
- [6.2 捡分规则](#62-捡分规则)
- [6.3 扣底](#63-扣底)
7. [结算:子数与升级](#7-结算子数与升级)
- [7.0 四层关系总览 / 查表索引](#70-四层关系总览)
- [7.1 常规算子:叫分 → 基础子数](#71-常规算子叫分-→-基础子数未勾选爬坡时的默认规则)
- [7.2 常规算子的判定表](#72-捡分-vs-叫分庄家的过庄-小光-大光闲家的升级常规算子)
- [7.3 爬坡](#73-爬坡叫分-→-基础子数与保小光分段可选规则开房时勾选爬坡生效)
8. [算奖规则](#8-算奖规则)
- [8.1 冲关](#81-冲关)
- [8.2 亮牌规则](#82-亮牌规则)
- [8.3 傍王(可选规则)](#83-傍王可选规则)
- [8.4 算奖如何影响最终结算](#84-算奖如何影响最终结算)
9. [牌局查看模式](#9-牌局查看模式)
10. [房间设置选项](#10-房间设置选项)
11. [牌局交互提示](#11-牌局交互提示)
12. [完整牌局游玩流程](#12-完整牌局游玩流程)
---
## 1. 术语约定
| 术语 | 含义 |
| --- | --- |
| 主牌 / 副牌 | 庄家选定的花色为"主牌",其余三个花色为"副牌";主牌大于副牌 |
| 固定主牌 | 不论选择哪个花色为主,大王、小王、以及所有花色的"2""7"恒为主牌 |
| 正2 / 正7 | 选定花色的"2""7",大于其余花色的"2""7"(口语里也叫"主2""主7",本文档统一用"正2""正7")|
| 副2 / 副7 | 非选定花色的"2""7" |
| 底牌 | 发牌时**没有发给玩家**、扣在桌面的 8 张牌;庄家确认后可摸起查看,摸起后就不再扣在桌面,而是并入庄家手牌 |
| 埋牌底牌 | 庄家"埋牌"时从手牌中选出重新扣下的 8 张牌;"底牌"与"埋牌底牌"是两批不同的牌,不通用、不互换 |
| 埋牌 | 庄家看到底牌、摸起查看后,从手牌中选出 8 张重新扣下,作为"埋牌底牌" |
| 捡分 | 出牌中赢下的分牌(5、10、K)计入闲家总得分的过程 |
| 扣底 | 出完所有手牌后的最后一轮,若被闲家用主牌牌型压过庄家,则埋下的 8 张埋牌底牌也计入闲家捡分,并按牌型翻倍 |
| 亮牌 | 庄家埋牌后、出牌前,若手中固定主牌达到门槛(总数 ≥10 / 王 ≥3 / 7 ≥6 / 2 ≥6 任一),向两个闲家**亮出自己全部固定主牌的具体牌面**(见第 8.2 节)。不限叫分,仅可查牌模式 |
| 余主公示 | 任一玩家主牌出空(报无主)后,为**全体三人**展示另外两家各自的**剩余主牌数与剩余主对数**(见第 9 节)。只给数量,不给具体牌面 |
| 明牌 | 可查牌模式下,查看**另外两家**手中全部未出主牌的具体牌面(见第 9 节)。与"亮牌"同样给牌面,区别在:亮牌是**庄家公开自己的**,明牌是**去看别人的** |
| 摸底 | 庄家坐定后把那 8 张**底牌**摸起查看并并入手牌(见第 4 节)。**非 70 分坐庄时只有庄家看得到这 8 张** |
| 开底 | **70 分坐庄**时,在庄家摸底**之前**,先把这 8 张底牌向**所有玩家**翻开 3 秒,之后才由庄家摸入手牌(见第 4 节)。给的是具体牌面 |
| 查底牌 | 对局中随时回看那 8 张底牌的功能(前端底栏按钮)。庄家全程可用;闲家仅在**开底**过的局(即 70 分坐庄)可用 |
| 拖拉机 | 同一花色(含主牌)里点数相邻的连续对子,如 K K Q Q 为"两托"(两连对) |
| 甩牌 | 首家出牌时,一次性打出多组主牌组合(单张 / 对子 / 拖拉机自由搭配);仅主牌可以甩牌,副牌禁止甩牌,只能分单张 / 对子 / 拖拉机依次打出(见第 5.4 节) |
| 毙牌 | 跟牌时手中没有本轮花色(缺门),改用主牌打出、且牌面大过本轮目前所有人的牌(见第 5.1 / 5.2 节) |
| 垫牌 | 跟牌时手中没有本轮花色(缺门)、且不用主牌毙牌,随意出一张其他副牌,不参与、不争夺本轮的出牌权(见第 5.1 节) |
| 混合出牌 | 首家出副牌,你手中有该花色副牌但张数不够凑齐首家总张数,又没有(或不用)别的副牌补差额,只能用主牌补足缺口——打出的是"该花色副牌 + 主牌"的混合牌。这既不是垫牌(不是缺门、不是纯副牌),也不是毙牌(不是纯主牌牌型、压不过首家),服务端判定其牌面为 0,**与垫牌等效:不参与、不争夺本轮出牌权**(见第 5.1 / 5.2 节) |
| 子 | 结算时用于计算输赢筹码的单位,与"倍数"是两个独立概念(见第 7 节) |
| 冲关 | 局末对固定组合(三王/四王、六 2/七 2/八 2、六 7/七 7/八 7、连续对子链等)的额外奖励结算,**只计庄家**(见第 8.1 节)。旧称"常规算奖",现统一叫**冲关**;服务端字段名 `chongguan` 与此同源 |
| 算奖 | **上位概念** = 冲关(8.1,只计庄家)+ 傍王(8.3,可选、庄闲都算)。两个分量求和后才是某玩家的总奖数 `N`,据此结算(见第 8.4 节)。**说"冲关"指的是庄家那一份,说"算奖"指的是合计** |
| 傍王 | 可选房间规则:勾选后每张王额外算一奖,庄家、闲家都要算;这是在 8.1 冲关之外**额外叠加**的奖励,与冲关互不冲突(见第 8.3 节) |
| 爬坡 | 可选房间规则:改变叫分对应的基础子数与保小光分数线,未勾选时按"常规算子"结算(见第 7 节) |
> **术语变更记录(2026-08-25)**:原「**暗牌**」(发牌留桌的 8 张)改称「**底牌**」;原「**底牌**」(庄家埋下的 8 张)改称「**埋牌底牌**」。
>
> 改名原因:前端界面上「底牌」按钮查看的正是发牌留桌那 8 张,旧命名与界面倒挂、极易写反。
>
> **本文已全文改用新术语。** 但 `docs/protocol/packet_protocol.md` 与服务端代码里的 `bottomcards` 字段名**尚未拆分**——两批牌目前仍共用这一个字段名,只靠「出现在哪个包」区分语义:`shangzhuang` / `ChooseMain` / `BuryCards` 里的是**底牌**,`maipai` / `PushCards` / 结算 `bottom` 分组里的是**埋牌底牌**。拆分计划见 `docs_dev/二七王-UI资源与精灵清单.md` §7.5 的 S-4。
> **易混术语对照(都带"亮/明"字,但方向与粒度各不相同,务必分清)**:
>
> | 术语 | 谁公开给谁 | 给的是什么 | 触发条件 | 协议字段 |
> | --- | --- | --- | --- | --- |
> | **亮牌** | 庄家 → 两个闲家 | **具体牌面**(庄家全部固定主牌) | 庄家埋牌后手牌达门槛(§8.2) | `liangpai` |
> | **余主公示** | 全体 → 全体 | **只有数量**(剩余主牌数、主对数) | 任一玩家报无主(§9) | `seatlist[seat][4]` + `baozhu` |
> | **明牌** | 他家 → 请求者 | **具体牌面**(他家全部未出主牌) | 可查牌 + 报无主,主动点击(§9) | `mingpai.others[].zhucards` |
> | **开底** | 桌面 → 所有玩家 | **具体牌面**(8 张底牌) | 70 分坐庄,**摸底之前** 3 秒(§4) | `ancard3s` + `bottomcards` |
>
> 一句话记:**只有「余主公示」是统计类(给数字),「亮牌」「明牌」「开底」都给具体牌面**——区别在给谁的牌:亮牌给庄家自己的、明牌给他家的、开底给桌上那 8 张。
> 另有一组「底」字族专管那 8 张底牌:**摸底**(庄家摸起看)、**开底**(70 分时全场看 3 秒)、**查底牌**(随时回看)、**扣底**(末轮翻开埋牌底牌计分)。
> 另注:**「亮主」不是本文档的术语**——它是前端选主界面的标题文案(庄家选主牌花色那一步),与上表四项无关。
---
## 2. 牌局构成
两副扑克牌共 108 张,去掉两副牌中的"3"和"4"(2 种点数 × 4 花色 × 2 副 = 16 张),剩余 **92 张**:
- 点数 5、6、7、8、9、10、J、Q、K、A、2:共 11 种点数,每种 8 张(2 副 × 4 花色)
- 大王、小王:各 2 张(每副各 1 对)
花色为黑桃 ♠、红桃 ♥、梅花 ♣、方块 ♦。
---
## 3. 主牌顺序
每局开局前庄家选择一个花色作为本局主牌,其余三个花色为副牌。
固定主牌(大王、小王、所有花色的 2 和 7)不论选择哪个花色为主,恒定属于主牌;选定花色里的其余普通牌也算主牌。
主牌从大到小排列:
| 顺序 | 牌 |
| --- | --- |
| 1 | 大王 |
| 2 | 小王 |
| 3 | 正 7(选定花色的 7) |
| 4 | 副 7(其余花色的 7) |
| 5 | 正 2(选定花色的 2) |
| 6 | 副 2(其余花色的 2) |
| 7 及以下 | 选定花色的其余普通牌:A、K、Q、J、10、9、8、6、5 |
副牌(非选定花色,且非 2、7、王)从大到小:A、K、Q、J、10、9、8、6、5。
> **关于"对子"的判定**:一副牌里同一张牌有两副(deck1 / deck2),"对子"指**同一张具体牌的两副**(如两张♦7、两张大王)。正 7、副 7、正 2、副 2 虽在上表中各占一个**大小等级**(用于排序与比大小时,同级的三张副 7 视为等大),但**成对只认同一花色同点数的两副**——即两张**不同花色**的副 7(如♠7 + ♥7)**不构成一对**,两张不同花色的副 2 同理。拖拉机的连对判定也以此为基础(每一节连对里的每个对子都须是同花色同点数两副)。
---
## 4. 开局与坐庄流程
1. 每人摸 28 张牌,剩余 8 张扣在桌面作为"底牌"。
2. 由暂定庄家开始,按逆时针方向依次叫分:
- 叫分范围 5 ~ 70 分,步进 5 分。
- 叫分数值代表"庄家承诺闲家捡分总数将严格低于该数值"(即闲家捡分只要达到或超过这个数值,庄家就算没达成承诺,见第 7 节);数值越低代表叫分者越有信心,因此后叫的人必须叫出比当前更低的分数才能压过前者,不能叫相同或更高的分数。
- **暂定庄家必须叫分,不能"不叫"**:叫分从暂定庄家开始,他必须报出一个 5 ~ 70 之间的具体分数,不存在"暂定庄家弃权、三家都不叫"这种情况;起始叫分确定之后,后面的玩家才可以选择"叫出更低的分数"或"不叫"。
- 若某玩家直接叫出 5 分,则无人能再压过,立即确定为庄家。
- 若一家叫分后,另外两家都选择"不叫",则该叫分玩家成为本局庄家。
3. **摸底**:庄家确定后把 8 张底牌摸入手牌(摸完后庄家共 36 张):
- **非 70 分坐庄**:桌面 8 张底牌**只有庄家可见**,庄家查看后摸入手牌。
- **70 分坐庄**(触发**开底**):在庄家**摸底**之前,先把这 8 张底牌**向所有玩家(含两个闲家)翻开 3 秒**,然后才由庄家把它们摸入手牌。
4. **选主 / 投降(同一决策点,二选一,互斥)**:
- **选主**:庄家在 4 个花色中选定一个作为本局主牌(见第 3 节)→ 进入下一步埋牌、随后出牌。
- **投降**:仅当**叫分为 70 分**时,此决策点才额外提供"投降"选项;选择投降表示放弃本局,**直接结束、不再选主、不埋牌、不出牌**,按 7.1 节"70 分坐庄 · 投降"的固定结果结算(基础子数 1 个,庄家直接输;算奖仍照常,见 8.4 节)。选主与投降互斥——**选了花色就等于放弃投降、正常打牌;选了投降就不再选主**。
- 叫分 < 70 分时没有投降选项,只能选主。
- (**开底**——70 分坐庄的 8 张底牌向所有玩家翻开 3 秒,发生在上一步"庄家摸底之前",见步骤 3。)
- **前端表现**:此阶段界面显示 4 个花色选主按钮,且**每个花色按钮上要显示该花色在庄家手中有多少对**(供庄家判断选哪门为主);70 分坐庄时并列再显示一个投降按钮。
5. **埋牌**(仅"选主/打牌"路径):庄家从 36 张里选出 8 张牌重新扣下("埋牌"),作为"埋牌底牌",供最后判断"扣底"使用;埋牌后庄家保留 28 张。(**本局先选主、后埋牌**——庄家先定主牌花色,知道主副后再决定埋哪 8 张。)
6. 坐庄轮换规则(决定下一局的"暂定庄家"):
- 第一局的暂定庄家为 **0 号座位**的玩家(此前没有打过任何牌局、还不存在"上一局的庄家",故取固定座位)。
- 若庄家本局获胜(庄赢),下一局暂定庄家仍为该玩家(连庄)。
- 若庄家本局失败(闲家捡分达标 / 庄家投降),下一局暂定庄家变为该玩家的下家(按逆时针顺延)。
---
## 5. 出牌规则
### 5.1 跟牌基本原则
- 每一轮由上一轮牌面最大的一方先出牌;**每一局(每一手牌)的第一轮固定由庄家先出**——此时还没有"上一轮"可以比较,只能由庄家开局。
- 首家出什么花色,其余两家必须跟出同花色的牌;若手中没有该花色的牌或数量不够(缺门),可以任选以下一种方式补齐:
- **毙牌**:改用主牌打出,且牌面要大过本轮目前所有人的牌,即可抢下本轮的出牌权;
- **垫牌**:随意出一张其他副牌,不参与、不争夺本轮出牌权。
- 首家若出主牌,其余两家必须跟出主牌,规则同上(此时缺门只能垫副牌,不存在再用主牌毙牌的情况)。
### 5.2 按牌型跟牌
跟牌的总原则:**在本轮首家花色内,尽量凑出与首家相同的牌型结构;能凑出的高规格部分必须优先打出,凑不齐的部分才用低规格牌补足。** 服务端按此层级精确计算每名跟牌者的"必出牌"与"可选牌",玩家无法跳过更高规格的强制部分。
| 首家牌型 | 跟牌要求 |
| --- | --- |
| 单张 | 跟同花色任意单张;同花色缺门则任意牌补齐 |
| 对子 | 有同花色对子必须打出对子;没有对子则打两张同花色单牌;同花色牌不足两张时,现有的同花色牌必须打出、不足部分任意补齐 |
| 拖拉机(N 连对) | 见下方「拖拉机跟牌的强制层级」 |
#### 拖拉机跟牌的强制层级(同花色牌足够时)
首家出 N 连对拖拉机,跟牌者手中该花色牌数量 ≥ 首家张数时,按以下优先级**从高到低**确定必出部分,高一级能凑出就必须先凑,不允许跳过:
1. **有同长(N 连对)的同花色拖拉机** → 必须打出该拖拉机;若手中有多组等长拖拉机,可任选其一打出。
2. **没有 N 连对,但持有更短的同花色拖拉机(仅 3 连对及以上首家才会走到这一步)** → 必须先拆出手中能凑到的**最长**拖拉机作为强制部分,再用次长拖拉机 / 对子 / 单张继续补足到相同张数——即"能凑多长的拖拉机,就必须先凑多长",不允许放着更长的拖拉机不出而只出零散对子。
3. **没有任何同花色拖拉机,但对子数量足够(对子数 × 2 ≥ 首家张数)** → 用现有对子凑够张数打出(多余的对子可自选保留)。
4. **对子数量不够(对子数 × 2 < 首家张数)** → 手中现有的同花色对子**全部**必须打出,剩余缺口用同花色单张补齐。
5. **完全没有同花色对子** → 用同花色单张随意补齐张数。
**同花色牌数量不足**首家张数时(手中还有该花色的牌,但张数不够——无论首家出的是单张、对子还是 N 连对拖拉机):手中该花色的牌**全部必须打出**(不论它们是否成对、是否成拖拉机),不足的缺口用**任意其他花色的牌**补齐——补齐的牌**不要求成对、不要求成拖拉机、也不要求是主牌**,随意垫即可。特别地,**即便首家出的是拖拉机,缺口部分也不强制补成对子或拖拉机**(服务端只校验总张数,不校验缺口部分的结构)。若缺口只能用主牌补(手中没有别的副牌可垫),打出的就是"该花色副牌 + 主牌"的**混合出牌**:它不是缺门、不是毙牌,牌面判为 0,**与垫牌等效,不争夺本轮出牌权**(见术语表「混合出牌」)。
若**完全缺门**(手中没有该花色的任何一张牌):整手都可任意出,可选择垫牌或用主牌毙牌(见下)。
毙牌的前提是**完全缺门**——手中**没有本轮首家花色的任何一张牌**。只要手里还剩该花色的牌(哪怕凑不成对子、凑不成拖拉机),都必须优先打出这些同花色牌(按上文 5.2 层级),**不允许留着该花色的牌改用主牌毙**。缺门后用主牌毙牌时,牌型与张数须与首家一一对应,才算"压过"、抢下出牌权:
- 若首家出副牌单张,其余玩家**完全缺门**该副牌花色,可以任意出牌(含主牌);但要压过首家,出的主牌也必须是单张(数量对应)。
- 若首家出副牌对子,其余玩家**完全缺门**该副牌花色,要压过首家,出的主牌必须也是对子(不能用任意两张主牌顶替)。
- 若首家出副牌拖拉机(N 连对),其余玩家**完全缺门**该副牌花色,要压过首家,出的主牌必须也是同样连对组数(N 连对)的主拖拉机,总张数完全对应;单张主牌或零散的主对子都不能顶替拖拉机。(注意:只要手里还有该副牌花色的牌,哪怕凑不成同长度拖拉机,也必须先出这些同花色牌,不能改用主牌毙。)
### 5.3 拖拉机定义与相邻关系
**拖拉机 = 在各自的大小序列里「位置相邻」的两个(或以上)对子**,如 K K Q Q 为两连对(两托),K K Q Q J J 为三连对(三托)。副牌与主牌各有自己的序列,相邻规则不同,分开说:
#### 副牌拖拉机(非主花色)
**必须同一花色**,点数在下面这条序列里相邻:
```
A - K - Q - J - 10 - 9 - 8 - 6 - 5
```
序列里没有 7 和 2(它们是固定主牌,见第 3 节),所以 **7 不与 6、8 相连,而 6 与 8 相连**(中间隔的 7 已被抽走);同理 4、3 不在牌堆里(见第 2 节),5 是副牌序列的最小一档。**跨花色不构成连对**——♥K 对 + ♣Q 对不是拖拉机。
#### 主牌拖拉机(主花色 + 固定主牌)
主牌只有**一条**完整的大小序列(即第 3 节的主牌顺序):
```
大王 - 小王 - 正7 - 副7 - 正2 - 副2 - 主A - 主K - 主Q - 主J - 主10 - 主9 - 主8 - 主6 - 主5
```
只要在这条序列里**位置相邻**的对子,就构成拖拉机,**不限花色**。因此「正7 对 + 副7 对」「副7 对 + 正2 对」「副2 对 + 主A 对」「大王对 + 小王对」都是合法的两连对,尽管两个对子的花色不同。序列末段(主A 到 主5)都是**选定花色**的普通牌,其中同样是 6 与 8 相连、7 不入此段。
> **同一档位的两个对子不相邻**:「副7」在序列里只占**一个**位置,所以 ♥7 对 + ♣7 对是**两个平级的对子、不是拖拉机**;副2 同理。这与第 3 节「成对只认同花色同点数的两副」是同一条原则的两面——先按同花色同点数成对,再看这些对子在序列里的位置是否相邻。
### 5.4 甩牌详细规则
#### 5.4.1 基础定义
**甩牌**:当你是本轮首家出牌人时,一次性打出多组主牌混合牌型(单张、对子、拖拉机自由组合),一轮打完多张主牌,无需分多轮依次打出。
两条硬性基础禁令:
1. **仅主牌允许甩牌,所有副牌完全禁止甩牌**:无论外面是否剩余该花色副牌,副牌只能分开单出、出对子、出拖拉机,不能一次性打包甩出。
2. **只有本轮先手才能执行甩牌**:若他人先出牌,你只能跟牌、垫牌、用主牌毙牌,不具备甩牌资格。
#### 5.4.2 生效条件(最大性原则)与报无主
想要成功甩牌,必须同时满足:**剩余两名对手手中,不存在任何一张、一对、一组拖拉机能压制你甩出的整套主牌组合**。简单说,全场所有更大的主牌、更大的主对子、更长的主拖拉机必须全部在你手中,两家闲家没有任何牌型能盖过你的甩牌组合。
**合法性由服务端在甩牌提交时自动判定**:服务端掌握全部玩家的手牌,甩牌一旦提交,立即检查另外两名玩家手中是否"持有"(不要求对方真正打出)能压过这套组合任意一部分的主牌;只要有人持有,就直接判定为甩错(见 5.4.5),不需要、也不会等到对方实际出牌反制才判定。
"报无主"是给玩家参考的辅助信息,**不是合法性判定的依据**(合法性判定始终由服务端按全部手牌精确计算):对局中非最后一轮,任意玩家手里没有主牌了,必须口头报无主,方便其他玩家在决定要不要甩牌前,大致判断风险:
- **另外两家都报了无主**:全场只剩你自己持有主牌,甩牌必然合法,无需记牌。
- **另外两家只有一家报了无主、还有一家没报**:那一家仍持有主牌,你若甩牌需要自行记牌估算风险;但即使判断失误,最终是否算甩错仍由服务端精确判定,不取决于你的记牌是否正确。
#### 5.4.3 甩牌可包含的牌型组合
甩牌仅限本局主牌(固定主牌 + 选定主花色的普通牌),内部可自由混搭任意数量、任意种类牌型:
- **纯单张**:大王、小王、正 7、副 7、正 2、副 2、主花色单牌混合;
- **纯对子**:大王对 / 小王对(各自成对,大王与小王点数不同,不能混成一对)、正七对、副七对、正二对、主花色数字对子;
- **主拖拉机**:大小王拖拉机、七七连对、二二连对、主花色数字连对;
- **混合搭配**:单张 + 对子 + 多连拖拉机一同甩出,无组合数量限制。
#### 5.4.4 其余两家应对甩牌的强制跟牌规则
甩牌一旦被判定合法(见 5.4.2:服务端已确认两名闲家都不持有能盖压这套组合任何一部分的主牌),闲家就不可能再用主牌把它压下去——"能不能盖压"在甩牌提交的那一刻就已经由服务端裁定完毕,不会走到"闲家出牌盖压"这一步。
甩牌本身可能是单张、对子、多个不同长度拖拉机自由混搭的复杂组合(见 5.4.3)。闲家跟牌沿用 5.2「拖拉机跟牌的强制层级」同一原则(高规格能凑就必须先凑:拖拉机 → 对子 → 单张),只是这里要把整套甩牌**拆解成一个个独立的分量,逐个分量分别匹配**,而不是把整套甩牌当成一个笼统的整体来对待:
1. **每一组拖拉机分量**:闲家手中若有同等连对数的主拖拉机,必须优先拆出来跟这一组;没有同等连对数的拖拉机,退而求其次用同等张数的主对子顶替;主对子也不够,才能拆主单张顶替。甩牌里若有多组不同长度的拖拉机,逐组分别按此规则处理。
2. **每一组单独的对子分量**:闲家手中若有主对子,必须优先拆出来跟这一组;没有主对子,才能拆主单张顶替。
3. **每一组单独的单张分量**:用主单张顶替即可。
4. **闲家手中的主牌拆解完仍不够覆盖甩牌剩余的分量**:不够的那部分只能随意垫副牌。
5. **闲家完全没有主牌**:只能随意垫任意副牌,本轮无法压制你的甩牌,本轮出牌权归你。
无论怎么拆解跟牌,都不影响这套甩牌本身的合法性——合法性在提交时已经由服务端按 5.4.2 判定完毕,闲家的跟牌动作只是"凑够同等张数、同等结构"的手牌垫上去,不可能真正反超压制。
#### 5.4.5 甩错判定与惩罚规则
**甩错判定**:甩牌提交时,服务端按全部玩家的真实手牌自动检查——只要两名对手任意一人**持有**(不要求已经打出)能盖压你甩牌内任意一组牌型的主牌,本次甩牌立即判定为甩错,对方是否愿意打出来压制不影响判定结果。
**统一惩罚标准**:
- 本次甩牌全部收回手牌;
- 本轮仅强制打出你甩出组合里最小的那一张牌;
- 本轮失去再次甩牌、一次性多出牌的资格,剩余手牌只能分轮正常打出;
- 线下娱乐、线上棋牌平台均统一执行该惩罚,不额外扣分。
甩错判罚后,本轮实际只算打出了那一张最小的单张主牌,其余两家按 5.2 节的"单张"跟牌规则正常应对即可,不再涉及甩牌相关规则。
---
## 6. 捡分与扣底
### 6.1 分牌
牌面 5 记 5 分,牌面 10 记 10 分,牌面 K 记 10 分,其余牌不计分。
### 6.2 捡分规则
每一轮出完牌后比较牌面大小:
- 若闲家最大,则本轮牌面上的分牌全部计入闲家捡分(两个闲家谁大不影响,都算闲家捡到)。
- 若庄家最大,则本轮牌面上的分牌作废——不计入闲家捡分总数,也不属于庄家的个人分数(庄家本身没有"捡分"这个概念,庄家的输赢只看闲家捡分总数是否达到叫分,见第 7 节)。
### 6.3 扣底
所有手牌出完后,若最后一轮闲家**用主牌**(不论单张、对子、拖拉机)**牌面压过庄家**、赢下最后一轮,才视为"扣底"(若闲家赢了最后一轮但用的不是主牌,则不算扣底)。触发后,庄家埋下的 8 张埋牌底牌翻开,若有分则计入闲家捡分。
埋牌底牌的分要不要翻倍、翻几倍,取决于闲家扣底所用的那手牌(赢下最后一轮的牌)本身是什么牌型:
| 闲家扣底所用牌型 | 倍数 |
| --- | --- |
| 单张主牌 | 不翻倍,按原始分值计入 |
| 主对子(一对主牌) | 2 倍 |
| 两连对拖拉机 | 4 倍 |
| 三连对拖拉机 | 6 倍 |
| N 连对拖拉机 | 2 × N 倍,以此类推递增 |
若最后一轮闲家是用第 5.4 节的**甩牌**(单张 + 对子 + 拖拉机混合的组合)扣底,取这套组合里**最高规格牌型**对应的倍数计算,其余较低规格的部分不额外叠加。例如甩出"单张 + 一对 + 两连对拖拉机"并以此扣底,最高规格是两连对拖拉机,整手按 4 倍计算。
> **重要区分**:扣底翻的是**闲家的捡分得分(grade)**本身,属于"算分"阶段;这与第 7 节里"大光 ×3""小光 ×2""升 N 级 ×N"这些子数倍率是完全不同的两件事——那些乘的是**子数**,属于"算子"阶段。两者的先后顺序是:扣底先把埋牌底牌的分(可能翻倍)计入闲家的捡分总数,这个最终捡分总数再拿去跟庄家叫分比较,判定过庄 / 小光 / 大光 / 闲家赢(升级),最后才由判定结果决定子数乘几倍。
---
## 7. 结算:子数与升级
叫分与结算涉及两套并存的数值:**倍数**(用于计算"捡分得分" grade)与**子数**(用于计算最终筹码输赢),两者是独立概念,不能混用。
> **重要区分**:本节里提到的倍率(大光 ×3、小光 ×2、过庄 ×1、升 N 级 ×N),乘的都是**子数**(最终结算给玩家的筹码单位),不是对局过程中闲家捡到的分数(捡分得分)本身。捡分得分只用来判定庄家的过庄 / 小光 / 大光,或闲家的升级级数,一旦等级判定完成,实际相乘的对象是这一局最终要结算的子数。
> **算子模式的选择**:7.1 + 7.2 描述的"常规算子"是默认结算方式;开房时勾选"爬坡"后,才改用 7.3 的子数梯度和保小光分段;未勾选"爬坡"时,一律按常规算子(7.1 + 7.2)结算,两套规则不会同时生效。
### 7.0 四层关系总览
结算分四层,前三层是"算子"(本节),第四层是"算奖"(第 8 节),一层的输出是下一层的输入:
1. **叫分 → 基础子数**:叫分档位本身决定这一局的基础子数(见 7.1 / 7.3 的对照表),叫分越低(庄家承诺越苛刻),基础子数越高。
2. **捡分 vs 叫分 → 判定结果**:局末闲家的捡分总数(含扣底,见 6.3 节)与庄家叫分作比较,只会落入互斥的两个分支之一:
- **闲家没达到叫分**(庄家赢)→ 结果记在**庄家**名下,分三级:**过庄 / 小光 / 大光**。
- **闲家达到或超过叫分**(闲家赢,庄家倒庄)→ 结果记在**闲家**名下,只有一个概念:**升级**(第几级)。
- **大光 / 小光和升级是两套完全独立的命名体系,分别只属于庄家一侧和闲家一侧,不能混用或类比**:庄家赢的时候没有"升级",闲家赢的时候也没有"大光 / 小光"。
3. **判定结果 → 子数倍率**:过庄 / 小光 / 大光 / 升级第几级,各自对应一个子数倍率,乘上第 1 层的基础子数,得到这一局的**子数结算结果 `X`**(前三层,即"算子",到此为止)。`X` 是庄家与**每一个**闲家之间"一对一"结算的基础金额:庄家赢(过庄 / 小光 / 大光)时,两个闲家各输给庄家 `X` 子;闲家赢(升级)时,庄家各输给两个闲家 `X` 子。
4. **算奖 → 结算倍率**:第 8 节算出的"奖数"(8.1 冲关 + 8.3 傍王,各玩家分别求和)**是"持有奖数的这个人"从其余两名玩家那里各自多收一笔钱的独立结算线,作用对象是这个人本身,不是整场结算的统一系数**——某玩家 P 这一局总共有 `N_P` 奖,则**另外两名玩家(不分庄闲)都要各自额外付给 P:`X × N_P` 子**。三名玩家可能同时都持有各自的奖数,会产生最多 3 条互相独立的额外支付线,与第 1~3 层的基础输赢结算加总,才是每个人这一局的最终盈亏。算奖的计算过程本身不受叫分、过庄 / 小光 / 大光、升级影响(只看手牌结构),详见 8.3 节的完整举例。
> **例外**:70 分坐庄选择投降时不走第 1~3 层模型——直接固定基础子数 1 个、庄家输掉,不比较捡分、不判定过庄 / 小光 / 大光,见 7.1 末尾说明;但第 4 层算奖仍然照常叠加(投降是选主阶段的选择、未选主也未埋牌,按庄家"发牌 + 底牌"共 36 张、无主牌花色计算,见 8.4 节)。
**查表索引**:本节按叫分档位给出了完整判定表,位置如下:
| 叫分 | 常规算子(未勾选"爬坡") | 爬坡(勾选"爬坡") |
| --- | --- | --- |
| 70 分 | 7.2.1 末尾"70 分坐庄" | 与常规算子一致,见 7.3.3 末尾说明 |
| 65 / 60 / 55 / 50 / 45 分 | 7.2.1 对应表格 | 与常规算子数值一致(45 分子数不同),见 7.3.3 末尾说明 |
| 40 / 35 / 30 / 25 / 20 / 15 / 10 分 | 7.2.1"40 分及以下坐庄"通用规则 | 7.3.3 对应表格(子数、`Q` 值均与常规算子不同) |
| 5 分 | 7.2.1"40 分及以下坐庄"内的"5 分坐庄没有小光"说明 | 7.3.3"5 分坐庄"(没有小光) |
### 7.1 常规算子:叫分 → 基础子数(未勾选"爬坡"时的默认规则)
叫分范围是 5 ~ 70 分(见第 4 节),常规算子下完整的"叫分 → 基础子数"对照:
| 叫分 | 65 分 | 60 分 | 55 分 | 50 分及以下(50 / 45 / 40 / 35 / 30 / 25 / 20 / 15 / 10 / 5) |
| --- | --- | --- | --- | --- |
| 基础子数 | 2 个子 | 3 个子 | 4 个子 | 统一 6 个子,不再随叫分继续增加 |
即叫分从 65 分往下每降 5 分子数依次是 2 → 3 → 4,但**到 50 分封顶**:50 分及以下所有档位(50、45、40、35、30、25、20、15、10、5)基础子数都固定是 6 个子,不会像爬坡那样继续往上加(爬坡的区别见 7.3)。
70 分叫庄是特例,基础子数不接着上面的阶梯往下走,而是在"投降"和"打牌"两条路径下分别取值:
- **选择投降**:基础子数按 **1 个**算,庄家直接输 1 个子(此时局面还没有真正出牌捡分,不经过 7.2 的大光 / 小光 / 升级判定,是一次性的固定结算)。
- **选择打牌**:基础子数按 **2 个**算(与 65 分相同),之后**照样按 7.2 节的方法,用捡分总数判定大光 / 小光 / 过庄 / 升级**,不是不细分——完整判定表见 7.2.1。
70 分叫庄时,发牌时桌面留下的那 8 张底牌(见第 4 节;不是埋牌时埋下的那 8 张「埋牌底牌」)需要**在庄家把它们摸入手牌之前、向所有玩家亮出 3 秒**,之后庄家才摸入手中;投降和打牌两种情况都适用(因为亮牌发生在选主 / 投降决策之前)。
### 7.2 捡分 vs 叫分:庄家的过庄 / 小光 / 大光,闲家的升级(常规算子)
#### 7.2.0 判定方法(对任意叫分档位通用)
**大光的判定条件是固定的、不随叫分或算子模式变化**:闲家捡分 `grade == 0`(一分未捡到)就是大光,常规算子和爬坡完全一样。会随叫分变化的,是**小光 / 过庄**之间的分界线——常规算子下这条分界线**固定为 40 分**,不随叫分变化(这是与爬坡最核心的区别,爬坡的分界线 `Q` 会随叫分变化,见 7.3.2)。设叫分为 `call`,闲家捡分为 `grade`,按以下顺序依次判断(**必须先判断第 1 步,即是否达标,再判断大光 / 小光 / 过庄**,顺序不能颠倒):
1. **先判断是否达标**:若 `grade ≥ call`,闲家赢,判为**升级**——先升 1 级,此后每再多捡 40 分再升 1 级;子数倍率就是当前的级数本身:升 1 级 ×1、升 2 级 ×2、升 3 级 ×3……逐级 **+1**,不是逐级翻倍。
2. **未达标(`grade < call`)时,再看捡分具体落在哪个区间**:
- `grade == 0`(一分未捡到)→ **庄家:大光**,子数倍率 ×3。
- `0 < grade < 40`(捡了一点,但没满 40 分)→ **庄家:小光**,子数倍率 ×2。
- `40 ≤ grade < call`(捡满 40 分,但仍没达标)→ **庄家:过庄**,子数倍率 ×1(即基础子数,不翻倍)。
之所以要先判断"是否达标":当叫分本身 ≤ 40 时(比如叫 35 分坐庄),闲家只要捡到 35 分就已经达标升级了,根本不可能出现"捡到 40 分却还没达标"这种情况——"过庄"这一档这时候是空的,直接从"小光"跳到"升级"(完整例子见 7.2.1"40 分及以下坐庄")。
**大光 / 小光只描述庄家(闲家没达标时,庄家赢得有多彻底);升级只描述闲家(闲家达标之后,赢得有多彻底),两套命名不通用、不能互换。**
#### 7.2.1 常规算子完整判定表
按上面的方法,叫分 > 40 分的档位(65 / 60 / 55 / 50 / 45)"过庄"区间是存在的;叫分 ≤ 40 分的档位(40 / 35 / 30 / 25 / 20 / 15 / 10 / 5)"过庄"区间不存在,闲家捡分只要达到叫分就直接升级。
**65 分坐庄(基础 2 个子)**
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
| --- | --- | --- | --- |
| 0 分 | 庄家:大光 | ×3 | 6 个子 |
| 1 ~ 39 分 | 庄家:小光 | ×2 | 4 个子 |
| 40 ~ 64 分 | 庄家:过庄 | ×1 | 2 个子 |
| 65 ~ 104 分 | 闲家:升 1 级 | ×1 | 2 个子 |
| 105 ~ 144 分 | 闲家:升 2 级 | ×2 | 4 个子 |
| 145 分起,每再多 40 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +2 个子 |
**60 分坐庄(基础 3 个子)**
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
| --- | --- | --- | --- |
| 0 分 | 庄家:大光 | ×3 | 9 个子 |
| 1 ~ 39 分 | 庄家:小光 | ×2 | 6 个子 |
| 40 ~ 59 分 | 庄家:过庄 | ×1 | 3 个子 |
| 60 ~ 99 分 | 闲家:升 1 级 | ×1 | 3 个子 |
| 100 ~ 139 分 | 闲家:升 2 级 | ×2 | 6 个子 |
| 140 分起,每再多 40 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +3 个子 |
**55 分坐庄(基础 4 个子)**
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
| --- | --- | --- | --- |
| 0 分 | 庄家:大光 | ×3 | 12 个子 |
| 1 ~ 39 分 | 庄家:小光 | ×2 | 8 个子 |
| 40 ~ 54 分 | 庄家:过庄 | ×1 | 4 个子 |
| 55 ~ 94 分 | 闲家:升 1 级 | ×1 | 4 个子 |
| 95 ~ 134 分 | 闲家:升 2 级 | ×2 | 8 个子 |
| 135 分起,每再多 40 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +4 个子 |
**50 分坐庄(基础 6 个子)**
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
| --- | --- | --- | --- |
| 0 分 | 庄家:大光 | ×3 | 18 个子 |
| 1 ~ 39 分 | 庄家:小光 | ×2 | 12 个子 |
| 40 ~ 49 分 | 庄家:过庄 | ×1 | 6 个子 |
| 50 ~ 89 分 | 闲家:升 1 级 | ×1 | 6 个子 |
| 90 ~ 129 分 | 闲家:升 2 级 | ×2 | 12 个子 |
| 130 分起,每再多 40 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +6 个子 |
**45 分坐庄(基础 6 个子——从这一档开始进入"50 分及以下统一 6 个子"的范围)**
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
| --- | --- | --- | --- |
| 0 分 | 庄家:大光 | ×3 | 18 个子 |
| 1 ~ 39 分 | 庄家:小光 | ×2 | 12 个子 |
| 40 ~ 44 分 | 庄家:过庄 | ×1 | 6 个子 |
| 45 ~ 84 分 | 闲家:升 1 级 | ×1 | 6 个子 |
| 85 ~ 124 分 | 闲家:升 2 级 | ×2 | 12 个子 |
| 125 分起,每再多 40 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +6 个子 |
**40 分及以下坐庄(40 / 35 / 30 / 25 / 20 / 15 / 10 / 5,基础都是 6 个子;此时 `call ≤ 40`,没有"过庄"这一档)**
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
| --- | --- | --- | --- |
| 0 分 | 庄家:大光 | ×3 | 18 个子 |
| 1 分 ~(叫分 − 1)分 | 庄家:小光 | ×2 | 12 个子 |
| 达到叫分 | 闲家:升 1 级 | ×1 | 6 个子 |
| 达到叫分 + 40 分 | 闲家:升 2 级 | ×2 | 12 个子 |
| 此后每再多 40 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +6 个子 |
例如叫 10 分坐庄(基础 6 个子):0 分为大光(18 子);1 ~ 9 分为小光(12 子);捡到 10 分起升 1 级(6 子);捡到 50 分起升 2 级(12 子);之后每再多 40 分再升 1 级(每级再 +6 子)。
> **5 分坐庄没有"小光"**:分牌只有 5 分、10 分两种面值(见 6.1 节),闲家捡分永远是 5 的倍数。上面"1 分 ~(叫分 − 1)分为小光"这一行,在叫分为 5 分时对应区间是 `1 ~ 4 分`,里面不存在任何 5 的倍数,是空区间。所以叫 5 分坐庄时,闲家捡分只有 0(大光)和达到 5 分及以上(直接升 1 级,之后每再多 40 分再升 1 级)两种结果,没有小光这一档;这是"叫分 ≤ 40 分"这组档位里唯一的特例,其余档位(40/35/30/25/20/15/10)小光区间都至少包含一个 5 的倍数,正常存在。
**70 分坐庄**
- **投降**:基础子数按 1 个算,庄家直接输 1 个子,不经过大光 / 小光 / 过庄 / 升级判定(还没出牌捡分,直接放弃)。
- **打牌**:基础子数按 2 个算,之后按 7.2.0 的方法正常判定,`Q` 同样固定为 40 分——判定表和 65 分坐庄完全一致,只是把"基础 2 个子"原样代入(数值恰好和 65 分相同):
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
| --- | --- | --- | --- |
| 0 分 | 庄家:大光 | ×3 | 6 个子 |
| 1 ~ 39 分 | 庄家:小光 | ×2 | 4 个子 |
| 40 ~ 69 分 | 庄家:过庄 | ×1 | 2 个子 |
| 70 ~ 109 分 | 闲家:升 1 级 | ×1 | 2 个子 |
| 110 ~ 149 分 | 闲家:升 2 级 | ×2 | 4 个子 |
| 150 分起,每再多 40 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +2 个子 |
### 7.3 爬坡:叫分 → 基础子数与保小光分段(可选规则,开房时勾选"爬坡"生效)
#### 7.3.1 基础子数:叫分 → 子数的完整对照
叫分从 65 分(2 个子)开始,每叫低 5 分加 1 个子,直到 50 分(6 个子)——这一段与常规算子的四档数值完全相同;50 分以下,继续按每叫低 5 分加 1 个子延伸,一直到叫分范围的下限 5 分:
| 叫分 | 65 | 60 | 55 | 50 | 45 | 40 | 35 | 30 | 25 | 20 | 15 | 10 | 5 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 基础子数 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
#### 7.3.2 保小光分段:叫分 → 小光 / 过庄分界线(Q 值)
判定方法与 7.2.0 完全相同,只是把固定的 `Q = 40` 换成按叫分区间取值的 `Q`;**大光的判定条件不受 `Q` 影响,永远是 `grade == 0`**,常规算子和爬坡完全一样——`Q` 只决定"小光"和"过庄"之间的分界线:
| 叫分区间 | 小光 / 过庄的分界线(Q) |
| --- | --- |
| ≥ 45 分(含 65 / 60 / 55 / 50 / 45) | 40 分 |
| 40 / 35 分 | 20 分 |
| 30 / 25 分 | 15 分 |
| 20 / 15 分 | 10 分 |
| 10 分 | 5 分 |
| 5 分 | 5 分(升级级距仍是 5,只是"小光"这一档因为区间为空而不存在,见下方说明) |
`≥ 45 分`这一档的 `Q = 40`,与 7.2 节常规算子沿用的分界线数值相同,只是子数改按 7.3.1 的公式取值(65/60/55/50 与常规算子数值一致,45 分是爬坡独有的新增档位)。
**5 分档没有"小光"**:分牌只有牌面 5(5 分)和牌面 10 / K(10 分)两种面值(见 6.1 节),闲家的捡分总数(`grade`)因此永远是 5 的倍数(0、5、10、15…),不可能出现 1 ~ 4 这样的中间值。当叫分是 5 分时,"小光"原本对应的区间是 `0 < grade < 5`,这个开区间里不存在任何 5 的倍数,也就是说这个区间**必然是空的**——闲家捡分要么是 0(大光),要么一旦捡到任何分牌(最少 5 分)就已经达到叫分、直接升级,中间不存在"捡到了一点、但没达标"的"小光"状态。所以 5 分档只有**大光**和**升级**两种结果,没有"小光"这一档;但 `Q` 值本身仍然是 5(与 10 分档一样),只是在"小光 / 过庄分界线"这个用途上区间恰好收窄为空,`Q = 5` 仍然继续作为"升级级距"生效(见 7.3.3 的 5 分坐庄判定表)。
**升级级距的通用规则**:不管哪个叫分档位,闲家赢了之后,"超过叫分多少分算升一级",用的分数差**就是该档位的 `Q` 值**——即上表里每一档的 `Q` 值,既是"小光 / 过庄"的分界线,也是"升级"的级距单位,两者共用同一个数字。例如叫分 40 分时 `Q = 20`:闲家捡分达到 40 分先升 1 级,之后每再多捡 20 分(60、80、100…)就再升一级。
#### 7.3.3 各分段的完整判定表(按 7.2.0 的方法代入对应 `call`、`Q`、基础子数)
**40 分坐庄(基础 8 个子,Q = 20)**
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
| --- | --- | --- | --- |
| 0 分 | 庄家:大光 | ×3 | 24 个子 |
| 1 ~ 19 分 | 庄家:小光 | ×2 | 16 个子 |
| 20 ~ 39 分 | 庄家:过庄 | ×1 | 8 个子 |
| 40 ~ 59 分 | 闲家:升 1 级 | ×1 | 8 个子 |
| 60 ~ 79 分 | 闲家:升 2 级 | ×2 | 16 个子 |
| 80 分起,每再多 20 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +8 个子 |
**35 分坐庄(基础 9 个子,Q = 20)**
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
| --- | --- | --- | --- |
| 0 分 | 庄家:大光 | ×3 | 27 个子 |
| 1 ~ 19 分 | 庄家:小光 | ×2 | 18 个子 |
| 20 ~ 34 分 | 庄家:过庄 | ×1 | 9 个子 |
| 35 ~ 54 分 | 闲家:升 1 级 | ×1 | 9 个子 |
| 55 ~ 74 分 | 闲家:升 2 级 | ×2 | 18 个子 |
| 75 分起,每再多 20 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +9 个子 |
**30 分坐庄(基础 10 个子,Q = 15)**
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
| --- | --- | --- | --- |
| 0 分 | 庄家:大光 | ×3 | 30 个子 |
| 1 ~ 14 分 | 庄家:小光 | ×2 | 20 个子 |
| 15 ~ 29 分 | 庄家:过庄 | ×1 | 10 个子 |
| 30 ~ 44 分 | 闲家:升 1 级 | ×1 | 10 个子 |
| 45 ~ 59 分 | 闲家:升 2 级 | ×2 | 20 个子 |
| 60 分起,每再多 15 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +10 个子 |
**25 分坐庄(基础 11 个子,Q = 15)**
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
| --- | --- | --- | --- |
| 0 分 | 庄家:大光 | ×3 | 33 个子 |
| 1 ~ 14 分 | 庄家:小光 | ×2 | 22 个子 |
| 15 ~ 24 分 | 庄家:过庄 | ×1 | 11 个子 |
| 25 ~ 39 分 | 闲家:升 1 级 | ×1 | 11 个子 |
| 40 ~ 54 分 | 闲家:升 2 级 | ×2 | 22 个子 |
| 55 分起,每再多 15 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +11 个子 |
**20 分坐庄(基础 12 个子,Q = 10)**
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
| --- | --- | --- | --- |
| 0 分 | 庄家:大光 | ×3 | 36 个子 |
| 1 ~ 9 分 | 庄家:小光 | ×2 | 24 个子 |
| 10 ~ 19 分 | 庄家:过庄 | ×1 | 12 个子 |
| 20 ~ 29 分 | 闲家:升 1 级 | ×1 | 12 个子 |
| 30 ~ 39 分 | 闲家:升 2 级 | ×2 | 24 个子 |
| 40 分起,每再多 10 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +12 个子 |
**15 分坐庄(基础 13 个子,Q = 10)**
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
| --- | --- | --- | --- |
| 0 分 | 庄家:大光 | ×3 | 39 个子 |
| 1 ~ 9 分 | 庄家:小光 | ×2 | 26 个子 |
| 10 ~ 14 分 | 庄家:过庄 | ×1 | 13 个子 |
| 15 ~ 24 分 | 闲家:升 1 级 | ×1 | 13 个子 |
| 25 ~ 34 分 | 闲家:升 2 级 | ×2 | 26 个子 |
| 35 分起,每再多 10 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +13 个子 |
**10 分坐庄(基础 14 个子,Q = 5——这一档同样没有"小光")**
分牌只有 5 分、10 分两种面值,捡分永远是 5 的倍数。`Q = 5` 时,"小光"对应的区间 `0 < grade < 5` 里不存在任何 5 的倍数,同样是空区间:闲家捡分不可能停在 1 ~ 4 分,只要捡到分就已经是 5 分(直接落入"过庄"区间)。
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
| --- | --- | --- | --- |
| 0 分 | 庄家:大光 | ×3 | 42 个子 |
| 5 ~ 9 分 | 庄家:过庄(没有小光这一档) | ×1 | 14 个子 |
| 10 ~ 14 分 | 闲家:升 1 级 | ×1 | 14 个子 |
| 15 ~ 19 分 | 闲家:升 2 级 | ×2 | 28 个子 |
| 20 分起,每再多 5 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +14 个子 |
**5 分坐庄(基础 15 个子,没有"小光",见 7.3.2 的说明)**
5 分是叫分范围的下限,捡分只能是 5 的倍数:一旦捡到任何分牌就已经是 5 分,等于直接达标(`grade ≥ call = 5`)。所以"小光"和"过庄"这两个区间都不存在,闲家捡分只有 0(大光)和达到 5 分及以上(直接升 1 级)两种结果。
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
| --- | --- | --- | --- |
| 0 分 | 庄家:大光 | ×3 | 45 个子 |
| 5 ~ 9 分 | 闲家:升 1 级(没有小光、过庄这两档) | ×1 | 15 个子 |
| 10 ~ 14 分 | 闲家:升 2 级 | ×2 | 30 个子 |
| 15 分起,每再多 5 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +15 个子 |
`≥ 45` 分档(65 / 60 / 55 / 50 / 45)判定逻辑与 7.2.1 完全相同(Q = 40),只是 45 分档基础子数按 7.3.1 取 7 个子,其余数值套用 7.2.1 对应叫分的表即可,此处不重复列出。
70 分叫庄时与常规算子规则一致(见 7.2.1 末尾"70 分坐庄":投降固定基础子数 1 个,打牌基础子数 2 个并正常套用大光 / 小光 / 过庄 / 升级判定),不适用本节的保小光分段。
---
## 8. 算奖规则
算奖在**每个小局结算时**计算(与第 7 节的子数结算属于同一次小局结算,不是等大局所有局数打完才统一算,也不是在出牌过程中途算)。8.1 冲关只计入庄家;8.3 傍王是额外叠加的独立奖励,庄家、闲家都要算——两者的计入范围不同,注意区分。
**8.1 冲关只认庄家的身份,不看闲家的手牌是否达标**:即使某位闲家的手牌结构凑巧也满足 8.1 表里的条件(比如手里也有 3 张王),这份冲关也**不会**计入这位闲家——冲关从规则上就只针对庄家一人。闲家想要获得算奖,只能靠 8.3 傍王(且必须房间勾选了这条可选规则)。
**算奖依据的手牌快照时间点**:不管是 8.1(只看庄家)还是 8.3 傍王(庄闲都算),都按**静态初始手牌**计算——庄家是埋牌完成后的那一刻手牌(28 张),闲家是发牌完成后的那 28 张手牌;这个快照全程固定,不随之后的出牌、被吃、被打出而改变。**例外**:70 分投降局庄家未选主也未埋牌,其快照为"发牌 + 底牌"共 36 张(无主牌花色),见 8.4 节。
**算奖(8.1 / 8.3)与亮牌(8.2)是两个互不干涉的独立概念**:算奖是结算阶段"该给多少额外结算分"的规则;亮牌只是出牌开始前"要不要向对手公开一部分手牌"的展示规则。两者各自有自己的触发条件,只是恰好都以庄家埋牌后的手牌结构为依据,因此部分门槛数值相同——但达成亮牌的条件不代表一定触发算奖,反之亦然,具体对照见 8.2 末尾的说明。
### 8.1 冲关
> **旧称「常规算奖」**,现统一叫**冲关**(与服务端字段 `chongguan`、界面「冲关分」「冲关牌型」同口径)。
> 它只是「算奖」的**其中一个分量**——另一个是 8.3 傍王;两者求和才是某玩家的总奖数 `N`(见 8.4)。
| 组合 | 奖数 |
| --- | --- |
| 三个王 | 1 奖 |
| 四个王 | 3 奖 |
| 六个 7 | 1 奖 |
| 七个 7 | 2 奖 |
| 八个 7 | 3 奖 |
| 六个 2 | 1 奖 |
| 七个 2 | 2 奖 |
| 八个 2 | 3 奖 |
三王 / 四王在拥有"正 7"及以后连续对子的情况下,每多一对连续对子再加 1 奖,连续顺序为:
```
正7 - 副7 - 正2 - 副2 - A - K - Q - J - 10 - 9 - 8 - 6 - 5
```
其中从 A 到 5(即 A、K、Q、J、10、9、8、6、5 这一段普通点数)都必须是选定花色的主牌,才能计入连续对子——非主花色的普通对子不算数,链条到这里就断了。
### 8.2 亮牌规则
**亮牌与算奖(8.1 / 8.3)是两个独立概念,互不干涉**:亮牌只是"要不要向对手公开一部分手牌"的展示规则,本身不产生任何奖数、不影响结算;算奖该怎么算、算多少,完全按 8.1 / 8.3 的规则来,不因为触发了亮牌而增加或减少。
**亮牌要求**(仅针对庄家):庄家**埋牌后**、**出牌开始前**,若手中剩余的**固定主牌**满足下表任一条件,就要向两个闲家**亮出自己全部固定主牌的具体牌面**。
| 触发条件(任一满足即亮牌) |
| --- |
| 固定主牌(双王 + 全部 2 + 全部 7)总数 ≥ 10 张 |
| 王 ≥ 3 张 |
| 7 ≥ 6 张 |
| 2 ≥ 6 张 |
**亮出的内容是固定的一份**——**庄家手中全部固定主牌的具体牌面**(双王 + 全部 2 + 全部 7;**不含**主花色的普通牌 A/K/Q/J/10/9/8/6/5)。不论是被上表哪一条触发、还是同时满足多条,亮出的都是这同一份牌,不因触发条件不同而增减。
- **不限叫分**:任何叫分档位都适用,不是 70 分坐庄专有。
- **仅可查牌模式**:房间勾选了"不查牌"时,闲家看不到亮牌(见第 9 节)。
- **只亮庄家的**:闲家不亮牌。
- 亮牌依据的是**埋牌后**的手牌,与 8.1 冲关用的是同一份静态快照(见第 8 节开头),全局固定、不随之后出牌缩水。
> **亮牌给的是牌面,不是统计**。这一点与「余主公示」(只给剩余主牌数与对子数,见第 9 节)不同;与「明牌」(查看**他家**全部未出主牌的牌面,见第 9 节)也不同——亮牌是**庄家主动公开自己的**,明牌是**闲家去看别人的**。
**亮牌门槛与 8.1 冲关起点的数值巧合,不代表两者是同一条规则**:王 ≥ 3 张 / 7 ≥ 6 张 / 2 ≥ 6 张这三个门槛,数值上恰好分别与 8.1 表里"三个王""六个 7""六个 2"的冲关起点相同,达到时会同时触发"亮牌"和"冲关"这两件独立的事——但它们是各自独立判定后凑巧同时发生,不是"亮牌导致冲关"或者"冲关导致亮牌"。这一点在"固定主牌 ≥ 10 张"这一档表现得最清楚:它只在亮牌这条规则里有定义、会触发亮牌,但 8.1 的冲关表里根本没有为这一档定义奖数,所以**不产生任何冲关奖**——如果亮牌和冲关是同一回事,这里就该矛盾;现在不矛盾,正说明两者互不干涉。
### 8.3 傍王(可选规则)
开房时勾选"傍王"后,每张王额外算一奖:这个奖是**庄家、闲家都要各自计算**(每个人按自己手里的王算,不限庄家)。
**傍王奖是在 8.1 冲关之外额外叠加的一份奖励,与冲关互不冲突、不互相替代**:庄家照常按 8.1 的规则算冲关(只计庄家),同时如果勾选了傍王,庄家、闲家还要**各自另外**再按自己手里的王数算一份傍王奖;两份奖励分开计算,最后相加。
### 8.4 算奖如何影响最终结算
**算奖是"持有奖数的那个人"从其余两名玩家那里各自多收一笔钱的独立结算线,不是把第 7 节算出的输赢结果整体重新乘一遍**:设第 7 节前三层算出的结算子数为 `X`(庄家与每个闲家之间"一对一"结算的基础金额,见 7.0 第 3 层),玩家 P 这一局总共积累了 `N_P` 奖(8.1 冲关 + 8.3 傍王,各自求和后相加;只有庄家可能有 8.1 的分量,8.3 傍王则庄闲都可能有):
- 若 `N_P > 0`,**另外两名玩家(不分是庄家还是闲家)都要各自额外付给 P:`X × N_P` 子**;
- 若 `N_P = 0`,P 没有额外收入。
三名玩家可能同时都持有各自的奖数,这时会产生最多 3 条互相独立、方向不同的额外支付线,互不冲突、按各自的 `N` 值同时生效,最后与第 1~3 层的基础输赢结算加总,才是每个人这一局的最终盈亏。
**举例**(`X = 6` 个子,庄家为 A,闲家为 B、C):
- 若庄家 A 本局有 1 奖(`N_A = 1`):B、C 各自额外付给 A `6 × 1 = 6` 子。
- 若闲家 B 同时勾选了"傍王"、本局有 2 奖(`N_B = 2`):A、C 各自额外付给 B `6 × 2 = 12` 子。
- 这两条支付线同时成立、互不影响:A 从 B、C 各多收 6 子,B 从 A、C 各多收 12 子——包括"闲家 C 要付给闲家 B"这样的支付,即使 B、C 之间在第 1~3 层的基础输赢结算里并不直接结算(基础结算只发生在"庄家 vs 单个闲家"之间),算奖这一层是单独叠加在所有玩家两两之间的。
以下三点已确认:
- **庄家倒庄(闲家达标升级)时,8.1 冲关仍然生效**:冲关只看庄家埋牌后的手牌结构,与本局谁输谁赢无关。
- **70 分投降时,仍叠加算奖倍率**:投降只是跳过了第 1~3 层的叫分 / 捡分判定(此时 `X = 1`)。投降是选主阶段的选择,庄家**未选主、未埋牌**,故算奖按庄家"发牌 + 底牌"共 36 张手牌计算——因没有选定主牌花色,**没有连对链**,只有三 / 四王、六~八个 7、六~八个 2 这些组合,以及(勾选傍王时)按王数的傍王奖计入;8.1(及勾选傍王时的 8.3)照常计算并叠加。
- **算奖对象的身份限制会带进 `N` 的计算**:8.1 只算庄家、8.3 傍王勾选后才不分庄闲(见第 8 节开头说明),把这名玩家实际适用的分量求和,才是他自己的 `N`;"其余两人向他支付 `X × N`"这条支付规则本身不因身份而改变,但闲家的 `N` 从一开始就不可能包含 8.1 的分量。
---
## 9. 牌局查看模式
"查牌"指的是**回看那些不该随时可见的信息**:已经打过去的牌、他家手里还剩多少主牌、他家主牌的具体牌面。房间的"查牌模式"开关决定下列四项功能**整体**是否提供。
- **可查牌模式**(四项功能全部提供):
1. **出牌历史**:当前牌局中,所有玩家已经打出的牌(含之前每一轮打出、已被收走的牌),任何时候都可以回看。
2. **余主公示**:一旦有玩家的主牌全部打空(即 5.4.2 节所说的"报无主"状态;"主牌"指大王、小王、所有花色的 2、所有花色的 7,以及主花色的其余普通牌,见第 3 节),系统就为**全体三人**(含刚刚报无主的这位玩家自己)显示另外两家各自的主牌数量、以及这些主牌里有多少对子。**只给数量、不给具体牌面**——要看具体牌面是下一条的"明牌"。
3. **明牌**:同时**为这三个人都**出现一个"明牌"按钮——判定依据是「**场上有人**报无主」,不是「**自己**报无主」,所以报无主的那位和另外两位一样都能点。点击后可以查看另外两家手中全部主牌的**具体牌面**(不只是数量 / 对子结构),再点一次取消查看、恢复隐藏;**不限次数,整个出牌阶段随时可查**。
4. 庄家若触发了 8.2 节的"亮牌要求",两个闲家可以看到**庄家全部固定主牌的具体牌面**(见 8.2 节)。
- **不可查牌模式**:以上四项**一项都不提供**——
- **不提供出牌历史**:之前各轮打出过什么牌一律不能回看,只能靠玩家自己记牌;
- **不提供余主公示**:即使有玩家报无主,也不会显示任何人的主牌数量、对子结构,也没有"明牌"按钮;
- 庄家即使触发了 8.2 节的亮牌条件,闲家也看不到那些牌(见 8.2 节)。
> **"查牌"与"当前这一轮桌面上的牌"是两回事**:本轮已经出过牌的玩家、打在桌面上的这几张牌,**两种模式下都必须对全体可见**——否则后出的人无从跟牌、也无从判断本轮谁最大。查牌模式管的是**已经收走的往轮牌**与**他家手牌信息**能不能回看,不影响当前这一轮的正常出牌展示(断线重连回到牌桌时同理:当前轮桌面上的牌照常恢复,往轮历史则按本节的模式开关决定给不给)。
---
## 10. 房间设置选项
创建页依次展示:**局数、扣卡方式、查牌模式、傍王、爬坡**。前三项各自是独立的单选必选组,每组必须且只能保留一个有效选项;傍王和爬坡各自独立可选,可以均不选、任选其一或同时勾选。
### 10.1 局数与牌局扣卡方式
局数为 6 局 / 12 局,单选必选;扣卡方式为房主扣卡 / AA 制,单选必选。两组分别选择,费用按下表联动展示。
| 扣卡方式 | 6 局 | 12 局 |
| --- | --- | --- |
| AA 制(每人扣卡) | 每人 1 张 | 每人 2 张 |
| 房主扣卡 | 2 张 | 4 张 |
### 10.2 查牌模式
可查牌 / 不查牌,单选必选(见第 9 节)。
### 10.3 附加规则
傍王、爬坡:均可不选,也可以同时勾选,互不冲突。未选傍王不计算傍王奖励;未选爬坡使用常规算子。
### 10.4 创建参数与当前实现差异
默认选择:6 局、房主扣卡、可查牌,傍王和爬坡均不勾选。前三项已有有效默认选择,用户无需重复点击才能提交。
规则设计者要求 `roomtype` 使用 11 位字符串,前 5 位依次为局数、扣卡方式、查牌模式、傍王、爬坡,后 6 位预留。
**2026-09-07 服务端核验:长度约定已支持,位序尚不一致。** 当前 `class.config.js` 按局数、扣卡方式、傍王、爬坡、查牌模式解析前 5 位;尾部 6 位当前约定为 `000000`。默认完整值为 `00000000000`。设计要求的位序不能直接当作当前服务器已经支持的编码,否则会把查牌、傍王、爬坡解释成另一项规则。
实际传输编码、示例和兼容行为统一见 [协议 §0.5](../protocol/packet_protocol.md#05-roomtype-房间选项位串)。界面顺序可以独立于传输位序;当前前端接入应通过规则编码层匹配现有服务端。若要求传输位序也改为设计顺序,需要另行明确服务端契约变更,本次文档核验未修改服务器实现。
---
## 11. 牌局交互提示
- 选主阶段:4 个花色选主按钮上各显示"该花色在庄家手中的对子数"(供庄家判断选哪门为主);70 分坐庄时并列再显示一个投降按钮——选主与投降是同一决策点的互斥选择(见第 4 节)。
- 首家出主牌时可以甩牌,甩牌的生效条件、跟牌规则、甩错惩罚详见 5.4 节。
- 牌局结束需要亮出埋牌底牌。
- 闲家在出牌时需要有 3 个提示选项:
1. **踩**:提示对家"我能大过庄家"。
2. **没分**:提示对家"我手上没分了"。
3. **有分**:提示对家"我手上有分"。
- **倒计时只作展示,不触发任何自动操作**:叫分 / 选主 / 埋牌 / 出牌四个阶段都会给出一个倒计时秒数,用于界面提醒当前该谁操作。**倒计时归零后不做任何代打**——不自动叫分、不自动选主、不自动埋牌、不自动出牌,也不判负、不跳过该玩家;轮到谁而谁不操作,牌局就停在这一步一直等。
> **"无超时托管"是确认过的规则,不是待实现的缺口**:本局不设服务端代打/AI 托管,也不因超时改变任何对局状态。若玩家长时间不操作导致牌局停住,由玩家走房间的**解散**流程收场,按当前累计分结算(见 12.2)。**后续核对不要把"没有超时动作"当成缺陷去补"超时托管/自动出牌"。**
---
## 12. 完整牌局游玩流程
本节把前面各节的规则串成一局(一个"小局")从头到尾的完整流程,供整体理解与实现参照。这里只给出**步骤与先后顺序**,每一步的具体细则以其对应章节为准(避免与细则重复而产生分歧)。
### 12.1 一个小局的完整流程
1. **开局准备**:房间按第 10 节的设置(局数、扣卡方式、傍王、爬坡、查牌)建好后开战。第一局的暂定庄家为 0 号座位;之后每局按 4.6 的轮庄规则确定暂定庄家。
2. **发牌**(第 2 节):92 张牌洗匀,三家各摸 28 张,剩余 8 张扣在桌面作为"底牌"。
3. **叫分坐庄**(4.2):由暂定庄家起、按逆时针依次叫分。暂定庄家必叫(5~70 分、步进 5,不能"不叫");其后每家只能叫比当前更低的分,或选择"不叫";有人叫出 5 分即立即坐庄,或一家叫分后另外两家都"不叫"也即坐庄。叫分越低表示庄家对"压住闲家捡分"越有信心,对应的基础子数越高(第 7 节)。
4. **摸底**:庄家坐定后把 8 张底牌摸入手牌(摸完共 36 张)。非 70 分:这 8 张仅庄家可见。**70 分坐庄先触发「开底」:庄家摸底之前,先把这 8 张向所有玩家翻开 3 秒,再摸入手牌**(4.4 / 第 4 节)。
5. **选主 / 投降(同一决策点,二选一,互斥)**(第 4 节):
- **选主**:庄家选定一门花色为本局主牌(大王、小王、所有花色的 2、7 恒为主牌)→ 进入下一步埋牌。前端此阶段每个花色按钮上要显示该花色在庄家手中的对子数;70 分坐庄时并列显示投降按钮。
- **投降**:仅 70 分坐庄才有此选项;选投降即放弃本局,**不选主、不埋牌、不出牌**,直接按 7.1"70 分 · 投降"结算(算奖仍按下方 8 计,但用庄家 36 张、无主牌花色)。选花色就等于放弃投降、正常打牌。
- (**开底**——70 分的 8 张底牌翻开 3 秒,发生在步骤 4"摸底之前"。)
6. **埋牌**(第 4 节,仅"选主/打牌"路径):庄家从 36 张里选 8 张重新扣下作为"埋牌底牌"(供最后"扣底"判断),保留 28 张。(**先选主、后埋牌**。)
7. **出牌对局**(第 5、6 节):
- 第一轮固定由庄家先出;此后每轮由上一轮牌面最大的一方先出。
- 首家出什么花色 / 牌型,其余两家按 5.1~5.3 跟牌(同花色不够时可用主牌"毙牌"抢权,或"垫牌"不争)。
- 首家出主牌时可"甩牌"(5.4):一次性打出多组主牌组合;服务端按全场手牌判定"最大性",甩错则按 5.4.5 收回、只强制打出最小一张。副牌一律不能甩。
- 每轮比大小定出胜者:闲家赢下的那一轮,牌面上的分牌(5→5 分、10→10 分、K→10 分)计入闲家捡分;庄家赢的轮分牌作废(6.1~6.2)。
- 过程中:有玩家主牌出空需"报无主",可查牌模式下触发**余主公示**(为全体展示他家主牌数量 / 对子结构,第 9 节);庄家手牌达到 8.2 的门槛时触发**亮牌**。
8. **末轮扣底**(6.3):所有手牌出完后,若最后一轮由闲家**用主牌**赢下,则庄家埋的 8 张埋牌底牌翻开,其中的分按闲家赢牌的牌型翻倍(单张 ×1、主对 ×2、N 连对 ×2N)后计入闲家捡分。
9. **小局结算**(第 7、8 节,两部分同时结算):
- **算子**:闲家总捡分(含扣底)与庄家叫分比较 → 判定庄家的**过庄 / 小光 / 大光**,或闲家的**升级第几级** → 结合叫分档的基础子数,得到庄家与每个闲家"一对一"的结算子数 `X`(勾选爬坡则改用 7.3 的梯度与分段)。
- **算奖**:以静态初始手牌(正常局:庄家埋牌后 28 张、闲家发牌后 28 张;**70 分投降局**:庄家用发牌+底牌共 36 张、无主牌花色)计算奖数 `N` —— 冲关(8.1)**只计庄家**;勾选傍王(8.3)则庄闲每张王各算一奖。持有 `N` 奖的玩家从另外两人各多收 `X × N`(8.4)。
- 以上两部分加总即各家本局盈亏;牌局结束需亮出埋牌底牌(第 11 节)。
10. **进入下一局**(4.6):庄家本局赢则连庄(暂定庄家仍是他);庄家输(闲家捡分达标 / 庄家投降)则暂定庄家顺延到其下家。回到第 2 步开新的一小局。
### 12.2 大局与结束
按房间设定的局数(6 局或 12 局,见第 10 节)打满全部小局后,进行大局结算(累计各家总分并记录战绩);若中途解散,则按当前累计分结算。
### 12.3 阶段速览
```
建房(§10) → 发牌(§2) → 叫分坐庄(§4.2) → 摸底[70分先开底] → 选主(§3) → 埋牌(§4)
→ [70分:投降/打牌决策(§4.4)] → 出牌对局(§5,§6.1-6.2) → 末轮扣底(§6.3)
→ 小局结算=算子(§7)+算奖(§8) → 轮庄(§4.6) → 下一局 …… 打满局数 → 大局结算
```
@@ -0,0 +1,722 @@
# 二七王协议包列表
说明:`data` 均为发送方与接收方之间约定的 JSON 内容;成败判定只看推送包里的 `data.success`。
---
## 0. 通用约定(所有包适用,下文各表不再逐一重复)
### 0.0 术语:底牌 vs 埋牌底牌(先读这一条)
本项目于 2026-08-25 修订了这两个术语(`design.md` §1 已同步,本文亦已改用新称):
| 术语 | 指哪 8 张 | 旧称 |
| --- | --- | --- |
| **底牌** | 发牌时**没有发给玩家**、扣在桌面的 8 张 | ~~暗牌~~ |
| **埋牌底牌** | 庄家**埋牌**时从手里扣下的 8 张 | ~~底牌~~ |
**两批牌的字段名已拆分**(2026-08-25,原先共用 `bottomcards`):
| 字段名 | 含义 | 出现在 |
| --- | --- | --- |
| **`bottomcards`** | **底牌** | `shangzhuang`、`deskinfo.ChooseMain`、`deskinfo.BuryCards` |
| **`burycards`** | **埋牌底牌** | `maipai`、`deskinfo.PushCards` |
| `bottom.cards` | **埋牌底牌** | 结算包的 `bottom` 分组(分组名已表明是抠底相关,字段未改名) |
同一个包里不会同时出现这两个字段。**下发面**:`burycards` 与 `bottomcards`(非 70 分时)都**只发给庄家**,闲家两者皆无——已由 `test/test_leak.js` 的泄露审计覆盖。
### 0.0b 四类「公开信息」术语(都带亮/明字,别读混)
| 术语 | 谁公开给谁 | 给的是什么 | 触发 | 本文相关字段 |
| --- | --- | --- | --- | --- |
| **亮牌** | 庄家 → 两个闲家 | **具体牌面**(庄家全部固定主牌) | 庄家埋牌后固定主牌达门槛(design §8.2) | `liangpai.cards` |
| **余主公示** | 全体 → 全体 | **只有数量**(剩余主牌数、主对数) | 任一玩家报无主(design §9) | `seatlist[seat][4]`、`baozhu` |
| **明牌** | 他家 → 请求者 | **具体牌面**(他家全部未出主牌) | 可查牌 + **任一**玩家报无主 → **三家均可点、随时可查**(design §9) | `mingpai.others[].zhucards` |
| **开底** | 桌面 → 所有玩家 | **具体牌面**(8 张底牌) | 70 分坐庄,**摸底之前** 3 秒(design §4) | `ancard3s` + `bottomcards` |
一句话记:**只有「余主公示」是统计类(给数字),「亮牌」「明牌」「开底」都给具体牌面**——区别在给谁的牌:亮牌给庄家自己的、明牌给他家的、开底给桌上那 8 张。
另有一组「底」字族专管发牌留桌的那 8 张:**摸底**(庄家摸起看,仅庄家)、**开底**(70 分时全场看 3 秒)、**查底牌**(对局中随时回看)、**扣底**(末轮翻开埋牌底牌计分)。
(「亮主」不是术语,是前端选主界面的标题文案,与上表无关。)
### 0.0c 算奖 = 冲关 + 傍王
| 术语 | 范围 | 相关字段 |
| --- | --- | --- |
| **冲关** | design §8.1,**只计庄家**。旧称"常规算奖",现统一叫冲关 | `chongguan`(奖数)、`cards`(冲关牌型) |
| **傍王** | design §8.3,可选规则,勾选后**庄闲都算**,每张王 1 奖 | `wang`(王数)、`bangwang`(开关) |
| **算奖** | **上位概念** = 冲关 + 傍王 | `naward`(总奖数 N)、`grade_aw`(算奖得分) |
说「**冲关**」指庄家那一份,说「**算奖**」指两者合计。界面上小局结算显示「冲关分」、按钮叫「冲关牌型」。
> ⚠️ `grade_aw` 是按合计后的 `N` 算出来的,**冲关与傍王的贡献事后无法拆分**。大局结算若要分别显示「冲关分」「傍王分」,须服务端在算钱时就分开代入公式——见前端清单 §7.5 **S-6**。
### 0.1 成败标志 `data.success`
**每一个「服务器 → 客户端」的包,`data` 都必带 `success`**(布尔):
- `success: true` —— 操作成功 / 正常推送。下文各包的字段表描述的都是这种情况,表里不再重复列 `success` 这一行。
- `success: false` —— 操作失败,见 0.2。
前端一律 `if (!data.success)` 判成败,**不看 `status` / `code`,也不写 `status` 兼容兜底**。
### 0.2 失败回包
服务端受理请求时,任一校验不通过都会**回一个失败包**(此前是静默丢弃、前端只能干等倒计时):
- **rpc 与请求包同名**(如 `chupai` 失败回 `chupai`,而不是 `chupai1/2/3`);
- **只回发给请求者**(`conmode`/`fromid` 取自请求包),其他两家收不到;
- `data` 只含 `success: false` 与 `errcode`,无业务字段。
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| success | 布尔 | 恒为 `false` |
| errcode | 整数 | 失败原因,见下表 |
`errcode` 取值(服务端 `youle_erqiwang.ERR`,`mod.js`):
| 值 | 名称 | 含义 |
| --- | --- | --- |
| 1 | PLAYER | 玩家/房间/座位校验不通过(平台 `check_player` 返回 null) |
| 2 | NODESK | 牌桌或牌局不存在 |
| 3 | STEP | 当前阶段不允许该操作(如非出牌阶段发 `chupai`) |
| 4 | SEAT | 位置不符,或还没轮到该玩家操作 |
| 5 | PARAM | 参数非法:类型/范围/张数不对、牌不在手上、牌id 重复 |
| 6 | RULE | 规则不允许:叫分未更低、出牌不合法、投降条件不满足、房间模式禁止等 |
> **牌id 列表的入参约束**(`cards` 字段,`maipai`/`chupai`):必须是**非空数组**,元素必须是 `0 ~ 107` 的**整数**牌id,且**互不重复**。字符串形式的数字(如 `"5"`)、小数、越界值、重复值一律按 `PARAM` 拒绝,服务端不做类型兜底转换。
> **例外:`tishi` 成功时不回执**。该包是闲家给对家的主观提示、不含任何对局状态,服务端只转发给对家(design §11),发送者收不到成功包;只有失败才回给发送者。
### 0.3 `countdown` 只是展示用的秒数
多个包带 `countdown`(叫分 / 选主 / 埋牌 / 出牌倒计时,取自 `class.desk.js` 的四个常量)。它**只供客户端显示提醒,服务端不据此做任何事**:
- 服务端**没有**对应的定时器,倒计时归零后**不会**自动叫分 / 选主 / 埋牌 / 出牌,也不判负、不跳过该玩家;
- 轮到谁而谁不操作,牌局就停在该阶段一直等,`step` 与 `playproc` 都不变;
- 这是 design §11 确认过的规则(无超时托管),不是未实现的功能。牌局因此停住时,由玩家走平台的**房间解散**流程收场(平台回调子游戏 `get_disbandRoom` → 解散结算,见 §14)。
因此**客户端不要在倒计时归零时做任何乐观的界面推进**(不要自行跳阶段、不要清控制权),一切仍以收到服务端推送为准。
### 0.4 断线重连包不在此约定内
`deskinfo` 由平台组装在 `pack.data.deskinfo` 下(见文末「断线重连」),`data.success` 由**平台**填写,子游戏不注入。
### 0.5 `roomtype` 房间选项位串
**核对日期:2026-09-07。** 本节描述当前服务端实际编码,依据 [class.config.js](../../class.config.js) 的 `IDX_*`、`RESERVE_LEN`、`parse()`,以及 [test_config.js](../../test/test_config.js)。局数与扣卡由 [class.export.js](../../class.export.js) 的对应接口读取解析结果。
当前新前端发送约定为 **11 位字符串 = 前 5 位选项 + 后 6 位预留扩展位**,后 6 位当前填 `000000`。默认完整值为 `00000000000`。索引从 0 开始,对应 `charAt(0..4)`;前端不得把字符串转换为数字、数组或按界面排列顺序直接拼接。
| 当前传输位索引 | 含义 | '0' | '1' | 创建页要求 |
| --- | --- | --- | --- | --- |
| 0 | 局数 | 6 局(默认) | 12 局 | 单选必选 |
| 1 | 扣卡方式 | 房主扣卡(默认) | AA 每人扣卡 | 单选必选 |
| 2 | 傍王 | 关(默认) | 开 | 独立可选 |
| 3 | 爬坡 | 常规算子(默认) | 爬坡 | 独立可选 |
| 4 | 查牌模式 | 可查牌(默认) | 不查牌 | 单选必选 |
| 5~10 | 预留扩展位 | 当前发送填 0 | 当前无玩法定义 | 无控件 |
**设计与实现差异:** 规则设计者指定的前 5 位顺序为“局数、扣卡方式、查牌模式、傍王、爬坡”,而当前源码与测试仍按上表解析。创建页按设计顺序展示,编码层按已核验的服务器位索引构造参数;这不表示设计要求的传输位序已在服务器落地。若后续变更服务端位序,需要同步核验协议和兼容方案,不能仅更新文档就声称契约已改变。
局数、扣卡、查牌分别单选必选;傍王与爬坡允许均不勾选、任选其一或同时勾选,未选仍写对应位的 '0'。编码示例(均为当前服务端顺序):
- `00000000000`:6 局、房主扣卡、可查牌、不傍王、不爬坡。
- `10100000000`:12 局、房主扣卡、可查牌、傍王、不爬坡。
- `00001000000`:6 局、房主扣卡、不查牌、不傍王、不爬坡。
- `11011000000`:12 局、AA 扣卡、不查牌、不傍王、爬坡。
**实际解析兼容行为:** 服务端 `parse()` 接受至少 5 位的字符串,仅读取前 5 位;旧 5 位串及更长扩展串均可解析,不强制长度为 11。缺失、非字符串或不足 5 位时整体使用 `00000` 的语义;每个选项位只有字符 '1' 判为开启,其余字符判为关闭。预留位不校验内容,也不影响当前五项配置。此处记录的是来源实现,前端仍应在编码入口生成约定的合法 11 位串,不能依赖服务器容错掩盖错误。
`class.config.js` 是玩法配置的唯一解析入口;下游使用具名配置,不重新按下标解释。`multiple` / `bangwang` / `climb` / `baozhu` / `seatlist` / `liangpai` / `pushlist` 按相应配置取值或门控。源码 `cfg.climb` 行的行末注释写着“true=常规算子”,与位定义及算法分支不符;实际 true 表示启用爬坡,以执行逻辑为准。
对应的房卡与局数(`export.get_asetcount` / `get_needroomcard` / `get_needroomcard_joinroom`,design §10.1):
| 扣卡方式(位1) | 局数(位0) | 房主开房扣 | 加入者扣 |
| --- | --- | --- | --- |
| 房主扣卡 `'0'` | 6 局 | 2 张 | 0 |
| 房主扣卡 `'0'` | 12 局 | 4 张 | 0 |
| AA 每人 `'1'` | 6 局 | 1 张 | 1 张 |
| AA 每人 `'1'` | 12 局 | 2 张 | 2 张 |
### 0.6 「一手出牌」的顺序口径
同一份业务数据「某一手打出的牌」会出现在**三个下发位置**:
| 位置 | 字段 |
|---|---|
| 出牌推送 | `chupai1` / `chupai2` / `chupai3` 的 `data.cards` |
| 本轮进行态 | `playproc.cards[座位]`(`chupai*` 与重连包 `PushCards` 都带)|
| 重连出牌历史 | `PushCards.pushlist[轮次-1][座位]` |
**这三处的数组内容逐元素完全相等**——顺序统一为「按**本局主牌花色**从大到小」的**权威顺序**。
服务端只在落牌那一刻归一化一次(`class.paiju.js` 的 `order_playcards`),随后:
出牌推送读它、`playproc.cards` 存它、出牌历史 `playhistory` 归档它,下游一律**只读、不重排**。
因此前端**「增量回放」与「重连重建」得到的出牌历史必定一致**,两条路径可以复用同一份解析。
> ⚠️ **请求包里的 `cards` 顺序无意义**:那是玩家的点击顺序,同一手牌每次都可能不同。
> 服务端不采信、不回显、也不据它判定牌型(server dev-guide 04 §8「前端不是数据源」)。
> 一手单张时看不出差别;**一手多张(甩牌 / 对子 / 拖拉机)时才会显形**,尤其是同编码的一对牌
> ——排序本身区分不了它们,只有「出牌当场归档」这一个写入处才能保证两条路径给出同一个数组。
---
## 1. 发牌(fapai)
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:fapai
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| asetidx | 整数 | 当前局数 |
| asetcount | 整数 | 总局数 |
| cards | 数组 | 自己得到的牌id列表 |
| step | 整数 | **本局阶段**(发完牌恒为 `1` 叫分;取值 1叫分 2选主/投降 3埋牌 5出牌 6结算)。阶段由服务端唯一维护、逐包显式给出,客户端**不得按 rpc 名反推**(server 03 §1.4「任何状态变更都要有包承载」/ 前端红线「数据驱动、前端无对局状态机」)。与同一时刻的重连包 `deskinfo.step` 同源同值 |
| seat | 整数 | **控制权**:当前等待叫分者的位置。与重连包 `CallRun.seat` 同源(`get_callgrade_seat()`)|
| countdown | 整数 | 叫分倒计时 |
---
## 2. 叫分或不叫(jiaofen,客户端→服务器)
发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:jiaofen
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| agentid | 字符 | 代理id |
| playerid | 整数 | 玩家id |
| gameid | 字符 | 游戏id |
| roomcode | 整数 | 房间号 |
| seat | 整数 | 叫分者的位置序号 |
| call | 整数 | 分数,0表示不叫 |
---
## 3. 叫分或不叫(jiaofen,服务器→客户端)
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:jiaofen
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| seat | 整数 | 叫分者的位置序号 |
| call | 整数 | 分数,0表示不叫 |
| currcall | 整数 | 当前叫到的分数 |
| multiple | 整数 | 叫分对应的基础子数 `get_base_bycall(call, climb)`,**随房间「爬坡」开关取值**(roomtype 位3):常规算子 65→2 60→3 55→4 50及以下→6;爬坡 65→2 60→3 55→4 50→6 45→7 40→8 35→9 30→10 25→11 20→12 15→13 10→14 5→15;两种模式下 70→2;无叫分→0。与结算包 `aset.multiple` 同源同值(design §7.1/§7.3.1)|
| step | 整数 | **本局阶段**(叫分尚未结束,恒为 `1`;取值 1叫分 2选主/投降 3埋牌 5出牌 6结算)。阶段由服务端唯一维护、逐包显式给出,客户端**不得按 rpc 名反推**(server 03 §1.4「任何状态变更都要有包承载」/ 前端红线「数据驱动、前端无对局状态机」)。与同一时刻的重连包 `deskinfo.step` 同源同值 |
| nextseat | 整数 | **控制权**:下一个叫分者的位置序号,即本包之后「轮到谁」。与重连包 `CallRun.seat` 同源(`get_callgrade_seat()`)|
| countdown | 整数 | 叫分倒计时 |
---
## 4. 上庄(shangzhuang)
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:shangzhuang
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| seat | 整数 | 叫分者的位置序号 |
| call | 整数 | 分数,0表示不叫 |
| banker | 整数 | 庄家的位置序号 |
| grade | 整数 | 庄家的叫分 |
| step | 整数 | **本局阶段**(叫分结束、进入选主/投降,恒为 `2`;取值 1叫分 2选主/投降 3埋牌 5出牌 6结算)。阶段由服务端唯一维护、逐包显式给出,客户端**不得按 rpc 名反推**(server 03 §1.4「任何状态变更都要有包承载」/ 前端红线「数据驱动、前端无对局状态机」)。与同一时刻的重连包 `deskinfo.step` 同源同值 |
| nextseat | 整数 | **控制权**:本包之后「轮到谁」——恒为 `banker`,因为选主与投降都只由庄家做(服务端 `mod.xuanzhu`/`mod.touxiang` 的座位校验同样以 `banker` 为准)。<br>⚠️ **别和本包的 `seat` 混用**:`seat` 是**最后一个叫分者**,不是控制权。与重连包 `ChooseMain.seat` 同源同值 |
| multiple | 整数 | 叫分对应的基础子数 `get_base_bycall(call, climb)`,**随房间「爬坡」开关取值**(roomtype 位3):常规算子 65→2 60→3 55→4 50及以下→6;爬坡 65→2 60→3 55→4 50→6 45→7 40→8 35→9 30→10 25→11 20→12 15→13 10→14 5→15;两种模式下 70→2;无叫分→0。与结算包 `aset.multiple` 同源同值(design §7.1/§7.3.1)|
| bottomcards | 数组 | 8 张**底牌**(发牌时没发给玩家、扣在桌面的 8 张)。**庄家恒有**;**闲家仅当 70 分坐庄时才有**(供 3 秒亮牌用),非 70 分时闲家没有该属性(底牌只有庄家可见,design §4)。<br>**顺序在发牌结束时即冻结**(服务端 `paiju.bottomcards`,按「还没有主牌」的口径排一次):底牌是在**选主之前**翻给庄家看的,那时主牌花色尚不存在,故不按主牌花色排。本包与重连包 `ChooseMain.bottomcards` / `BuryCards.bottomcards` 三处**同序**,客户端存一次即可全程复用(含「查底牌」回看)|
| ancard3s | 整数 | **开底**标志。仅 70 分坐庄时出现且为 `1`:表示庄家**摸底**之前,需将 `bottomcards` 这 8 张**底牌**向所有玩家翻开 3 秒(design §4/§7.1);非 70 分无此属性 |
| cards | 数组 | 拿了底牌后手上的牌(36 张,含底牌),庄家才有此属性,闲家没有该属性 |
| countdown | 整数 | 选主倒计时 |
| touxiang | 整数 | 是否允许投降 0:不允许 1:允许(仅 70 分坐庄为 1)。投降与选主互斥、同为选主阶段(step2)的决策,见 touxiang 包与 design §4 |
| curmultiple | 整数 | **当前抓分倍数**(design §7.2.0):按此刻累计捡分实时算出的判定倍率,**带符号**,与结算包 `aset.upgrade` 同口径——`3`/`2`/`1` = 庄家 大光/小光/过庄,`-N` = 闲家升 N 级,`0` = 叫分未定。上庄时捡分恒为 0,故必为 `3`。三家同值(捡分本就公开)。**不含扣底**——扣底要到末轮才产生。<br>顶部「抓分」角标只关心倍数大小,取 `Math.abs` 即可;**符号供客户端的判定动画分辨该播哪个**(不能取绝对值,否则 3 大光与 -3 升3级会撞在一起)|
---
## 5. 投降(touxiang,客户端→服务器)
发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:touxiang
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| agentid | 字符 | 代理id |
| playerid | 整数 | 玩家id |
| gameid | 字符 | 游戏id |
| roomcode | 整数 | 房间号 |
| seat | 整数 | 庄家的位置序号。投降是**选主阶段(step 2)与选主互斥**的选择:`mod.touxiang` 仅在 `step==2`、`banker==seat`、`call==70` 时受理;点投降即直接结算,不选主、不埋牌、不出牌(design §4) |
---
## 6. 选主(xuanzhu,客户端→服务器)
发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:xuanzhu
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| agentid | 字符 | 代理id |
| playerid | 整数 | 玩家id |
| gameid | 字符 | 游戏id |
| roomcode | 整数 | 房间号 |
| seat | 整数 | 选主者的位置序号 |
| flower | 整数 | 花色 1方块 2梅花 3红心 4黑桃 |
---
## 7. 选主(xuanzhu,服务器→客户端)
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:xuanzhu
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| banker | 整数 | 庄家的位置序号 |
| flower | 整数 | 花色 1方块 2梅花 3红心 4黑桃 |
| step | 整数 | **本局阶段**(选主完成、进入埋牌,恒为 `3`;取值 1叫分 2选主/投降 3埋牌 5出牌 6结算)。阶段由服务端唯一维护、逐包显式给出,客户端**不得按 rpc 名反推**(server 03 §1.4「任何状态变更都要有包承载」/ 前端红线「数据驱动、前端无对局状态机」)。与同一时刻的重连包 `deskinfo.step` 同源同值 |
| nextseat | 整数 | **控制权**:本包之后「轮到谁」——恒为 `banker`,埋牌只由庄家做(服务端 `mod.maipai` 的座位校验以 `banker` 为准)。与重连包 `BuryCards.seat` 同源同值 |
| countdown | 整数 | 埋牌倒计时 |
| cards | 数组 | 选主后自己手上的牌id列表 |
---
## 8. 埋牌(maipai,客户端→服务器)
发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:maipai
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| agentid | 字符 | 代理id |
| playerid | 整数 | 玩家id |
| gameid | 字符 | 游戏id |
| roomcode | 整数 | 房间号 |
| seat | 整数 | 埋牌者的位置序号 |
| cards | 数组 | 埋牌的id列表 |
---
## 9. 埋牌(maipai,服务器→客户端)
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:maipai
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| cards | 数组 | 埋牌后手上的牌,去掉了埋牌,庄家才有此属性,闲家没有该属性 |
| burycards | 数组 | **埋牌底牌**(庄家埋下的 8 张),庄家才有此属性,闲家没有该属性。注意与 `shangzhuang.bottomcards`(**底牌**,发牌留桌 8 张)是两批不同的牌,见 §0.0。<br>**取服务端权威快照 `get_burycard()`,不是客户端请求包里 `cards` 的原序**:按本局主牌花色从大到小排好,与重连包 `PushCards.burycards` **同源同序** |
| seatlist | 数组 | 三家座位牌况,**仅可查牌模式下发**(不查牌无此属性,design §9),结构与门控同 `chupai1/2/3.seatlist` 与重连包 `PushCards.seatlist`。<br>埋牌完成时它是刚初始化的**空表**(每家 `[[0,0],[0,0],[0,0],[0,0],[-1,-1]]`)——下发它是为了让「埋牌完成 → 庄家首出」这段窗口内,增量路径与重连路径拿到同一张表,客户端无需为这段窗口特判 |
| playproc | json | **本轮进行态**(design §5.1),**恒有、三家同值**,结构与门控同 [`chupai1.playproc`](#11-第一个玩家出牌chupai1)/重连包 `PushCards.playproc`(同一个快照函数 `get_playproc()`)。`do_burycard` 内部已 `new_playround` 就地初始化好 round-1 的进行态,此包带的正是这份初值:`round=1`、`start`/`currseat` 均为即将首出的庄家、`cards` 全空、`shuai_demand` 为 `null`。<br>**可见性**:全部字段由桌面公开信息推出(此刻尚无一张牌打出),三家整体下发、不逐座位裁剪 |
| step | 整数 | **本局阶段**(埋牌完成、进入出牌,恒为 `5`;取值 1叫分 2选主/投降 3埋牌 5出牌 6结算)。阶段由服务端唯一维护、逐包显式给出,客户端**不得按 rpc 名反推**(server 03 §1.4「任何状态变更都要有包承载」/ 前端红线「数据驱动、前端无对局状态机」)。与同一时刻的重连包 `deskinfo.step` 同源同值 |
| seat | 整数 | **控制权**:出牌者的位置序号(即将首出的庄家)。服务端取自权威的 `playproc.currseat`,与重连包 `PushCards.playproc.currseat` 同源同值 |
| countdown | 整数 | 出牌倒计时 |
| liangpai | json | **亮牌**(design §8.2)。**只有闲家、且可查牌模式、且庄家达门槛时才有**;不达标或不查牌则无此属性。结构:`{ cards: [牌id...] }`——**庄家手中全部固定主牌的具体牌面**,按本局主牌序从大到小排好。<br>**固定主牌 = 双王 + 全部花色的 2 + 全部花色的 7**,**不含**主花色的普通牌 A/K/Q/J/10/9/8/6/5。<br>**门槛**(任一满足即下发):固定主牌总数 ≥10 / 王 ≥3 / 7 ≥6 / 2 ≥6。**不限叫分**。<br>亮出的是**固定的一份**,不随被哪条门槛触发而增减;也**不给数量统计**——数量前端自己数 `cards.length` 即可。<br>口径是庄家**埋牌后的静态快照**(排除已埋的 8 张,但包含之后已打出的牌),全局固定不随出牌缩水,故重连包里取值一致 |
---
## 10. 出牌(chupai,客户端→服务器)
发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:chupai
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| agentid | 字符 | 代理id |
| playerid | 整数 | 玩家id |
| gameid | 字符 | 游戏id |
| roomcode | 整数 | 房间号 |
| seat | 整数 | 出牌者的位置序号 |
| cards | 数组 | 出牌的id列表 |
---
## 11. 第一个玩家出牌(chupai1)
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:chupai1
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| seat | 整数 | 出牌者的位置序号 |
| cards | 数组 | 实际打出的牌id列表;**甩错时**(见 `shuaicuo`)为被强制打出的那一张最小主牌单张。**顺序:按本局主牌花色从大到小(权威顺序)**,不是客户端提交时的点击顺序——请求包里的 `cards` 顺序服务端不采信、不回显(见 [§0.6](#06-一手出牌的顺序口径))|
| shuaicuo | 整数 | 甩错标志,仅甩错时出现且为 `1`(design §5.4.5:甩牌未通过最大性判定,整套甩牌收回,本轮只强制打出最小一张、失去本轮甩牌资格);正常出牌无此属性 |
| seatlist | 数组 | 三家座位牌况 `o_paiju.seatlist`(长度 3,下标 = 座位序号),**仅可查牌模式下发**(不查牌无此属性,design §9)。每个座位为 5 元素数组:前 4 个对应花色1~4 的 `[无该花色标志, 该花色无对标志]`(0/1),第 5 个为**余主公示**数据 `[剩余主牌数, 剩余主对数]`(初始 `[-1,-1]`,代码内旧称「报副」)。**一旦全场有人报无主,服务端会按各家实际手牌同时刷新三个座位并整表下发**(design §9「为全体三人显示另外两家」),无需等另两家各自出牌才补。字段名与结构同重连包 `PushCards.seatlist` |
| count | 整数 | 出牌数量(甩错时为 1) |
| flower | 整数 | 出牌花色 |
| cardtype | 整数 | 出牌牌型:`>100` 单张(101 一张、102 两张…)、`>200` 对子(201 一对、202 两对…)、`>300` 拖拉机(302 两连对、303 三连对…),见 `class.pai.js` 顶部注释。**注意:cardtype 只能表达单一牌型**,而甩牌是单张/对子/拖拉机自由混搭(design §5.4.3),会被牌型推导压平成 1xx 或 2xx(例如「主K对 + 主5」得 103、「两连对 + 一散对」得 203)。**甩牌的真实结构请读 `shuai`,不要据 cardtype 反推** |
| shuai | json | 甩牌分量构成,**仅本次出牌是合法甩牌时才有**(非甩牌、以及甩错退化为单张时都没有该属性)。结构 `{ tractors: [连对数...], pairs: 独立对子数, singles: 单张数 }`,例如「主K对 + 主5」为 `{tractors:[],pairs:1,singles:1}`。与服务端跟牌时逐分量强制匹配用的 `shuai_demand` 同源(design §5.4.3/§5.4.4)|
| nextseat | 整数 | 下一个出牌者的位置序号 |
| countdown | 整数 | 下一个出牌者的出牌倒计时 |
| baozhu | 整数 | 是否已有玩家报无主(主牌出空)0否 1是;对应服务端 `have_baofu()`(任一玩家 `seatlist[seat][4][0]==0`)。**仅可查牌模式**下才可能为 1,是**余主公示**与"明牌"按钮的开关;**不查牌模式恒为 0**(design §9)|
| cardsinhand | 数组 | 出牌者出牌后手上剩下的牌id列表,只有出牌者才有此属性 |
| curmultiple | 整数 | **当前抓分倍数**(design §7.2.0):**带符号**,与结算包 `aset.upgrade` 同口径(`3`/`2`/`1` 庄家大光/小光/过庄,`-N` 闲家升 N 级,`0` 叫分未定)。同源同算法,差别仅在此处**不含扣底**。三家同值,随本包下发、不另开推送。符号是「谁赢」的区分,客户端判定动画靠它分辨 |
| playproc | json | <a id="playproc-def"></a>**本轮进行态**(design §5.1)。**`chupai1/2/3` 三个包恒有,三家同值**;结构与重连包 [`PushCards.playproc`](#断线重连deskinfo) **完全一致**(服务端同一个快照函数 `get_playproc()`,前端增量回放与重连重建复用同一份解析)。<br>字段:`round` 第几轮、`start` 本轮首出位置、`currseat` 当前该谁出、`startcount` 首出张数、`startflower` 首出花色、`starttype` 首出牌型、`maxseat` 本轮暂时最大者、`maxcard` 其牌编码、`cards` 本轮三家各自出的牌(**定长 3,下标 = 座位序号**,未出的位置为 `null`)、`shuai_demand` 首家甩牌的分量需求 `{tractors:[连对数...],pairs,singles}`(非甩牌为 `null`)。<br>⚠️ **`chupai3` 带的是【下一轮】的进行态**(`round+1`、`cards` 全空、`currseat == nextseat == maxseat`):本轮第三家一出完,服务端就地开了新一轮。这与「此刻断线重连拿到的 `PushCards.playproc`」完全相同——本轮那三手牌客户端已由 `chupai1/2/3` 各自的 `seat`+`cards` 收到,收牌动画后即清台。**唯一例外**:`chupai3` 打完最后一张牌时本包会转成 `jiesuan`(见 §14),那种情况下**不带** `playproc`。<br>**可见性**:全部字段都由桌面公开信息推出(`cards` 就是已摊在桌上的牌,`shuai_demand` 与 `chupai1.shuai` 等价且甩出的牌本身已公开),故三家整体下发、不逐座位裁剪 |
| mustcard | 数组 | **下一个出牌者本轮跟牌的必出牌**(design §5.2),供其客户端自动选中。**只发给 `nextseat` 那一家**,其余两家无此属性——它是该玩家自己手牌的子集,整表下发会泄露他家手牌结构。以下情形不下发:`nextseat` 是本轮首家、首家为**甩牌**(甩牌跟牌走逐分量匹配,见 design §5.4.4)、或算出的必出牌为空 |
---
## 12. 第二个玩家出牌(chupai2)
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:chupai2
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| seat | 整数 | 出牌者的位置序号 |
| cards | 数组 | 出的牌id列表。**顺序:按本局主牌花色从大到小(权威顺序)**,不是客户端提交时的点击顺序(见 [§0.6](#06-一手出牌的顺序口径))|
| seatlist | 数组 | 三家座位牌况 `o_paiju.seatlist`(长度 3,下标 = 座位序号),**仅可查牌模式下发**(不查牌无此属性,design §9)。每个座位为 5 元素数组:前 4 个对应花色1~4 的 `[无该花色标志, 该花色无对标志]`(0/1),第 5 个为**余主公示**数据 `[剩余主牌数, 剩余主对数]`(初始 `[-1,-1]`,代码内旧称「报副」)。**一旦全场有人报无主,服务端会按各家实际手牌同时刷新三个座位并整表下发**(design §9「为全体三人显示另外两家」),无需等另两家各自出牌才补。字段名与结构同重连包 `PushCards.seatlist` |
| nextseat | 整数 | 下一个出牌者的位置序号 |
| countdown | 整数 | 出牌倒计时 |
| baozhu | 整数 | 是否已有玩家报无主(主牌出空)0否 1是;对应服务端 `have_baofu()`(任一玩家 `seatlist[seat][4][0]==0`)。**仅可查牌模式**下才可能为 1,是**余主公示**与"明牌"按钮的开关;**不查牌模式恒为 0**(design §9)|
| cardsinhand | 数组 | 出牌后出牌者手上剩下的牌id列表,只有出牌者才有此属性 |
| playproc | json | **本轮进行态**,恒有、三家同值,结构见 [§11 `playproc`](#11-第一个玩家出牌chupai1)。此时 `cards` 已含首家与本家两手牌,`currseat` 指向第三家 |
| mustcard | 数组 | **下一个出牌者本轮跟牌的必出牌**(design §5.2),供其客户端自动选中。**只发给 `nextseat` 那一家**,其余两家无此属性——它是该玩家自己手牌的子集,整表下发会泄露他家手牌结构。以下情形不下发:`nextseat` 是本轮首家、首家为**甩牌**、或算出的必出牌为空 |
---
## 13. 第三个玩家出牌(chupai3)
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:chupai3
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| seat | 整数 | 出牌者的位置序号 |
| cards | 数组 | 出的牌id列表。**顺序:按本局主牌花色从大到小(权威顺序)**,不是客户端提交时的点击顺序(见 [§0.6](#06-一手出牌的顺序口径))|
| seatlist | 数组 | 三家座位牌况 `o_paiju.seatlist`(长度 3,下标 = 座位序号),**仅可查牌模式下发**(不查牌无此属性,design §9)。每个座位为 5 元素数组:前 4 个对应花色1~4 的 `[无该花色标志, 该花色无对标志]`(0/1),第 5 个为**余主公示**数据 `[剩余主牌数, 剩余主对数]`(初始 `[-1,-1]`,代码内旧称「报副」)。**一旦全场有人报无主,服务端会按各家实际手牌同时刷新三个座位并整表下发**(design §9「为全体三人显示另外两家」),无需等另两家各自出牌才补。字段名与结构同重连包 `PushCards.seatlist` |
| nextseat | 整数 | 下一个出牌者的位置序号 |
| countdown | 整数 | 出牌倒计时 |
| baozhu | 整数 | 是否已有玩家报无主(主牌出空)0否 1是;对应服务端 `have_baofu()`(任一玩家 `seatlist[seat][4][0]==0`)。**仅可查牌模式**下才可能为 1,是**余主公示**与"明牌"按钮的开关;**不查牌模式恒为 0**(design §9)|
| cardsinhand | 数组 | 出牌后出牌者手上剩下的牌id列表,只有出牌者才有此属性 |
| maxseat | 整数 | 本轮出牌谁最大,只有本轮最后一个玩家出牌后才有此属性 |
| grade | 整数 | 本轮闲家得分,只有本轮最后一个玩家出牌后且闲家有得分才有此属性 |
| curmultiple | 整数 | **当前抓分倍数**(design §7.2.0):**带符号**,口径同 `aset.upgrade`。同源同算法,差别仅在此处**不含扣底**。三家同值,随本包下发、不另开推送 |
| playproc | json | **本轮进行态**,三家同值,结构见 [§11 `playproc`](#11-第一个玩家出牌chupai1)。⚠️ 本包带的是**下一轮**的进行态(`round+1`、`cards` 全空、`currseat == nextseat == maxseat`),与此刻重连拿到的 `PushCards.playproc` 一致。**牌局在本包打完(转为 `jiesuan`)时不带此属性** |
---
## 13.5 明牌(mingpai,客户端→服务器)
发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:mingpai
查看另外两家手中全部主牌的具体牌面(design §9)。服务端仅在**可查牌模式、出牌阶段(step5)、且已有【任一】玩家报无主**(`have_baofu()`)时受理——注意判定的是「场上有人报无主」而非「请求者本人报无主」,报无主那位与另外两位同样有权查看(design §9.3);不满足时按 0.2 回 `mingpai` 失败包(不查牌 / 未报无主 → `RULE`,非出牌阶段 → `STEP`)。
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| agentid | 字符 | 代理id |
| playerid | 整数 | 玩家id |
| gameid | 字符 | 游戏id |
| roomcode | 整数 | 房间号 |
| seat | 整数 | 请求者的位置序号 |
## 13.6 明牌(mingpai,服务器→客户端)
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:mingpai (只回发给请求者)
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| seat | 整数 | 请求者的位置序号 |
| others | 数组 | 另外两家各自未出的全部主牌,元素 `{ seat: 位置序号, zhucards: [主牌id列表] }` |
> "再点一次取消查看"是客户端的显示开关,无需再请求服务端。
## 13.7 出牌提示(tishi,客户端→服务器)
发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:tishi
闲家在出牌阶段向对家(另一闲家)发"踩/没分/有分"提示(design §11)。服务端仅在**出牌阶段(step5)、且发起者为闲家**(`seat != banker`)、**tip 合法(1/2/3)** 时受理;**不校验提示真实性**(玩家可主观发送);庄家无对家,不受理。不满足时按 0.2 回 `tishi` 失败包给发送者(庄家发 → `RULE`,非出牌阶段 → `STEP`,tip 非法或缺失 → `PARAM`)。**成功时不给发送者回执**,只转发给对家。
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| agentid | 字符 | 代理id |
| playerid | 整数 | 玩家id |
| gameid | 字符 | 游戏id |
| roomcode | 整数 | 房间号 |
| seat | 整数 | 发出提示的闲家位置序号 |
| tip | 整数 | 提示类型:1=踩(我能大过庄家)、2=没分(我手上没分了)、3=有分(我手上有分) |
## 13.8 出牌提示(tishi,服务器→客户端)
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:tishi (**只转发给对家**,即另一闲家 `3 - banker - seat`)
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| seat | 整数 | 发出提示的闲家位置序号 |
| tip | 整数 | 提示类型:1=踩、2=没分、3=有分 |
---
## 14. 结算(jiesuan)
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:jiesuan
结算包由以下几个子结构拼装而成。三种结算来源的组成不同:
- **正常出牌结算**(`mod.chupai` 中最后一张牌出完,`get_paiju_account(0, ...)`):含 `chupai` + `bottom` + `aset`(末局再加 `account`)。
- **投降结算**(`mod.touxiang`,`get_paiju_account(1, ...)`):只含 `aset`(末局再加 `account`),无 `chupai`、无 `bottom`。
- **解散结算**(`export.get_disbandRoom`,`get_paiju_account(2, ...)`):只含 `aset` + `account`,无 `chupai`、无 `bottom`。**投递方式与上面两种不同,见 §14.1。**
上面这张表头(`route: erqiwang / rpc: jiesuan`)**只适用于前两种**——正常出牌结算与投降结算,由子游戏自己 `sendpack_toother` 广播,客户端在 `rpc == "jiesuan"` 的分支里按 `data.aset` 取值。
### 14.1 解散结算的投递方式(与前两种不同,客户端需单独处理)
解散**不是**由子游戏发包,而是平台在解散流程里回调 `youle_erqiwang.export.get_disbandRoom(o_room)`,把**返回值整个对象**塞进**房间路由**的解散包里下发(平台 `server_room/rpc.js`、`server_room/class.room.js`)。因此客户端收到的是:
```json
{
"app": "youle", "route": "room", "rpc": "free_room",
"data": {
"seats": [],
"deskfree": {
"rpc": "jiesuan",
"data": { "success": true, "aset": {}, "account": [] }
}
}
}
```
要点(照 §14 的表去 `data.aset` 取值会取空):
- **rpc 是平台的 `free_room`(route 也是 `room`),不是 `erqiwang/jiesuan`**;子游戏返回的 `rpc: "jiesuan"` 只是嵌在 `deskfree` 里的一个普通字段。
- **多一层 `data` 包装**:真实取值路径是 `data.deskfree.data.aset` 与 `data.deskfree.data.account`。
- 本包的成败标志有两层:外层 `data.success` 由**平台**填写(解散流程本身成功与否),子游戏结算自带的 `success` 在 `data.deskfree.data.success`。§0.1「每个包 `data` 必带 `success`」说的是子游戏自己发的包;本包外层归平台。
- `aset` / `account` 的字段结构与下面 §14 的表完全一致(`aset.multiple = 0`、`upgrade = 0`、各家 `grade = 0`,即解散局不结算子数与算奖;`account` 恒有,见 design §12.2「按当前累计分结算」)。
- 若解散发生在**开战后、首局牌发出前**的窗口内(本游戏首局由 `makewar` 延迟 1 秒创建),`get_disbandRoom` 返回 `null`,平台走「不带 `deskfree`」分支——客户端需容忍 `data.deskfree` 缺失。
**顶层字段(三种结算来源恒有)**
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| success | 布尔 | 恒 `true`(§0.1)|
| step | 整数 | **本局阶段**,结算包恒为 `6`。三种结算来源(正常/投降/解散)统一由 `get_paiju_account` 给出——解散在此之前 `step` 可能还停在 1/2/3/5,由它落定为 6。客户端**不得按 rpc 名硬编码**,一律读此字段 |
**chupai(出牌包,仅正常出牌结算存在)**
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| seat | 整数 | 出牌者的位置序号,投降和解散无此属性 |
| cards | 数组 | 出的牌id列表,投降和解散无此属性 |
| maxseat | 整数 | 本轮出牌谁最大,投降和解散无此属性 |
| grade | 整数 | 本轮闲家得分,投降和解散无此属性 |
| gradecards | 数组 | 本轮闲家得分分牌列表,投降和解散无此属性。**注意:当前源码中赋值语句被注释(`class.paiju.js`/`mod.js` 中相关行均被注释掉),该字段实际永远不会出现在下发的包里,属于失效字段** |
**bottom(抠底包,仅正常出牌结算存在;`cards` 恒有,其余仅闲家抠底时有)**
> 本分组的 `cards` 是**埋牌底牌**(庄家埋下的 8 张),不是发牌留桌的底牌。
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| cards | 数组 | **埋牌底牌**(庄家埋下的 8 张),投降和解散无此属性 |
| multiple | 整数 | 闲家抠底倍数,闲家没抠底无此属性,投降和解散无此属性 |
| grade1 | 整数 | 埋牌底牌的分数,闲家没抠底无此属性,投降和解散无此属性 |
| grade2 | 整数 | 闲家抠底得分,闲家没抠底无此属性,投降和解散无此属性 |
**aset(单局结算包)**
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| banker | 整数 | 庄家的位置序号,-1:无庄 |
| call | 整数 | 庄家叫分,-1:无叫分 |
| multiple | 整数 | 基础子数 `get_base_bycall(call, climb)`(design §7.1/§7.3.1):常规算子 65→2、60→3、55→4、50 及以下→6、70打牌→2;投降固定 1、解散 0;无叫分 0 |
| flower | 整数 | 主牌花色,-1:无主牌花色 |
| grade | 整数 | 闲家捡分(含抠底) |
| upgrade | 整数 | 判定倍率(带符号,design §7.2.0):3大光 / 2小光 / 1过庄 / -N升N级(倒庄)/ -99投降 / 0解散或无判定 |
| bangwang | 整数 | 本局是否启用傍王规则 0否 1是(roomtype 位2) |
| climb | 整数 | 本局是否启用爬坡规则 0否 1是(roomtype 位3) |
| seatlist | 数组 | 玩家列表,元素结构见下 |
`seatlist` 数组元素结构:
```json
{
"cards": [], // 冲关牌型:参与冲关的牌(王 + 冲关组合牌)列表,供「冲关牌型」界面展示
"chongguan": 0, // 冲关奖数(design §8.1,旧称"常规算奖";只有庄家才计入自己的 N)
"wang": 0, // 手牌中的王数(傍王按此计奖)
"naward": 0, // 该家总奖数 N =(庄家?冲关奖数:0)+(傍王?王数:0)
"grade_aw": 0, // 算奖得分 = X×(2Ni−Nj−Nk),X 为每对子子数。= grade_cg + grade_bw
"grade_cg": 0, // 其中的【冲关】分量(design §8.1,不含傍王)
"grade_bw": 0, // 其中的【傍王】分量(design §8.3;未勾傍王时恒 0)
"grade_jf": 0, // 捡分子数得分
"grade": 0, // 本局总分 = grade_aw + grade_jf
"score": 0 // 累计得分
}
```
> 结算数值模型(design §7~§8):每「庄–闲」对子的基础金额 `X = multiple × |upgrade|`(投降 X=1、解散 X=0)。捡分子数:庄赢时两闲家各付庄家 X、庄家收 2X;闲赢(升级)时庄家各付两闲家 X。算奖:持有 N 奖的玩家从另外两人各多收 `X×N`,三家两两独立叠加(含闲–闲),即 `grade_aw = X×(2Ni−Nj−Nk)`。
>
> **算奖的两个分量**(供大局结算分项展示):`N = N_冲关 + N_傍王`(冲关只计庄家、傍王勾选后庄闲都算)。
> 服务端把两个分量**分别代入同一公式**各算一次,得到 `grade_cg` 与 `grade_bw`。
> 公式对 `N` 是线性的,因此恒有 **`grade_cg + grade_bw == grade_aw`**,且两个分量各自零和。
> 之所以必须在算钱时就拆,是因为两个 `N` 一旦相加就再也分不开——事后无法从 `grade_aw` 反推各自占比。
**account(大局结算包)**
> 仅当打到最后一局(`o_paiju.idx >= o_room.asetcount`)或房间中途解散(解散结算)时,`jiesuan` 包才会带上此分组;普通的中间局结算包没有 `account` 字段(`class.paiju.js` `get_paiju_account`)。
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| account | 数组 | 玩家列表(长度 3,下标 = 座位序号),元素结构见下 |
```json
[
{ "score": 0, "grades": [], "grade_jf_total": 0, "grade_cg_total": 0, "grade_bw_total": 0 },
{ "score": 0, "grades": [], "grade_jf_total": 0, "grade_cg_total": 0, "grade_bw_total": 0 },
{ "score": 0, "grades": [], "grade_jf_total": 0, "grade_cg_total": 0, "grade_bw_total": 0 }
]
```
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| score | 整数 | 累积总分(全部小局 `aset.seatlist[i].grade` 之和) |
| grades | 数组 | 每局总分列表,按局序 |
| grade_jf_total | 整数 | **基础分**累计:各局 `grade_jf`(捡分子数得分)之和 |
| grade_cg_total | 整数 | **冲关分**累计:各局 `grade_cg` 之和(design §8.1,**不含**傍王) |
| grade_bw_total | 整数 | **傍王分**累计:各局 `grade_bw` 之和(design §8.3;未勾傍王时恒 0) |
> 恒等式:`grade_jf_total + grade_cg_total + grade_bw_total == score`,供大局结算面板分项展示(基础分 / 冲关分 / 傍王分 / 总分)。
>
> ⚠️ **结构变更(2026-08-26)**:原为 `[[累积得分, [每局得分...]], ...]` 的二元数组,现改为**对象数组**并增加三项分解。数组下标语义不清,且前端尚未开工,此时改代价最小。
---
## 15. 准备(zhunbei,客户端→服务器)
发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:zhunbei
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| agentid | 字符 | 代理id |
| playerid | 整数 | 玩家id |
| gameid | 字符 | 游戏id |
| roomcode | 整数 | 房间号 |
| seat | 整数 | 位置序号 |
---
## 16. 准备(zhunbei,服务器→客户端)
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:zhunbei
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| seat | 整数 | 位置序号 |
---
## 断线重连(deskinfo)
> 由平台在玩家进入房间/断线重连时回调 `youle_erqiwang.export.get_deskinfo(o_room, seat)` 生成并下发(服务器→客户端);下发的 app/route/rpc 由平台的进房/重连流程决定,不在本子游戏代码内固定。平台把它挂在 `pack.data.deskinfo` 下,**`data.success` 由平台填写、子游戏不注入**(见 0.4)。下列字段即该函数返回的 `deskinfo` 对象结构,按当前 `paiju.step` 只带对应阶段的分组。
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| count | 整数 | 总局数 |
| idx | 整数 | 当前局数 |
| PlayerInfo | 数组 | 三个玩家目前的总积分 `[0,0,0]` |
| step | 整数 | 牌桌状态:1发完牌叫分 2选主/投降 3埋牌 5出牌 6结算(无独立投降阶段——投降在 step2 与选主互斥;埋牌后直接进入 step5 出牌) |
| MyCards | 数组 | 自己手上的牌 |
**CallRun(不在叫分阶段无此属性)**
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| seat | 整数 | 当前叫分位置 |
| countdown | 整数 | 叫分倒计时 |
| nowcall | 整数 | 当前叫分 |
| multiple | 整数 | 叫分对应的基础子数 `get_base_bycall(call, climb)`,**随房间「爬坡」开关取值**(roomtype 位3):常规算子 65→2 60→3 55→4 50及以下→6;爬坡 65→2 60→3 55→4 50→6 45→7 40→8 35→9 30→10 25→11 20→12 15→13 10→14 5→15;两种模式下 70→2;无叫分→0。与结算包 `aset.multiple` 同源同值(design §7.1/§7.3.1)|
| call | 数组 | 三家叫分 `[null,0,65]`,null还未叫分,0不叫,>0叫了多少分 |
**ChooseMain(step 2 选主/投降阶段,不在此阶段无此属性)**
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| seat | 整数 | **控制权**:此刻轮到谁操作——恒为 `banker`(选主与投降都只由庄家做)。字段名与 `CallRun.seat` 一致:**各阶段分组里的 `seat` 统一表示「轮到谁」**。与增量推送 `shangzhuang.nextseat` 同源同值;客户端据此设控制权,**不要自己用 `banker` 反推**(那是把「选主=庄家」这条规则搬到前端)|
| banker | 整数 | 庄家 |
| call | 整数 | 叫分 |
| multiple | 整数 | 叫分对应的基础子数 `get_base_bycall(call, climb)`,**随房间「爬坡」开关取值**(roomtype 位3):常规算子 65→2 60→3 55→4 50及以下→6;爬坡 65→2 60→3 55→4 50→6 45→7 40→8 35→9 30→10 25→11 20→12 15→13 10→14 5→15;两种模式下 70→2;无叫分→0。与结算包 `aset.multiple` 同源同值(design §7.1/§7.3.1)|
| countdown | 整数 | 选主倒计时 |
| bottomcards | 数组 | 8 张**底牌**(发牌留桌的 8 张),**只有庄家有此属性**(底牌仅庄家可见;70 分的 3 秒亮牌是上庄时的一次性事件,重连不重放)。顺序取发牌时冻结的快照,与 `shangzhuang.bottomcards`、`BuryCards.bottomcards` **完全同序**(见 §4)|
| touxiang | 整数 | 是否允许投降 0:不允许 1:允许(仅 70 分坐庄为 1)。投降与选主互斥、同一决策点,见 touxiang 包与 design §4 |
| curmultiple | 整数 | **当前抓分倍数**(design §7.2.0):**带符号**,与 `shangzhuang.curmultiple` 同源同值。选主阶段一张牌都还没出、捡分恒为 0,故**必为 `3`**(大光)。三家同值。有此字段,重连后顶部「抓分」角标才不会掉回 0 |
> 选主阶段前端需在每个花色按钮上显示"该花色在庄家手中的对子数"(design §4/§11)——庄家的完整手牌由 `MyCards` 提供(庄家为 36 张),对子数由前端据此计算,服务端不额外下发。
**BuryCards(step 3 埋牌阶段,不在此阶段无此属性)**
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| seat | 整数 | **控制权**:此刻轮到谁操作——恒为 `banker`(埋牌只由庄家做)。与增量推送 `xuanzhu.nextseat` 同源同值 |
| banker | 整数 | 庄家 |
| call | 整数 | 叫分 |
| multiple | 整数 | 叫分对应的基础子数 `get_base_bycall(call, climb)`,**随房间「爬坡」开关取值**(roomtype 位3):常规算子 65→2 60→3 55→4 50及以下→6;爬坡 65→2 60→3 55→4 50→6 45→7 40→8 35→9 30→10 25→11 20→12 15→13 10→14 5→15;两种模式下 70→2;无叫分→0。与结算包 `aset.multiple` 同源同值(design §7.1/§7.3.1)|
| flower | 整数 | 主牌花色 |
| countdown | 整数 | 埋牌倒计时 |
| bottomcards | 数组 | 8 张**底牌**(发牌留桌的 8 张),**只有庄家有此属性**(底牌仅庄家可见;70 分的 3 秒亮牌是上庄时的一次性事件,重连不重放)。顺序取发牌时冻结的快照,与 `shangzhuang.bottomcards`、`ChooseMain.bottomcards` **完全同序**——**不**按本局主牌花色重排(见 §4)|
| curmultiple | 整数 | **当前抓分倍数**(design §7.2.0):**带符号**,与 `shangzhuang.curmultiple` 同源同值。埋牌阶段同样一张牌未出、捡分恒为 0,故**必为 `3`**(大光)。三家同值 |
> 埋牌阶段无投降(投降是 step2 与选主互斥的选择,选主后即不可再投降)。
**PushCards(step 5 出牌阶段,不在此阶段无此属性)**
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| banker | 整数 | 庄家 |
| call | 整数 | 叫分 |
| multiple | 整数 | 叫分对应的基础子数 `get_base_bycall(call, climb)`,**随房间「爬坡」开关取值**(roomtype 位3):常规算子 65→2 60→3 55→4 50及以下→6;爬坡 65→2 60→3 55→4 50→6 45→7 40→8 35→9 30→10 25→11 20→12 15→13 10→14 5→15;两种模式下 70→2;无叫分→0。与结算包 `aset.multiple` 同源同值(design §7.1/§7.3.1)|
| flower | 整数 | 主牌花色 |
| countdown | 整数 | 出牌倒计时 |
| burycards | 数组 | **埋牌底牌**(庄家埋下的 8 张),只有庄家有此属性。注意与 `ChooseMain`/`BuryCards.bottomcards`(**底牌**)是两批不同的牌,见 §0.0。与 `maipai.burycards` **同源同序**(同一个 `get_burycard()`,按本局主牌花色从大到小)|
| grade | 整数 | 当前的捡分分数 |
| gradecards | 数组 | 当前的捡分分牌,只有闲家有此属性 |
| playproc | json | **本轮进行态**,与 `chupai1/2/3.playproc` **同源同结构**(服务端同一个 `get_playproc()` 快照函数,见 [§11 `playproc`](#11-第一个玩家出牌chupai1)):`round` 第几轮、`start` 本轮首出位置、`currseat` 当前出牌位置、`startcount` 首出张数、`startflower` 首出花色、`starttype` 首出牌型、`maxseat` 本轮最大者位置、`maxcard` 最大牌编码、`cards` 本轮三家各自出的牌(**定长 3**,下标=位置序号,未出为 `null`)、`shuai_demand` 首家甩牌的分量需求 `{tractors:[连对数...],pairs,singles}`(非甩牌为 null,供跟牌逐分量强制匹配,design §5.4.4)。**两种查牌模式下恒有此属性**——`cards` 是当前这一轮桌面上的牌,不属于「查牌」,屏蔽了后出的人就无从跟牌(design §9 末尾)|
| baozhu | 整数 | 是否已有玩家报无主(主牌出空)0否 1是,**恒有此属性**;与 `chupai1/2/3` 的 `baozhu` **同源同值同门控**(同一个 `have_baofu()` + 同一个查牌位)。它是**余主公示**与“明牌”按钮的开关,重连必须一并恢复,否则重连后按钮凭空消失;**不查牌模式恒为 0**(design §9)|
| seatlist | 数组 | 三家座位牌况,与上文 `maipai` / `chupai1/2/3` 包的 `seatlist` 同名同结构(每个玩家一个 5 元素数组:4 个花色的 `[无该花色,无对]` + 报副 `[剩余主牌数,剩余主对数]`)。**仅可查牌模式下有此属性**(design §9)|
| liangpai | json | **亮牌**,结构同 maipai 包的 `liangpai`(`{ cards: [牌id...] }`);**仅可查牌模式、且请求者为闲家、且庄家达标时有**(供闲家重连后仍能看到,design §8.2)|
| pushlist | 数组 | 出牌历史 `[[[], [], []], [[], [], []], ...]`,外层下标=轮次(从第 1 轮起,含进行中的当前轮),内层**恒为 3 个数组**、按位置序号存该轮各家出的牌id。**每一手与当时那个 `chupai1/2/3` 包的 `cards` 逐元素完全相等**(同一份数据、同一个顺序口径,见 [§0.6](#06-一手出牌的顺序口径)),因此前端“增量回放”与“重连重建”得到的出牌历史必定一致。**仅可查牌模式下有此属性**——往轮打出、已被收走的牌属于「查牌」范畴,不查牌模式一律不下发(design §9)。注意当前这一轮桌面上的牌由 `playproc.cards` 恢复,**两种模式下都有**,否则后出的人无从跟牌 |
| curmultiple | 整数 | **当前抓分倍数**(design §7.2.0):**带符号**,与 `chupai1/2/3` 同源同值,供重连后顶部「抓分」角标与判定动画状态立即正确 |
| mustcard | 数组 | **本轮跟牌的必出牌**(design §5.2)。**仅当 `playproc.currseat == 请求者座位` 时才有**,且只算请求者自己的手牌;未轮到本家、本轮首家、甩牌局面、必出牌为空时均无此属性 |
**Balance(不在结算阶段无此属性)**
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| readystate | 数组 | 所有玩家的准备状态 |
| aset | json | 单局结算包,同上面结算包中的 `aset`,只有当自己的准备状态为0时才有此属性 |
| bottom | json | **抠底包**,结构同 §14 的 `bottom`,与该局 `jiesuan` 推送的 `data.bottom` **同源同值**(服务端在 `get_paiju_account` 末尾冻结的同一份快照)。**仅正常出牌结算才有**(投降/解散没有抠底这一步);同样受「自己的准备状态为 0」门控。<br>**为什么必须有**:结算面板还开着时断线重连/硬刷新,只恢复 `aset` 会让抠底明细整块空白——同一份数据两条路径给的不一样(server 红线「发全下发面」)。<br>**可见性**:埋牌底牌在本局结算时已随 `jiesuan` 广播给三家(design §11 结束亮底),此处不构成额外泄露 |
| account | json | **大局结算包**,结构同 §14 的 `account`,与该局 `jiesuan` 推送的 `data.account` **同源同值**。**仅末局或中途解散才有**,与 `jiesuan` 的取舍完全一致(有就带、没有就不带)|
---
## 战绩(大局列表)gameinfo1
> 非客户端收发包:大局结束/解散时由 `class.desk.js` `get_desk_account` 组装,经 `import.save_grade(o_room, o_gameinfo1, o_gameinfo2, 1)` 传给平台战绩服务持久化(服务器→平台)。
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| roomcode | 整数 | 房号 |
| asetcount | 整数 | 实际局数 |
| createtime | 字符 | 开房时间 |
| makewartime | 字符 | 开战时间 |
| players | 数组 | 玩家列表,元素结构见下 |
`players` 数组元素结构:
```json
{
"seat": 0, // 座位
"playerid": 100001, // 玩家ID
"name": "", // 昵称
"avatar": "", // 头像
"score": 0 // 成绩
}
```
---
## 战绩(大局)gameinfo2
> 非客户端收发包:与 gameinfo1 同批,由 `import.save_grade` 的第 3 个参数传给平台战绩服务(服务器→平台)。
牌局列表,数组元素结构:
```json
{
"starttime": "", // 开始时间
"endtime": "", // 结束时间
"seatlist": [6, -6, 0], // 玩家成绩
"callproc": [], // 叫分过程,详情见代码中的注释
"banker": 0, // 庄
"call": 0, // 叫分
"flower": 0, // 主牌花色
"result": 0, // 牌局结果
"cards": [] // 发牌出牌情况,详情见代码中的注释
}
```