Files
erqiwang_youle/server/games/erqiwang/class.paiju.js
T
joywayerandClaude Opus 5 4aab71e6a3 二七王:实现 S-1 curmultiple 与 S-2 mustcard
S-1 当前抓分倍数(design §7.2.0):
- class.paiju.js 新增 get_curmultiple(),复用 get_qvalue + get_upgrade,
  与结算 aset.upgrade 同源同算法,差别仅在不含扣底(末轮才产生)。
- 随现有包下发、不另开推送:shangzhuang / chupai1-3 / deskinfo.PushCards。
- 三家同值,整表下发不涉及泄露(捡分本就公开);叫分未定时为 0。

S-2 跟牌必出牌(design §5.2):
- class.paiju.js 新增 get_mustcard(seat),内部复用与出牌校验完全相同的
  get_followcard,故建议与校验天然一致。
- 【只发给 nextseat 一家】:它是该玩家自己手牌的子集,整表下发会泄露
  他家手牌结构(server 红线:发全 ≠ 发多)。
- 四种情形不下发:非出牌阶段、该座位是本轮首家、首家甩牌、必出牌为空。
  甩牌一条尤其重要——甩牌跟牌走 flush_follow_ok 的逐分量匹配,与
  get_followcard 不是同一条路径,给建议会给错。

测试(新增 test/test_hint.js,33 checks):
- curmultiple:算法层逐档对齐 design §7.2.0 判定表(含爬坡 Q 分段)、
  三家同值、重连与出牌包一致、叫分未定为 0、与 get_upgrade 绝对值一致。
- mustcard:只有 nextseat 收到、另两家没有、内容确属收包者自己的牌、
  首家无、缺门有富余时无、手牌数恰等于首家张数时整手必出、甩牌不发、
  非出牌阶段不发、重连按 currseat 门控。

过程中修正的两个测试自身问题(均为脚本缺陷,非业务缺陷,业务判定经证据核实正确):
- 「缺门方无必出」原用例给闲2 只发 2 张牌,恰等于首家张数,get_followcard
  正确判定为「整手必出」;改为发 4 张才留出选择余地,并补一条对照用例
  锁住「恰等于张数则整手必出」这个正确行为。
- 「甩牌不发」「非出牌阶段不发」两条原本是假通过——桩的 startcount 仍是 -1,
  前置守卫先返回了 null,根本没测到被测因素。改为先把桩推进到跟牌态、
  每个用例只变动一个因素,并加一条「自证」用例确认推进后确实能算出必出牌。

另:mustcard 补进 test_leak.js 的 CARD_FIELDS 审计白名单(与 burycards 同理,
不加则误发不会被泄露审计发现)。反向验证:改成整表下发后,审计立刻报出
5 张越权牌;把 curmultiple 写死为 1 后,两条断言失败。均已还原、全绿。

协议文档同步 shangzhuang / chupai1-3 / PushCards 的字段说明。
至此 §7.5 服务端待补 5 条全部完成,服务端侧无遗留。

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

1039 lines
34 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);
}
//获取当前抓分倍数(design §7.2.0,实时预览)
paiju.method.get_curmultiple = function(){
return cls_youle_erqiwang_paiju.get_curmultiple(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;
}
}
},
//获取底牌
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;
//报无主处理(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 = [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;
//合法甩牌的分量构成: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;
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_seat_cards_award 的【静态快照】(埋牌后 28 张,含之后已打出的),
//不能用 get_seat_cards(只取未出的牌):出牌开始亮出的这份统计全局固定、不随出牌缩水,
//否则闲家断线重连后 get_deskinfo 会按庄家当前剩余手牌重算,亮牌信息缩水甚至变成 null
get_liangpai: function(o_paiju){
if (o_paiju.banker < 0){
return null;
}
var cards = cls_youle_erqiwang_paiju.get_seat_cards_award(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;
},
//当前抓分倍数(design §7.2.0):按【当前累计捡分】实时算出的判定倍率绝对值,
//供客户端顶部「抓分」列角标显示「若此刻结束是几倍」。
//与结算包 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);
var _u = cls_youle_erqiwang_arith.get_upgrade(_call, _grade, _q);
return (_u < 0) ? (0 - _u) : _u;
},
//某座位本轮跟牌的「必出牌」(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;
//读取房间可选规则(缺省:常规算子、不傍王;见 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;
}