Files
erqiwang_youle/server/games/erqiwang/class.export.js
T
joywayerandClaude Opus 5 4508fe005f 二七王:出牌顺序收敛为唯一口径,重连包补发 baozhu
同一份数据「这一手打出的牌」此前有两个顺序口径:chupai*/playproc.cards
回显客户端请求原序,重连包 pushlist 则从 playround 反查后自己再排一次序。
一手多张(甩牌/对子/拖拉机)时两者必然分岔;同编码的一对牌更是排序都
区分不了,靠"两处各自排序"永远收敛不了。

改为单一写入处:
- 新增 order_playcards(),do_playcard 入口归一化一次(顺带隔离
  can_playcard 的原地排序,不再就地改写 pack.data.cards)
- 新增 paiju.playhistory,出牌当场归档;新增 get_pushlist() 只做深拷贝
- class.export.js 删掉整段反查+重排,改为直读归档
- do_playcard 三个分支都给 re.cards,mod.js 去掉回落到入参的兜底

顺带修复重连包漏发 baozhu:它是余主公示与明牌按钮的开关,此前只在
chupai 包里给,报无主后重连按钮会凭空消失。现与 chupai 同源同门控下发。

协议文档新增 §0.6「一手出牌」的顺序口径,并补 PushCards.baozhu 字段。
测试:pushlist 一节改为真实链路驱动(一手多张 + 乱序提交),断言升级为
「重连每一手与当时的 chupai.cards 逐元素相等」;三处修复均已退回验证转红。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 20:12:03 +08:00

249 lines
11 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 = {};
//庄家
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 = {};
//庄家
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){
deskinfo.PushCards.seatlist = paiju.seatlist;
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){
//结算情况
deskinfo.Balance.aset = paiju.tmp_jiesuan_aset;
}
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;
}