Files
erqiwang_youle/server/games/erqiwang/class.export.js
T
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

271 lines
12 KiB
JavaScript
Raw 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_export: 输出接口 ///////
///////////////////////////////////////////////////
//跨模块依赖:Node 走 require 守卫;友乐/浏览器由 min_loadJsFile 加载为同名全局(dev-guide 01 §1)
if (typeof require !== "undefined"){
var cls_youle_erqiwang_config = require("./class.config.js");
var cls_youle_erqiwang_arith = require("./class.arith.js");
var cls_youle_erqiwang_desk = require("./class.desk.js");
var cls_youle_erqiwang_paiju = require("./class.paiju.js");
}
var cls_youle_erqiwang_export = cls_youle_erqiwang_export || {
new: function() {
var exp = {};
//解析roomtype获取开房所需房卡(design §10.1)
exp.get_needroomcard = function(roomtype){
var cfg = cls_youle_erqiwang_config.parse(roomtype);
if (cfg.deductAA){
//AA每人扣卡:6局每人1张 / 12局每人2张
return cfg.aset12 ? 2 : 1;
}
//房主扣卡:6局2张 / 12局4张
return cfg.aset12 ? 4 : 2;
}
//解析roomtype获取总局数(design §10.1:6局 / 12局)
exp.get_asetcount = function(roomtype){
var cfg = cls_youle_erqiwang_config.parse(roomtype);
return cfg.aset12 ? 12 : 6;
}
//加入房间时根据roomtype获取需要的房卡数量
exp.get_needroomcard_joinroom = function(roomtype){
var cfg = cls_youle_erqiwang_config.parse(roomtype);
if (cfg.deductAA){
//AA每人扣卡:加入者按自己那份扣(6局1张 / 12局2张)
return cfg.aset12 ? 2 : 1;
}
//房主扣卡:加入者不扣
return 0;
}
//开战
exp.makewar = function(o_room){
//牌桌对象
o_room.o_desk = cls_youle_erqiwang_desk.new(o_room);
for (var i = 0; i < o_room.seatlist.length; i++) {
o_room.seatlist[i].gameinfo.isbet = 1;
}
//开始第一局
var do_newpaiju = function(){
o_room.o_desk.method.do_new_paiju(0);
}
min_ontimeout(do_newpaiju, 1000);
}
//获取某个玩家的牌桌整桌当前信息
exp.get_deskinfo = function(o_room, seat){
if (!o_room.o_desk){
return null;
}
var paiju = o_room.o_desk.method.curr_paiju();
//本局房间可选规则:爬坡改变「叫分 → 基础子数」的取值(design §7.3.1),
//各阶段下发的 multiple 必须与结算包 aset.multiple 用同一套规则
var cfg = cls_youle_erqiwang_config.parse(o_room.roomtype);
var deskinfo = {};
deskinfo.count = o_room.asetcount; //总局数
deskinfo.idx = paiju.idx; //当前局数
deskinfo.PlayerInfo = []; //玩家当前累积得分
for (var i = 0; i < o_room.o_desk.seatlist.length; i++) {
deskinfo.PlayerInfo.push(o_room.o_desk.seatlist[i][0]);
};
deskinfo.step = paiju.step; //牌局状态
deskinfo.MyCards = paiju.method.get_seat_cards(seat); //我手上的牌
switch (paiju.step){
case 1: //发完牌叫分
deskinfo.CallRun = {};
//当前叫分位置
deskinfo.CallRun.seat = paiju.method.get_callgrade_seat();
//叫分倒计时
deskinfo.CallRun.countdown = o_room.o_desk.method.get_countdown_jiaofen();
//当前叫分
deskinfo.CallRun.nowcall = paiju.method.get_callgrade_value();
//当前叫分倍数
deskinfo.CallRun.multiple = cls_youle_erqiwang_arith.get_base_bycall(deskinfo.CallRun.nowcall, cfg.climb);
//所有玩家的叫分情况
deskinfo.CallRun.call = [null, null, null];
deskinfo.CallRun.call[0] = paiju.method.get_seat_callgrade(0);
deskinfo.CallRun.call[1] = paiju.method.get_seat_callgrade(1);
deskinfo.CallRun.call[2] = paiju.method.get_seat_callgrade(2);
break;
case 2: //选主
deskinfo.ChooseMain = {};
//控制权:选主/投降都只由庄家做(mod.xuanzhu / mod.touxiang 的 SEAT 校验以 banker 为准)。
//与 shangzhuang 推送的 nextseat 同源同值——两条路径都由服务端算好给出,
//前端不再自己套「选主 = 庄家」这条规则(字段名与 CallRun.seat 一致:分组内的 seat = 轮到谁)
deskinfo.ChooseMain.seat = paiju.banker;
//庄家
deskinfo.ChooseMain.banker = paiju.banker;
//庄家叫分
deskinfo.ChooseMain.call = paiju.call;
//庄家叫分倍数
deskinfo.ChooseMain.multiple = cls_youle_erqiwang_arith.get_base_bycall(paiju.call, cfg.climb);
//选主倒计时
deskinfo.ChooseMain.countdown = o_room.o_desk.method.get_countdown_xuanzhu();
//底牌(发牌时没发给玩家、扣在桌面的 8 张):只有庄家可见(70分的3秒亮牌是上庄时的一次性事件,重连不重放)
if (seat == paiju.banker){
deskinfo.ChooseMain.bottomcards = paiju.method.get_bottomcards();
}
//是否可投降
if (paiju.call != 70){
deskinfo.ChooseMain.touxiang = 0;
} else {
deskinfo.ChooseMain.touxiang = 1;
}
//当前抓分倍数(design §7.2.0):与 shangzhuang 推送同源同值。
//选主阶段一张牌都还没出,捡分恒为 0 → 必为 +3(大光);
//漏发会让重连后顶部「抓分」角标掉回 0,与增量路径不一致
deskinfo.ChooseMain.curmultiple = paiju.method.get_curmultiple();
break;
case 3: //埋牌
deskinfo.BuryCards = {};
//控制权:埋牌只由庄家做(mod.maipai 的 SEAT 校验以 banker 为准)。
//与 xuanzhu 推送的 nextseat 同源同值
deskinfo.BuryCards.seat = paiju.banker;
//庄家
deskinfo.BuryCards.banker = paiju.banker;
//庄家叫分
deskinfo.BuryCards.call = paiju.call;
//庄家叫分倍数
deskinfo.BuryCards.multiple = cls_youle_erqiwang_arith.get_base_bycall(paiju.call, cfg.climb);
//埋牌倒计时
deskinfo.BuryCards.countdown = o_room.o_desk.method.get_countdown_maipai();
//底牌(发牌留桌的 8 张):只有庄家可见
if (seat == paiju.banker){
deskinfo.BuryCards.bottomcards = paiju.method.get_bottomcards();
}
//主牌花色
deskinfo.BuryCards.flower = paiju.flower;
//当前抓分倍数(design §7.2.0):埋牌阶段同样一张牌未出,捡分恒为 0 → 必为 +3
deskinfo.BuryCards.curmultiple = paiju.method.get_curmultiple();
//埋牌阶段无投降(投降是选主阶段与选主互斥的选择)
break;
case 5: //出牌
deskinfo.PushCards = {};
//庄家
deskinfo.PushCards.banker = paiju.banker;
//庄家叫分
deskinfo.PushCards.call = paiju.call;
//庄家叫分倍数
deskinfo.PushCards.multiple = cls_youle_erqiwang_arith.get_base_bycall(paiju.call, cfg.climb);
//出牌倒计时
deskinfo.PushCards.countdown = o_room.o_desk.method.get_countdown_chupai();
//主牌花色
deskinfo.PushCards.flower = paiju.flower;
//捡分
var jian_grade = paiju.method.get_jian_grade();
if (seat == paiju.banker){
//埋牌底牌(庄家埋下的 8 张)。与「底牌」(发牌留桌 8 张,见 ChooseMain/BuryCards.bottomcards)
//是两批不同的牌,字段名刻意区分,勿混用
deskinfo.PushCards.burycards = paiju.method.get_burycard();
} else {
//闲家当前的捡分分牌
deskinfo.PushCards.gradecards = jian_grade.cards;
}
//闲家当前的捡分分数
deskinfo.PushCards.grade = jian_grade.grade;
//当前抓分倍数(design §7.2.0):与 chupai 包同源同值,重连后角标立即正确
deskinfo.PushCards.curmultiple = paiju.method.get_curmultiple();
//当前的出牌情况:与 chupai1/2/3 共用同一处快照函数(结构一致、且是深拷贝,
//不把活对象挂进下发包,见 class.paiju.js get_playproc)
deskinfo.PushCards.playproc = cls_youle_erqiwang_paiju.get_playproc(paiju);
//本轮跟牌的必出牌(design §5.2):只在【轮到本座位】且本轮已有人出牌时才有,
//算的也只是本座位自己的手牌,不涉及他家(server 红线:按可见性下发)
if (paiju.playproc && paiju.playproc.currseat == seat){
var _must = paiju.method.get_mustcard(seat);
if (_must){
deskinfo.PushCards.mustcard = _must;
}
}
//报无主:与 chupai1/2/3 的 baozhu 同源同门控(mod.chupai)。
//【必须带】它是「余主公示 + 明牌按钮」的开关(design §9,前端
//Sprites_Table.js 的明牌按钮据此显隐)。只在 chupai 包里给、重连包不给,
//等于重连后按钮凭空消失——状态变更没有包承载(server 红线:发全下发面)。
//不查牌模式恒 0(与 chupai 一致,协议 §11–§13 baozhu 条目)
deskinfo.PushCards.baozhu = (!cfg.nocheck && paiju.method.have_baofu()) ? 1 : 0;
//座位列表(报副统计)与亮牌:仅可查牌模式下展示(§9/§8.2)
if (!cfg.nocheck){
//【深拷贝】与 maipai / chupai1/2/3 共用同一处快照函数,且不把活表挂进下发包
//(paiju.seatlist 是 do_playcard 每手就地改写的活对象,见 class.paiju.js get_seatlist)
deskinfo.PushCards.seatlist = cls_youle_erqiwang_paiju.get_seatlist(paiju);
if (seat != paiju.banker){
var _lp = paiju.method.get_liangpai();
if (_lp){
deskinfo.PushCards.liangpai = _lp;
}
}
}
//出牌历史:往轮打出、已被收走的牌属于「查牌」范畴,仅可查牌模式下发(design §9)。
//不查牌模式下不带此属性——当前这一轮桌面上的牌由上面的 playproc 恢复,两种模式都有,
//否则后出的人无从跟牌(design §9 末尾的边界说明)
if (!cfg.nocheck){
//【直读归档、不重建】pushlist 的唯一来源是 do_playcard 出牌当场写下的
//paiju.playhistory(见 class.paiju.js)。旧实现是从 paiju.cards 的 playround/dealowner
//反查再自己排一次序——那是同一份数据「这一手打出的牌」的第二个写入处:
//一手多张(甩牌/对子/拖拉机)时它与 chupai1/2/3 的 data.cards 会给出不同顺序,
//前端增量回放与重连重建因此对不上(同编码的一对牌尤其:排序无法区分它们)。
deskinfo.PushCards.pushlist = paiju.method.get_pushlist();
}
break;
case 6: //结算
deskinfo.Balance = {};
//玩家的准备状态
deskinfo.Balance.readystate = o_room.o_desk.prepare;
if (o_room.o_desk.prepare[seat] == 0){
//结算面板的三件套:与 jiesuan 推送【同源同值】(都取 get_paiju_account 末尾
//冻结的那份快照)。只给 aset 会让「结算面板还开着时断线重连/硬刷新」丢掉
//抠底明细与末局大结算——增量路径有、重连路径空白,正是「同一份数据两条路径
//给的不一样」(server 红线:发全下发面,漏发修在服务端发包处)。
//结算情况
deskinfo.Balance.aset = paiju.tmp_jiesuan_aset;
//抠底明细:仅正常出牌结算存在(投降/解散没有抠底这一步),与 jiesuan.bottom 同一份
if (paiju.tmp_jiesuan_bottom){
deskinfo.Balance.bottom = paiju.tmp_jiesuan_bottom;
}
//大局结算:仅末局或解散时存在,与 jiesuan.account 同一份。
//「有就带、没有就不带」与 jiesuan 推送的取舍完全一致,前端两条路径同一套解析
if (paiju.tmp_jiesuan_account){
deskinfo.Balance.account = paiju.tmp_jiesuan_account;
}
}
break;
}
return deskinfo;
}
//解散房间
//平台在 battlestate==1 时即会调用(rpc.js 开战处 makewar 后立刻置 1),而本游戏的首个牌局
//是 makewar 里 min_ontimeout(...,1000) 延迟创建的——这段窗口内 o_desk 已存在但 paiju_list 为空。
//此时返回 null(平台对 null 的 _deskfree 有分支:照常发解散包、不带 deskfree),
//而不是让 curr_paiju() 的 undefined 解引用抛异常打断整条解散链路。
exp.get_disbandRoom = function(o_room){
if (!o_room.o_desk){
return null;
}
var paiju = o_room.o_desk.method.curr_paiju();
if (!paiju){
return null;
}
var msg = {};
return paiju.method.get_paiju_account(2, msg);
}
return exp;
}
}
//对内输出接口(友乐/浏览器:youle_erqiwang 由 mod.js 先建;Node 单测无此全局时跳过)
if (typeof youle_erqiwang !== "undefined"){
youle_erqiwang.export = cls_youle_erqiwang_export.new();
}
//Node 导出;友乐/浏览器无 module 时跳过(dev-guide 01 §1)
if (typeof module !== "undefined"){
module.exports = cls_youle_erqiwang_export;
}