Files
joywayerandClaude Opus 5 5d981a722e 二七王: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>
2026-08-27 21:30:56 +08:00

218 lines
7.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
///////////////////////////////////////////////////
////////// cls_youle_erqiwang_desk: 牌桌 //////////
///////////////////////////////////////////////////
//跨模块依赖:Node 走 require 守卫;友乐/浏览器由 min_loadJsFile 加载为同名全局(dev-guide 01 §1)
if (typeof require !== "undefined"){
var cls_youle_erqiwang_paiju = require("./class.paiju.js");
}
var cls_youle_erqiwang_desk = cls_youle_erqiwang_desk || {
countdown_jiaofen: 15, //叫分倒计时
countdown_xuanzhu: 20, //选主倒计时
countdown_maipai: 25, //埋牌倒计时
countdown_chupai: 30, //出牌倒计时
//创建牌桌对象
new: function(o_room){
var desk = {};
desk.o_room = o_room; //桌所属的房间对象
//玩家得分。每个元素:[累积总分, [每局总分...], 基础分累计, 冲关分累计, 傍王分累计]
//后三位是大局结算面板要分项展示的累计(design §7 捡分子数 / §8.1 冲关 / §8.3 傍王),
//三者之和恒等于累积总分——下发时组装成对象(见 get_desk_account),数组下标只在内部用
desk.seatlist = [];
desk.seatlist.push([0, [], 0, 0, 0]);
desk.seatlist.push([0, [], 0, 0, 0]);
desk.seatlist.push([0, [], 0, 0, 0]);
desk.prepare = [0,0,0]; //玩家准备状态
desk.paiju_list = []; //牌局列表
//方法
desk.method = {};
//当前牌局
desk.method.curr_paiju = function(){
return desk.paiju_list[desk.paiju_list.length - 1];
}
//新牌局
desk.method.do_new_paiju = function(firstseat){
cls_youle_erqiwang_desk.do_new_paiju(desk, firstseat);
}
//叫分倒计时
desk.method.get_countdown_jiaofen = function(){
return cls_youle_erqiwang_desk.countdown_jiaofen;
}
//叫主倒计时
desk.method.get_countdown_xuanzhu = function(){
return cls_youle_erqiwang_desk.countdown_xuanzhu;
}
//埋牌倒计时
desk.method.get_countdown_maipai = function(){
return cls_youle_erqiwang_desk.countdown_maipai;
}
//出牌倒计时
desk.method.get_countdown_chupai = function(){
return cls_youle_erqiwang_desk.countdown_chupai;
}
//准备
desk.method.do_prepare = function(seat){
return cls_youle_erqiwang_desk.do_prepare(desk, seat);
}
//获取大局结算包
desk.method.get_desk_account = function(msg){
return cls_youle_erqiwang_desk.get_desk_account(desk, msg);
}
return desk;
},
//新开一局
do_new_paiju: function(o_desk, firstseat){
//恢复玩家准备状态
o_desk.prepare = [0,0,0];
if (o_desk.paiju_list.length > 0){
//上一局的结算快照三件套一并清除(新局开始后重连不该再看到上一局的结算面板)
delete o_desk.paiju_list[o_desk.paiju_list.length - 1].tmp_jiesuan_aset;
delete o_desk.paiju_list[o_desk.paiju_list.length - 1].tmp_jiesuan_bottom;
delete o_desk.paiju_list[o_desk.paiju_list.length - 1].tmp_jiesuan_account;
}
//新开一局
cls_youle_erqiwang_paiju.new(o_desk, firstseat);
var msg = {};
msg.app = "youle";
msg.route = "erqiwang";
msg.rpc = "fapai";
msg.data = {};
msg.data.success = true;
msg.data.asetidx = o_desk.paiju_list.length;
msg.data.asetcount = o_desk.o_room.asetcount;
//阶段:发完牌即 step1(叫分)。由服务端权威给出、不让前端按 rpc 推——
//没有包承载的状态变化等于逼前端去猜(server 03 §1.4 / 红线「状态机唯一在服务端」)
msg.data.step = o_desk.method.curr_paiju().step;
//控制权:当前等待叫分者。与重连包 CallRun.seat 同一个 get_callgrade_seat()(SSOT)
msg.data.seat = o_desk.method.curr_paiju().method.get_callgrade_seat();
msg.data.countdown = o_desk.method.get_countdown_jiaofen();
for (var i = 0; i < o_desk.o_room.seatlist.length; i++) {
msg.conmode = o_desk.o_room.seatlist[i].conmode;
msg.fromid = o_desk.o_room.seatlist[i].fromid;
msg.data.cards = o_desk.method.curr_paiju().method.get_seat_cards(i);
youle_erqiwang.app.SendPack(msg);
}
},
//准备
do_prepare: function(o_desk, seat){
o_desk.prepare[seat] = 1;
if (o_desk.prepare[0] && o_desk.prepare[1] && o_desk.prepare[2]){
//新开一局
var paiju = o_desk.method.curr_paiju();
var firstseat = paiju.banker;
if (paiju.result == 1 || paiju.result == 2){
firstseat = (firstseat + 1) % 3;
}
cls_youle_erqiwang_desk.do_new_paiju(o_desk, firstseat);
} else {
var msg = {};
msg.app = "youle";
msg.route = "erqiwang";
msg.rpc = "zhunbei";
msg.data = {};
msg.data.success = true;
msg.data.seat = seat;
o_desk.o_room.method.sendpack_toother(msg, -1);
}
},
//大局结算包
get_desk_account: function(o_desk, msg){
//保存战绩
var do_save_grade = function(){
var o_gameinfo1 = {};
o_gameinfo1.roomcode = o_desk.o_room.roomcode;
o_gameinfo1.asetcount = o_desk.paiju_list.length;
o_gameinfo1.createtime = o_desk.o_room.createtime;
o_gameinfo1.makewartime = o_desk.o_room.makewartime;
o_gameinfo1.players = [];
for (var i = 0; i < o_desk.o_room.seatlist.length; i++){
var _player = {};
_player.seat = i;
_player.playerid = o_desk.o_room.seatlist[i].playerid;
_player.name = o_desk.o_room.seatlist[i].nickname;
_player.avatar = o_desk.o_room.seatlist[i].avatar;
_player.score = o_desk.seatlist[i][0];
o_gameinfo1.players.push(_player);
}
var o_gameinfo2 = [];
for (var i = 0; i < o_desk.paiju_list.length; i++) {
var _paiju = {};
_paiju.starttime = o_desk.paiju_list[i].starttime;
_paiju.endtime = o_desk.paiju_list[i].endtime;
_paiju.seatlist = [0, 0, 0];
_paiju.seatlist[0] = o_desk.seatlist[0][1][i];
_paiju.seatlist[1] = o_desk.seatlist[1][1][i];
_paiju.seatlist[2] = o_desk.seatlist[2][1][i];
_paiju.callproc = o_desk.paiju_list[i].callproc;
_paiju.banker = o_desk.paiju_list[i].banker;
_paiju.call = o_desk.paiju_list[i].call;
_paiju.flower = o_desk.paiju_list[i].flower;
_paiju.result = o_desk.paiju_list[i].result;
//数据量太多,json转数组后进行存储
_paiju.cards = [];
for (var j = 0; j < o_desk.paiju_list[i].cards.length; j++) {
var _pai = o_desk.paiju_list[i].cards[j];
var _card = [];
_card.push(_pai.id);
_card.push(_pai.flower);
_card.push(_pai.number);
_card.push(_pai.score);
_card.push(_pai.dealowner);
_card.push(_pai.playround);
_card.push(_pai.playindex);
_card.push(_pai.playowner);
_paiju.cards.push(_card);
}
o_gameinfo2.push(_paiju);
}
//累积游戏得分
for (var i = 0; i < o_desk.o_room.seatlist.length; i++) {
o_desk.o_room.seatlist[i].gameinfo.grade = o_desk.seatlist[i][0];
}
//保存战绩
youle_erqiwang.import.save_grade(o_desk.o_room, o_gameinfo1, o_gameinfo2, 1);
}
min_ontimeout(do_save_grade, 1000);
//大局结算下发:组装成对象数组,字段自解释(内部 o_desk.seatlist 是数组,下标语义不清,不直接外抛)
msg.data.account = [];
for (var s = 0; s < o_desk.seatlist.length; s++){
var _a = {};
_a.score = o_desk.seatlist[s][0]; //累积总分
_a.grades = o_desk.seatlist[s][1]; //每局总分列表
_a.grade_jf_total = o_desk.seatlist[s][2]; //基础分(捡分子数得分)累计
_a.grade_cg_total = o_desk.seatlist[s][3]; //冲关分累计(design §8.1,不含傍王)
_a.grade_bw_total = o_desk.seatlist[s][4]; //傍王分累计(design §8.3)
msg.data.account.push(_a);
}
return msg;
}
}
//Node 导出;友乐/浏览器无 module 时跳过(dev-guide 01 §1)。SendPack/youle_erqiwang.* 为平台全局
if (typeof module !== "undefined"){
module.exports = cls_youle_erqiwang_desk;
}