二七王:亮牌改为下发庄家全部固定主牌的具体牌面
规则变更(design §8.2):原先「只亮数量/结构,不亮具体是哪几张牌」,现改为
向两个闲家亮出【庄家手中全部固定主牌的具体牌面】。
- 固定主牌 = 双王 + 全部花色的 2 + 全部花色的 7,【不含】主花色普通牌
A/K/Q/J/10/9/8/6/5。注意这与旧实现收集的「所有主牌」范围不同。
- 门槛不变(固定主牌总数 ≥10 / 王 ≥3 / 7 ≥6 / 2 ≥6,任一满足)。
- 不限叫分;仅可查牌模式下发;亮出的是固定的一份,不随被哪条门槛触发而增减。
- 协议 liangpai 由 {zhu,zhupair,zhutuo,wang,qi,er} 数量结构改为 {cards:[牌id...]},
数量交由前端从 cards 自行计算。
服务端 get_liangpai() 重写(class.paiju.js);design §8.2 整节重写,术语表、
易混对照表、§9 相应更新;协议 §0.0b 与 maipai / PushCards 两处字段说明同步。
测试:
- test_paiju 亮牌用例按新规则重写(54 checks)。新增两条针对性用例:
「只含固定主牌·主花色普通牌未混入」与「同时满足多档仍只亮同一份」——
前者正是这次范围变更最容易写错的地方。
- test_leak 增加亮牌可见性规则。关键点:期望集合【独立于 get_liangpai 重算】,
而不是复用被测实现——否则实现若误塞非固定主牌,审计会跟着放行、抓不住。
反向验证证实了这一点:故意让 get_liangpai 混入主花色普通牌后,审计确实报警。
发现并修复一处假绿:原 6 局随机牌序下庄家一次都没达到亮牌门槛,即
「下发面无越权泄露」对亮牌这条路径根本没走到。现追加换牌序的 4 局(保留
原 6 局覆盖不动),并加两条覆盖断言钉住「可查牌达标 ≥1 局」「不查牌达标
≥1 局」,防止以后再退化成碰巧没触发。反向验证:去掉不查牌门控后审计立即
报出 5 张越权牌。
待定:T-34 亮牌的展示时机(出牌前一次性弹出 / 随时可查)。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -762,49 +762,42 @@ var cls_youle_erqiwang_paiju = cls_youle_erqiwang_paiju || {
|
||||
//手牌来源必须是 get_seat_cards_award 的【静态快照】(埋牌后 28 张,含之后已打出的),
|
||||
//不能用 get_seat_cards(只取未出的牌):出牌开始亮出的这份统计全局固定、不随出牌缩水,
|
||||
//否则闲家断线重连后 get_deskinfo 会按庄家当前剩余手牌重算,亮牌信息缩水甚至变成 null
|
||||
//亮牌(design §8.2):庄家埋牌后、出牌前,若手中【固定主牌】达到门槛,
|
||||
//就向两个闲家亮出【自己全部固定主牌的具体牌面】。
|
||||
// 固定主牌 = 双王 + 全部花色的 2 + 全部花色的 7,【不含】主花色的普通牌 A/K/Q/J/10/9/8/6/5
|
||||
// 门槛(任一满足即亮):固定主牌总数 ≥10 / 王 ≥3 / 7 ≥6 / 2 ≥6
|
||||
// 不限叫分;是否真的发给闲家由调用方按查牌模式门控(design §9)
|
||||
//返回 { cards: [牌id...] }(按本局主牌序从大到小),不达标返回 null。
|
||||
//注意:亮出的内容是固定的一份,不随"被哪一条门槛触发"而增减。
|
||||
get_liangpai: function(o_paiju){
|
||||
if (o_paiju.banker < 0){
|
||||
return null;
|
||||
}
|
||||
//静态初始手牌快照(与 8.1 算奖同源):庄家埋牌后的 28 张
|
||||
var cards = cls_youle_erqiwang_paiju.get_seat_cards_award(o_paiju, o_paiju.banker);
|
||||
var wang = 0, qi = 0, er = 0;
|
||||
var zhucards = [];
|
||||
var gudingcards = []; //固定主牌
|
||||
for (var i = 0; i < cards.length; i++){
|
||||
var num = cls_youle_erqiwang_arith.id_to_number(cards[i]);
|
||||
var flw = cls_youle_erqiwang_arith.id_to_flower(cards[i]);
|
||||
if (num >= 53){ wang = wang + 1; }
|
||||
if (num == 7){ qi = qi + 1; }
|
||||
if (num == 2){ er = er + 1; }
|
||||
//主牌 = 主花色普通牌 或 王 或 任意 2/7
|
||||
if (flw == o_paiju.flower || flw == 5 || num == 2 || num == 7){
|
||||
zhucards.push(cards[i]);
|
||||
if (num >= 53){ //大王 54 / 小王 53
|
||||
wang = wang + 1;
|
||||
gudingcards.push(cards[i]);
|
||||
} else if (num == 7){ //任意花色的 7
|
||||
qi = qi + 1;
|
||||
gudingcards.push(cards[i]);
|
||||
} else if (num == 2){ //任意花色的 2
|
||||
er = er + 1;
|
||||
gudingcards.push(cards[i]);
|
||||
}
|
||||
}
|
||||
var guding = wang + qi + er; //固定主牌(王 + 全部 2 + 全部 7)
|
||||
var re = {};
|
||||
var has = false;
|
||||
if (guding >= 10){
|
||||
zhucards = cls_youle_erqiwang_arith.order_cards(o_paiju.flower, zhucards);
|
||||
var pairs = cls_youle_erqiwang_arith.get_pairlist(o_paiju.flower, zhucards);
|
||||
var comps = cls_youle_erqiwang_arith.decompose_trump(o_paiju.flower, zhucards);
|
||||
var tuo = 0;
|
||||
for (var c = 0; c < comps.length; c++){
|
||||
if (comps[c].type == "tractor"){ tuo = tuo + 1; }
|
||||
}
|
||||
re.zhu = zhucards.length; //主牌总数
|
||||
re.zhupair = pairs.length; //主对子数
|
||||
re.zhutuo = tuo; //主拖拉机组数
|
||||
has = true;
|
||||
}
|
||||
if (wang >= 3){ re.wang = wang; has = true; }
|
||||
if (qi >= 6){ re.qi = qi; has = true; }
|
||||
if (er >= 6){ re.er = er; has = true; }
|
||||
if (!has){
|
||||
//门槛判定(design §8.2)
|
||||
if (gudingcards.length < 10 && wang < 3 && qi < 6 && er < 6){
|
||||
return null;
|
||||
}
|
||||
var re = {};
|
||||
re.cards = cls_youle_erqiwang_arith.order_cards(o_paiju.flower, gudingcards);
|
||||
return re;
|
||||
},
|
||||
|
||||
//获取抠底包
|
||||
get_bottom_account: function(o_paiju, msg){
|
||||
msg.data.bottom = {};
|
||||
|
||||
@@ -47,9 +47,9 @@
|
||||
| 埋牌 | 庄家看到底牌、摸起查看后,从手牌中选出 8 张重新扣下,作为"埋牌底牌" |
|
||||
| 捡分 | 出牌中赢下的分牌(5、10、K)计入闲家总得分的过程 |
|
||||
| 扣底 | 出完所有手牌后的最后一轮,若被闲家用主牌牌型压过庄家,则埋下的 8 张埋牌底牌也计入闲家捡分,并按牌型翻倍 |
|
||||
| 亮牌 | 庄家埋牌后手牌达到门槛(固定主牌 ≥10 / 王 ≥3 / 7 ≥6 / 2 ≥6)时,向两个闲家公开对应的**数量与结构**统计——**只亮数量 / 结构,不亮具体是哪几张牌**(见第 8.2 节)|
|
||||
| 亮牌 | 庄家埋牌后、出牌前,若手中固定主牌达到门槛(总数 ≥10 / 王 ≥3 / 7 ≥6 / 2 ≥6 任一),向两个闲家**亮出自己全部固定主牌的具体牌面**(见第 8.2 节)。不限叫分,仅可查牌模式 |
|
||||
| 余主公示 | 任一玩家主牌出空(报无主)后,为**全体三人**展示另外两家各自的**剩余主牌数与剩余主对数**(见第 9 节)。只给数量,不给具体牌面 |
|
||||
| 明牌 | 可查牌模式下,查看另外两家手中**全部未出主牌的具体牌面**(见第 9 节)。与"亮牌""余主公示"只给数量不同,明牌给的是具体牌 |
|
||||
| 明牌 | 可查牌模式下,查看**另外两家**手中全部未出主牌的具体牌面(见第 9 节)。与"亮牌"同样给牌面,区别在:亮牌是**庄家公开自己的**,明牌是**去看别人的** |
|
||||
| 摸底 | 庄家坐定后把那 8 张**底牌**摸起查看并并入手牌(见第 4 节)。**非 70 分坐庄时只有庄家看得到这 8 张** |
|
||||
| 开底 | **70 分坐庄**时,在庄家摸底**之前**,先把这 8 张底牌向**所有玩家**翻开 3 秒,之后才由庄家摸入手牌(见第 4 节)。给的是具体牌面 |
|
||||
| 查底牌 | 对局中随时回看那 8 张底牌的功能(前端底栏按钮)。庄家全程可用;闲家仅在**开底**过的局(即 70 分坐庄)可用 |
|
||||
@@ -73,12 +73,12 @@
|
||||
>
|
||||
> | 术语 | 谁公开给谁 | 给的是什么 | 触发条件 | 协议字段 |
|
||||
> | --- | --- | --- | --- | --- |
|
||||
> | **亮牌** | 庄家 → 两个闲家 | **只有数量/结构** | 庄家埋牌后手牌达门槛(§8.2) | `liangpai` |
|
||||
> | **亮牌** | 庄家 → 两个闲家 | **具体牌面**(庄家全部固定主牌) | 庄家埋牌后手牌达门槛(§8.2) | `liangpai` |
|
||||
> | **余主公示** | 全体 → 全体 | **只有数量**(剩余主牌数、主对数) | 任一玩家报无主(§9) | `seatlist[seat][4]` + `baozhu` |
|
||||
> | **明牌** | 他家 → 请求者 | **具体牌面**(他家全部未出主牌) | 可查牌 + 报无主,主动点击(§9) | `mingpai.others[].zhucards` |
|
||||
> | **开底** | 桌面 → 所有玩家 | **具体牌面**(8 张底牌) | 70 分坐庄,**摸底之前** 3 秒(§4) | `ancard3s` + `bottomcards` |
|
||||
>
|
||||
> 一句话记:**「亮牌」「余主公示」是统计类,「明牌」「开底」是牌面类**。
|
||||
> 一句话记:**只有「余主公示」是统计类(给数字),「亮牌」「明牌」「开底」都给具体牌面**——区别在给谁的牌:亮牌给庄家自己的、明牌给他家的、开底给桌上那 8 张。
|
||||
> 另有一组「底」字族专管那 8 张底牌:**摸底**(庄家摸起看)、**开底**(70 分时全场看 3 秒)、**查底牌**(随时回看)、**扣底**(末轮翻开埋牌底牌计分)。
|
||||
> 另注:**「亮主」不是本文档的术语**——它是前端选主界面的标题文案(庄家选主牌花色那一步),与上表四项无关。
|
||||
|
||||
@@ -589,7 +589,7 @@ A - K - Q - J - 10 - 9 - 8 - 6 - 5
|
||||
|
||||
**算奖依据的手牌快照时间点**:不管是 8.1(只看庄家)还是 8.3 傍王(庄闲都算),都按**静态初始手牌**计算——庄家是埋牌完成后的那一刻手牌(28 张),闲家是发牌完成后的那 28 张手牌;这个快照全程固定,不随之后的出牌、被吃、被打出而改变。**例外**:70 分投降局庄家未选主也未埋牌,其快照为"发牌 + 底牌"共 36 张(无主牌花色),见 8.4 节。
|
||||
|
||||
**算奖(8.1 / 8.3)与亮牌(8.2)是两个互不干涉的独立概念**:算奖是结算阶段"该给多少额外结算分"的规则;亮牌只是出牌开始前"要不要向对手公开一部分手牌统计信息"的展示规则。两者各自有自己的触发条件,只是恰好都以庄家埋牌后的手牌结构为依据,因此部分门槛数值相同——但达成亮牌的条件不代表一定触发算奖,反之亦然,具体对照见 8.2 末尾的说明。
|
||||
**算奖(8.1 / 8.3)与亮牌(8.2)是两个互不干涉的独立概念**:算奖是结算阶段"该给多少额外结算分"的规则;亮牌只是出牌开始前"要不要向对手公开一部分手牌"的展示规则。两者各自有自己的触发条件,只是恰好都以庄家埋牌后的手牌结构为依据,因此部分门槛数值相同——但达成亮牌的条件不代表一定触发算奖,反之亦然,具体对照见 8.2 末尾的说明。
|
||||
|
||||
### 8.1 常规算奖
|
||||
|
||||
@@ -614,18 +614,25 @@ A - K - Q - J - 10 - 9 - 8 - 6 - 5
|
||||
|
||||
### 8.2 亮牌规则
|
||||
|
||||
**亮牌与算奖(8.1 / 8.3)是两个独立概念,互不干涉**:亮牌只是"要不要向对手公开一部分手牌统计信息"的展示规则,本身不产生任何奖数、不影响结算;算奖该怎么算、算多少,完全按 8.1 / 8.3 的规则来,不因为触发了亮牌而增加或减少。
|
||||
**亮牌与算奖(8.1 / 8.3)是两个独立概念,互不干涉**:亮牌只是"要不要向对手公开一部分手牌"的展示规则,本身不产生任何奖数、不影响结算;算奖该怎么算、算多少,完全按 8.1 / 8.3 的规则来,不因为触发了亮牌而增加或减少。
|
||||
|
||||
**亮牌要求**(仅针对庄家):庄家埋牌后,若剩余手牌满足以下任一条件,出牌开始需要向两个闲家亮出对应的统计信息——**只亮数量 / 结构,不亮具体是哪几张牌**:
|
||||
**亮牌要求**(仅针对庄家):庄家**埋牌后**、**出牌开始前**,若手中剩余的**固定主牌**满足下表任一条件,就要向两个闲家**亮出自己全部固定主牌的具体牌面**。
|
||||
|
||||
| 触发条件 | 亮出的信息 |
|
||||
| --- | --- |
|
||||
| 固定主牌(王 + 2 + 7)总数 ≥ 10 张 | 主牌总数、其中的对子数、组成的拖拉机数(如"x 张主,x 对主带 x 拖") |
|
||||
| 王 ≥ 3 张 | 王的数量(如"x 张王") |
|
||||
| 7 ≥ 6 张 | 7 的数量(如"x 张 7") |
|
||||
| 2 ≥ 6 张 | 2 的数量(如"x 张 2") |
|
||||
| 触发条件(任一满足即亮牌) |
|
||||
| --- |
|
||||
| 固定主牌(双王 + 全部 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 的算奖表里根本没有为这一档定义奖数,所以**不触发任何算奖**——如果亮牌和算奖是同一回事,这里就该矛盾;现在不矛盾,正说明两者互不干涉。
|
||||
|
||||
@@ -666,12 +673,12 @@ A - K - Q - J - 10 - 9 - 8 - 6 - 5
|
||||
1. **出牌历史**:当前牌局中,所有玩家已经打出的牌(含之前每一轮打出、已被收走的牌),任何时候都可以回看。
|
||||
2. **余主公示**:一旦有玩家的主牌全部打空(即 5.4.2 节所说的"报无主"状态;"主牌"指大王、小王、所有花色的 2、所有花色的 7,以及主花色的其余普通牌,见第 3 节),系统就为**全体三人**(含刚刚报无主的这位玩家自己)显示另外两家各自的主牌数量、以及这些主牌里有多少对子。**只给数量、不给具体牌面**——要看具体牌面是下一条的"明牌"。
|
||||
3. 同时界面上会出现一个"明牌"按钮:点击后可以查看另外两家手中全部主牌的**具体牌面**(不只是数量 / 对子结构),再点一次取消查看、恢复隐藏。
|
||||
4. 庄家若触发了 8.2 节的"亮牌要求",两个闲家可以看到对应的统计信息(只亮数量 / 结构,不亮具体牌面,见 8.2 节)。
|
||||
4. 庄家若触发了 8.2 节的"亮牌要求",两个闲家可以看到**庄家全部固定主牌的具体牌面**(见 8.2 节)。
|
||||
|
||||
- **不可查牌模式**:以上四项**一项都不提供**——
|
||||
- **不提供出牌历史**:之前各轮打出过什么牌一律不能回看,只能靠玩家自己记牌;
|
||||
- **不提供余主公示**:即使有玩家报无主,也不会显示任何人的主牌数量、对子结构,也没有"明牌"按钮;
|
||||
- 庄家即使触发了 8.2 节的亮牌条件,闲家也看不到相应的统计信息(见 8.2 节)。
|
||||
- 庄家即使触发了 8.2 节的亮牌条件,闲家也看不到那些牌(见 8.2 节)。
|
||||
|
||||
> **"查牌"与"当前这一轮桌面上的牌"是两回事**:本轮已经出过牌的玩家、打在桌面上的这几张牌,**两种模式下都必须对全体可见**——否则后出的人无从跟牌、也无从判断本轮谁最大。查牌模式管的是**已经收走的往轮牌**与**他家手牌信息**能不能回看,不影响当前这一轮的正常出牌展示(断线重连回到牌桌时同理:当前轮桌面上的牌照常恢复,往轮历史则按本节的模式开关决定给不给)。
|
||||
|
||||
|
||||
@@ -29,12 +29,12 @@
|
||||
|
||||
| 术语 | 谁公开给谁 | 给的是什么 | 触发 | 本文相关字段 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| **亮牌** | 庄家 → 两个闲家 | **只有数量/结构** | 庄家埋牌后手牌达门槛(design §8.2) | `liangpai` |
|
||||
| **亮牌** | 庄家 → 两个闲家 | **具体牌面**(庄家全部固定主牌) | 庄家埋牌后固定主牌达门槛(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 秒)、**查底牌**(对局中随时回看)、**扣底**(末轮翻开埋牌底牌计分)。
|
||||
(「亮主」不是术语,是前端选主界面的标题文案,与上表无关。)
|
||||
@@ -246,7 +246,7 @@
|
||||
| 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 张、但包含之后已打出的牌),全局固定不随出牌缩水,故重连包里的同名字段取值一致 |
|
||||
| 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 张,但包含之后已打出的牌),全局固定不随出牌缩水,故重连包里取值一致 |
|
||||
|
||||
---
|
||||
|
||||
@@ -568,7 +568,7 @@
|
||||
| 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 末尾)|
|
||||
| seatlist | 数组 | 三家座位牌况,与上文 chupai 包的 `seatlist` 同名同结构(每个玩家一个 5 元素数组:4 个花色的 `[无该花色,无对]` + 报副 `[剩余主牌数,剩余主对数]`)。**仅可查牌模式下有此属性**(design §9)|
|
||||
| liangpai | json | 庄家亮牌信息,结构同 maipai 包的 `liangpai`;**仅可查牌模式、且请求者为闲家、且庄家达标时有**(供闲家重连后仍能看到亮牌,design §8.2)|
|
||||
| liangpai | json | **亮牌**,结构同 maipai 包的 `liangpai`(`{ cards: [牌id...] }`);**仅可查牌模式、且请求者为闲家、且庄家达标时有**(供闲家重连后仍能看到,design §8.2)|
|
||||
| pushlist | 数组 | 出牌历史 `[[[], [], []], [[], [], []], ...]`,外层下标=轮次(从第 1 轮起,含进行中的当前轮),内层**恒为 3 个数组**、按位置序号存该轮各家出的牌id,各自已按**本局主牌花色**从大到小排序。**仅可查牌模式下有此属性**——往轮打出、已被收走的牌属于「查牌」范畴,不查牌模式一律不下发(design §9)。注意当前这一轮桌面上的牌由 `playproc.cards` 恢复,**两种模式下都有**,否则后出的人无从跟牌 |
|
||||
| curmultiple | 整数 | **当前抓分倍数**(design §7.2.0):与 `chupai1/2/3` 同源同值,供重连后顶部「抓分」角标立即正确 |
|
||||
| mustcard | 数组 | **本轮跟牌的必出牌**(design §5.2)。**仅当 `playproc.currseat == 请求者座位` 时才有**,且只算请求者自己的手牌;未轮到本家、本轮首家、甩牌局面、必出牌为空时均无此属性 |
|
||||
|
||||
@@ -22,6 +22,28 @@ function collectCards(node, key, out) {
|
||||
if (typeof node === 'object') { for (const k in node) collectCards(node[k], CARD_FIELDS.indexOf(k) >= 0 ? k : key, out); }
|
||||
}
|
||||
|
||||
// design §8.2 亮牌:庄家埋牌后手中【固定主牌】达门槛时,这些牌的牌面对两个闲家公开(仅可查牌模式)。
|
||||
// 【独立于 get_liangpai 重算一遍】——若实现误把非固定主牌(主花色普通牌、副牌)塞进 liangpai,
|
||||
// 下面的集合里不会有它,审计照样报泄露。这是审计的意义所在,不能图省事直接复用被测实现。
|
||||
function bankerLiangpaiCards(pj) {
|
||||
const out = [];
|
||||
let wang = 0, qi = 0, er = 0;
|
||||
for (const c of pj.cards) {
|
||||
// 与 get_seat_cards_award 同口径:庄家手牌 = 发到自己的 + 摸起的底牌,且排除已埋的
|
||||
if (!(c.dealowner === pj.banker + 1 || c.dealowner === 0)) continue;
|
||||
if (c.playround === 0) continue;
|
||||
if (c.number >= 53) { wang++; out.push(c.id); }
|
||||
else if (c.number === 7) { qi++; out.push(c.id); }
|
||||
else if (c.number === 2) { er++; out.push(c.id); }
|
||||
}
|
||||
if (out.length < 10 && wang < 3 && qi < 6 && er < 6) return new Set(); // 不达标:一张都不该下发
|
||||
return new Set(out);
|
||||
}
|
||||
let curLiangpai = new Set(); // 本局亮牌应公开的牌(埋牌后确定,全程固定)
|
||||
let curNoCheck = false; // 本局是否「不查牌」
|
||||
let liangpaiRounds = 0; // 可查牌 + 庄家达标的局数(亮牌真的下发过)
|
||||
let liangpaiRoundsNoCheck = 0; // 不查牌 + 庄家达标的局数(此时一张都不该发)
|
||||
|
||||
const leaks = [];
|
||||
let pkts = 0, scanned = 0;
|
||||
let ctxRef = null;
|
||||
@@ -35,6 +57,8 @@ function mayKnow(pj, seat, cid, rpc) {
|
||||
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 === 'mingpai') return true; // §9 明牌:单独在下面按内容校验
|
||||
// §8.2 亮牌:可查牌模式下,庄家的固定主牌牌面对闲家公开(只在 maipai / 重连包里下发)
|
||||
if (!curNoCheck && (rpc === 'maipai' || rpc === 'deskinfo') && curLiangpai.has(cid)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -45,6 +69,8 @@ function run(roomtype, call) {
|
||||
seatlist: [0, 1, 2].map(i => ({ conmode: 0, fromid: i, playerid: 100 + i, nickname: 'P', avatar: '', gameinfo: {} })),
|
||||
method: { sendpack_toother: m => sent.push({ to: 'ALL', m: JSON.parse(JSON.stringify(m)) }) }
|
||||
};
|
||||
curNoCheck = (String(roomtype).charAt(4) === '1'); // roomtype 位4:1=不查牌
|
||||
curLiangpai = new Set();
|
||||
const desk = D.new(o_room); o_room.o_desk = desk;
|
||||
global.youle_erqiwang.app = { SendPack: m => sent.push({ to: m.fromid, m: JSON.parse(JSON.stringify(m)) }) };
|
||||
global.youle_erqiwang.import = { check_player: () => o_room, deduct_roomcard: () => { }, save_grade: () => { } };
|
||||
@@ -74,7 +100,11 @@ function run(roomtype, call) {
|
||||
if (pj.step === 1) { mod.jiaofen(pk(pj.method.get_callgrade_seat(), { call: 0 })); audit(); }
|
||||
const b = pj.banker;
|
||||
mod.xuanzhu(pk(b, { flower: 1 + rnd(4) })); audit();
|
||||
mod.maipai(pk(b, { cards: P.get_seat_cards(pj, b).slice(-8) })); audit();
|
||||
// 亮牌快照在埋牌后才固定;先算出期望集合,再审计这一包(maipai 正是亮牌的下发时机)
|
||||
mod.maipai(pk(b, { cards: P.get_seat_cards(pj, b).slice(-8) }));
|
||||
curLiangpai = bankerLiangpaiCards(pj);
|
||||
if (curLiangpai.size > 0) { if (curNoCheck) { liangpaiRoundsNoCheck++; } else { liangpaiRounds++; } }
|
||||
audit();
|
||||
// 重连快照也要审计(每个座位各取一次)
|
||||
for (let s = 0; s < 3; s++) {
|
||||
const di = EX.new().get_deskinfo(o_room, s);
|
||||
@@ -122,9 +152,21 @@ function run(roomtype, call) {
|
||||
}
|
||||
|
||||
for (const rt of ['00000', '00001']) for (const call of [65, 70, 5]) run(rt, call);
|
||||
// 上面 6 局按 0xBEEF01 的牌序,庄家一次都没达到亮牌门槛(§8.2)。亮牌是【有意的信息公开】,
|
||||
// 必须真的被审计走过,否则「无越权泄露」对这条路径只是碰巧没触发。故换牌序再补几局:
|
||||
// 可查牌局验「该发的发了且不越权」,不查牌局验「达标也一张都不发」。
|
||||
seed = 0x12345;
|
||||
for (const call of [65, 5]) run('00000', call);
|
||||
seed = 0x777001;
|
||||
run('00000', 65);
|
||||
run('00001', 65);
|
||||
t.eq('下发面无越权泄露', [...new Set(leaks)].slice(0, 5), []);
|
||||
// 覆盖下限:若收集器失效(扫不到牌),上面的断言会假绿,这里钉住实际扫描量
|
||||
t.eq('审计覆盖 下发面 ≥ 1000 个', pkts >= 1000, true);
|
||||
t.eq('审计覆盖 可见性判定 ≥ 10000 次', scanned >= 10000, true);
|
||||
console.log(` [统计] 扫描 ${pkts} 个下发面 / ${scanned} 次「座位×牌」可见性判定`);
|
||||
// 覆盖下限(续):随机手牌下庄家未必达到亮牌门槛。若一局都没触发,
|
||||
// 上面「无越权泄露」对亮牌这条路径就是【碰巧没走到】而不是【验证过安全】——必须钉住。
|
||||
t.eq('审计覆盖 亮牌路径(可查牌) 至少 1 局', liangpaiRounds >= 1, true);
|
||||
t.eq('审计覆盖 亮牌路径(不查牌) 至少 1 局', liangpaiRoundsNoCheck >= 1, true);
|
||||
console.log(` [统计] 扫描 ${pkts} 个下发面 / ${scanned} 次「座位×牌」可见性判定 / 亮牌达标 可查牌${liangpaiRounds}局·不查牌${liangpaiRoundsNoCheck}局`);
|
||||
process.exit(t.done('leak') ? 0 : 1);
|
||||
|
||||
@@ -20,27 +20,73 @@ function fill(req, n) {
|
||||
}
|
||||
const mkHand = ids => ({ flower: 1, banker: 0, cards: ids.map(x => ({ id: x, playround: -1, dealowner: 1 })) });
|
||||
const lp = ids => P.get_liangpai(mkHand(fill(ids, 28)));
|
||||
t.eq('亮牌 3王', lp([big(1), small(1), small(2)]), { wang: 3 });
|
||||
t.eq('亮牌 4王+6个7(固定主10)', lp([big(1), big(2), small(1), small(2), id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 3, 7), id(2, 3, 7)]), { zhu: 10, zhupair: 5, zhutuo: 1, wang: 4, qi: 6 });
|
||||
t.eq('亮牌 6个2', lp([id(1, 1, 2), id(2, 1, 2), id(1, 2, 2), id(2, 2, 2), id(1, 3, 2), id(2, 3, 2)]), { er: 6 });
|
||||
t.eq('亮牌 2王2个7(不达标)', lp([big(1), small(1), id(1, 1, 7), id(2, 1, 7)]), null);
|
||||
t.eq('亮牌 6个7(仅qi)', lp([id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 3, 7), id(2, 3, 7)]), { qi: 6 });
|
||||
t.eq('亮牌 固定主9不达标→null', lp([big(1), small(1), id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 1, 2), id(2, 1, 2), id(1, 2, 2)]), null);
|
||||
t.eq('亮牌 固定主10边界(仅zhu)', lp([big(1), small(1), id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 1, 2), id(2, 1, 2), id(1, 2, 2), id(2, 2, 2)]), { zhu: 10, zhupair: 4, zhutuo: 1 });
|
||||
// 亮牌返回 { cards: [固定主牌id...] }(按主牌序排好);不达标返回 null。
|
||||
// 断言用「排序后的 id 集合」比较,避免把排序细节写死进用例。
|
||||
const sortIds = a => a.slice().sort((x, y) => x - y);
|
||||
const lpCards = ids => { const r = lp(ids); return r === null ? null : sortIds(r.cards); };
|
||||
|
||||
// —— 四条门槛各自能触发,且亮出的都是【全部固定主牌】——
|
||||
{
|
||||
const h = [big(1), small(1), small(2)]; // 3 王
|
||||
t.eq('亮牌 3王 → 亮出这3张王', lpCards(h), sortIds(h));
|
||||
}
|
||||
{
|
||||
const h = [id(1, 1, 2), id(2, 1, 2), id(1, 2, 2), id(2, 2, 2), id(1, 3, 2), id(2, 3, 2)]; // 6 个 2
|
||||
t.eq('亮牌 6个2 → 亮出这6张2', lpCards(h), sortIds(h));
|
||||
}
|
||||
{
|
||||
const h = [id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 3, 7), id(2, 3, 7)]; // 6 个 7
|
||||
t.eq('亮牌 6个7 → 亮出这6张7', lpCards(h), sortIds(h));
|
||||
}
|
||||
{
|
||||
const h = [big(1), small(1), id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7),
|
||||
id(1, 1, 2), id(2, 1, 2), id(1, 2, 2), id(2, 2, 2)]; // 固定主 10(各档单看都不达标)
|
||||
t.eq('亮牌 固定主10边界 → 亮出全部10张', lpCards(h), sortIds(h));
|
||||
}
|
||||
// —— 不达标 → null ——
|
||||
t.eq('亮牌 2王2个7 不达标→null', lp([big(1), small(1), id(1, 1, 7), id(2, 1, 7)]), null);
|
||||
t.eq('亮牌 固定主9 不达标→null',
|
||||
lp([big(1), small(1), id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 1, 2), id(2, 1, 2), id(1, 2, 2)]), null);
|
||||
|
||||
// —— 关键:亮出的只有【固定主牌】,主花色普通牌【不能】混进来 ——
|
||||
// 手牌里塞满主花色(♦)普通牌 A/K/Q/J/10/9/8/6/5,它们是主牌但不是固定主牌
|
||||
{
|
||||
const guding = [big(1), small(1), small(2)]; // 3 王 → 达标
|
||||
const zhuNormal = [id(1, 1, 1), id(1, 1, 13), id(1, 1, 12), id(1, 1, 11), id(1, 1, 10)]; // ♦A/K/Q/J/10
|
||||
const r = lp(guding.concat(zhuNormal));
|
||||
t.eq('亮牌 只含固定主牌 张数=3', r.cards.length, 3);
|
||||
t.eq('亮牌 主花色普通牌未混入', sortIds(r.cards), sortIds(guding));
|
||||
}
|
||||
// —— 亮出内容与「被哪条门槛触发」无关:4王+6个7 应亮出全部 10 张固定主 ——
|
||||
{
|
||||
const h = [big(1), big(2), small(1), small(2), id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 3, 7), id(2, 3, 7)];
|
||||
t.eq('亮牌 同时满足多档 → 仍只亮同一份固定主牌', lpCards(h), sortIds(h));
|
||||
}
|
||||
// —— 排序:按本局主牌序从大到小(大王 > 小王 > 正7 > 副7 > 正2 > 副2)——
|
||||
{
|
||||
const A = require('../class.arith.js');
|
||||
const h = [big(1), small(1), id(1, 1, 7), id(1, 2, 7), id(1, 1, 2), id(1, 2, 2),
|
||||
id(2, 1, 7), id(2, 2, 7), id(2, 1, 2), id(2, 2, 2)]; // 固定主 10,主花色=♦(1)
|
||||
const r = lp(h);
|
||||
t.eq('亮牌 按主牌序排好', r.cards, A.order_cards(1, r.cards.slice()));
|
||||
t.eq('亮牌 首张为大王', r.cards[0], big(1));
|
||||
}
|
||||
|
||||
// design §8.2:亮牌依据是【庄家埋牌后】的静态手牌快照,全程固定、不随出牌缩水
|
||||
// (闲家断线重连时 get_deskinfo 会再取一次;若按当前剩余手牌重算,亮牌会缩水甚至变 null)
|
||||
const LIANG28 = fill([big(1), big(2), small(1), small(2), id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 3, 7), id(2, 3, 7)], 28);
|
||||
const EXPECT_LIANG = { zhu: 10, zhupair: 5, zhutuo: 1, wang: 4, qi: 6 };
|
||||
t.eq('亮牌 埋牌后快照(出牌开始)', P.get_liangpai(mkHand(LIANG28)), EXPECT_LIANG);
|
||||
const EXPECT_LIANG = sortIds([big(1), big(2), small(1), small(2), id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 3, 7), id(2, 3, 7)]);
|
||||
t.eq('亮牌 埋牌后快照(出牌开始)', sortIds(P.get_liangpai(mkHand(LIANG28)).cards), EXPECT_LIANG);
|
||||
const played = mkHand(LIANG28); // 同一手牌,已打出2大王+1对♦7
|
||||
for (const c of played.cards){
|
||||
if (c.id === big(1) || c.id === big(2) || c.id === id(1, 1, 7) || c.id === id(2, 1, 7)){ c.playround = 1; }
|
||||
}
|
||||
t.eq('亮牌 打出4张后重连仍按快照', P.get_liangpai(played), EXPECT_LIANG);
|
||||
t.eq('亮牌 打出4张后重连仍按快照', sortIds(P.get_liangpai(played).cards), EXPECT_LIANG);
|
||||
// 埋下的牌(playround==0)不算在快照里
|
||||
const buried = mkHand(LIANG28);
|
||||
for (const c of buried.cards){ if (c.id === big(1) || c.id === big(2)){ c.playround = 0; } }
|
||||
t.eq('亮牌 已埋的牌不计入快照', P.get_liangpai(buried), { qi: 6 });
|
||||
t.eq('亮牌 已埋的牌不计入快照', sortIds(P.get_liangpai(buried).cards),
|
||||
sortIds([small(1), small(2), id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 3, 7), id(2, 3, 7)]));
|
||||
|
||||
// ============ §7/§8 结算 get_paiju_account 集成 ============
|
||||
// 受控 o_paiju/o_desk/o_room:控制庄家算奖手牌(dealowner=1)与闲1捡分(playowner=1,score)
|
||||
|
||||
Reference in New Issue
Block a user