Files
erqiwang_youle/server/games/erqiwang/mod.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

770 lines
27 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.
//////////////////////////////////////////////////////////////////
/////////////////////////////咻一咻/////////////////////////////
//////////////////////////////////////////////////////////////////
var youle_erqiwang = youle_erqiwang || cls_mod.new("youle_erqiwang", "erqiwang", youle_app);
//配置解析类(roomtype 位串解析,SSOT)
min_loadJsFile("games/erqiwang/class.config.js", function(){
//牌类
min_loadJsFile("games/erqiwang/class.pai.js", function(){
//牌局类
min_loadJsFile("games/erqiwang/class.paiju.js", function(){
//牌桌类
min_loadJsFile("games/erqiwang/class.desk.js", function(){
//前后端共享牌值算法
min_loadJsFile("games/erqiwang/shared/cards.js", function(){
//算法类
min_loadJsFile("games/erqiwang/class.arith.js", function(){
//对内的输入接口类
min_loadJsFile("games/erqiwang/class.import.js", function(){
//对内的输出接口类
min_loadJsFile("games/erqiwang/class.export.js", function(){
});
});
});
});
});
});
});
});
//对内的输入接口类(需要的外部接口)
youle_erqiwang.import = null;
//对内的输出接口类(提供给外部使用的接口)
youle_erqiwang.export = null;
//失败码(协议层约定,非 design 玩法规则;详见 docs/protocol/packet_protocol.md)
//前端一律只看 data.success 判成败,errcode 仅用于区分失败原因、给出对应提示
youle_erqiwang.ERR = {
PLAYER: 1, //玩家/房间/座位校验不通过
NODESK: 2, //牌桌或牌局不存在
STEP: 3, //当前阶段不允许该操作
SEAT: 4, //位置不符,或还没轮到该玩家操作
PARAM: 5, //参数非法(类型/范围/张数/牌不在手上)
RULE: 6 //规则不允许(叫分未更低、出牌不合法、投降条件不满足、房间模式禁止等)
};
//失败回包:只回发给请求者,data.success=false
//(dev-guide server 03 §4/§5:成败标志唯一是 data.success,主动推送的 data 必须自带 success)
youle_erqiwang.do_sendfail = function(pack, rpc, errcode){
var msg = {};
msg.app = "youle";
msg.route = "erqiwang";
msg.rpc = rpc;
msg.data = {};
msg.data.success = false;
msg.data.errcode = errcode;
msg.conmode = pack.conmode;
msg.fromid = pack.fromid;
youle_erqiwang.app.SendPack(msg);
}
//叫分或不叫
youle_erqiwang.jiaofen = function(pack){
var agentid = pack.data.agentid;
var playerid = parseInt(pack.data.playerid);
var gameid = pack.data.gameid;
var roomcode = parseInt(pack.data.roomcode);
var seat = parseInt(pack.data.seat);
var call = parseInt(pack.data.call);
//检查玩家的位置是否正确
var o_room = youle_erqiwang.import.check_player(agentid, gameid, roomcode, seat, playerid, pack.conmode, pack.fromid);
if (!o_room){
youle_erqiwang.do_sendfail(pack, "jiaofen", youle_erqiwang.ERR.PLAYER);
return;
}
//牌桌
var o_desk = o_room.o_desk;
if (!o_desk){
youle_erqiwang.do_sendfail(pack, "jiaofen", youle_erqiwang.ERR.NODESK);
return;
}
//牌局
var o_paiju = o_desk.method.curr_paiju();
if (!o_paiju){
youle_erqiwang.do_sendfail(pack, "jiaofen", youle_erqiwang.ERR.NODESK);
return;
}
//检查当前的游戏阶段是否是叫分阶段
if (o_paiju.step != 1){
youle_erqiwang.do_sendfail(pack, "jiaofen", youle_erqiwang.ERR.STEP);
return;
}
//检查当前的叫分位置
if (o_paiju.method.get_callgrade_seat() != seat){
youle_erqiwang.do_sendfail(pack, "jiaofen", youle_erqiwang.ERR.SEAT);
return;
}
//检查叫分:0(不叫)或 5~70 且为 5 的整数倍(design §4)
if (call < 0 || call > 70 || call % 5 != 0){
youle_erqiwang.do_sendfail(pack, "jiaofen", youle_erqiwang.ERR.PARAM);
return;
}
var curr_call = o_paiju.method.get_callgrade_value();
//第一家不能不叫分
if (curr_call == 0 && call == 0){
youle_erqiwang.do_sendfail(pack, "jiaofen", youle_erqiwang.ERR.RULE);
return;
}
//叫分必须小于之前的叫分
if (curr_call != 0 && call >= curr_call){
youle_erqiwang.do_sendfail(pack, "jiaofen", youle_erqiwang.ERR.RULE);
return;
}
//本局房间可选规则:爬坡改变「叫分 → 基础子数」的取值(design §7.3.1),
//下发面的 multiple 必须与结算包 aset.multiple 用同一套规则,否则界面与结算不符
var _cfg = cls_youle_erqiwang_config.parse(o_room.roomtype);
//叫分成功
o_paiju.method.do_callgrade(call);
//返回前端
if (o_paiju.step == 1){
//等待下一家叫分
var msg = {};
msg.app = "youle";
msg.route = "erqiwang";
msg.rpc = "jiaofen";
msg.data = {};
msg.data.success = true;
msg.data.seat = seat;
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);
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);
return;
}
if (o_paiju.step == 2){
//等待庄家叫主
var msg = {};
msg.app = "youle";
msg.route = "erqiwang";
msg.rpc = "shangzhuang";
msg.data = {};
msg.data.success = true;
msg.data.seat = seat;
msg.data.call = call;
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);
msg.data.countdown = o_desk.method.get_countdown_xuanzhu();
if (o_paiju.call != 70){
msg.data.touxiang = 0;
} else {
msg.data.touxiang = 1;
}
//70分坐庄:庄家摸底牌前,8张底牌向所有玩家亮3秒(design §4/§7.1)
if (o_paiju.call == 70){
msg.data.ancard3s = 1;
}
var _bottomcards = o_paiju.method.get_bottomcards();
//当前抓分倍数(design §7.2.0):此刻捡分为 0,即大光×3;供客户端顶部「抓分」角标显示
msg.data.curmultiple = o_paiju.method.get_curmultiple();
for (var i = 0; i < o_room.seatlist.length; i++) {
msg.conmode = o_room.seatlist[i].conmode;
msg.fromid = o_room.seatlist[i].fromid;
if (i == o_paiju.banker){
//庄家:手牌(含摸起的底牌)与底牌本身都可见
msg.data.cards = o_paiju.method.get_seat_cards(i);
msg.data.bottomcards = _bottomcards;
} else {
delete msg.data.cards;
//闲家:仅70分坐庄时才下发底牌(供亮3秒),否则底牌只有庄家可见
if (o_paiju.call == 70){
msg.data.bottomcards = _bottomcards;
} else {
delete msg.data.bottomcards;
}
}
youle_erqiwang.app.SendPack(msg);
}
return;
}
}
//选主
youle_erqiwang.xuanzhu = function(pack){
var agentid = pack.data.agentid;
var playerid = parseInt(pack.data.playerid);
var gameid = pack.data.gameid;
var roomcode = parseInt(pack.data.roomcode);
var seat = parseInt(pack.data.seat);
var flower = parseInt(pack.data.flower);
//检查玩家的位置是否正确
var o_room = youle_erqiwang.import.check_player(agentid, gameid, roomcode, seat, playerid, pack.conmode, pack.fromid);
if (!o_room){
youle_erqiwang.do_sendfail(pack, "xuanzhu", youle_erqiwang.ERR.PLAYER);
return;
}
//牌桌
var o_desk = o_room.o_desk;
if (!o_desk){
youle_erqiwang.do_sendfail(pack, "xuanzhu", youle_erqiwang.ERR.NODESK);
return;
}
//牌局
var o_paiju = o_desk.method.curr_paiju();
if (!o_paiju){
youle_erqiwang.do_sendfail(pack, "xuanzhu", youle_erqiwang.ERR.NODESK);
return;
}
//检查当前的游戏阶段是否是选主阶段
if (o_paiju.step != 2){
youle_erqiwang.do_sendfail(pack, "xuanzhu", youle_erqiwang.ERR.STEP);
return;
}
//检查庄家位置
if (o_paiju.banker != seat){
youle_erqiwang.do_sendfail(pack, "xuanzhu", youle_erqiwang.ERR.SEAT);
return;
}
//检查花色
if (flower != 1 && flower != 2 && flower != 3 && flower != 4){
youle_erqiwang.do_sendfail(pack, "xuanzhu", youle_erqiwang.ERR.PARAM);
return;
}
//选主成功
o_paiju.method.do_choiceflower(flower);
//返回前端
var msg = {};
msg.app = "youle";
msg.route = "erqiwang";
msg.rpc = "xuanzhu";
msg.data = {};
msg.data.success = true;
msg.data.banker = o_paiju.banker;
msg.data.flower = flower;
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;
msg.fromid = o_room.seatlist[i].fromid;
msg.data.cards = o_paiju.method.get_seat_cards(i);
youle_erqiwang.app.SendPack(msg);
}
}
//埋牌
youle_erqiwang.maipai = function(pack){
var agentid = pack.data.agentid;
var playerid = parseInt(pack.data.playerid);
var gameid = pack.data.gameid;
var roomcode = parseInt(pack.data.roomcode);
var seat = parseInt(pack.data.seat);
var cards = pack.data.cards;
//检查玩家的位置是否正确
var o_room = youle_erqiwang.import.check_player(agentid, gameid, roomcode, seat, playerid, pack.conmode, pack.fromid);
if (!o_room){
youle_erqiwang.do_sendfail(pack, "maipai", youle_erqiwang.ERR.PLAYER);
return;
}
//牌桌
var o_desk = o_room.o_desk;
if (!o_desk){
youle_erqiwang.do_sendfail(pack, "maipai", youle_erqiwang.ERR.NODESK);
return;
}
//牌局
var o_paiju = o_desk.method.curr_paiju();
if (!o_paiju){
youle_erqiwang.do_sendfail(pack, "maipai", youle_erqiwang.ERR.NODESK);
return;
}
//检查当前的游戏阶段是否是埋牌阶段
if (o_paiju.step != 3){
youle_erqiwang.do_sendfail(pack, "maipai", youle_erqiwang.ERR.STEP);
return;
}
//检查庄家位置
if (o_paiju.banker != seat){
youle_erqiwang.do_sendfail(pack, "maipai", youle_erqiwang.ERR.SEAT);
return;
}
//检查埋牌入参本身是否合法(数组/整数牌id/范围/无重复),须先于 length 判断
if (!o_paiju.method.check_cards_valid(cards)){
youle_erqiwang.do_sendfail(pack, "maipai", youle_erqiwang.ERR.PARAM);
return;
}
//检查埋牌是否是8张
if (cards.length != 8){
youle_erqiwang.do_sendfail(pack, "maipai", youle_erqiwang.ERR.PARAM);
return;
}
//检查埋牌是否都在庄家手上
if (!o_paiju.method.check_cards_inhand(cards, seat)){
youle_erqiwang.do_sendfail(pack, "maipai", youle_erqiwang.ERR.PARAM);
return;
}
//埋牌成功
o_paiju.method.do_burycard(cards);
//亮牌(design §8.2):可查牌模式下,出牌开始向两个闲家亮出庄家手牌统计
var _cfg = cls_youle_erqiwang_config.parse(o_room.roomtype);
var _liangpai = null;
if (!_cfg.nocheck){
_liangpai = o_paiju.method.get_liangpai();
}
//返回前端
var msg = {};
msg.app = "youle";
msg.route = "erqiwang";
msg.rpc = "maipai";
msg.data = {};
msg.data.success = true;
msg.data.seat = o_paiju.playproc.currseat;
msg.data.countdown = o_desk.method.get_countdown_chupai();
if (!_cfg.nocheck){
//三家座位牌况(缺门/无对 + 报副统计),仅可查牌下发——与 chupai1/2/3、重连包
//PushCards.seatlist 同一份数据、同一道门控。埋牌完成时它是刚初始化的空表
//(每家 [[0,0],[0,0],[0,0],[0,0],[-1,-1]]),本身不含任何信息;但「埋牌完成 →
//庄家首出」这段窗口内前端也要有表可画,漏发会让增量路径此刻为空、重连路径却有表。
//可见性:表内只有「某家缺某花色/某花色无对」(由公开的跟牌行为推出)与报无主后
//全场公示的余主数量(design §9 明确要求向全体三人展示),故三家整表下发不构成泄露
msg.data.seatlist = o_paiju.seatlist;
}
//本轮进行态(design §5.1):do_burycard 内部已调用 new_playround 就地初始化好了 round-1
//的进行态,与重连包 PushCards.playproc 同一个快照函数、同一结构(三家无差别)。
//没有它,「埋牌完成 → 庄家首出」这段窗口内增量路径的 table.playproc 是 null,
//重连路径却已有 round-1 初值,两条路径不一致。可见性:round/start/currseat 等全部
//由桌面公开信息推出,cards 此刻全空、shuai_demand 为 null,不含任何私密信息,
//与 chupai1/2/3.playproc 一样三家整体下发、不逐座位裁剪。
msg.data.playproc = cls_youle_erqiwang_paiju.get_playproc(o_paiju);
//埋牌底牌(本次埋下的 8 张):取服务端权威快照,【不回显客户端提交的 cards 原序】。
//get_burycard 与重连包 PushCards.burycards 是同一个函数、同一套按本局主牌花色的排序,
//两条路径因此顺序一致(SSOT);直接回显 cards 会把客户端的选牌顺序当成下发顺序。
var _burycards = cls_youle_erqiwang_paiju.get_burycard(o_paiju);
for (var i = 0; i < o_room.seatlist.length; i++) {
msg.conmode = o_room.seatlist[i].conmode;
msg.fromid = o_room.seatlist[i].fromid;
if (i == o_paiju.banker){
msg.data.cards = o_paiju.method.get_seat_cards(i);
//埋牌底牌(本次埋下的 8 张)。与 shangzhuang.bottomcards(底牌,发牌留桌 8 张)
//是两批不同的牌,字段名刻意区分,勿混用
msg.data.burycards = _burycards;
delete msg.data.liangpai;
} else {
delete msg.data.cards;
delete msg.data.burycards;
//闲家:可查牌且庄家达标时亮牌
if (_liangpai){
msg.data.liangpai = _liangpai;
} else {
delete msg.data.liangpai;
}
}
youle_erqiwang.app.SendPack(msg);
}
}
//投降
youle_erqiwang.touxiang = function(pack){
var agentid = pack.data.agentid;
var playerid = parseInt(pack.data.playerid);
var gameid = pack.data.gameid;
var roomcode = parseInt(pack.data.roomcode);
var seat = parseInt(pack.data.seat);
//检查玩家的位置是否正确
var o_room = youle_erqiwang.import.check_player(agentid, gameid, roomcode, seat, playerid, pack.conmode, pack.fromid);
if (!o_room){
youle_erqiwang.do_sendfail(pack, "touxiang", youle_erqiwang.ERR.PLAYER);
return;
}
//牌桌
var o_desk = o_room.o_desk;
if (!o_desk){
youle_erqiwang.do_sendfail(pack, "touxiang", youle_erqiwang.ERR.NODESK);
return;
}
//牌局
var o_paiju = o_desk.method.curr_paiju();
if (!o_paiju){
youle_erqiwang.do_sendfail(pack, "touxiang", youle_erqiwang.ERR.NODESK);
return;
}
//投降是选主阶段(step 2)与选主互斥的选择:点投降直接结算,不选主、不埋牌(design §4)
if (o_paiju.step != 2){
youle_erqiwang.do_sendfail(pack, "touxiang", youle_erqiwang.ERR.STEP);
return;
}
//检查庄家位置
if (o_paiju.banker != seat){
youle_erqiwang.do_sendfail(pack, "touxiang", youle_erqiwang.ERR.SEAT);
return;
}
//仅 70 分坐庄可投降(design §4 / §7.1)
if (o_paiju.call != 70){
youle_erqiwang.do_sendfail(pack, "touxiang", youle_erqiwang.ERR.RULE);
return;
}
//投降结算
o_paiju.step = 6;
var msg = {};
msg.app = "youle";
msg.route = "erqiwang";
msg.rpc = "jiesuan";
msg.data = {};
msg = o_paiju.method.get_paiju_account(1, msg);
o_room.method.sendpack_toother(msg, -1);
}
//出牌
youle_erqiwang.chupai = function(pack){
var agentid = pack.data.agentid;
var playerid = parseInt(pack.data.playerid);
var gameid = pack.data.gameid;
var roomcode = parseInt(pack.data.roomcode);
var seat = parseInt(pack.data.seat);
var cards = pack.data.cards;
//检查玩家的位置是否正确
var o_room = youle_erqiwang.import.check_player(agentid, gameid, roomcode, seat, playerid, pack.conmode, pack.fromid);
if (!o_room){
youle_erqiwang.do_sendfail(pack, "chupai", youle_erqiwang.ERR.PLAYER);
return;
}
//牌桌
var o_desk = o_room.o_desk;
if (!o_desk){
youle_erqiwang.do_sendfail(pack, "chupai", youle_erqiwang.ERR.NODESK);
return;
}
//牌局
var o_paiju = o_desk.method.curr_paiju();
if (!o_paiju){
youle_erqiwang.do_sendfail(pack, "chupai", youle_erqiwang.ERR.NODESK);
return;
}
//检查当前的游戏阶段是否是出牌阶段
if (o_paiju.step != 5){
youle_erqiwang.do_sendfail(pack, "chupai", youle_erqiwang.ERR.STEP);
return;
}
//检查出牌位置
if (seat != o_paiju.playproc.currseat){
youle_erqiwang.do_sendfail(pack, "chupai", youle_erqiwang.ERR.SEAT);
return;
}
//检查出牌入参本身是否合法(数组/整数牌id/范围/无重复,含非空)
if (!o_paiju.method.check_cards_valid(cards)){
youle_erqiwang.do_sendfail(pack, "chupai", youle_erqiwang.ERR.PARAM);
return;
}
//检查出牌是否都在玩家手上
if (!o_paiju.method.check_cards_inhand(cards, seat)){
youle_erqiwang.do_sendfail(pack, "chupai", youle_erqiwang.ERR.PARAM);
return;
}
//检查选中的牌是否可出,并出牌
var re = o_paiju.method.do_playcard(cards);
if (!re.result){
youle_erqiwang.do_sendfail(pack, "chupai", youle_erqiwang.ERR.RULE);
return;
}
//查牌模式(design §9):不查牌时不下发任何主牌统计(info/报无主)
var _cfg = cls_youle_erqiwang_config.parse(o_room.roomtype);
//返回前端
var msg = {};
msg.app = "youle";
msg.route = "erqiwang";
msg.data = {};
msg.data.success = true;
msg.data.seat = seat;
//实际打出的牌(甩错时为单张),顺序由 do_playcard 的 order_playcards 统一给出——
//与 playproc.cards、重连包 PushCards.pushlist 同一个口径(SSOT)。
//【不回落到入参 cards】那是客户端的点击顺序,服务端不采信、更不回显(server 04 §8);
//do_playcard 成功时三个分支都会给出 re.cards,缺了就是它的缺陷,显式暴露、不兜底。
msg.data.cards = re.cards;
if (!_cfg.nocheck){
//三家座位牌况(缺门/无对 + 报副统计),仅可查牌下发。
//必须整表下发、不能只带出牌者一家:design §9 要求「一旦有玩家报无主,就为全体三人
//显示另外两家各自的主牌数量与对子数」,服务端已在 do_playcard 里同时刷新三个座位
//(class.paiju.js),只带一家会让另两家的统计滞后到它们各自出牌时才补上。
//字段名与重连包 PushCards.seatlist 一致,前端两条路径复用同一份解析
msg.data.seatlist = o_paiju.seatlist;
}
msg.data.nextseat = o_paiju.playproc.currseat;
msg.data.countdown = o_desk.method.get_countdown_chupai();
//当前抓分倍数(design §7.2.0):随本包一起下发、不另开推送;三家同值,grade 本就公开
msg.data.curmultiple = o_paiju.method.get_curmultiple();
//本轮进行态(design §5.1):与重连包 PushCards.playproc 同一个快照函数、同一结构。
//没有它,前端在非重连时就拿不到「本轮谁先手/谁最大/三家各出了什么/第几轮」,只能自己
//累积推导——那就是把对局推进逻辑搬到前端(前端红线「数据驱动、前端无对局状态机」)。
//注意 re.idx==3 时 do_playcard 已在内部调用 new_playround 就地开了新一轮,
//故 chupai3 带的是【新一轮】的进行态(与 nextseat 同步),这与此刻重连拿到的完全一致。
//可见性:round/start/currseat/startcount/startflower/starttype/maxseat/maxcard 都是
//由桌面公开信息直接推出的;cards 就是本轮已摊在桌上的牌;shuai_demand 是首家甩牌的
//分量构成,而甩出的牌本身已公开、chupai1 也已把等价的 shuai 下发给三家。全部对三家可见,
//无需逐座位裁剪(重连包 PushCards.playproc 本就对三家无差别下发,两条路径口径一致)
msg.data.playproc = cls_youle_erqiwang_paiju.get_playproc(o_paiju);
if (re.shuaicuo){
msg.data.shuaicuo = 1; //甩错:本次甩牌被收回,只打出了最小一张(§5.4.5)
}
//报无主:仅可查牌模式下才据此展示他家主牌数量/对子(§9)
if (!_cfg.nocheck && o_paiju.method.have_baofu()){
msg.data.baozhu = 1;
} else {
msg.data.baozhu = 0;
}
switch (re.idx)
{
case 1: //第一个出牌
msg.rpc = "chupai1";
msg.data.count = re.count;
msg.data.flower = re.flower;
msg.data.cardtype = re.cardtype;
//合法甩牌的分量构成(design §5.4.3),供前端按 拖拉机/对子/单张 分组展示;
//非甩牌(含甩错退化为单张)无此属性
if (re.shuai){
msg.data.shuai = re.shuai;
}
break;
case 2: //第二个出牌
msg.rpc = "chupai2";
break;
case 3: //第三个出牌
msg.rpc = "chupai3";
msg.data.maxseat = re.maxseat;
if (re.grade > 0){
msg.data.grade = re.grade;
// msg.data.gradecards = re.gradecards;
}
break;
}
if (o_paiju.step == 5){
//下一个出牌者本轮跟牌的必出牌(design §5.2),供其客户端自动选中。
//【只发给 nextseat 一家】——mustcard 是该玩家自己手牌的子集,发给他本人不泄露;
//整表下发三家会把另两家的手牌结构透露出去(server 红线:发全 ≠ 发多,按可见性下发)。
var _nextseat = o_paiju.playproc.currseat;
var _mustcard = o_paiju.method.get_mustcard(_nextseat);
for (var i = 0; i < o_room.seatlist.length; i++){
msg.conmode = o_room.seatlist[i].conmode;
msg.fromid = o_room.seatlist[i].fromid;
if (i == seat){
msg.data.cardsinhand = o_paiju.method.get_seat_cards(i);
} else {
delete msg.data.cardsinhand;
}
if (i == _nextseat && _mustcard){
msg.data.mustcard = _mustcard;
} else {
delete msg.data.mustcard;
}
youle_erqiwang.app.SendPack(msg);
}
} else if (o_paiju.step == 6){
//小局结算
//出牌包的调整
msg.data.chupai = {};
msg.data.chupai.seat = msg.data.seat;
msg.data.chupai.cards = msg.data.cards;
msg.data.chupai.maxseat = msg.data.maxseat;
if (msg.data.grade){
msg.data.chupai.grade = msg.data.grade;
}
if (msg.data.gradecards){
msg.data.chupai.gradecards = msg.data.gradecards;
}
delete msg.data.seat;
delete msg.data.cards;
delete msg.data.seatlist;
delete msg.data.nextseat;
delete msg.data.countdown;
delete msg.data.baozhu;
//本轮进行态只对「还要继续出牌」有意义;本包已转为 jiesuan,不带
delete msg.data.playproc;
delete msg.data.cardsinhand;
delete msg.data.maxseat;
delete msg.data.grade;
delete msg.data.gradecards;
//获取抠底包
msg = o_paiju.method.get_bottom_account(msg);
//获取小局结算包
msg = o_paiju.method.get_paiju_account(0, msg);
o_room.method.sendpack_toother(msg, -1);
}
}
//明牌:查看另外两家手中全部主牌的具体牌面(design §9,仅可查牌模式且已有人报无主)
youle_erqiwang.mingpai = function(pack){
var agentid = pack.data.agentid;
var playerid = parseInt(pack.data.playerid);
var gameid = pack.data.gameid;
var roomcode = parseInt(pack.data.roomcode);
var seat = parseInt(pack.data.seat);
//检查玩家的位置是否正确
var o_room = youle_erqiwang.import.check_player(agentid, gameid, roomcode, seat, playerid, pack.conmode, pack.fromid);
if (!o_room){
youle_erqiwang.do_sendfail(pack, "mingpai", youle_erqiwang.ERR.PLAYER);
return;
}
//牌桌
var o_desk = o_room.o_desk;
if (!o_desk){
youle_erqiwang.do_sendfail(pack, "mingpai", youle_erqiwang.ERR.NODESK);
return;
}
//牌局
var o_paiju = o_desk.method.curr_paiju();
if (!o_paiju){
youle_erqiwang.do_sendfail(pack, "mingpai", youle_erqiwang.ERR.NODESK);
return;
}
//只在出牌阶段
if (o_paiju.step != 5){
youle_erqiwang.do_sendfail(pack, "mingpai", youle_erqiwang.ERR.STEP);
return;
}
//不查牌模式禁止明牌
var cfg = cls_youle_erqiwang_config.parse(o_room.roomtype);
if (cfg.nocheck){
youle_erqiwang.do_sendfail(pack, "mingpai", youle_erqiwang.ERR.RULE);
return;
}
//必须已有玩家报无主才允许明牌
if (!o_paiju.method.have_baofu()){
youle_erqiwang.do_sendfail(pack, "mingpai", youle_erqiwang.ERR.RULE);
return;
}
//另外两家未出的全部主牌,只回发给请求者
var msg = {};
msg.app = "youle";
msg.route = "erqiwang";
msg.rpc = "mingpai";
msg.data = {};
msg.data.success = true;
msg.data.seat = seat;
msg.data.others = [];
for (var i = 0; i < 3; i++){
if (i != seat){
var one = {};
one.seat = i;
one.zhucards = cls_youle_erqiwang_paiju.get_seat_zhucards(o_paiju, i);
msg.data.others.push(one);
}
}
msg.conmode = pack.conmode;
msg.fromid = pack.fromid;
youle_erqiwang.app.SendPack(msg);
}
//闲家出牌提示(design §11):闲家在出牌阶段向对家(另一闲家)发"踩/没分/有分"信号。
//纯转发、不校验真实性、只发给对家;庄家无对家,不参与。tip:1=踩 2=没分 3=有分。
youle_erqiwang.tishi = function(pack){
var agentid = pack.data.agentid;
var playerid = parseInt(pack.data.playerid);
var gameid = pack.data.gameid;
var roomcode = parseInt(pack.data.roomcode);
var seat = parseInt(pack.data.seat);
var tip = parseInt(pack.data.tip);
//检查玩家的位置是否正确
var o_room = youle_erqiwang.import.check_player(agentid, gameid, roomcode, seat, playerid, pack.conmode, pack.fromid);
if (!o_room){
youle_erqiwang.do_sendfail(pack, "tishi", youle_erqiwang.ERR.PLAYER);
return;
}
//牌桌
var o_desk = o_room.o_desk;
if (!o_desk){
youle_erqiwang.do_sendfail(pack, "tishi", youle_erqiwang.ERR.NODESK);
return;
}
//牌局
var o_paiju = o_desk.method.curr_paiju();
if (!o_paiju){
youle_erqiwang.do_sendfail(pack, "tishi", youle_erqiwang.ERR.NODESK);
return;
}
//只在出牌阶段(step5)任意时刻可发
if (o_paiju.step != 5){
youle_erqiwang.do_sendfail(pack, "tishi", youle_erqiwang.ERR.STEP);
return;
}
//只有闲家可发提示,庄家无对家、不参与
if (seat == o_paiju.banker){
youle_erqiwang.do_sendfail(pack, "tishi", youle_erqiwang.ERR.RULE);
return;
}
//提示类型:1=踩 2=没分 3=有分
if (tip != 1 && tip != 2 && tip != 3){
youle_erqiwang.do_sendfail(pack, "tishi", youle_erqiwang.ERR.PARAM);
return;
}
//对家 = 另一个闲家(三座位号之和为 0+1+2=3)
var partner = 3 - o_paiju.banker - seat;
//只转发给对家
var msg = {};
msg.app = "youle";
msg.route = "erqiwang";
msg.rpc = "tishi";
msg.data = {};
msg.data.success = true;
msg.data.seat = seat; //发出提示的闲家
msg.data.tip = tip;
msg.conmode = o_room.seatlist[partner].conmode;
msg.fromid = o_room.seatlist[partner].fromid;
youle_erqiwang.app.SendPack(msg);
}
//准备
youle_erqiwang.zhunbei = function(pack){
var agentid = pack.data.agentid;
var playerid = parseInt(pack.data.playerid);
var gameid = pack.data.gameid;
var roomcode = parseInt(pack.data.roomcode);
var seat = parseInt(pack.data.seat);
//检查玩家的位置是否正确
var o_room = youle_erqiwang.import.check_player(agentid, gameid, roomcode, seat, playerid, pack.conmode, pack.fromid);
if (!o_room){
youle_erqiwang.do_sendfail(pack, "zhunbei", youle_erqiwang.ERR.PLAYER);
return;
}
//牌桌
var o_desk = o_room.o_desk;
if (!o_desk){
youle_erqiwang.do_sendfail(pack, "zhunbei", youle_erqiwang.ERR.NODESK);
return;
}
//牌局
var o_paiju = o_desk.method.curr_paiju();
if (!o_paiju){
youle_erqiwang.do_sendfail(pack, "zhunbei", youle_erqiwang.ERR.NODESK);
return;
}
//检查当前的游戏阶段是否是结算阶段
if (o_paiju.step != 6){
youle_erqiwang.do_sendfail(pack, "zhunbei", youle_erqiwang.ERR.STEP);
return;
}
//检查当前局数小于总局数
if (o_desk.paiju_list.length >= o_room.asetcount){
youle_erqiwang.do_sendfail(pack, "zhunbei", youle_erqiwang.ERR.RULE);
return;
}
//准备成功
o_desk.method.do_prepare(seat);
}
//Node(本地/单元测试)导出模块对象;友乐/浏览器无 module 时跳过(dev-guide 01 §1)
if (typeof module !== "undefined"){
module.exports = youle_erqiwang;
}