二七王:maipai 补发 playproc(增量路径与重连路径对齐)

前端一致性测试剩余 3 红的根因:do_burycard() 内部已调 new_playround 初始化好
round-1 的进行态,但 mod.js 组 maipai 包时没有放 playproc(协议 §9 字段表也没有),
「埋牌完成 → 庄家首出」这段窗口增量路径 table.playproc 为 null,重连路径却已有初值——
与上一个 commit(8452535)给 maipai 补 seatlist 是同一个窗口、同一条理由,被漏在了同一处。

修法:复用既有的 get_playproc() 快照函数(与 chupai1/2/3、PushCards.playproc 同源,
本身就是逐字段深拷贝,不引入活引用风险)。可见性:此刻一张牌未出,round/start/currseat
等全部由桌面公开信息推出,cards 全空、shuai_demand 为 null,不含私密信息,故与
chupai1/2/3.playproc 一样三家整体下发、不受查牌模式门控。

协议文档 §9 同步补字段说明。test_rpc.js 新增 5 checks:庄闲同值、与重连快照比对、
round-1 初值断言、不查牌模式下仍恒有、深拷贝守卫;已用「退回修复必转红」反向验证
(3/5 转红)。既有 121 checks 未改一条断言,全绿(17 脚本 / 675 checks)。未涉及 shared/。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-27 19:46:32 +08:00
co-authored by Claude Opus 5
parent 729128aa9d
commit cdf2bc730e
3 changed files with 39 additions and 0 deletions
+31
View File
@@ -225,6 +225,37 @@ t.eq('埋牌 seatlist 埋牌完成时为初始化空表', JS(mpZhuang3.data.seat
t.eq('埋牌 不查牌 maipai 无 seatlist',
en.sent.filter(m => m.rpc === 'maipai').every(m => m.data.seatlist === undefined), true);
// ---- ③b maipai.playproc:与 PushCards.playproc 同一个快照函数、同结构,且是深拷贝 ----
// 曾经的缺陷:mod.js 组 maipai 包时没有放 playproc(do_burycard 内部已 new_playround 就地初始化好
// round-1 的进行态),「埋牌完成 → 庄家首出」这段窗口增量路径的 table.playproc 是 null,
// 重连路径却已有初值——与 seatlist 是同一个窗口、同一条理由,被漏在了同一处。
t.eq('埋牌 maipai 带 playproc(庄闲同值)',
JS(mpZhuang3.data.playproc), JS(E.get_deskinfo(e.o_room, 0).PushCards.playproc));
t.eq('埋牌 maipai 闲家同一份 playproc(三家公开、不裁剪)',
JS(mpXian3.data.playproc), JS(mpZhuang3.data.playproc));
t.eq('埋牌 playproc 埋牌完成时为 round-1 初值(庄家首出、桌面全空)', mpZhuang3.data.playproc, {
round: 1, start: 0, currseat: 0, startcount: -1, startflower: -1, starttype: -1,
maxseat: -1, maxcard: -1, cards: [null, null, null], shuai_demand: null
});
// 反面:playproc 不受查牌门控——不像 seatlist,它只是桌面公开信息,不查牌模式下也恒有
t.eq('埋牌 不查牌 maipai 仍带 playproc',
en.sent.filter(m => m.rpc === 'maipai').every(m => m.data.playproc !== undefined), true);
// 深拷贝守卫:playproc 是全程复用的活对象,new_playround 就地重置同一个对象。
// 若下发挂的是活引用,埋牌后紧跟着打一手牌,已发出的 maipai.playproc 会被回改成下一状态的样子。
const dcPj = P.new({ paiju_list: [] }, 0);
const dcC = setup("00000", dcPj);
mod.jiaofen(pack(0, { call: 65 })); mod.jiaofen(pack(1, { call: 0 })); mod.jiaofen(pack(2, { call: 0 }));
mod.xuanzhu(pack(0, { flower: 1 }));
const dcInner = mod.app.SendPack;
let dcLive = null;
mod.app.SendPack = m => { if (m.rpc === 'maipai' && m.data.cards !== undefined) dcLive = m.data.playproc; dcInner(m); };
mod.maipai(pack(0, { cards: P.get_seat_cards(dcPj, 0).slice(0, 8) }));
mod.app.SendPack = dcInner;
const dcSnapAtSend = JS(dcLive);
const dcHand = P.get_seat_cards(dcPj, dcPj.playproc.currseat);
mod.chupai(pack(dcPj.playproc.currseat, { cards: [dcHand[dcHand.length - 1]] })); // 推进出一手,制造"若为活引用会被回改"的条件
t.eq('埋牌 playproc 下发的是深拷贝快照(已发出的包不被后续出牌回改)', JS(dcLive), dcSnapAtSend);
// ---- ④ burycards:maipai 必须发服务端权威快照,不得回显客户端提交的 cards 原序 ----
// 曾经的缺陷:msg.data.burycards = cards(请求包原样回显),而 PushCards.burycards 走 get_burycard()
// 按主牌花色排序 —— 同一副埋牌底牌两条路径两种顺序(SSOT)。