Files
erqiwang_youle/server/games/erqiwang/class.paiju.js
T
joywayerandClaude Opus 5 a4a8eb8408 二七王:补齐客户端牌id入参校验(重复/越界/非数组)
check_cards_inhand 此前只逐个校验牌id是否在手上,不查重复、不查
类型与范围,且在牌已出/已埋时 return 空值而非 false,造成两个可
被利用的缺陷:

1. 重复牌id 可伪造牌型:do_playcard([方块K,方块K]) 实测返回
   result=true、cardtype=201(同一张牌被判成一对),可进而伪造
   拖拉机与甩牌分量,本轮结算时该分牌的 score 还会被重复累加。
2. 埋牌传 8 个相同 id 实测只埋下 1 张,庄家带 35 张进入出牌阶段,
   整局牌数错乱。
3. 越界 id 或非数组入参会在解引用处抛异常,中断 DoPack。

新增 check_cards_valid 作为唯一入参校验入口(非空数组、元素为本
局牌表内的整数牌id、互不重复),由 check_cards_inhand 先行调用;
mod.maipai/mod.chupai 在读取 cards.length 之前先过该校验。按
engineering §03「显式失败优于隐式兜底」直接拒绝,不做类型兜底。

新增 test_input.js 28 项正/反/边界用例回归上述三点。

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

964 lines
31 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_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;
}
//进入叫分阶段
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;
//当前出牌情况
paiju.playproc = {};
//输赢结果 0:庄赢 1:闲赢 2:庄投降 3:中途解散
paiju.result = null;
//结算包,供断线重连时使用,新开一局时清除
paiju.tmp_jiesuan_aset = null;
//初始化座位列表
init_seatlist();
//初始化牌列表并发牌
init_cards();
//进入叫分阶段
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);
}
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;
}
}
},
//获取底牌
get_bottomcards: function(o_paiju){
var aryCardIDs = [];
for (var i = 0; i < o_paiju.cards.length; i++){
if (o_paiju.cards[i].dealowner == 0){
aryCardIDs.push(o_paiju.cards[i].id);
}
}
return cls_youle_erqiwang_arith.order_cards(o_paiju.flower, aryCardIDs);
},
//选主
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
},
//在牌列表中获取分牌和分值
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){
//出牌
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;
//进入出牌阶段
o_paiju.step = 5;
//是否报副处理
if (o_paiju.seatlist[o_paiju.playproc.currseat][4][0] != 0){
var _zhu = cls_youle_erqiwang_paiju.get_seat_zhucards(o_paiju, o_paiju.playproc.currseat);
if (_zhu.length == 0){
o_paiju.seatlist[o_paiju.playproc.currseat][4][0] = 0;
o_paiju.seatlist[o_paiju.playproc.currseat][4][1] = 0;
}
if (o_paiju.method.have_baofu()){
var _parilist = cls_youle_erqiwang_arith.get_pairlist(o_paiju.flower, _zhu);
o_paiju.seatlist[o_paiju.playproc.currseat][4][0] = _zhu.length;
o_paiju.seatlist[o_paiju.playproc.currseat][4][1] = _parilist.length;
if (o_paiju.seatlist[o_paiju.playproc.currseat][4][0] == 0){
o_paiju.seatlist[o_paiju.playproc.currseat][o_paiju.flower - 1][0] = 1;
o_paiju.seatlist[o_paiju.playproc.currseat][o_paiju.flower - 1][1] = 1;
} else if (o_paiju.seatlist[o_paiju.playproc.currseat][4][1] == 0){
o_paiju.seatlist[o_paiju.playproc.currseat][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 = [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;
re1.cards = cards; //实际打出的牌(甩错时为单张)
re1.shuaicuo = _shuaicuo;
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;
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.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_liangpai: function(o_paiju){
if (o_paiju.banker < 0){
return null;
}
var cards = cls_youle_erqiwang_paiju.get_seat_cards(o_paiju, o_paiju.banker);
var wang = 0, qi = 0, er = 0;
var zhucards = [];
for (var i = 0; i < cards.length; i++){
var num = cls_youle_erqiwang_arith.id_to_number(cards[i]);
var flw = cls_youle_erqiwang_arith.id_to_flower(cards[i]);
if (num >= 53){ wang = wang + 1; }
if (num == 7){ qi = qi + 1; }
if (num == 2){ er = er + 1; }
//主牌 = 主花色普通牌 或 王 或 任意 2/7
if (flw == o_paiju.flower || flw == 5 || num == 2 || num == 7){
zhucards.push(cards[i]);
}
}
var guding = wang + qi + er; //固定主牌(王 + 全部 2 + 全部 7)
var re = {};
var has = false;
if (guding >= 10){
zhucards = cls_youle_erqiwang_arith.order_cards(o_paiju.flower, zhucards);
var pairs = cls_youle_erqiwang_arith.get_pairlist(o_paiju.flower, zhucards);
var comps = cls_youle_erqiwang_arith.decompose_trump(o_paiju.flower, zhucards);
var tuo = 0;
for (var c = 0; c < comps.length; c++){
if (comps[c].type == "tractor"){ tuo = tuo + 1; }
}
re.zhu = zhucards.length; //主牌总数
re.zhupair = pairs.length; //主对子数
re.zhutuo = tuo; //主拖拉机组数
has = true;
}
if (wang >= 3){ re.wang = wang; has = true; }
if (qi >= 6){ re.qi = qi; has = true; }
if (er >= 6){ re.er = er; has = true; }
if (!has){
return null;
}
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;
},
//获取小局结算包,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 = {};
}
//读取房间可选规则(缺省:常规算子、不傍王;见 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 _aw = [
_X * (2 * _nlist[0] - _nlist[1] - _nlist[2]),
_X * (2 * _nlist[1] - _nlist[0] - _nlist[2]),
_X * (2 * _nlist[2] - _nlist[0] - _nlist[1])
];
//捡分子数:_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)
_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][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);
}
//保存结算包,供断线重连时使用
o_paiju.tmp_jiesuan_aset = msg.data.aset;
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;
}