二七王:服务端下发面四处缺陷修复(增量路径与重连路径对齐)

前端一致性测试(同一局真包:逐包增量回放 vs 一次性 deskinfo 重连)暴露的四处
服务端缺陷,均属「同一份数据两条路径给的不一样 / 有一条根本没给」:

① chupai1/2/3 补发 playproc(本轮进行态)
   与 deskinfo.PushCards 共用新增的 get_playproc() 快照函数,结构取值完全一致。
   playproc 是全程复用的活对象(new_playround 就地重置),故快照做逐字段深拷贝,
   避免已发出的包被后续出牌回改。可见性:全部字段由桌面公开信息推出,
   与既有 PushCards.playproc 一样三家无差别下发。

② deskinfo ChooseMain(step2)/BuryCards(step3) 补发 curmultiple
   与 shangzhuang 推送同源同值;此刻一张牌未出、捡分恒 0,必为 +3(大光)。
   原先重连重建为 0,顶部「抓分」角标掉档。

③ maipai 补发 seatlist
   与 chupai/PushCards 同一张表、同一道查牌模式门控;埋牌完成时为初始化空表。

④ bottomcards / burycards 收敛到单一权威源(SSOT)
   - burycards:maipai 原样回显了客户端请求包里 cards 的顺序,改取 get_burycard()
     权威快照,与 PushCards.burycards 同一函数。请求包只承载「意图」,其顺序不可信。
   - bottomcards:get_bottomcards 按「调用时的 flower」排序,导致上庄推送(flower=-1)
     与重连 BuryCards(flower 已定) 两种顺序。底牌是选主【之前】就翻给玩家看的、
     发牌结束即固定的快照,故改为发牌时算一次并冻结,get_bottomcards 返回其副本
     (order_cards 是原地排序,交出本体会被调用方就地重排)。三个调用点无需改动。

协议文档同步 7 处(新增 playproc/seatlist/curmultiple 字段说明与两处排序口径)。
新增 test_rpc.js「下发面一致性」用例组 21 checks,含 28 轮真实牌局逐包逐座位比对
(249 次、0 不一致)与深拷贝守卫;每条均已用「退回修复必转红」反向验证。
既有 649 checks 未改一条断言,全绿(17 脚本 / 670 checks)。未涉及 shared/。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-27 19:17:09 +08:00
co-authored by Claude Opus 5
parent c4c061b0e1
commit 84525359fc
5 changed files with 245 additions and 16 deletions
+67 -6
View File
@@ -116,6 +116,24 @@ var cls_youle_erqiwang_paiju = cls_youle_erqiwang_paiju || {
tmpdeal.length = tmpdeal.length - 1;
}
//冻结底牌快照(发牌留桌的 8 张)
//——内容在发牌结束时就已确定(dealowner==0 之后不再变),【显示顺序也必须一并冻结】。
//理由(SSOT):排序用的 order_cards 依赖主牌花色,而底牌是在【选主之前】的上庄时刻
//就翻给庄家看的——那时 paiju.flower 还是 -1,根本还没有主牌。若每次调用都按「当时的
//flower」重排,上庄推送(flower=-1)与重连 BuryCards(step3,flower 已定)会对同一份数据
//给出两种顺序(实测:主花色的那张 K 被排到队首),前端增量路径与重连路径因此对不上。
//故在发牌结束时按【翻底那一刻的口径】算一次并冻结,之后所有下发点只取这份快照的副本。
var init_bottomcards = function(){
var ary = [];
for (var i = 0; i < paiju.cards.length; i++){
if (paiju.cards[i].dealowner == 0){
ary.push(paiju.cards[i].id);
}
}
//此处 paiju.flower 必为 -1(选主在 step2 才发生),即「无主牌」序 = 翻底时的顺序
paiju.bottomcards = cls_youle_erqiwang_arith.order_cards(paiju.flower, ary);
}
//进入叫分阶段
var start_call = function(){
paiju.step = 1;
@@ -149,6 +167,8 @@ var cls_youle_erqiwang_paiju = cls_youle_erqiwang_paiju || {
paiju.call = -1;
//主牌花色
paiju.flower = -1;
//底牌快照(发牌留桌的 8 张,含显示顺序):发牌结束时冻结,见 init_bottomcards
paiju.bottomcards = [];
//当前出牌情况
paiju.playproc = {};
//输赢结果 0:庄赢 1:闲赢 2:庄投降 3:中途解散
@@ -160,6 +180,8 @@ var cls_youle_erqiwang_paiju = cls_youle_erqiwang_paiju || {
init_seatlist();
//初始化牌列表并发牌
init_cards();
//冻结底牌快照(必须在发牌之后、选主之前)
init_bottomcards();
//进入叫分阶段
start_call();
@@ -445,15 +467,54 @@ var cls_youle_erqiwang_paiju = cls_youle_erqiwang_paiju || {
}
},
//获取底牌
//获取底牌(发牌留桌的 8 张):唯一来源是发牌时冻结的快照 o_paiju.bottomcards(见 init_bottomcards)。
//【返回副本】——order_cards 是原地排序,把快照本体交出去会被调用方就地重排,冻结即失效。
get_bottomcards: function(o_paiju){
var aryCardIDs = [];
for (var i = 0; i < o_paiju.cards.length; i++){
if (o_paiju.cards[i].dealowner == 0){
aryCardIDs.push(o_paiju.cards[i].id);
return o_paiju.bottomcards.concat();
},
//获取【供下发的】当前出牌情况快照:deskinfo.PushCards 与 chupai1/2/3 共用这一处,
//两条路径因此结构完全一致(前端增量回放与重连重建可复用同一份解析)。
//必须【深拷贝】:playproc 是一个全程复用的活对象(new_playround 就地重置同一个对象,
//do_playcard 也就地改 cards/maxseat/maxcard),直接把它挂进 msg 会让「已发出的包」
//随后续出牌被回改;平台真实链路虽然立刻序列化看不出来,但这是隐患,不留。
//structure:round/start/currseat/startcount/startflower/starttype/maxseat/maxcard/
// cards(定长3、下标=座位)/shuai_demand
get_playproc: function(o_paiju){
var _p = o_paiju.playproc;
if (!_p){
return null;
}
var re = {};
re.round = _p.round;
re.start = _p.start;
re.currseat = _p.currseat;
re.startcount = _p.startcount;
re.startflower = _p.startflower;
re.starttype = _p.starttype;
re.maxseat = _p.maxseat;
re.maxcard = _p.maxcard;
//本轮各位置出的牌:定长 3、下标 = 座位序号;尚未出牌的位置留空(序列化后为 null)
re.cards = [];
re.cards.length = 3;
if (_p.cards){
for (var i = 0; i < 3; i++){
if (_p.cards[i]){
re.cards[i] = _p.cards[i].concat();
}
}
}
return cls_youle_erqiwang_arith.order_cards(o_paiju.flower, aryCardIDs);
//首家甩牌的分量需求(§5.4.4);tractors 是数组,同样要拷贝,非甩牌为 null
if (_p.shuai_demand){
var _sd = {};
_sd.tractors = _p.shuai_demand.tractors ? _p.shuai_demand.tractors.concat() : [];
_sd.pairs = _p.shuai_demand.pairs;
_sd.singles = _p.shuai_demand.singles;
re.shuai_demand = _sd;
} else {
re.shuai_demand = null;
}
return re;
},
//选主