二七王:chupai1 下发甩牌分量构成 shuai

cardtype 只能表达单一牌型(1xx 单张 / 2xx 对子 / 3xx 拖拉机),而
甩牌是单张、对子、拖拉机自由混搭(design §5.4.3),会被 can_playcard
的牌型推导压平——例如「主K对 + 主5」得到 103、「两连对 + 散对」得到
203,客户端拿到 cardtype 无从还原分组,甚至看不出这是一次甩牌。

服务端本就已经算出了分量需求 shuai_demand(供跟牌逐分量强制匹配,
design §5.4.4),按「每个下发包必须携带前端界面所需的全部核心数据」
一并下发:chupai1 在合法甩牌时带 shuai = {tractors,pairs,singles},
非甩牌与甩错退化为单张时不带此属性。

test_rpc 新增 8 项:合法甩牌带 shuai、非甩牌不带、甩错只打最小一张
且不带 shuai。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-12 04:21:11 +08:00
co-authored by Claude Opus 5
parent 4276d38267
commit 8948a2950f
3 changed files with 55 additions and 0 deletions
+5
View File
@@ -638,6 +638,11 @@ var cls_youle_erqiwang_paiju = cls_youle_erqiwang_paiju || {
re1.cardtype = can.cardtype; re1.cardtype = can.cardtype;
re1.cards = cards; //实际打出的牌(甩错时为单张) re1.cards = cards; //实际打出的牌(甩错时为单张)
re1.shuaicuo = _shuaicuo; re1.shuaicuo = _shuaicuo;
//合法甩牌的分量构成:cardtype 只能表达单一牌型,甩牌(单张/对子/拖拉机自由混搭,
//design §5.4.3)会被压平成 1xx/2xx,客户端无从据此还原分组,故另行下发分量需求
if (o_paiju.playproc.shuai_demand){
re1.shuai = o_paiju.playproc.shuai_demand;
}
return re1; return re1;
} }
+5
View File
@@ -477,6 +477,11 @@ youle_erqiwang.chupai = function(pack){
msg.data.count = re.count; msg.data.count = re.count;
msg.data.flower = re.flower; msg.data.flower = re.flower;
msg.data.cardtype = re.cardtype; msg.data.cardtype = re.cardtype;
//合法甩牌的分量构成(design §5.4.3),供前端按 拖拉机/对子/单张 分组展示;
//非甩牌(含甩错退化为单张)无此属性
if (re.shuai){
msg.data.shuai = re.shuai;
}
break; break;
case 2: //第二个出牌 case 2: //第二个出牌
msg.rpc = "chupai2"; msg.rpc = "chupai2";
+45
View File
@@ -133,6 +133,51 @@ 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(e.o_room, 1).PushCards.seatlist !== undefined, true);
t.eq('重连 不查牌 PushCards无seatlist', E.get_deskinfo(en.o_room, 1).PushCards.seatlist, undefined); t.eq('重连 不查牌 PushCards无seatlist', E.get_deskinfo(en.o_room, 1).PushCards.seatlist, undefined);
// ===================== §5.4 甩牌:chupai1 需下发分量构成 shuai(cardtype 表达不了甩牌)=====================
// 主花色=♦(1)。庄家(0) 甩「主♦K对 + 主♦5单张」;两闲家手上一张主牌都没有 → 甩牌合法
function mkShuai(extraToSeat1) {
const pj = {
idx: 1, step: 5, banker: 0, call: 65, flower: 1, callproc: [],
seatlist: [[[0, 0], [0, 0], [0, 0], [0, 0], [-1, -1]], [[0, 0], [0, 0], [0, 0], [0, 0], [-1, -1]], [[0, 0], [0, 0], [0, 0], [0, 0], [-1, -1]]],
playproc: {}, cards: make108()
};
for (let i = 0; i < 108; i++) pj.cards[i].dealowner = -1; // 先全部清空
const give = (cid, s) => { pj.cards[cid].dealowner = s + 1; pj.cards[cid].playround = -1; };
give(id(1, 1, 13), 0); give(id(2, 1, 13), 0); give(id(1, 1, 5), 0); // 庄家:主♦K对 + 主♦5
give(id(1, 3, 9), 1); give(id(1, 3, 8), 1); give(id(2, 3, 9), 1); // 闲1:全副♥,无主牌
give(id(1, 2, 9), 2); give(id(1, 2, 8), 2); give(id(2, 2, 9), 2); // 闲2:全副♣,无主牌
if (extraToSeat1) give(extraToSeat1, 1); // 需要造甩错时给闲1一张大主牌
pj.method = {
check_cards_valid: c => P.check_cards_valid(pj, c),
check_cards_inhand: (c, s) => P.check_cards_inhand(pj, c, s),
do_playcard: c => P.do_playcard(pj, c),
get_seat_cards: s => P.get_seat_cards(pj, s),
have_baofu: () => (pj.seatlist[0][4][0] == 0 || pj.seatlist[1][4][0] == 0 || pj.seatlist[2][4][0] == 0)
};
P.new_playround(pj, 1, 0);
return pj;
}
// 合法甩牌 → 带 shuai 分量
let sh = mkShuai(null); let shc = setup("00000", sh);
mod.chupai(pack(0, { cards: [id(1, 1, 13), id(2, 1, 13), id(1, 1, 5)] }));
let shPk = shc.sent.filter(m => m.rpc === 'chupai1')[0];
t.eq('甩牌 合法 下发 chupai1', !!shPk, true);
t.eq('甩牌 打出3张', shPk && shPk.data.cards.length, 3);
t.eq('甩牌 shuai=1对+1单张', shPk && shPk.data.shuai, { tractors: [], pairs: 1, singles: 1 });
t.eq('甩牌 无 shuaicuo', shPk && shPk.data.shuaicuo, undefined);
// 非甩牌(单一对子)→ 无 shuai
sh = mkShuai(null); shc = setup("00000", sh);
mod.chupai(pack(0, { cards: [id(1, 1, 13), id(2, 1, 13)] }));
shPk = shc.sent.filter(m => m.rpc === 'chupai1')[0];
t.eq('非甩牌(单一对子) 无 shuai', shPk && shPk.data.shuai, undefined);
// 甩错(闲1 持大王,能压过主K对里的单张分量)→ 收回、只打最小一张、无 shuai
sh = mkShuai((2 - 1) * 54 + 53); shc = setup("00000", sh); // 第二副大王给闲1
mod.chupai(pack(0, { cards: [id(1, 1, 13), id(2, 1, 13), id(1, 1, 5)] }));
shPk = shc.sent.filter(m => m.rpc === 'chupai1')[0];
t.eq('甩错 shuaicuo=1', shPk && shPk.data.shuaicuo, 1);
t.eq('甩错 只打出最小一张(主♦5)', shPk && shPk.data.cards, [id(1, 1, 5)]);
t.eq('甩错 无 shuai 分量', shPk && shPk.data.shuai, undefined);
// ===================== 重连 PushCards.pushlist:结构恒 3 座位 + 按本局主牌花色排序 ===================== // ===================== 重连 PushCards.pushlist:结构恒 3 座位 + 按本局主牌花色排序 =====================
// 造 5 轮已出牌的牌局(每轮每家 1 张),外加 seat0 在第 1 轮同时出「主5 + 副♥A」用于验证排序 // 造 5 轮已出牌的牌局(每轮每家 1 张),外加 seat0 在第 1 轮同时出「主5 + 副♥A」用于验证排序
function mkPushlist() { function mkPushlist() {