二七王:S-4 拆分 bottomcards 字段名,S-5 术语同步收口

S-4(服务端改动):底牌与埋牌底牌不再共用一个字段名。
- bottomcards = 底牌(发牌留桌 8 张)—— shangzhuang / ChooseMain / BuryCards,保持不变
- burycards   = 埋牌底牌(庄家埋下 8 张)—— maipai(mod.js)、PushCards(class.export.js)
- 结算 bottom.cards 未改名(分组名已表明是抠底相关),文档注明其语义

测试:
- test_leak.js 的 CARD_FIELDS 白名单加入 burycards。这一步是必须的——不加的话
  新字段会静默脱离下发面审计,闲家误收也发现不了。
- test_rpc.js 新增 11 条正反用例:庄家有 burycards 且为 8 张、庄家不再有
  bottomcards(抓改名漏改)、闲家两个字段皆无、重连 PushCards 同上。

验证(不止「跑过了」):
- 改动前基线 16 个文件全绿;改动后仍全绿,新增后 rpc 从 89 → 100 checks。
- 反向验证一:把 mod.js 的 burycards 改回 bottomcards → test_rpc 新用例 FAIL,
  且 test_leak 报出真实泄露(赋值改了而 delete 没改时,闲家会收到庄家埋的牌)。
- 反向验证二:删掉闲家侧的 delete msg.data.burycards → test_leak 立刻抓出
  5 张越权牌,确认 burycards 确已纳入审计白名单。
两次验证后均已还原并复跑全绿。

S-5:术语同步收口。design.md / packet_protocol.md / 代码注释均已改用
「底牌 / 埋牌底牌」;compliance 三份历史核对记录有意不改(反映当时的 design
表述,下轮核对按新 design 重做)。

另:T-9 亮牌条文案暂定「x对 x主」(取 liangpai.zhupair 与 zhu,其余档位
先不渲染、协议不变)。至此 29 项待确认规格全部有结论。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-25 18:07:17 +08:00
co-authored by Claude Opus 5
parent 82cfc98502
commit ecec44e93a
6 changed files with 102 additions and 58 deletions
+48 -36
View File
@@ -45,12 +45,12 @@
| 术语 | 指哪 8 张 | design.md 原称 | 当前协议字段 |
| --- | --- | --- | --- |
| **底牌** | 发牌时**没有发给玩家**、留在桌面的 8 张 | ~~暗牌~~ | `shangzhuang.bottomcards`、`ChooseMain`/`BuryCards.bottomcards` |
| **埋牌底牌** | 庄家埋牌时从手里扣下的 8 张 | ~~底牌~~ | `maipai.bottomcards`、`PushCards.bottomcards`、结算 `bottom.cards` |
| **底牌** | 发牌时**没有发给玩家**、留在桌面的 8 张 | ~~暗牌~~ | **`bottomcards`** |
| **埋牌底牌** | 庄家埋牌时从手里扣下的 8 张 | ~~底牌~~ | **`burycards`**(结算包为 `bottom.cards`) |
其余术语(埋牌、扣底、甩牌、毙牌、垫牌、捡分、子、奖…)沿用 design.md §1 不变。注意「**扣底**」仍指 design §6.3 那条规则——闲家用主牌赢下最后一轮后翻开**埋牌底牌**计分;底栏那个 `扣底` 按钮是「查看埋牌底牌」的入口,两者同名但不同义。
> ⚠️ **design.md 与 packet_protocol.md 尚未同步此变更**(见 §7.5 **S-5**)。在同步前,读那两份文档时要做一次心算转换;协议里两批牌**共用 `bottomcards` 字段名**,需按所在包判断语义(拆分建议见 **S-4**)。
> ✅ `design.md` 与 `packet_protocol.md` **已同步此变更**(S-5 完成),协议字段名也已拆分为 `bottomcards` / `burycards`(S-4 完成)。
---
@@ -555,7 +555,13 @@ EQW_RoomOptions = {
| `qi` | 7 ≥ 6 | 如 `6 张 7` |
| `er` | 2 ≥ 6 | 如 `6 张 2` |
多条同时满足时组合展示。参考图显示为 `主牌对子: 1对` —— 文案与上表不完全对应【待确认 T-9】。
**文案(暂定)**:显示 **`x对 x主`** —— 即「对子数 + 主牌总数」,例如 `3对 12主`。
- 取 `liangpai.zhupair`(对子数)与 `liangpai.zhu`(主牌总数)。这两项同属「固定主牌 ≥10 张」那一档,要么都有、要么都没有。
- 其余三档(`wang` / `qi` / `er`)与 `zhutuo`(拖拉机数)当前**不进文案**;服务端仍照常下发,前端先不渲染,后续要加时不必改协议。
- 若 `liangpai` 里没有 `zhu` / `zhupair`(庄家未达到「固定主牌 ≥10 张」这一档),**整条亮牌条不显示**。
> 暂定文案,正式稿到位后只改这一处模板。
### 1.7 出牌对局 `chupai1` / `chupai2` / `chupai3` 【图:出牌.png / 出牌2.png】
@@ -878,14 +884,14 @@ y≈648–720 深色条。左侧头像/昵称/分数由平台渲染(§0.2)
>
> 改后底栏按钮**名实相符**:`底牌` 按钮看底牌、`扣底` 按钮看埋牌底牌,不再有命名倒挂。
>
> **但协议字段目前仍两批共用 `bottomcards`**,只靠「在哪个包」区分语义,仍是易错点:
> **协议字段也已拆开**(S-4 已完成),前端按字段名取值即可,不必再按所在包判断语义:
>
> | 按钮 | 展示 | 当前协议字段 | 建议改名(S-4) |
> | --- | --- | --- | --- |
> | `底牌` | 底牌(发牌留桌 8 张) | `shangzhuang.bottomcards`(庄家恒有,闲家仅 70 分时有)、`ChooseMain`/`BuryCards.bottomcards`(仅庄家) | 保持 `bottomcards` |
> | `扣底` | 埋牌底牌(庄家埋的 8 张) | `maipai.bottomcards`、`PushCards.bottomcards`(仅庄家)、结算 `bottom.cards` | 改为 **`burycards`** |
> | 按钮 | 展示 | 协议字段 |
> | --- | --- | --- |
> | `底牌` | 底牌(发牌留桌 8 张) | **`bottomcards`** —— `shangzhuang`(庄家恒有,闲家仅 70 分时有)、`ChooseMain`/`BuryCards`(仅庄家) |
> | `扣底` | 埋牌底牌(庄家埋的 8 张) | **`burycards`** —— `maipai`、`PushCards`(仅庄家);结算包为 `bottom.cards` |
>
> **实现要求**:前端内部用 `bottomCards`(底牌)/ `buryCards`(埋牌底牌)两个变量名区分;在 S-4 落地前,取值时必须按**所在包**判断 `bottomcards` 的语义。已列入验收清单(§7.3)。
> **实现要求**:前端内部变量仍建议用 `bottomCards` / `buryCards` 区分,与字段名一一对应。已列入验收清单(§7.3)。
- 精灵:群组 205,号段 1040–1069(5 个按钮 × 2 精灵 = 10)。
@@ -1555,8 +1561,9 @@ codes/config/
## 7. 待补充清单
> **状态汇总**(2026-08-25):待确认规格 29 项**已答 27 项**,剩 T-9(亮牌条文案)与 T-20(音效,已明确后续补充)。
> 新增 **§7.5 服务端待补项**——3 条需要改服务端才能实现的前端需求。
> **状态汇总**(2026-08-25):待确认规格 29 项**已全部有结论**(T-9 亮牌文案暂定 `x对 x主`;T-20 音效明确后续补充)。
> **§7.5 服务端待补项**共 5 条:**S-4 / S-5 已完成**(字段拆分 + 三份文档术语统一),**S-3 已定论**(无需改服务端),
> 剩 **S-1**(抓分倍数字段 `curmultiple`)与 **S-2**(下发应选中的牌 `mustcard`)待实现。
### 7.1 待出设计稿的界面(后续统一补充)
@@ -1582,7 +1589,7 @@ codes/config/
| 编号 | 问题 | 影响 |
| --- | --- | --- |
| **T-9** | 亮牌条文案 `主牌对子: 1对` 与 design §8.2 四类信息(主牌总数/对子数/拖拉机数、王数、7 数、2 数)的对应关系与组合展示格式 | 文案模板;`liangpai` 各字段怎么拼成一行 |
| ~~T-9~~ | ~~亮牌条文案~~ | **已暂定**:显示 `x对 x主`(见 §1.6) |
| **T-20** | **整套音效**:清单、是否需要语音包、是否分男女声、倒计时提示音从第几秒起 | §4 全节;**已明确「等待后续补充」** |
**已定论(27 项)**
@@ -1644,7 +1651,7 @@ codes/config/
| 10 | `EQW_Layout` 与 `Game_Modify.PLAYER_INFO_LAYOUT` 的头像框坐标一致 | §6.11 |
| 11 | 丢弃 `this.data`、仅凭最近一次服务端快照重画,界面完全一致 | 前端 05 §6.2 |
| 12 | 所有布局参数都在配置文件里,业务代码无裸值 | §6 |
| 13 | **底牌 / 埋牌底牌未写反**:`底牌` 按钮显示底牌(发牌留桌)、`扣底` 按钮显示埋牌底牌;内部变量用 `bottomCards`/`buryCards` 区分;S-4 落地前按「所在包」判断 `bottomcards` 语义 | §2.6 |
| 13 | **底牌 / 埋牌底牌未写反**:`底牌` 按钮读 `bottomcards`、`扣底` 按钮读 `burycards`;内部变量 `bottomCards`/`buryCards` 一一对应 | §2.6 |
| 14 | 叫分按钮配色按**档位位置**取(非按子数),已叫过的档位置灰 | §1.3 |
| 15 | 自己发的提示本地回显后,收到失败回包时能撤掉 | §1.7 |
@@ -1734,34 +1741,39 @@ codes/config/
**服务端无需改动**——现有下发面正好支持这条规则:闲家在非 70 分局本来就收不到数据,按钮不显示即可,不存在「拿到了但不渲染」的泄露风险(服务端红线:下发即泄露)。
#### S-4 · 拆分 `bottomcards` 字段名(新增)
#### S-4 · 拆分 `bottomcards` 字段名 —— ✅ 已完成(2026-08-25)
**需求**:底牌与埋牌底牌是两批不同的牌,协议里却共用 `bottomcards`,只靠所在包区分语义——易错,需要拆开。
两批牌原先共用 `bottomcards`、只靠所在包区分语义,现已拆分:
**建议改法**(与新术语对齐,`bottomcards` 留给字面意义相符的「底牌」):
| 字段名 | 含义 | 出现在 |
| --- | --- | --- |
| **`bottomcards`** | **底牌**(发牌留桌 8 张) | `shangzhuang`、`deskinfo.ChooseMain`、`deskinfo.BuryCards` |
| **`burycards`** | **埋牌底牌**(庄家埋下 8 张) | `maipai`、`deskinfo.PushCards` |
| `bottom.cards` | **埋牌底牌** | 结算包 `bottom` 分组(分组名已表明是抠底相关,未改名) |
| 包 / 分组 | 当前字段 | 实际含义 | 建议 |
| --- | --- | --- | --- |
| `shangzhuang` | `bottomcards` | **底牌**(发牌留桌 8 张) | 保持 `bottomcards` |
| `deskinfo.ChooseMain` | `bottomcards` | **底牌** | 保持 |
| `deskinfo.BuryCards` | `bottomcards` | **底牌** | 保持 |
| `maipai` | `bottomcards` | **埋牌底牌**(庄家埋的 8 张) | 改为 **`burycards`** |
| `deskinfo.PushCards` | `bottomcards` | **埋牌底牌** | 改为 **`burycards`** |
| 结算 `bottom` 分组 | `bottom.cards` | **埋牌底牌** | 分组名可一并改为 `bury`,或保持 `bottom.cards` 但在文档注明 |
**服务端改动**:`mod.js`(`maipai` 组包)、`class.export.js`(`deskinfo.PushCards`),另同步了两处的注释术语。
**测试**:`test/test_leak.js` 的 `CARD_FIELDS` 白名单加入 `burycards`(否则新字段会静默脱离下发面审计);`test/test_rpc.js` 新增 11 条正反用例锁死拆分结果。
**协议文档**:`packet_protocol.md` §0.0 与相关字段表已更新。
**影响面**:`packet_protocol.md`、服务端 `mod.js` / `class.paiju.js` / `class.desk.js` 的组包处,以及尚未开工的前端。**前端未开工,现在改代价最小。**
**前端取值**:直接按字段名区分即可,不再需要「按所在包判断语义」:
#### S-5 · 同步 design.md 与 packet_protocol.md 的术语(新增)
```js
// 底牌(发牌留桌 8 张)——底栏「底牌」按钮
data.bottomcards // shangzhuang / ChooseMain / BuryCards
// 埋牌底牌(庄家埋下 8 张)——底栏「扣底」按钮
data.burycards // maipai / PushCards
data.bottom.cards // 结算包
```
术语变更(暗牌→**底牌**、底牌→**埋牌底牌**)目前**只在本文生效**。`design.md` 是玩法**唯一权威源**,`packet_protocol.md` 是协议权威源,两者仍用旧术语——**三份文档术语打架会直接违反 SSOT**。
#### S-5 · 同步 design.md 与 packet_protocol.md 的术语 —— ✅ 已完成(2026-08-25)
需要同步修订的位置(初步):
术语变更(暗牌→**底牌**、底牌→**埋牌底牌**)已在各处生效:
| 文档 | 位置 |
| 文档 / 代码 | 状态 |
| --- | --- |
| `design.md` | §1 术语约定表(「暗牌」「底牌」「埋牌」「扣底」四条);§4 开局与坐庄流程 3/5 步;§6.3 扣底;§7.1 70 分说明;§8 算奖快照;§9 查牌;§12.1 流程 4/6/8 步;§12.3 阶段速览 |
| `packet_protocol.md` | `shangzhuang` / `maipai` / `deskinfo` 各分组的 `bottomcards` 说明;结算 `bottom` 分组;与 S-4 的字段改名一并做 |
| 服务端代码 | 注释与变量名(非必须,但建议一并对齐) |
> **本文已先行采用新术语**,并在此显式记录差异,避免读者误以为 design.md 也已改。**建议尽快同步**,否则「底牌」一词在两份权威文档里指两批不同的牌,是最危险的那类不一致。
| `docs_dev/二七王-UI资源与精灵清单.md`(本文) | ✅ 全文新术语 |
| `server/games/erqiwang/docs/design/design.md` | ✅ 全文 38 处替换;术语表后补「术语变更记录」说明改名缘由 |
| `server/games/erqiwang/docs/protocol/packet_protocol.md` | ✅ 新增 §0.0 术语小节;字段说明同步;字段名随 S-4 拆分 |
| `mod.js` / `class.export.js` / `test/test_leak.js` 注释 | ✅ 随 S-4 一并更新 |
| `docs/compliance/*.md`(3 份,24 处) | ⬜ **有意不改**——这三份是历次核对的**历史记录**,反映当时的 design 表述;下轮核对按新 design 重做即可,不追溯修改 |
| `test/*.js` 其余注释(约 14 处) | ⬜ 仅注释、不影响行为,后续顺手清理 |
+5 -4
View File
@@ -102,7 +102,7 @@ var cls_youle_erqiwang_export = cls_youle_erqiwang_export || {
deskinfo.ChooseMain.multiple = cls_youle_erqiwang_arith.get_base_bycall(paiju.call, cfg.climb);
//选主倒计时
deskinfo.ChooseMain.countdown = o_room.o_desk.method.get_countdown_xuanzhu();
//底牌:暗牌只有庄家可见(70分的3秒亮牌是上庄时的一次性事件,重连不重放)
//底牌(发牌时没发给玩家、扣在桌面的 8 张):只有庄家可见(70分的3秒亮牌是上庄时的一次性事件,重连不重放)
if (seat == paiju.banker){
deskinfo.ChooseMain.bottomcards = paiju.method.get_bottomcards();
}
@@ -123,7 +123,7 @@ var cls_youle_erqiwang_export = cls_youle_erqiwang_export || {
deskinfo.BuryCards.multiple = cls_youle_erqiwang_arith.get_base_bycall(paiju.call, cfg.climb);
//埋牌倒计时
deskinfo.BuryCards.countdown = o_room.o_desk.method.get_countdown_maipai();
//底牌:暗牌只有庄家可见
//底牌(发牌留桌的 8 张):只有庄家可见
if (seat == paiju.banker){
deskinfo.BuryCards.bottomcards = paiju.method.get_bottomcards();
}
@@ -146,8 +146,9 @@ var cls_youle_erqiwang_export = cls_youle_erqiwang_export || {
//捡分
var jian_grade = paiju.method.get_jian_grade();
if (seat == paiju.banker){
//埋的牌
deskinfo.PushCards.bottomcards = paiju.method.get_burycard();
//埋牌底牌(庄家埋下的 8 张)。与「底牌」(发牌留桌 8 张,见 ChooseMain/BuryCards.bottomcards)
//是两批不同的牌,字段名刻意区分,勿混用
deskinfo.PushCards.burycards = paiju.method.get_burycard();
} else {
//闲家当前的捡分分牌
deskinfo.PushCards.gradecards = jian_grade.cards;
@@ -15,14 +15,15 @@
| **底牌** | 发牌时**没有发给玩家**、扣在桌面的 8 张 | ~~暗牌~~ |
| **埋牌底牌** | 庄家**埋牌**时从手里扣下的 8 张 | ~~底牌~~ |
⚠️ **两批牌目前共用同一个字段名 `bottomcards`**,只能靠「出现在哪个包」区分语义:
**两批牌的字段名已拆分**(2026-08-25,原先共用 `bottomcards`):
| 字段位置 | 实际含义 |
| --- | --- |
| `shangzhuang.bottomcards`、`deskinfo.ChooseMain.bottomcards`、`deskinfo.BuryCards.bottomcards` | **底牌** |
| `maipai.bottomcards`、`deskinfo.PushCards.bottomcards`、结算 `bottom.cards` | **埋牌底牌** |
| 字段名 | 含义 | 出现在 |
| --- | --- | --- |
| **`bottomcards`** | **底牌** | `shangzhuang`、`deskinfo.ChooseMain`、`deskinfo.BuryCards` |
| **`burycards`** | **埋牌底牌** | `maipai`、`deskinfo.PushCards` |
| `bottom.cards` | **埋牌底牌** | 结算包的 `bottom` 分组(分组名已表明是抠底相关,字段未改名) |
字段名拆分(底牌保持 `bottomcards`、埋牌底牌改 `burycards`)的计划见 `docs_dev/二七王-UI资源与精灵清单.md` §7.5 的 **S-4**;**在拆分落地前,收发两侧都必须按上表判断语义**。
同一个包里不会同时出现这两个字段。**下发面**:`burycards` 与 `bottomcards`(非 70 分时)都**只发给庄家**,闲家两者皆无——已由 `test/test_leak.js` 的泄露审计覆盖。
### 0.1 成败标志 `data.success`
@@ -227,7 +228,7 @@
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| cards | 数组 | 埋牌后手上的牌,去掉了埋牌,庄家才有此属性,闲家没有该属性 |
| bottomcards | 数组 | **埋牌底牌**(庄家埋下的 8 张),庄家才有此属性,闲家没有该属性。⚠️ 与 `shangzhuang.bottomcards`(底牌)**同名不同义**,见文首「术语」|
| burycards | 数组 | **埋牌底牌**(庄家埋下的 8 张),庄家才有此属性,闲家没有该属性。注意与 `shangzhuang.bottomcards`(**底牌**,发牌留桌 8 张)是两批不同的牌,见 §0.0 |
| seat | 整数 | 出牌者的位置序号(即将首出的庄家)|
| countdown | 整数 | 出牌倒计时 |
| liangpai | json | 庄家亮牌信息(design §8.2),**只有闲家、且可查牌模式、且庄家埋牌后手牌达标时才有**。结构(各字段按是否达标出现):`zhu` 主牌总数、`zhupair` 主对子数、`zhutuo` 主拖拉机组数(固定主牌王+2+7 总数 ≥10 时有这三项);`wang` 王数(王≥3 时);`qi` 7 数(7≥6 时);`er` 2 数(2≥6 时)。都不达标则无此属性。统计口径是庄家**埋牌后 28 张的静态快照**(排除已埋的 8 张、但包含之后已打出的牌),全局固定不随出牌缩水,故重连包里的同名字段取值一致 |
@@ -543,7 +544,7 @@
| 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 张),只有庄家有此属性。⚠️ 与 `ChooseMain`/`BuryCards.bottomcards`(底牌)**同名不同义** |
| burycards | 数组 | **埋牌底牌**(庄家埋下的 8 张),只有庄家有此属性。注意与 `ChooseMain`/`BuryCards.bottomcards`(**底牌**)是两批不同的牌,见 §0.0 |
| grade | 整数 | 当前的捡分分数 |
| gradecards | 数组 | 当前的捡分分牌,只有闲家有此属性 |
| playproc | json | 当前出牌情况 `o_paiju.playproc`:`round` 第几轮、`start` 本轮首出位置、`currseat` 当前出牌位置、`startcount` 首出张数、`startflower` 首出花色、`starttype` 首出牌型、`maxseat` 本轮最大者位置、`maxcard` 最大牌编码、`cards` 本轮三家各自出的牌(数组下标=位置序号)、`shuai_demand` 首家甩牌的分量需求 `{tractors:[连对数...],pairs,singles}`(非甩牌为 null,供跟牌逐分量强制匹配,design §5.4.4)。**两种查牌模式下恒有此属性**——`cards` 是当前这一轮桌面上的牌,不属于「查牌」,屏蔽了后出的人就无从跟牌(design §9 末尾)|
+7 -5
View File
@@ -155,7 +155,7 @@ youle_erqiwang.jiaofen = function(pack){
} else {
msg.data.touxiang = 1;
}
//70分坐庄:庄家摸暗牌前,8张暗牌向所有玩家亮3秒(design §4/§7.1)
//70分坐庄:庄家摸底牌前,8张底牌向所有玩家亮3秒(design §4/§7.1)
if (o_paiju.call == 70){
msg.data.ancard3s = 1;
}
@@ -165,12 +165,12 @@ youle_erqiwang.jiaofen = function(pack){
msg.conmode = o_room.seatlist[i].conmode;
msg.fromid = o_room.seatlist[i].fromid;
if (i == o_paiju.banker){
//庄家:手牌(含摸起的暗牌)与暗牌本身都可见
//庄家:手牌(含摸起的底牌)与底牌本身都可见
msg.data.cards = o_paiju.method.get_seat_cards(i);
msg.data.bottomcards = _bottomcards;
} else {
delete msg.data.cards;
//闲家:仅70分坐庄时才下发暗牌(供亮3秒),否则暗牌只有庄家可见
//闲家:仅70分坐庄时才下发底牌(供亮3秒),否则底牌只有庄家可见
if (o_paiju.call == 70){
msg.data.bottomcards = _bottomcards;
} else {
@@ -322,11 +322,13 @@ youle_erqiwang.maipai = function(pack){
msg.fromid = o_room.seatlist[i].fromid;
if (i == o_paiju.banker){
msg.data.cards = o_paiju.method.get_seat_cards(i);
msg.data.bottomcards = cards;
//埋牌底牌(本次埋下的 8 张)。与 shangzhuang.bottomcards(底牌,发牌留桌 8 张)
//是两批不同的牌,字段名刻意区分,勿混用
msg.data.burycards = cards;
delete msg.data.liangpai;
} else {
delete msg.data.cards;
delete msg.data.bottomcards;
delete msg.data.burycards;
//闲家:可查牌且庄家达标时亮牌
if (_liangpai){
msg.data.liangpai = _liangpai;
+5 -5
View File
@@ -1,7 +1,7 @@
// 下发面泄露审计(server 红线「发全 ≠ 发多,按可见性下发」+ design §4/§9/§11)
// 跑完整局,对每一个「服务器 → 某座位」的包做深度扫描,取出其中出现的所有牌 id,
// 逐个判定「此刻该座位是否有权知道这张牌」。依据 design §4(暗牌只有庄家可见 / 70分亮3秒)、
// §9(查牌模式)、§11(结束亮底牌)与 server 红线「发全 ≠ 发多,按可见性下发」。
// 逐个判定「此刻该座位是否有权知道这张牌」。依据 design §4(底牌只有庄家可见 / 70分亮3秒)、
// §9(查牌模式)、§11(结束亮埋牌底牌)与 server 红线「发全 ≠ 发多,按可见性下发」。
const R = require('./_rpc.js'); const mod = R.mod;
const P = global.cls_youle_erqiwang_paiju, A = global.cls_youle_erqiwang_arith;
const D = require('../class.desk.js'), EX = require('../class.export.js');
@@ -14,7 +14,7 @@ const pk = (s, d) => ({ conmode: 0, fromid: s, data: Object.assign({ agentid: 1,
// 深度收集一个对象里所有「看起来是牌 id」的整数(0~107)。
// 为避免把 seat/count/grade 之类误当牌 id,只扫描已知承载牌 id 的字段名。
const CARD_FIELDS = ['cards', 'bottomcards', 'cardsinhand', 'zhucards', 'gradecards', 'pushlist', 'MyCards'];
const CARD_FIELDS = ['cards', 'bottomcards', 'burycards', 'cardsinhand', 'zhucards', 'gradecards', 'pushlist', 'MyCards'];
function collectCards(node, key, out) {
if (node === null || node === undefined) return;
if (Array.isArray(node)) { node.forEach(x => collectCards(x, key, out)); return; }
@@ -31,9 +31,9 @@ function mayKnow(pj, seat, cid, rpc) {
if (c.dealowner === -1) return true; // 规则去除的 3/4,不可能出现
if (c.playround > 0) return true; // 已打出,全场可见
if (c.dealowner === seat + 1) return true; // 自己的牌
if (seat === pj.banker && (c.dealowner === 0 || c.dealowner === seat + 1)) return true; // 庄家可见暗牌/底牌
if (seat === pj.banker && (c.dealowner === 0 || c.dealowner === seat + 1)) return true; // 庄家可见底牌/埋牌底牌
if (rpc === 'shangzhuang' && pj.call === 70 && c.dealowner === 0) return true; // §4 70分亮3秒
if (rpc === 'jiesuan' && c.playround === 0) return true; // §11 结束亮底牌
if (rpc === 'jiesuan' && c.playround === 0) return true; // §11 结束亮埋牌底牌
if (rpc === 'mingpai') return true; // §9 明牌:单独在下面按内容校验
return false;
}
+28
View File
@@ -137,6 +137,34 @@ t.eq('埋牌 不查牌 闲家无liangpai', mpXian && mpXian.data.liangpai, undef
t.eq('重连 可查牌 PushCards有seatlist', E.get_deskinfo(e.o_room, 1).PushCards.seatlist !== undefined, true);
t.eq('重连 不查牌 PushCards无seatlist', E.get_deskinfo(en.o_room, 1).PushCards.seatlist, undefined);
// ===================== 底牌 vs 埋牌底牌:字段名必须分开(S-4)=====================
// 术语:底牌 = 发牌时没发给玩家、扣在桌面的 8 张(字段 bottomcards)
// 埋牌底牌 = 庄家埋牌扣下的 8 张(字段 burycards)
// 两者曾共用 bottomcards、只靠所在包区分语义,已拆分。以下用例锁死拆分结果,防回归。
const mpAll = e.sent.filter(m => m.rpc === 'maipai');
const mpZhuang = mpAll.find(m => m.data.cards !== undefined); // 庄家包(带手牌)
const mpXian2 = mpAll.find(m => m.data.cards === undefined); // 闲家包
// 正面:庄家的 maipai 包带 burycards,且就是埋下的那 8 张
t.eq('maipai 庄家有burycards', mpZhuang && Array.isArray(mpZhuang.data.burycards), true);
t.eq('maipai burycards为8张', mpZhuang && mpZhuang.data.burycards.length, 8);
// 反面:maipai 包里不得再出现 bottomcards(改名漏改会被这条抓住)
t.eq('maipai 庄家无bottomcards', mpZhuang && mpZhuang.data.bottomcards, undefined);
// 可见性:闲家两个字段都不该有(埋牌底牌只有庄家可见)
t.eq('maipai 闲家无burycards', mpXian2 && mpXian2.data.burycards, undefined);
t.eq('maipai 闲家无bottomcards', mpXian2 && mpXian2.data.bottomcards, undefined);
// 重连 PushCards:庄家有 burycards、无 bottomcards;闲家两者皆无
const pcZhuang = E.get_deskinfo(e.o_room, 0).PushCards;
const pcXian = E.get_deskinfo(e.o_room, 1).PushCards;
t.eq('重连 PushCards 庄家有burycards', Array.isArray(pcZhuang.burycards), true);
t.eq('重连 PushCards 庄家burycards为8张', pcZhuang.burycards.length, 8);
t.eq('重连 PushCards 庄家无bottomcards', pcZhuang.bottomcards, undefined);
t.eq('重连 PushCards 闲家无burycards', pcXian.burycards, undefined);
t.eq('重连 PushCards 闲家无bottomcards', pcXian.bottomcards, undefined);
// 对照:底牌一侧字段名不受影响,仍叫 bottomcards(ChooseMain/BuryCards 为庄家专属)
const cmZhuang = E.get_deskinfo(e.o_room, 0);
t.eq('重连 ChooseMain/BuryCards 侧仍用bottomcards命名未被误改',
cmZhuang.PushCards.burycards !== undefined && cmZhuang.PushCards.bottomcards === undefined, true);
// ===================== §5.4 甩牌:chupai1 需下发分量构成 shuai(cardtype 表达不了甩牌)=====================
// 主花色=♦(1)。庄家(0) 甩「主♦K对 + 主♦5单张」;两闲家手上一张主牌都没有 → 甩牌合法
function mkShuai(extraToSeat1) {