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>
1224 lines
46 KiB
JavaScript
1224 lines
46 KiB
JavaScript
///////////////////////////////////////////////////
|
||
////////// cls_youle_erqiwang_paiju: 牌局 /////////
|
||
///////////////////////////////////////////////////
|
||
//跨模块依赖:Node 走 require 守卫;友乐/浏览器无 require,由 min_loadJsFile 加载为同名全局(dev-guide 01 §1)
|
||
if (typeof require !== "undefined"){
|
||
var cls_youle_erqiwang_arith = require("./class.arith.js");
|
||
var cls_youle_erqiwang_config = require("./class.config.js");
|
||
var cls_youle_erqiwang_pai = require("./class.pai.js");
|
||
}
|
||
|
||
var cls_youle_erqiwang_paiju = cls_youle_erqiwang_paiju || {
|
||
|
||
new: function(o_desk, firstseat) {
|
||
var paiju = {};
|
||
o_desk.paiju_list.push(paiju);
|
||
|
||
// ================= 实例的私有属性 ================= //
|
||
//发牌过程中待发的位置列表
|
||
var tmpdeal = [];
|
||
//三家需要各发28张牌
|
||
for (var i = 0; i < 28; i++){
|
||
tmpdeal.push(0); //0表示第一个玩家的位置
|
||
tmpdeal.push(1); //1表示第二个玩家的位置
|
||
tmpdeal.push(2); //2表示第三个玩家的位置
|
||
}
|
||
//8张底牌
|
||
for (var i = 0; i < 8; i++){
|
||
tmpdeal.push(-1); //-1 表示是底牌
|
||
}
|
||
|
||
// ================= 实例的私有方法 ================= //
|
||
|
||
//初始化座位列表
|
||
var init_seatlist = function(){
|
||
//第一层是玩家列表,与位置序号对应
|
||
//第二层前4个数组是花色列表,与花色-1对应
|
||
//第三层的第一位是无该花色的牌标志
|
||
//第三层的第二位是该花色无对标志
|
||
//第二层第5个数组是报副情况
|
||
//第三层的第一位是剩余主牌数量
|
||
//第三层的第二位是剩余主牌对子
|
||
//0号位
|
||
paiju.seatlist.push([[0,0],[0,0],[0,0],[0,0],[-1,-1]]);
|
||
//1号位
|
||
paiju.seatlist.push([[0,0],[0,0],[0,0],[0,0],[-1,-1]]);
|
||
//2号位
|
||
paiju.seatlist.push([[0,0],[0,0],[0,0],[0,0],[-1,-1]]);
|
||
}
|
||
|
||
//初始化牌列表并发牌
|
||
var init_cards = function(){
|
||
//两副牌
|
||
for (var i = 1; i <= 2; i++){
|
||
//四种花色
|
||
for (var j = 1; j <= 4; j++){
|
||
for (var k = 1; k <= 13; k++){
|
||
//牌id
|
||
var id = (i - 1) * 54 + (j - 1) * 13 + k - 1;
|
||
//分值
|
||
var score = 0;
|
||
//发牌状态
|
||
var dealowner = 0;
|
||
//特殊处理
|
||
switch (k)
|
||
{
|
||
case 5:
|
||
score = 5;
|
||
break;
|
||
case 10:
|
||
score = 10;
|
||
break;
|
||
case 13:
|
||
score = 10;
|
||
break;
|
||
case 3:
|
||
dealowner = -1;
|
||
break;
|
||
case 4:
|
||
dealowner = -1;
|
||
break;
|
||
}
|
||
var pai = cls_youle_erqiwang_pai.new(id, j, k, score, dealowner);
|
||
paiju.cards.push(pai);
|
||
//发牌
|
||
if (k != 3 && k != 4){
|
||
do_dealpai(pai);
|
||
}
|
||
}
|
||
}
|
||
|
||
//小王
|
||
var pai = cls_youle_erqiwang_pai.new((i - 1) * 54 + 53 - 1, 5, 53, 0, 0);
|
||
paiju.cards.push(pai);
|
||
do_dealpai(pai);
|
||
|
||
//大王
|
||
var pai = cls_youle_erqiwang_pai.new((i - 1) * 54 + 54 - 1, 5, 54, 0, 0);
|
||
paiju.cards.push(pai);
|
||
do_dealpai(pai);
|
||
}
|
||
}
|
||
|
||
//将单张牌随机发到玩家手中或成为底牌
|
||
var do_dealpai = function(o_pai){
|
||
var idx = min_random(0, tmpdeal.length - 1);
|
||
if (tmpdeal[idx] == -1) { //底牌
|
||
o_pai.dealowner = 0;
|
||
} else {
|
||
o_pai.dealowner = tmpdeal[idx] + 1;
|
||
}
|
||
//将数组的最后一位至该位置
|
||
if (idx < tmpdeal.length - 1) {
|
||
tmpdeal[idx] = tmpdeal[tmpdeal.length - 1];
|
||
};
|
||
//数组长度减一
|
||
tmpdeal.length = tmpdeal.length - 1;
|
||
}
|
||
|
||
//冻结底牌快照(发牌留桌的 8 张)
|
||
//——内容在发牌结束时就已确定(dealowner==0 之后不再变),【显示顺序也必须一并冻结】。
|
||
//理由(SSOT):排序用的 order_cards 依赖主牌花色,而底牌是在【选主之前】的上庄时刻
|
||
//就翻给庄家看的——那时 paiju.flower 还是 -1,根本还没有主牌。若每次调用都按「当时的
|
||
//flower」重排,上庄推送(flower=-1)与重连 BuryCards(step3,flower 已定)会对同一份数据
|
||
//给出两种顺序(实测:主花色的那张 K 被排到队首),前端增量路径与重连路径因此对不上。
|
||
//故在发牌结束时按【翻底那一刻的口径】算一次并冻结,之后所有下发点只取这份快照的副本。
|
||
var init_bottomcards = function(){
|
||
var ary = [];
|
||
for (var i = 0; i < paiju.cards.length; i++){
|
||
if (paiju.cards[i].dealowner == 0){
|
||
ary.push(paiju.cards[i].id);
|
||
}
|
||
}
|
||
//此处 paiju.flower 必为 -1(选主在 step2 才发生),即「无主牌」序 = 翻底时的顺序
|
||
paiju.bottomcards = cls_youle_erqiwang_arith.order_cards(paiju.flower, ary);
|
||
}
|
||
|
||
//进入叫分阶段
|
||
var start_call = function(){
|
||
paiju.step = 1;
|
||
var o_call = {};
|
||
o_call.seat = paiju.firstseat;
|
||
o_call.call = null; //null:未叫分 0:不叫分 >0:叫分分数
|
||
paiju.callproc.push(o_call);
|
||
}
|
||
|
||
// ================= 实例的公有属性 ================= //
|
||
paiju.o_desk = o_desk;
|
||
//牌局号
|
||
paiju.idx = o_desk.paiju_list.length;
|
||
//第一个叫分者的位置
|
||
paiju.firstseat = firstseat;
|
||
//开始时间
|
||
paiju.starttime = min_now();
|
||
//结束时间
|
||
paiju.endtime = null;
|
||
//座位列表
|
||
paiju.seatlist = [];
|
||
//牌列表
|
||
paiju.cards = [];
|
||
//当前阶段
|
||
paiju.step = -1; //1发完牌叫分 2选主/投降 3埋牌 5出牌 6结算(无独立投降阶段,投降在step2)
|
||
//叫分过程
|
||
paiju.callproc = [];
|
||
//庄家位置
|
||
paiju.banker = -1;
|
||
//庄家叫分
|
||
paiju.call = -1;
|
||
//主牌花色
|
||
paiju.flower = -1;
|
||
//底牌快照(发牌留桌的 8 张,含显示顺序):发牌结束时冻结,见 init_bottomcards
|
||
paiju.bottomcards = [];
|
||
//当前出牌情况
|
||
paiju.playproc = {};
|
||
//出牌历史归档:外层下标 = 轮次-1,内层恒 3 个数组、下标 = 座位;每一项就是那一手
|
||
//【实际打出、且已归一成权威顺序】的牌。唯一写入处是 do_playcard 的 doplay()。
|
||
//【为什么要存】重连包 PushCards.pushlist 需要往轮的出牌历史,而牌一旦落地就只剩
|
||
//「第几轮 + 属于谁」(cards[i].playround/dealowner),一手之内的顺序无从恢复——
|
||
//从那里反查就必须自己再排一次序,那是同一份数据的第二个写入处(SSOT 违反),
|
||
//且遇到同编码的两张牌(一对)时两处必然分岔。改为出牌当场归档、下游只读。
|
||
paiju.playhistory = [];
|
||
//输赢结果 0:庄赢 1:闲赢 2:庄投降 3:中途解散
|
||
paiju.result = null;
|
||
//结算包快照,供断线重连时使用,新开一局时清除。三件套与 jiesuan 推送【同源同值】:
|
||
// tmp_jiesuan_aset 单局结算(三种结算来源恒有)
|
||
// tmp_jiesuan_bottom 抠底明细(仅正常出牌结算有;投降/解散为 null)
|
||
// tmp_jiesuan_account 大局结算(仅末局或解散有;其余为 null)
|
||
//只存 aset 会让「结算面板还开着时重连/硬刷新」丢掉抠底明细与大局结算(增量路径有、
|
||
//重连路径没有),即同一份数据两条路径给的不一样。唯一写入处是 get_paiju_account 末尾。
|
||
paiju.tmp_jiesuan_aset = null;
|
||
paiju.tmp_jiesuan_bottom = null;
|
||
paiju.tmp_jiesuan_account = null;
|
||
|
||
//初始化座位列表
|
||
init_seatlist();
|
||
//初始化牌列表并发牌
|
||
init_cards();
|
||
//冻结底牌快照(必须在发牌之后、选主之前)
|
||
init_bottomcards();
|
||
//进入叫分阶段
|
||
start_call();
|
||
|
||
// ================= 实例的公有方法 ================= //
|
||
paiju.method = {};
|
||
|
||
//获取某个位置玩家手上的牌
|
||
paiju.method.get_seat_cards = function(seat){
|
||
return cls_youle_erqiwang_paiju.get_seat_cards(paiju, seat);
|
||
}
|
||
|
||
//获取当前的叫分位置
|
||
paiju.method.get_callgrade_seat = function(){
|
||
return paiju.callproc[paiju.callproc.length - 1].seat;
|
||
}
|
||
|
||
//获取当前的叫分分数
|
||
paiju.method.get_callgrade_value = function(){
|
||
return cls_youle_erqiwang_paiju.get_callgrade_value(paiju);
|
||
}
|
||
|
||
//获取某个玩家的当前叫分情况
|
||
paiju.method.get_seat_callgrade = function(seat){
|
||
return cls_youle_erqiwang_paiju.get_seat_callgrade(paiju, seat);
|
||
}
|
||
|
||
//叫分或不叫
|
||
paiju.method.do_callgrade = function(call){
|
||
cls_youle_erqiwang_paiju.do_callgrade(paiju, call);
|
||
}
|
||
|
||
//获取底牌
|
||
paiju.method.get_bottomcards = function(){
|
||
return cls_youle_erqiwang_paiju.get_bottomcards(paiju);
|
||
}
|
||
|
||
//选主
|
||
paiju.method.do_choiceflower = function(flower){
|
||
cls_youle_erqiwang_paiju.do_choiceflower(paiju, flower);
|
||
}
|
||
|
||
//校验客户端提交的牌id列表本身是否合法(数组/整数/范围/无重复)
|
||
paiju.method.check_cards_valid = function(cards){
|
||
return cls_youle_erqiwang_paiju.check_cards_valid(paiju, cards);
|
||
}
|
||
|
||
//判断牌是否在玩家手上
|
||
paiju.method.check_cards_inhand = function(cards, seat){
|
||
return cls_youle_erqiwang_paiju.check_cards_inhand(paiju, cards, seat);
|
||
}
|
||
|
||
//埋牌
|
||
paiju.method.do_burycard = function(cards){
|
||
cls_youle_erqiwang_paiju.do_burycard(paiju, cards);
|
||
}
|
||
|
||
//检查选中的牌是否可出,并出牌
|
||
paiju.method.do_playcard = function(cards){
|
||
return cls_youle_erqiwang_paiju.do_playcard(paiju, cards);
|
||
}
|
||
|
||
//是否有人报副
|
||
paiju.method.have_baofu = function(){
|
||
return (paiju.seatlist[0][4][0] == 0 || paiju.seatlist[1][4][0] == 0 || paiju.seatlist[2][4][0] == 0);
|
||
}
|
||
|
||
//在牌列表中获取分牌和分值
|
||
paiju.method.get_grade_incard = function(cards){
|
||
return cls_youle_erqiwang_paiju.get_grade_incard(paiju, cards);
|
||
}
|
||
|
||
//获取埋牌
|
||
paiju.method.get_burycard= function(){
|
||
return cls_youle_erqiwang_paiju.get_burycard(paiju);
|
||
}
|
||
|
||
//获取抠底包
|
||
paiju.method.get_bottom_account = function(msg){
|
||
return cls_youle_erqiwang_paiju.get_bottom_account(paiju, msg);
|
||
}
|
||
|
||
//小局结算,type 0正常结算 1投降结算 2解散结算
|
||
paiju.method.get_paiju_account = function(type, msg){
|
||
return cls_youle_erqiwang_paiju.get_paiju_account(paiju, type, msg);
|
||
}
|
||
|
||
//获取闲家当前的捡分分数和分牌
|
||
paiju.method.get_jian_grade = function(){
|
||
return cls_youle_erqiwang_paiju.get_jian_grade(paiju);
|
||
}
|
||
|
||
//获取庄家亮牌信息(design §8.2)
|
||
paiju.method.get_liangpai = function(){
|
||
return cls_youle_erqiwang_paiju.get_liangpai(paiju);
|
||
}
|
||
|
||
//获取当前抓分倍数(design §7.2.0,实时预览)
|
||
paiju.method.get_curmultiple = function(){
|
||
return cls_youle_erqiwang_paiju.get_curmultiple(paiju);
|
||
}
|
||
|
||
//获取出牌历史快照(重连包 PushCards.pushlist)
|
||
paiju.method.get_pushlist = function(){
|
||
return cls_youle_erqiwang_paiju.get_pushlist(paiju);
|
||
}
|
||
|
||
//获取三家座位牌况快照(maipai / chupai1/2/3 / 重连包 PushCards.seatlist)
|
||
paiju.method.get_seatlist = function(){
|
||
return cls_youle_erqiwang_paiju.get_seatlist(paiju);
|
||
}
|
||
|
||
//获取某座位本轮跟牌的必出牌(design §5.2)
|
||
paiju.method.get_mustcard = function(seat){
|
||
return cls_youle_erqiwang_paiju.get_mustcard(paiju, seat);
|
||
}
|
||
|
||
return paiju;
|
||
},
|
||
|
||
//下一个位置
|
||
get_nextseat: function(seat){
|
||
return (seat + 1) % 3;
|
||
},
|
||
|
||
//获取某个位置玩家手上的牌
|
||
get_seat_cards: function(o_paiju, seat){
|
||
var aryCardIDs = [];
|
||
for (var i = 0; i < o_paiju.cards.length; i++){
|
||
var o_card = o_paiju.cards[i];
|
||
if (o_card.playround == -1){ //未出的牌
|
||
if ((o_card.dealowner == seat + 1) || //发到手上
|
||
(o_card.dealowner == 0 && seat == o_paiju.banker)){ //庄家包括底牌
|
||
aryCardIDs.push(o_paiju.cards[i].id);
|
||
}
|
||
}
|
||
}
|
||
return cls_youle_erqiwang_arith.order_cards(o_paiju.flower, aryCardIDs);
|
||
},
|
||
|
||
//获取某个位置玩家发到手上的牌
|
||
get_seat_cards_deal: function(o_paiju, seat){
|
||
var aryCardIDs = [];
|
||
for (var i = 0; i < o_paiju.cards.length; i++){
|
||
var o_card = o_paiju.cards[i];
|
||
if (o_card.dealowner == seat + 1){
|
||
aryCardIDs.push(o_paiju.cards[i].id);
|
||
}
|
||
}
|
||
return cls_youle_erqiwang_arith.order_cards(o_paiju.flower, aryCardIDs);
|
||
},
|
||
|
||
//获取某个位置玩家发到手上的牌(庄家包含底牌)
|
||
get_seat_cards_owner: function(o_paiju, seat){
|
||
var aryCardIDs = [];
|
||
for (var i = 0; i < o_paiju.cards.length; i++){
|
||
var o_card = o_paiju.cards[i];
|
||
if ((o_card.dealowner == seat + 1) || //发到手上
|
||
(o_card.dealowner == 0 && seat == o_paiju.banker)){ //庄家包括底牌
|
||
aryCardIDs.push(o_paiju.cards[i].id);
|
||
}
|
||
}
|
||
return cls_youle_erqiwang_arith.order_cards(o_paiju.flower, aryCardIDs);
|
||
},
|
||
|
||
//获取算奖用的静态初始手牌快照(design §8)
|
||
//庄家:埋牌后保留的28张(发牌+底,排除已埋 playround==0);闲家:发牌后的28张
|
||
get_seat_cards_award: function(o_paiju, seat){
|
||
var aryCardIDs = [];
|
||
for (var i = 0; i < o_paiju.cards.length; i++){
|
||
var o_card = o_paiju.cards[i];
|
||
if (seat == o_paiju.banker){
|
||
if ((o_card.dealowner == seat + 1 || o_card.dealowner == 0) && o_card.playround != 0){
|
||
aryCardIDs.push(o_card.id);
|
||
}
|
||
} else {
|
||
if (o_card.dealowner == seat + 1){
|
||
aryCardIDs.push(o_card.id);
|
||
}
|
||
}
|
||
}
|
||
return cls_youle_erqiwang_arith.order_cards(o_paiju.flower, aryCardIDs);
|
||
},
|
||
|
||
//获取某个位置玩家手上的主牌
|
||
get_seat_zhucards: function(o_paiju, seat){
|
||
var aryCardIDs = [];
|
||
for (var i = 0; i < o_paiju.cards.length; i++){
|
||
var o_card = o_paiju.cards[i];
|
||
if (o_card.playround == -1){ //未出的牌
|
||
if ((o_card.dealowner == seat + 1) || //发到手上
|
||
(o_card.dealowner == 0 && seat == o_paiju.banker)){ //庄家包括底牌
|
||
if (o_card.flower == o_paiju.flower || o_card.flower == 5 || o_card.number == 2 || o_card.number == 7){
|
||
aryCardIDs.push(o_paiju.cards[i].id);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return cls_youle_erqiwang_arith.order_cards(o_paiju.flower, aryCardIDs);
|
||
},
|
||
|
||
//获取当前的叫分分数
|
||
get_callgrade_value: function(o_paiju){
|
||
var value = 0;
|
||
for (var i = o_paiju.callproc.length - 2; i >= 0; i--) {
|
||
if (o_paiju.callproc[i].call) {
|
||
value = o_paiju.callproc[i].call;
|
||
break;
|
||
}
|
||
}
|
||
return value;
|
||
},
|
||
|
||
//获取某个位置的叫分 返回值:null未叫 0不叫 >0当前叫分
|
||
get_seat_callgrade: function(o_paiju, seat){
|
||
for (var i = o_paiju.callproc.length - 2; i >= 0; i--){
|
||
if (o_paiju.callproc[i].seat == seat){
|
||
return o_paiju.callproc[i].call;
|
||
}
|
||
}
|
||
return null;
|
||
},
|
||
|
||
//叫分
|
||
do_callgrade: function(o_paiju, call){
|
||
var o_call = o_paiju.callproc[o_paiju.callproc.length - 1];
|
||
o_call.call = call;
|
||
|
||
//等待下一个叫分者叫分
|
||
var do_wait_nextseat_call = function(seat){
|
||
var o_call = {};
|
||
o_call.seat = seat;
|
||
o_call.call = null;
|
||
o_paiju.callproc.push(o_call);
|
||
}
|
||
|
||
//上庄
|
||
var do_up_banker = function(seat, call){
|
||
//庄家位置
|
||
o_paiju.banker = seat;
|
||
//庄家叫分
|
||
o_paiju.call = call;
|
||
//进入选主阶段
|
||
o_paiju.step = 2;
|
||
}
|
||
|
||
//叫了5分直接上庄
|
||
if (call == 5){
|
||
do_up_banker(o_call.seat, call);
|
||
return;
|
||
}
|
||
//叫了分
|
||
if (call > 0){
|
||
//下一个位置
|
||
var nextseat = cls_youle_erqiwang_paiju.get_nextseat(o_call.seat);
|
||
//下一个位置的叫分
|
||
var nextcall = cls_youle_erqiwang_paiju.get_seat_callgrade(o_paiju, nextseat);
|
||
//下一个位置不叫分
|
||
if (nextcall == 0){
|
||
//再下一个位置
|
||
nextseat = cls_youle_erqiwang_paiju.get_nextseat(nextseat);
|
||
}
|
||
//等待下一个叫分者叫分
|
||
do_wait_nextseat_call(nextseat);
|
||
return;
|
||
}
|
||
|
||
//不叫
|
||
if (call == 0){
|
||
//下一个位置
|
||
var nextseat_1 = cls_youle_erqiwang_paiju.get_nextseat(o_call.seat);
|
||
//下一个位置的叫分
|
||
var nextcall_1 = cls_youle_erqiwang_paiju.get_seat_callgrade(o_paiju, nextseat_1);
|
||
//再下一个位置
|
||
var nextseat_2 = cls_youle_erqiwang_paiju.get_nextseat(nextseat_1);
|
||
//再下一个位置的叫分
|
||
var nextcall_2 = cls_youle_erqiwang_paiju.get_seat_callgrade(o_paiju, nextseat_2);
|
||
|
||
if (nextcall_1 != 0 && nextcall_2 != 0){
|
||
//另外两家都没有不叫分
|
||
do_wait_nextseat_call(nextseat_1);
|
||
return;
|
||
}
|
||
if (nextcall_1 != 0 && nextcall_2 == 0){
|
||
//nextseat_1上庄
|
||
do_up_banker(nextseat_1, nextcall_1);
|
||
return;
|
||
}
|
||
if (nextcall_1 == 0 && nextcall_2 != 0){
|
||
//nextseat_2上庄
|
||
do_up_banker(nextseat_2, nextcall_2);
|
||
return;
|
||
}
|
||
}
|
||
},
|
||
|
||
//获取底牌(发牌留桌的 8 张):唯一来源是发牌时冻结的快照 o_paiju.bottomcards(见 init_bottomcards)。
|
||
//【返回副本】——order_cards 是原地排序,把快照本体交出去会被调用方就地重排,冻结即失效。
|
||
get_bottomcards: function(o_paiju){
|
||
return o_paiju.bottomcards.concat();
|
||
},
|
||
|
||
//获取【供下发的】当前出牌情况快照:deskinfo.PushCards 与 chupai1/2/3 共用这一处,
|
||
//两条路径因此结构完全一致(前端增量回放与重连重建可复用同一份解析)。
|
||
//必须【深拷贝】:playproc 是一个全程复用的活对象(new_playround 就地重置同一个对象,
|
||
//do_playcard 也就地改 cards/maxseat/maxcard),直接把它挂进 msg 会让「已发出的包」
|
||
//随后续出牌被回改;平台真实链路虽然立刻序列化看不出来,但这是隐患,不留。
|
||
//structure:round/start/currseat/startcount/startflower/starttype/maxseat/maxcard/
|
||
// cards(定长3、下标=座位)/shuai_demand
|
||
//获取【供下发的】出牌历史快照(重连包 PushCards.pushlist)。
|
||
//唯一来源是 do_playcard 当场归档的 playhistory,这里只拷贝、不重算也不重排:
|
||
//它与 chupai1/2/3 的 data.cards、playproc.cards 是同一次 order_playcards 的产物,
|
||
//因此两条路径逐元素相等(前端增量回放与重连重建必定一致)。
|
||
//【返回深拷贝】不把活数组挂进下发包(同 get_playproc)。
|
||
get_pushlist: function(o_paiju){
|
||
var re = [];
|
||
for (var i = 0; i < o_paiju.playhistory.length; i++){
|
||
var _round = o_paiju.playhistory[i];
|
||
var _one = [];
|
||
for (var s = 0; s < 3; s++){
|
||
_one.push(_round[s].concat());
|
||
}
|
||
re.push(_one);
|
||
}
|
||
return re;
|
||
},
|
||
|
||
//获取【供下发的】三家座位牌况快照(maipai / chupai1/2/3 / 重连包 PushCards.seatlist)。
|
||
//结构:外层定长 3(下标=座位),每家 5 个二元数组——前 4 个是花色1~4 的 [无该花色, 无对],
|
||
//第 5 个是报副 [剩余主牌数, 剩余主对数]。
|
||
//【返回深拷贝】o_paiju.seatlist 是一张全程就地改写的活表(do_playcard 里按缺门/无对/报无主
|
||
//逐格赋值),把它本体挂进下发包会让「已发出的包」随后续出牌被回改;平台真实链路虽然立刻
|
||
//序列化看不出来,但这是隐患,与 get_playproc / get_pushlist / get_bottomcards 同一条标准。
|
||
get_seatlist: function(o_paiju){
|
||
var re = [];
|
||
for (var s = 0; s < o_paiju.seatlist.length; s++){
|
||
var _one = [];
|
||
for (var f = 0; f < o_paiju.seatlist[s].length; f++){
|
||
_one.push(o_paiju.seatlist[s][f].concat());
|
||
}
|
||
re.push(_one);
|
||
}
|
||
return re;
|
||
},
|
||
|
||
get_playproc: function(o_paiju){
|
||
var _p = o_paiju.playproc;
|
||
if (!_p){
|
||
return null;
|
||
}
|
||
var re = {};
|
||
re.round = _p.round;
|
||
re.start = _p.start;
|
||
re.currseat = _p.currseat;
|
||
re.startcount = _p.startcount;
|
||
re.startflower = _p.startflower;
|
||
re.starttype = _p.starttype;
|
||
re.maxseat = _p.maxseat;
|
||
re.maxcard = _p.maxcard;
|
||
//本轮各位置出的牌:定长 3、下标 = 座位序号;尚未出牌的位置留空(序列化后为 null)
|
||
re.cards = [];
|
||
re.cards.length = 3;
|
||
if (_p.cards){
|
||
for (var i = 0; i < 3; i++){
|
||
if (_p.cards[i]){
|
||
re.cards[i] = _p.cards[i].concat();
|
||
}
|
||
}
|
||
}
|
||
//首家甩牌的分量需求(§5.4.4);tractors 是数组,同样要拷贝,非甩牌为 null
|
||
if (_p.shuai_demand){
|
||
var _sd = {};
|
||
_sd.tractors = _p.shuai_demand.tractors ? _p.shuai_demand.tractors.concat() : [];
|
||
_sd.pairs = _p.shuai_demand.pairs;
|
||
_sd.singles = _p.shuai_demand.singles;
|
||
re.shuai_demand = _sd;
|
||
} else {
|
||
re.shuai_demand = null;
|
||
}
|
||
return re;
|
||
},
|
||
|
||
//选主
|
||
do_choiceflower: function(o_paiju, flower){
|
||
o_paiju.flower = flower;
|
||
//主牌的牌编码在 arith.id_to_code 里按 mainflower 动态计算,选主时只需记录花色
|
||
//进入埋牌阶段
|
||
o_paiju.step = 3;
|
||
},
|
||
|
||
//校验客户端提交的牌id列表本身是否合法:必须是非空数组、元素为本局牌表内的整数牌id、且互不重复
|
||
//(客户端入参不可信:重复id 会被 can_playcard 当成对子/拖拉机,也会让埋牌少埋;越界id 会取到
|
||
// undefined 而在后续解引用时抛异常中断 DoPack。此处显式失败,不做隐式兜底)
|
||
check_cards_valid: function(o_paiju, cards){
|
||
if (Object.prototype.toString.call(cards) != "[object Array]"){
|
||
return false;
|
||
}
|
||
if (cards.length <= 0){
|
||
return false;
|
||
}
|
||
var _seen = {};
|
||
for (var i = 0; i < cards.length; i++){
|
||
var _id = cards[i];
|
||
//必须是落在本局牌表范围内的整数牌id
|
||
if (typeof _id != "number" || _id != parseInt(_id) || _id < 0 || _id >= o_paiju.cards.length){
|
||
return false;
|
||
}
|
||
//同一张牌不得重复提交
|
||
if (_seen[_id]){
|
||
return false;
|
||
}
|
||
_seen[_id] = 1;
|
||
}
|
||
return true;
|
||
},
|
||
|
||
//判断牌是否在玩家手上(入参合法性由 check_cards_valid 一并把关,本函数是唯一的持牌校验入口)
|
||
check_cards_inhand: function(o_paiju, cards, seat){
|
||
if (!cls_youle_erqiwang_paiju.check_cards_valid(o_paiju, cards)){
|
||
return false;
|
||
}
|
||
for (var i = 0; i < cards.length; i++) {
|
||
var o_card = o_paiju.cards[cards[i]];
|
||
if (o_card.playround != -1){
|
||
//已埋下或已打出的牌
|
||
return false;
|
||
}
|
||
if (seat == o_paiju.banker){
|
||
//是庄家
|
||
if (o_card.dealowner != seat + 1 && o_card.dealowner != 0){
|
||
return false;
|
||
}
|
||
} else {
|
||
//是闲家
|
||
if (o_card.dealowner != seat + 1){
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
return true;
|
||
},
|
||
|
||
//埋牌
|
||
do_burycard: function(o_paiju, cards){
|
||
for (var i = 0; i < cards.length; i++) {
|
||
o_paiju.cards[cards[i]].playround = 0;
|
||
};
|
||
//埋牌后直接进入出牌阶段,由庄家首出(投降已在选主阶段 step2 决定,无独立投降阶段)
|
||
o_paiju.step = 5;
|
||
cls_youle_erqiwang_paiju.new_playround(o_paiju, 1, o_paiju.banker);
|
||
},
|
||
|
||
//新一轮出牌
|
||
new_playround: function(o_paiju, round, startseat){
|
||
o_paiju.playproc.round = round; //第几轮
|
||
o_paiju.playproc.start = startseat; //本轮的起始出牌位置
|
||
o_paiju.playproc.currseat = startseat; //本轮的当前的出牌位置
|
||
o_paiju.playproc.startcount = -1; //本轮第一个出牌的张数
|
||
o_paiju.playproc.startflower = -1; //本轮第一个出牌的花色
|
||
o_paiju.playproc.starttype = -1; //本轮第一个出牌的牌型
|
||
o_paiju.playproc.maxseat = -1; //谁的牌最大,位置
|
||
o_paiju.playproc.maxcard = -1; //满足第一个玩家出牌的牌型最大牌
|
||
o_paiju.playproc.cards = []; //本轮各位置出的牌,数组与位置序号对应
|
||
o_paiju.playproc.cards.length= 3;
|
||
o_paiju.playproc.shuai_demand= null; //首家甩牌的分量需求(供跟牌逐分量匹配),非甩牌为null
|
||
},
|
||
|
||
//【一手出牌的顺序:唯一口径】把一手牌规整成本局的权威展示顺序
|
||
//(按主牌花色从大到小)。只在 do_playcard 落牌那一刻调一次,之后全体下游只读:
|
||
// · playproc.cards[座位] → chupai1/2/3 与重连包的 playproc
|
||
// · do_playcard 返回的 re.cards → chupai1/2/3 的 data.cards
|
||
// · playhistory[轮次][座位] → 重连包的 PushCards.pushlist
|
||
//三者同源同数组内容,所以前端“增量回放”与“重连重建”必定逐元素相等。
|
||
//
|
||
//为什么不能用客户端提交的原序:那是玩家的点击顺序,同一手牌每次都可能不同,
|
||
//服务端不得依赖、也不回显(server 04 §8:前端不是数据源)。
|
||
//【返回新数组】——order_cards 是原地排序,直接排入参会就地改写调用方(含 pack.data.cards)。
|
||
order_playcards: function(o_paiju, cards){
|
||
if (!cards){
|
||
return cards;
|
||
}
|
||
return cls_youle_erqiwang_arith.order_cards(o_paiju.flower, cards.concat());
|
||
},
|
||
|
||
//在牌列表中获取分牌和分值
|
||
get_grade_incard: function(o_paiju, cards){
|
||
var re = {};
|
||
re.grade = 0;
|
||
re.cards = [];
|
||
for (var i = 0; i < cards.length; i++) {
|
||
if (o_paiju.cards[cards[i]].score > 0){
|
||
re.grade = re.grade + o_paiju.cards[cards[i]].score;
|
||
re.cards.push(cards[i]);
|
||
}
|
||
}
|
||
return re;
|
||
},
|
||
|
||
//检查选中的牌是否可出,并出牌
|
||
do_playcard: function(o_paiju, cards){
|
||
//【顺序归一:唯一写入处】入参是客户端提交的点击顺序,服务端不采信、不下发。
|
||
//在这里一次性转成权威顺序(order_playcards),此后 playproc.cards / 返回给下发包的
|
||
//re.cards / 重连包 pushlist 全都是同一个口径,不会再有第二处排序(SSOT)。
|
||
//顺带隔离了 can_playcard 的【原地排序】——否则它会就地改写 pack.data.cards。
|
||
cards = cls_youle_erqiwang_paiju.order_playcards(o_paiju, cards);
|
||
|
||
//出牌
|
||
var doplay = function(playindex){
|
||
for (var i = 0; i < cards.length; i++) {
|
||
o_paiju.cards[cards[i]].playround = o_paiju.playproc.round;
|
||
o_paiju.cards[cards[i]].playindex = playindex;
|
||
};
|
||
o_paiju.playproc.cards[o_paiju.playproc.currseat] = cards;
|
||
//归档这一手,供重连包 PushCards.pushlist 直读(唯一写入处,见 paiju.playhistory)
|
||
while (o_paiju.playhistory.length < o_paiju.playproc.round){
|
||
o_paiju.playhistory.push([[], [], []]);
|
||
}
|
||
o_paiju.playhistory[o_paiju.playproc.round - 1][o_paiju.playproc.currseat] = cards.concat();
|
||
|
||
//进入出牌阶段
|
||
o_paiju.step = 5;
|
||
|
||
//报无主处理(design §9)
|
||
//先看出牌者自己的主牌是否已出空(出空即"报无主");
|
||
//一旦全场有人报无主,立即为【全体三人】刷新剩余主牌数/主对子数——design §9 要求
|
||
//"一旦有玩家的主牌全部打空,系统就为全体三人显示另外两家各自的主牌数量与对子数",
|
||
//而不是等另外两家各自轮到出牌时才逐个补上。
|
||
var _self = o_paiju.playproc.currseat;
|
||
if (cls_youle_erqiwang_paiju.get_seat_zhucards(o_paiju, _self).length == 0){
|
||
o_paiju.seatlist[_self][4][0] = 0;
|
||
o_paiju.seatlist[_self][4][1] = 0;
|
||
}
|
||
if (o_paiju.method.have_baofu()){
|
||
for (var _s = 0; _s < 3; _s++){
|
||
var _zhu = cls_youle_erqiwang_paiju.get_seat_zhucards(o_paiju, _s);
|
||
var _zhupair = cls_youle_erqiwang_arith.get_pairlist(o_paiju.flower, _zhu);
|
||
o_paiju.seatlist[_s][4][0] = _zhu.length;
|
||
o_paiju.seatlist[_s][4][1] = _zhupair.length;
|
||
|
||
if (_zhu.length == 0){
|
||
//主牌出空,等同于主花色也没有了、也没有主花色的对子
|
||
o_paiju.seatlist[_s][o_paiju.flower - 1][0] = 1;
|
||
o_paiju.seatlist[_s][o_paiju.flower - 1][1] = 1;
|
||
} else if (_zhupair.length == 0){
|
||
o_paiju.seatlist[_s][o_paiju.flower - 1][1] = 1;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
//本轮中的第二个出牌位置
|
||
var nextseat_1 = cls_youle_erqiwang_paiju.get_nextseat(o_paiju.playproc.start);
|
||
//本轮中的第三个出牌位置
|
||
var nextseat_2 = cls_youle_erqiwang_paiju.get_nextseat(nextseat_1);
|
||
|
||
if (o_paiju.playproc.currseat == o_paiju.playproc.start){ //是本轮的第一个出牌
|
||
//两名对手未出的主牌,供甩牌最大性判定(§5.4.2)
|
||
var opp_zhulist = [];
|
||
for (var os = 0; os < 3; os++){
|
||
if (os != o_paiju.playproc.currseat){
|
||
opp_zhulist.push(cls_youle_erqiwang_paiju.get_seat_zhucards(o_paiju, os));
|
||
}
|
||
}
|
||
var can = cls_youle_erqiwang_arith.can_playcard(o_paiju.flower, cards, o_paiju.playproc.currseat, o_paiju.seatlist, opp_zhulist);
|
||
var _shuaicuo = false;
|
||
if (!can.result){
|
||
if (can.shuaicuo){
|
||
//甩错惩罚(§5.4.5):收回甩牌,本轮只强制打出最小一张
|
||
_shuaicuo = true;
|
||
cards = cls_youle_erqiwang_paiju.order_playcards(o_paiju, [can.smallest]);
|
||
can = cls_youle_erqiwang_arith.can_playcard(o_paiju.flower, cards, o_paiju.playproc.currseat, o_paiju.seatlist, opp_zhulist);
|
||
if (!can.result){
|
||
var reErr = {};
|
||
reErr.result = false;
|
||
return reErr;
|
||
}
|
||
} else {
|
||
var re = {};
|
||
re.result = false;
|
||
return re;
|
||
}
|
||
}
|
||
//出牌(cards 在甩错时已被替换为仅最小一张)
|
||
doplay(1);
|
||
//记录下牌型、花色、牌型最大牌
|
||
o_paiju.playproc.startcount = cards.length;
|
||
o_paiju.playproc.startflower = can.flower;
|
||
o_paiju.playproc.starttype = can.cardtype;
|
||
o_paiju.playproc.maxseat = o_paiju.playproc.currseat;
|
||
o_paiju.playproc.maxcard = can.cardvalue;
|
||
//合法甩牌的分量需求,供跟牌逐分量强制匹配(§5.4.4);甩错退化为单张则为 null
|
||
o_paiju.playproc.shuai_demand = can.shuai_demand ? can.shuai_demand : null;
|
||
o_paiju.playproc.currseat = nextseat_1;
|
||
//返回结果
|
||
var re1 = {};
|
||
re1.result = true;
|
||
re1.idx = 1;
|
||
re1.count = cards.length;
|
||
re1.flower = can.flower;
|
||
re1.cardtype = can.cardtype;
|
||
//实际打出的牌(甩错时为单张),已是权威顺序。
|
||
//【给副本】cards 本体已挂在 playproc.cards[座位] 上,直接交出去会让「已发出的包」
|
||
//随后续改动被回改(同 get_playproc 的深拷贝理由)
|
||
re1.cards = cards.concat();
|
||
re1.shuaicuo = _shuaicuo;
|
||
//合法甩牌的分量构成:cardtype 只能表达单一牌型,甩牌(单张/对子/拖拉机自由混搭,
|
||
//design §5.4.3)会被压平成 1xx/2xx,客户端无从据此还原分组,故另行下发分量需求
|
||
if (o_paiju.playproc.shuai_demand){
|
||
re1.shuai = o_paiju.playproc.shuai_demand;
|
||
}
|
||
return re1;
|
||
}
|
||
|
||
//不是本轮中的第一个出牌,是跟牌
|
||
var cardsinhand = cls_youle_erqiwang_paiju.get_seat_cards(o_paiju, o_paiju.playproc.currseat);
|
||
var can = cls_youle_erqiwang_arith.can_followcard(o_paiju.flower, cardsinhand, cards, o_paiju.playproc.startcount, o_paiju.playproc.startflower, o_paiju.playproc.starttype);
|
||
if (!can.result){
|
||
var re = {};
|
||
re.result = false;
|
||
return re;
|
||
}
|
||
//首家是甩牌时,追加"拖拉机→对子→单张"的强制分量匹配校验(§5.4.4)
|
||
if (o_paiju.playproc.shuai_demand){
|
||
if (!cls_youle_erqiwang_arith.flush_follow_ok(o_paiju.flower, cardsinhand, cards, o_paiju.playproc.shuai_demand)){
|
||
var reF = {};
|
||
reF.result = false;
|
||
return reF;
|
||
}
|
||
}
|
||
|
||
//与之前的出牌比较大小
|
||
if (can.cardvalue > o_paiju.playproc.maxcard){
|
||
o_paiju.playproc.maxseat = o_paiju.playproc.currseat;
|
||
o_paiju.playproc.maxcard = can.cardvalue;
|
||
}
|
||
|
||
//无花色、无对子处理
|
||
if (can.nopair){
|
||
o_paiju.seatlist[o_paiju.playproc.currseat][o_paiju.playproc.startflower - 1][1] = 1;
|
||
}
|
||
if (can.noflower){
|
||
o_paiju.seatlist[o_paiju.playproc.currseat][o_paiju.playproc.startflower - 1][0] = 1;
|
||
o_paiju.seatlist[o_paiju.playproc.currseat][o_paiju.playproc.startflower - 1][1] = 1;
|
||
}
|
||
|
||
//是本轮中的第二个出牌
|
||
if (o_paiju.playproc.currseat == nextseat_1){
|
||
//出牌
|
||
doplay(2);
|
||
//下一个出牌者
|
||
o_paiju.playproc.currseat = nextseat_2;
|
||
//返回结果
|
||
var re2 = {};
|
||
re2.idx = 2;
|
||
re2.result = true;
|
||
re2.cards = cards.concat(); //实际打出的牌,权威顺序(理由同 re1.cards)
|
||
return re2;
|
||
}
|
||
|
||
//是本轮中的第三个出牌
|
||
//出牌
|
||
doplay(3);
|
||
//本轮出牌结束
|
||
for (var i = 0; i < o_paiju.playproc.cards.length; i++) {
|
||
for (var j = 0; j < o_paiju.playproc.cards[i].length; j++) {
|
||
o_paiju.cards[o_paiju.playproc.cards[i][j]].playowner = o_paiju.playproc.maxseat;
|
||
}
|
||
}
|
||
|
||
//返回结果
|
||
var re3 = {};
|
||
re3.result = true;
|
||
re3.idx = 3;
|
||
re3.cards = cards.concat(); //实际打出的牌,权威顺序(理由同 re1.cards)
|
||
re3.maxseat = o_paiju.playproc.maxseat;
|
||
if (o_paiju.playproc.maxseat != o_paiju.banker){
|
||
re3.grade = 0;
|
||
// re3.gradecards = [];
|
||
for (var i = 0; i < o_paiju.playproc.cards.length; i++) {
|
||
var grade_incards = cls_youle_erqiwang_paiju.get_grade_incard(o_paiju, o_paiju.playproc.cards[i]);
|
||
re3.grade = re3.grade + grade_incards.grade;
|
||
// re3.gradecards = re3.gradecards.concat(grade_incards.cards);
|
||
}
|
||
}
|
||
|
||
//判断是否还有牌在玩家手上未出
|
||
var havecard_inhand = function(){
|
||
for (var i = 0; i < o_paiju.cards.length; i++) {
|
||
if (o_paiju.cards[i].playround == -1 && o_paiju.cards[i].number != 3 && o_paiju.cards[i].number != 4){
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
// if (!havecard_inhand() || o_paiju.playproc.round == 2){ //测试用
|
||
if (!havecard_inhand()){
|
||
//进入结算阶段
|
||
o_paiju.step = 6;
|
||
} else {
|
||
//新一轮出牌
|
||
cls_youle_erqiwang_paiju.new_playround(o_paiju, o_paiju.playproc.round + 1, o_paiju.playproc.maxseat);
|
||
}
|
||
return re3;
|
||
},
|
||
|
||
//获取埋牌
|
||
get_burycard: function(o_paiju){
|
||
var aryCardIDs = [];
|
||
for (var i = 0; i < o_paiju.cards.length; i++){
|
||
if (o_paiju.cards[i].playround == 0){
|
||
aryCardIDs.push(o_paiju.cards[i].id);
|
||
}
|
||
}
|
||
return cls_youle_erqiwang_arith.order_cards(o_paiju.flower, aryCardIDs);
|
||
},
|
||
|
||
//获取庄家亮牌信息(design §8.2):基于庄家埋牌后 28 张手牌,达标才亮,只亮数量/结构
|
||
//返回 null 表示无需亮牌;否则返回 {zhu,zhupair,zhutuo?, wang?, qi?, er?}
|
||
//手牌来源必须是 get_seat_cards_award 的【静态快照】(埋牌后 28 张,含之后已打出的),
|
||
//不能用 get_seat_cards(只取未出的牌):出牌开始亮出的这份统计全局固定、不随出牌缩水,
|
||
//否则闲家断线重连后 get_deskinfo 会按庄家当前剩余手牌重算,亮牌信息缩水甚至变成 null
|
||
//亮牌(design §8.2):庄家埋牌后、出牌前,若手中【固定主牌】达到门槛,
|
||
//就向两个闲家亮出【自己全部固定主牌的具体牌面】。
|
||
// 固定主牌 = 双王 + 全部花色的 2 + 全部花色的 7,【不含】主花色的普通牌 A/K/Q/J/10/9/8/6/5
|
||
// 门槛(任一满足即亮):固定主牌总数 ≥10 / 王 ≥3 / 7 ≥6 / 2 ≥6
|
||
// 不限叫分;是否真的发给闲家由调用方按查牌模式门控(design §9)
|
||
//返回 { cards: [牌id...] }(按本局主牌序从大到小),不达标返回 null。
|
||
//注意:亮出的内容是固定的一份,不随"被哪一条门槛触发"而增减。
|
||
get_liangpai: function(o_paiju){
|
||
if (o_paiju.banker < 0){
|
||
return null;
|
||
}
|
||
//静态初始手牌快照(与 8.1 算奖同源):庄家埋牌后的 28 张
|
||
var cards = cls_youle_erqiwang_paiju.get_seat_cards_award(o_paiju, o_paiju.banker);
|
||
var wang = 0, qi = 0, er = 0;
|
||
var gudingcards = []; //固定主牌
|
||
for (var i = 0; i < cards.length; i++){
|
||
var num = cls_youle_erqiwang_arith.id_to_number(cards[i]);
|
||
if (num >= 53){ //大王 54 / 小王 53
|
||
wang = wang + 1;
|
||
gudingcards.push(cards[i]);
|
||
} else if (num == 7){ //任意花色的 7
|
||
qi = qi + 1;
|
||
gudingcards.push(cards[i]);
|
||
} else if (num == 2){ //任意花色的 2
|
||
er = er + 1;
|
||
gudingcards.push(cards[i]);
|
||
}
|
||
}
|
||
//门槛判定(design §8.2)
|
||
if (gudingcards.length < 10 && wang < 3 && qi < 6 && er < 6){
|
||
return null;
|
||
}
|
||
var re = {};
|
||
re.cards = cls_youle_erqiwang_arith.order_cards(o_paiju.flower, gudingcards);
|
||
return re;
|
||
},
|
||
//获取抠底包
|
||
get_bottom_account: function(o_paiju, msg){
|
||
msg.data.bottom = {};
|
||
msg.data.bottom.cards = o_paiju.method.get_burycard();
|
||
if (o_paiju.playproc.maxseat != o_paiju.banker){ //闲家抠底
|
||
var multiple = cls_youle_erqiwang_arith.get_bottom_multiple(o_paiju.flower, o_paiju.playproc.cards[o_paiju.playproc.maxseat]);
|
||
if (multiple > 0){
|
||
msg.data.bottom.multiple = multiple;
|
||
var get = o_paiju.method.get_grade_incard(msg.data.bottom.cards);
|
||
msg.data.bottom.grade1 = get.grade;
|
||
msg.data.bottom.grade2 = multiple * msg.data.bottom.grade1;
|
||
}
|
||
}
|
||
return msg;
|
||
},
|
||
|
||
//获取闲家捡分(不包含抠底)
|
||
get_jian_grade: function(o_paiju){
|
||
var re = {};
|
||
re.grade = 0;
|
||
re.cards = [];
|
||
for (var i = 0; i < o_paiju.cards.length; i++){
|
||
if (o_paiju.cards[i].playowner > -1 && o_paiju.cards[i].playowner != o_paiju.banker){
|
||
if (o_paiju.cards[i].score > 0){
|
||
re.grade = re.grade + o_paiju.cards[i].score;
|
||
re.cards.push(i);
|
||
}
|
||
}
|
||
}
|
||
return re;
|
||
},
|
||
|
||
//当前抓分倍数(design §7.2.0):按【当前累计捡分】实时算出的判定倍率,【带符号】。
|
||
//供客户端顶部「抓分」列角标显示「若此刻结束是几倍」,以及对局中的判定动画(D-8)。
|
||
//与结算包 aset.upgrade 同源同算法(get_qvalue + get_upgrade),差别只在:
|
||
//这里的捡分【不含扣底】——扣底要到最后一轮打完才产生,出牌过程中尚不存在。
|
||
//叫分未定(call<=0)时返回 0,前端不显示角标。
|
||
get_curmultiple: function(o_paiju){
|
||
var _call = o_paiju.call;
|
||
if (!_call || _call <= 0){
|
||
return 0;
|
||
}
|
||
var _cfg = cls_youle_erqiwang_config.parse(o_paiju.o_desk.o_room.roomtype);
|
||
var _grade = cls_youle_erqiwang_paiju.get_jian_grade(o_paiju).grade;
|
||
var _q = cls_youle_erqiwang_arith.get_qvalue(_call, _cfg.climb);
|
||
//【带符号】返回,与结算包 aset.upgrade 完全同口径:
|
||
// 正数 3/2/1 = 庄家 大光/小光/过庄;负数 -N = 闲家升 N 级。
|
||
//符号就是「谁赢」的区分,不能取绝对值——否则 3(大光) 与 -3(升3级)、
|
||
//2(小光) 与 -2(升2级)、1(过庄) 与 -1(升1级) 三对会完全撞在一起,
|
||
//客户端的判定动画将无从分辨该播哪个(前端清单 S-8)。
|
||
//只关心倍数大小的用途(如顶部「抓分」角标)自行取绝对值即可。
|
||
return cls_youle_erqiwang_arith.get_upgrade(_call, _grade, _q);
|
||
},
|
||
|
||
//某座位本轮跟牌的「必出牌」(design §5.2),供客户端自动选中。
|
||
//与服务端出牌校验用的是同一个 get_followcard,故建议与校验结果天然一致。
|
||
//返回 null(不下发)的情形:
|
||
// 1. 非出牌阶段、或该座位就是本轮首家(首家自由出牌,无必出);
|
||
// 2. 本轮首家【甩牌】——甩牌跟牌走 flush_follow_ok 的逐分量匹配(design §5.4.4),
|
||
// 与 get_followcard 不是同一条路径,此时给建议会给错,宁可不发;
|
||
// 3. 算出来的必出牌为空(玩家可自由选)。
|
||
get_mustcard: function(o_paiju, seat){
|
||
if (o_paiju.step != 5){
|
||
return null;
|
||
}
|
||
var _proc = o_paiju.playproc;
|
||
//本轮还没人出牌 / 该座位是首家 → 无必出
|
||
if (!_proc || _proc.start == seat || _proc.startcount == null || _proc.startcount <= 0){
|
||
return null;
|
||
}
|
||
//甩牌不走这条路径,见上
|
||
if (_proc.shuai_demand){
|
||
return null;
|
||
}
|
||
var _inhand = cls_youle_erqiwang_paiju.get_seat_cards(o_paiju, seat);
|
||
if (!_inhand || _inhand.length <= 0){
|
||
return null;
|
||
}
|
||
var _get = cls_youle_erqiwang_arith.get_followcard(
|
||
o_paiju.flower, _inhand, _proc.startcount, _proc.startflower, _proc.starttype);
|
||
if (!_get || !_get.mustcard || _get.mustcard.length <= 0){
|
||
return null;
|
||
}
|
||
return _get.mustcard;
|
||
},
|
||
|
||
//获取小局结算包,type 0正常结算 1投降结算 2解散结算
|
||
get_paiju_account: function(o_paiju, type, msg){
|
||
o_paiju.endtime = min_now();
|
||
//第一局结算扣除房卡
|
||
if (o_paiju.idx == 1 && type != 2){
|
||
var do_deduct_roomcard = function(){
|
||
youle_erqiwang.import.deduct_roomcard(o_paiju.o_desk.o_room);
|
||
}
|
||
min_ontimeout(do_deduct_roomcard, 1000);
|
||
}
|
||
|
||
if (msg.rpc != "jiesuan"){
|
||
msg.rpc = "jiesuan";
|
||
}
|
||
if (!msg.data){
|
||
msg.data = {};
|
||
}
|
||
//主动推送的 data 必须自带成败标志(dev-guide server 03 §4:成败唯一是 data.success)
|
||
msg.data.success = true;
|
||
|
||
//【阶段唯一写入处】结算包一出,本局即进入 step6:
|
||
// 正常出牌结算 do_playcard 已置 6、投降结算 mod.touxiang 已置 6,此处是幂等重申;
|
||
// 解散结算此前没有任何地方推进过 step(可能停在 1/2/3/5),由这里落定。
|
||
//随后显式下发(见下方 msg.data.step):阶段变更必须有包承载,不让前端按 rpc 猜(server 03 §1.4)
|
||
o_paiju.step = 6;
|
||
msg.data.step = o_paiju.step;
|
||
|
||
//读取房间可选规则(缺省:常规算子、不傍王;见 class.config.js 位串约定)
|
||
var o_room = o_paiju.o_desk.o_room;
|
||
var _cfg = cls_youle_erqiwang_config.parse(o_room.roomtype);
|
||
var _climb = _cfg.climb; //爬坡
|
||
var _bangwang = _cfg.bangwang; //傍王
|
||
|
||
//闲家捡分(含抠底)
|
||
var _call = o_paiju.call;
|
||
var _jian = cls_youle_erqiwang_paiju.get_jian_grade(o_paiju);
|
||
var _grade = _jian.grade;
|
||
if (msg.data.bottom && msg.data.bottom.grade2){
|
||
_grade = _grade + msg.data.bottom.grade2;
|
||
}
|
||
|
||
//算子:_base 基础子数、_upgrade 判定倍率(带符号,送客户端)、_X 每对子子数、_win 胜负(1庄赢 -1闲赢 0无)
|
||
var _base, _upgrade, _X, _win;
|
||
switch (type){
|
||
case 0:
|
||
_base = cls_youle_erqiwang_arith.get_base_bycall(_call, _climb);
|
||
var _q = cls_youle_erqiwang_arith.get_qvalue(_call, _climb);
|
||
_upgrade = cls_youle_erqiwang_arith.get_upgrade(_call, _grade, _q);
|
||
_win = (_upgrade > 0) ? 1 : -1;
|
||
_X = ((_upgrade < 0) ? (0 - _upgrade) : _upgrade) * _base;
|
||
o_paiju.result = (_upgrade > 0) ? 0 : 1;
|
||
break;
|
||
case 1: //投降:70分坐庄,基础子数1,庄家直接输1个子
|
||
_base = 1;
|
||
_upgrade = -99; //送客户端的投降标识
|
||
_win = -1;
|
||
_X = 1;
|
||
o_paiju.result = 2;
|
||
break;
|
||
case 2: //解散:本局不结算子数与算奖
|
||
_base = 0;
|
||
_upgrade = 0;
|
||
_win = 0;
|
||
_X = 0;
|
||
o_paiju.result = 3;
|
||
break;
|
||
}
|
||
|
||
msg.data.aset = {};
|
||
msg.data.aset.banker = o_paiju.banker;
|
||
msg.data.aset.call = _call;
|
||
msg.data.aset.multiple = _base;
|
||
msg.data.aset.flower = o_paiju.flower;
|
||
msg.data.aset.grade = _grade;
|
||
msg.data.aset.upgrade = _upgrade;
|
||
msg.data.aset.bangwang = _bangwang ? 1 : 0;
|
||
msg.data.aset.climb = _climb ? 1 : 0;
|
||
|
||
//算奖:各家奖数 N =(庄家?常规算奖 : 0)+(傍王?王数 : 0)
|
||
//静态初始手牌快照:庄家取埋牌后28张、闲家取发牌后28张
|
||
var _c0 = cls_youle_erqiwang_paiju.get_seat_cards_award(o_paiju, 0);
|
||
var _c1 = cls_youle_erqiwang_paiju.get_seat_cards_award(o_paiju, 1);
|
||
var _c2 = cls_youle_erqiwang_paiju.get_seat_cards_award(o_paiju, 2);
|
||
var _cglist = [
|
||
cls_youle_erqiwang_arith.get_chongguan(o_paiju.flower, _c0),
|
||
cls_youle_erqiwang_arith.get_chongguan(o_paiju.flower, _c1),
|
||
cls_youle_erqiwang_arith.get_chongguan(o_paiju.flower, _c2)
|
||
];
|
||
|
||
var award_count = function(cgobj, seat){
|
||
var n = 0;
|
||
if (seat == o_paiju.banker){
|
||
n = n + cgobj.count; //常规算奖只计庄家(design §8.1)
|
||
}
|
||
if (_bangwang){
|
||
n = n + cgobj.wang; //傍王勾选时庄闲都算,每张王1奖(design §8.3)
|
||
}
|
||
return n;
|
||
}
|
||
var _nlist = [
|
||
award_count(_cglist[0], 0),
|
||
award_count(_cglist[1], 1),
|
||
award_count(_cglist[2], 2)
|
||
];
|
||
|
||
//算奖并入结算(design §8.4):玩家i从另两家各收 X×Ni、各付 X×Nj,净得 X×(2Ni−Nj−Nk)
|
||
var award_pay = function(n){
|
||
return [
|
||
_X * (2 * n[0] - n[1] - n[2]),
|
||
_X * (2 * n[1] - n[0] - n[2]),
|
||
_X * (2 * n[2] - n[0] - n[1])
|
||
];
|
||
}
|
||
var _aw = award_pay(_nlist);
|
||
|
||
//冲关与傍王【分别】再各算一次,供大局结算分项展示(S-6)。
|
||
//必须在这里拆——N = 冲关 + 傍王 求和后才代入公式,事后无法从 grade_aw 反推各自占比。
|
||
//公式对 N 是线性的,故 _awcg[i] + _awbw[i] 恒等于 _aw[i](下方 _p 赋值处有断言性注释)。
|
||
var _ncg = [0, 0, 0]; //冲关分量:只计庄家(design §8.1)
|
||
var _nbw = [0, 0, 0]; //傍王分量:勾选时庄闲都算(design §8.3)
|
||
for (var q = 0; q < 3; q++){
|
||
if (q == o_paiju.banker){ _ncg[q] = _cglist[q].count; }
|
||
if (_bangwang){ _nbw[q] = _cglist[q].wang; }
|
||
}
|
||
var _awcg = award_pay(_ncg);
|
||
var _awbw = award_pay(_nbw);
|
||
|
||
//捡分子数:_win=1庄赢→庄+X×2/闲−X;_win=-1闲赢→庄−X×2/闲+X
|
||
var _jf = [0, 0, 0];
|
||
if (o_paiju.banker >= 0){
|
||
for (var s = 0; s < 3; s++){
|
||
if (s == o_paiju.banker){
|
||
_jf[s] = _win * _X * 2;
|
||
} else {
|
||
_jf[s] = _win * _X * -1;
|
||
}
|
||
}
|
||
}
|
||
|
||
msg.data.aset.seatlist = [];
|
||
for (var i = 0; i < 3; i++){
|
||
var _p = {};
|
||
_p.cards = _cglist[i].cards; //算奖相关牌(王/冲关牌)
|
||
_p.chongguan = _cglist[i].count; //常规算奖奖数(仅庄家计入 N)
|
||
_p.wang = _cglist[i].wang; //王数
|
||
_p.naward = _nlist[i]; //该家总奖数 N
|
||
_p.grade_aw = _aw[i]; //算奖得分 X×(2Ni−Nj−Nk),= grade_cg + grade_bw
|
||
_p.grade_cg = _awcg[i]; //其中的冲关分量(design §8.1,不含傍王)
|
||
_p.grade_bw = _awbw[i]; //其中的傍王分量(design §8.3)
|
||
_p.grade_jf = _jf[i]; //捡分子数得分(基础分)
|
||
_p.grade = _p.grade_aw + _p.grade_jf; //本局总得分
|
||
//大局累积:总分 + 三项分量(基础/冲关/傍王),供大局结算面板分项展示
|
||
o_paiju.o_desk.seatlist[i][0] = o_paiju.o_desk.seatlist[i][0] + _p.grade;
|
||
_p.score = o_paiju.o_desk.seatlist[i][0];
|
||
o_paiju.o_desk.seatlist[i][2] = o_paiju.o_desk.seatlist[i][2] + _p.grade_jf;
|
||
o_paiju.o_desk.seatlist[i][3] = o_paiju.o_desk.seatlist[i][3] + _p.grade_cg;
|
||
o_paiju.o_desk.seatlist[i][4] = o_paiju.o_desk.seatlist[i][4] + _p.grade_bw;
|
||
//保存每局得分
|
||
o_paiju.o_desk.seatlist[i][1].push(_p.grade);
|
||
msg.data.aset.seatlist.push(_p);
|
||
}
|
||
|
||
if (o_paiju.idx >= o_paiju.o_desk.o_room.asetcount || type == 2){
|
||
msg = o_paiju.o_desk.method.get_desk_account(msg);
|
||
}
|
||
|
||
//保存结算包,供断线重连时使用(get_deskinfo 的 case 6 直读这三份,不重算)。
|
||
//必须与本次下发的 jiesuan 推送【同源】——取的就是刚组好的同一份 msg.data.*,
|
||
//这样「增量收到 jiesuan」与「结算面板开着时重连」得到的结算数据逐字段相等。
|
||
//bottom 只有正常出牌结算才有(get_bottom_account 组的)、account 只有末局/解散才有,
|
||
//没有的情形显式置 null(不是漏发,是本来就不存在;下发侧据此决定带不带该分组)
|
||
o_paiju.tmp_jiesuan_aset = msg.data.aset;
|
||
o_paiju.tmp_jiesuan_bottom = msg.data.bottom ? msg.data.bottom : null;
|
||
o_paiju.tmp_jiesuan_account = msg.data.account ? msg.data.account : null;
|
||
|
||
return msg;
|
||
}
|
||
}
|
||
|
||
//Node 导出;友乐/浏览器无 module 时跳过(dev-guide 01 §1)。min_random/min_now/min_ontimeout、youle_erqiwang.import 为平台全局
|
||
if (typeof module !== "undefined"){
|
||
module.exports = cls_youle_erqiwang_paiju;
|
||
}
|