二七王:亮牌改为下发庄家全部固定主牌的具体牌面

规则变更(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:
2026-08-25 23:46:06 +08:00
co-authored by Claude Opus 5
parent dc9123da92
commit b98f58effc
6 changed files with 171 additions and 79 deletions
+22 -29
View File
@@ -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 = {};