二七王:I-3/I-2/M-2 服务端补齐阶段·控制权·结算重连下发面

I-3 阶段与控制权改由服务端唯一维护并逐包下发:
- fapai / jiaofen / shangzhuang / xuanzhu / maipai / jiesuan 各补 step(取自 o_paiju.step)
- shangzhuang / xuanzhu 补 nextseat(控制权 = o_paiju.banker,选主/投降/埋牌均只由庄家做)
- deskinfo 的 ChooseMain / BuryCards 各补 seat(控制权,与增量推送 nextseat 同源同值)
- 各下发点的真实值先用探针跑真实一局逐个核实;顺带发现 shangzhuang.data.seat 是
  最后一个叫分者而非控制权(本局实测 2 vs 庄家 0),故控制权另起名 nextseat
- get_paiju_account 统一把 step 置 6:解散结算此前无人推进 step,包里给不出诚实的阶段值

I-2 结算阶段的重连包补发抠底明细与大局结算:
- get_paiju_account 末尾一并冻结 tmp_jiesuan_bottom / tmp_jiesuan_account
- get_deskinfo case 6 的 Balance 据此补 bottom / account,与 jiesuan 推送同源同值、
  门控一致(投降/解散无 bottom、非末局无 account);新局开始时三份快照一并清除
- 修复「结算面板开着时断线重连/硬刷新 → 抠底明细与末局大结算空白」

M-2 seatlist 不再以活引用挂进下发包:
- 新增 get_seatlist() 深拷贝快照,maipai / chupai / deskinfo.PushCards 三处改用它

测试:test_rpc 新增 I-3/I-2/M-2 三节(131→166 checks)、test_desk 新增 fapai
阶段与控制权用例(6→10);全部新增守卫已逐条做「退回修复必转红」反向验证(15/15)。
test_leak 的可见性规则同步收紧到「本局结算(step6)后埋牌底牌才全场公开」,
并修掉一处把阶段写死成 step5 的报错文案;整局泄露审计仍全绿。
协议文档同步补充上述字段说明。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-27 21:30:56 +08:00
co-authored by Claude Opus 5
parent cfa0409d94
commit 5d981a722e
8 changed files with 313 additions and 14 deletions
+47 -4
View File
@@ -180,8 +180,15 @@ var cls_youle_erqiwang_paiju = cls_youle_erqiwang_paiju || {
paiju.playhistory = [];
//输赢结果 0:庄赢 1:闲赢 2:庄投降 3:中途解散
paiju.result = null;
//结算包,供断线重连时使用,新开一局时清除
paiju.tmp_jiesuan_aset = null;
//结算包快照,供断线重连时使用,新开一局时清除。三件套与 jiesuan 推送【同源同值】:
// tmp_jiesuan_aset 单局结算(三种结算来源恒有)
// tmp_jiesuan_bottom 抠底明细(仅正常出牌结算有;投降/解散为 null)
// tmp_jiesuan_account 大局结算(仅末局或解散有;其余为 null)
//只存 aset 会让「结算面板还开着时重连/硬刷新」丢掉抠底明细与大局结算(增量路径有、
//重连路径没有),即同一份数据两条路径给的不一样。唯一写入处是 get_paiju_account 末尾。
paiju.tmp_jiesuan_aset = null;
paiju.tmp_jiesuan_bottom = null;
paiju.tmp_jiesuan_account = null;
//初始化座位列表
init_seatlist();
@@ -295,6 +302,11 @@ var cls_youle_erqiwang_paiju = cls_youle_erqiwang_paiju || {
return cls_youle_erqiwang_paiju.get_pushlist(paiju);
}
//获取三家座位牌况快照(maipai / chupai1/2/3 / 重连包 PushCards.seatlist)
paiju.method.get_seatlist = function(){
return cls_youle_erqiwang_paiju.get_seatlist(paiju);
}
//获取某座位本轮跟牌的必出牌(design §5.2)
paiju.method.get_mustcard = function(seat){
return cls_youle_erqiwang_paiju.get_mustcard(paiju, seat);
@@ -510,6 +522,24 @@ var cls_youle_erqiwang_paiju = cls_youle_erqiwang_paiju || {
return re;
},
//获取【供下发的】三家座位牌况快照(maipai / chupai1/2/3 / 重连包 PushCards.seatlist)。
//结构:外层定长 3(下标=座位),每家 5 个二元数组——前 4 个是花色1~4 的 [无该花色, 无对],
//第 5 个是报副 [剩余主牌数, 剩余主对数]。
//【返回深拷贝】o_paiju.seatlist 是一张全程就地改写的活表(do_playcard 里按缺门/无对/报无主
//逐格赋值),把它本体挂进下发包会让「已发出的包」随后续出牌被回改;平台真实链路虽然立刻
//序列化看不出来,但这是隐患,与 get_playproc / get_pushlist / get_bottomcards 同一条标准。
get_seatlist: function(o_paiju){
var re = [];
for (var s = 0; s < o_paiju.seatlist.length; s++){
var _one = [];
for (var f = 0; f < o_paiju.seatlist[s].length; f++){
_one.push(o_paiju.seatlist[s][f].concat());
}
re.push(_one);
}
return re;
},
get_playproc: function(o_paiju){
var _p = o_paiju.playproc;
if (!_p){
@@ -1028,6 +1058,13 @@ var cls_youle_erqiwang_paiju = cls_youle_erqiwang_paiju || {
//主动推送的 data 必须自带成败标志(dev-guide server 03 §4:成败唯一是 data.success)
msg.data.success = true;
//【阶段唯一写入处】结算包一出,本局即进入 step6:
// 正常出牌结算 do_playcard 已置 6、投降结算 mod.touxiang 已置 6,此处是幂等重申;
// 解散结算此前没有任何地方推进过 step(可能停在 1/2/3/5),由这里落定。
//随后显式下发(见下方 msg.data.step):阶段变更必须有包承载,不让前端按 rpc 猜(server 03 §1.4)
o_paiju.step = 6;
msg.data.step = o_paiju.step;
//读取房间可选规则(缺省:常规算子、不傍王;见 class.config.js 位串约定)
var o_room = o_paiju.o_desk.o_room;
var _cfg = cls_youle_erqiwang_config.parse(o_room.roomtype);
@@ -1167,8 +1204,14 @@ var cls_youle_erqiwang_paiju = cls_youle_erqiwang_paiju || {
msg = o_paiju.o_desk.method.get_desk_account(msg);
}
//保存结算包,供断线重连时使用
o_paiju.tmp_jiesuan_aset = msg.data.aset;
//保存结算包,供断线重连时使用(get_deskinfo 的 case 6 直读这三份,不重算)。
//必须与本次下发的 jiesuan 推送【同源】——取的就是刚组好的同一份 msg.data.*,
//这样「增量收到 jiesuan」与「结算面板开着时重连」得到的结算数据逐字段相等。
//bottom 只有正常出牌结算才有(get_bottom_account 组的)、account 只有末局/解散才有,
//没有的情形显式置 null(不是漏发,是本来就不存在;下发侧据此决定带不带该分组)
o_paiju.tmp_jiesuan_aset = msg.data.aset;
o_paiju.tmp_jiesuan_bottom = msg.data.bottom ? msg.data.bottom : null;
o_paiju.tmp_jiesuan_account = msg.data.account ? msg.data.account : null;
return msg;
}