二七王: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
+26 -4
View File
@@ -134,6 +134,10 @@ youle_erqiwang.jiaofen = function(pack){
msg.data.call = call;
msg.data.currcall = o_paiju.method.get_callgrade_value();
msg.data.multiple = cls_youle_erqiwang_arith.get_base_bycall(msg.data.currcall, _cfg.climb);
//阶段:叫分尚未结束,仍是 step1。阶段唯一由服务端维护、并在包里显式给出——
//前端不得按 rpc 名反推(server 红线「状态机唯一在服务端 / 任何状态变更都要有包承载」)
msg.data.step = o_paiju.step;
//控制权:下一个叫分者(本包的「轮到谁」就是它,与重连包 CallRun.seat 同一个函数)
msg.data.nextseat = o_paiju.method.get_callgrade_seat();
msg.data.countdown = o_desk.method.get_countdown_jiaofen();
o_room.method.sendpack_toother(msg, -1);
@@ -152,6 +156,12 @@ youle_erqiwang.jiaofen = function(pack){
msg.data.banker = o_paiju.banker;
msg.data.grade = o_paiju.call;
msg.data.multiple = cls_youle_erqiwang_arith.get_base_bycall(msg.data.grade, _cfg.climb);
//阶段:叫分结束、进入选主/投降(step2)。与重连包 deskinfo.step 同源同值
msg.data.step = o_paiju.step;
//控制权:选主与投降都只由庄家做(mod.xuanzhu / mod.touxiang 的 SEAT 校验就以 banker 为准),
//故这里的「轮到谁」= o_paiju.banker —— 权威值取自牌局对象本身,不是前端按 banker 自行推导。
//本包的 data.seat 是【最后一个叫分者】、不是控制权,别混用(协议 §4)
msg.data.nextseat = o_paiju.banker;
msg.data.countdown = o_desk.method.get_countdown_xuanzhu();
if (o_paiju.call != 70){
msg.data.touxiang = 0;
@@ -242,6 +252,11 @@ youle_erqiwang.xuanzhu = function(pack){
msg.data.success = true;
msg.data.banker = o_paiju.banker;
msg.data.flower = flower;
//阶段:选主完成即进入埋牌(step3)。与重连包 deskinfo.step 同源同值
msg.data.step = o_paiju.step;
//控制权:埋牌只由庄家做(mod.maipai 的 SEAT 校验同样以 banker 为准),
//权威值取自牌局对象;前端不再自己套「埋牌 = 庄家」这条规则
msg.data.nextseat = o_paiju.banker;
msg.data.countdown = o_desk.method.get_countdown_maipai();
for (var i = 0; i < o_room.seatlist.length; i++) {
msg.conmode = o_room.seatlist[i].conmode;
@@ -320,6 +335,10 @@ youle_erqiwang.maipai = function(pack){
msg.rpc = "maipai";
msg.data = {};
msg.data.success = true;
//阶段:埋牌完成即进入出牌(step5,无独立投降阶段)。与重连包 deskinfo.step 同源同值
msg.data.step = o_paiju.step;
//控制权:本包的 seat 就是「轮到谁」——即将首出的庄家,取自权威的 playproc.currseat
//(do_burycard 里 new_playround 刚写好),与重连包 PushCards.playproc.currseat 同源
msg.data.seat = o_paiju.playproc.currseat;
msg.data.countdown = o_desk.method.get_countdown_chupai();
if (!_cfg.nocheck){
@@ -328,8 +347,9 @@ youle_erqiwang.maipai = function(pack){
//(每家 [[0,0],[0,0],[0,0],[0,0],[-1,-1]]),本身不含任何信息;但「埋牌完成 →
//庄家首出」这段窗口内前端也要有表可画,漏发会让增量路径此刻为空、重连路径却有表。
//可见性:表内只有「某家缺某花色/某花色无对」(由公开的跟牌行为推出)与报无主后
//全场公示的余主数量(design §9 明确要求向全体三人展示),故三家整表下发不构成泄露
msg.data.seatlist = o_paiju.seatlist;
//全场公示的余主数量(design §9 明确要求向全体三人展示),故三家整表下发不构成泄露。
//【深拷贝】seatlist 是全程就地改写的活表,不把活对象挂进下发包(同 playproc/pushlist)
msg.data.seatlist = cls_youle_erqiwang_paiju.get_seatlist(o_paiju);
}
//本轮进行态(design §5.1):do_burycard 内部已调用 new_playround 就地初始化好了 round-1
//的进行态,与重连包 PushCards.playproc 同一个快照函数、同一结构(三家无差别)。
@@ -491,8 +511,10 @@ youle_erqiwang.chupai = function(pack){
//必须整表下发、不能只带出牌者一家:design §9 要求「一旦有玩家报无主,就为全体三人
//显示另外两家各自的主牌数量与对子数」,服务端已在 do_playcard 里同时刷新三个座位
//(class.paiju.js),只带一家会让另两家的统计滞后到它们各自出牌时才补上。
//字段名与重连包 PushCards.seatlist 一致,前端两条路径复用同一份解析
msg.data.seatlist = o_paiju.seatlist;
//字段名与重连包 PushCards.seatlist 一致,前端两条路径复用同一份解析。
//【深拷贝】seatlist 是全程就地改写的活表(本函数下方 do_playcard 每手都在改它),
//不把活对象挂进下发包(同 playproc/pushlist 的理由)
msg.data.seatlist = cls_youle_erqiwang_paiju.get_seatlist(o_paiju);
}
msg.data.nextseat = o_paiju.playproc.currseat;
msg.data.countdown = o_desk.method.get_countdown_chupai();