Files
erqiwang_youle/server/games/erqiwang/class.paiju.js
T
joywayerandClaude Opus 5 f04bff4e9f 二七王:S-8 curmultiple 改为带符号
问题:S-1 实现时取了绝对值,而判定的正负号恰恰是「谁赢」的区分——
3 大光 vs -3 升3级、2 小光 vs -2 升2级、1 过庄 vs -1 升1级,取绝对值后
三对完全撞在一起,D-8 的过程中判定动画无从分辨该播哪个。

改法:get_curmultiple 直接返回 get_upgrade 原值,与结算包 aset.upgrade
完全同口径(3/2/1 庄家大光/小光/过庄,-N 闲家升N级,0 叫分未定)。
顶部「抓分」角标只关心大小,取 Math.abs 即可;判定动画靠符号分辨,
前端一套映射通吃「过程中」与「结算前」两条路径。

协议 4 处 curmultiple 说明同步(shangzhuang / chupai1 / chupai2·3 /
PushCards)。

测试(hint 33 → 46 checks):
- 钉住三对「绝对值相同、判定相反」的组合可区分,以及符号语义
  (>0 庄赢 / <0 闲赢 / ==0 叫分未定)。
- 新增一组【直接调 get_curmultiple 本身】覆盖负数分支,含爬坡 Q 分段。

补上一个测试盲点:第一次反向验证(把实现退回取绝对值)竟然全绿——因为
原有断言里算法层的 cm() 走的是 A.get_upgrade、绕过了 get_curmultiple,
而端到端那局恰好是大光(正数),取不取绝对值都一样。补齐直接调用的用例后
再验,4 条断言立刻失败。

至此 §7.5 服务端待补 8 条全部完成,服务端侧无遗留;D-8 只差动画设计稿
与触发细节。

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

1057 lines
36 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
//亮牌(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;
//读取房间可选规则(缺省:常规算子、不傍王;见 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);
}
//保存结算包,供断线重连时使用
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;
}