Files
erqiwang_youle/server/games/erqiwang/class.arith.js
T
joywayerandClaude Opus 5 20b5425a65 二七王:下发面 multiple 按房间「爬坡」开关取值
arith.get_multiple_bycall 写死 get_base_bycall(call, false),被
mod.jiaofen(jiaofen/shangzhuang 两个包)与 export.get_deskinfo
的叫分/选主/埋牌/出牌四个阶段共 6 处使用,且都不传 roomtype。

后果:爬坡房里对局全程界面显示的是常规算子子数,与结算包
aset.multiple 不符。例如叫 45 分,界面显示 6 个子、结算按
design §7.3.1 实际是 7 个子。违反 server dev-guide「服务器权威 +
每个下发包必须携带前端界面所需的全部核心数据」与 engineering
SSOT(同一业务数据只有一个计算处)。

移除 get_multiple_bycall 这层写死 climb=false 的兼容包装,全部
改为直接调 get_base_bycall(call, cfg.climb),cfg 由
class.config.parse(o_room.roomtype) 得到——与结算处
get_paiju_account 用的是同一个取值入口。get_deskinfo 内顺带复用
同一份 cfg,去掉 PushCards 分支里重复解析 roomtype 的调用。

test_rpc 新增 7 项:叫 45 分在常规房/爬坡房分别驱动到上庄,断言
上庄包与重连 ChooseMain 的 multiple 为 6/7,并与结算包
aset.multiple 逐一对齐。

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

1374 lines
41 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_arith: 算法 /////////
///////////////////////////////////////////////////
var cls_youle_erqiwang_arith = cls_youle_erqiwang_arith || {
//根据叫分计算基础子数(design §7.1 常规算子 / §7.3.1 爬坡)
//climb: 是否勾选爬坡。70 分打牌基础子数 2;投降(基础1)在结算处特殊处理
get_base_bycall: function(callgrade, climb){
if (callgrade == 70){
return 2;
}
if (!climb){
//常规算子:65→2 60→3 55→4 50及以下统一6
if (callgrade == 65) return 2;
if (callgrade == 60) return 3;
if (callgrade == 55) return 4;
if (callgrade >= 5 && callgrade <= 50) return 6;
return 0;
}
//爬坡:65→2 起每低5分+1子,直到5分→15
switch (callgrade){
case 65: return 2;
case 60: return 3;
case 55: return 4;
case 50: return 6;
case 45: return 7;
case 40: return 8;
case 35: return 9;
case 30: return 10;
case 25: return 11;
case 20: return 12;
case 15: return 13;
case 10: return 14;
case 5: return 15;
}
return 0;
},
//小光/过庄分界与升级级距 Q(design §7.2.0 常规固定40 / §7.3.2 爬坡分段)
get_qvalue: function(callgrade, climb){
if (!climb){
return 40;
}
if (callgrade >= 45){
return 40;
}
if (callgrade == 40 || callgrade == 35){
return 20;
}
if (callgrade == 30 || callgrade == 25){
return 15;
}
if (callgrade == 20 || callgrade == 15){
return 10;
}
if (callgrade == 10 || callgrade == 5){
return 5;
}
return 40;
},
//牌id转花色
id_to_flower: function(cardid){
if (cardid == 52 || cardid == 106){
//小王
return 5;
}
if (cardid == 53 || cardid == 107){
//大王
return 5;
}
return parseInt((cardid % 54) / 13) + 1;
},
//牌id转牌面数值
id_to_number: function(cardid){
if (cardid == 52 || cardid == 106){
//小王
return 53;
}
if (cardid == 53 || cardid == 107){
//大王
return 54;
}
return (cardid % 54) % 13 + 1;
},
//牌id转牌编码
id_to_code: function(mainflower, cardid){
if (cardid == 52 || cardid == 106){
//小王
return 9553;
}
if (cardid == 53 || cardid == 107){
//大王
return 9554;
}
var flower = cls_youle_erqiwang_arith.id_to_flower(cardid);
var number = cls_youle_erqiwang_arith.id_to_number(cardid);
var code = flower * 100 + number;
//特殊处理
switch (number)
{
case 1:
code = code + 13;
break;
case 2:
code = code + 2000;
break;
case 7:
code = code + 7000;
break;
}
//主牌花色处理
if (flower == mainflower){
code = code + 1000;
}
return code;
},
//从大到小排序(冒泡排序)
order_cards: function(mainflower, aryCardIDs){
if (aryCardIDs){
for (var i = 0; i < aryCardIDs.length; i++) {
for (var j = i + 1; j < aryCardIDs.length; j++) {
var i_code = cls_youle_erqiwang_arith.id_to_code(mainflower, aryCardIDs[i]);
var j_code = cls_youle_erqiwang_arith.id_to_code(mainflower, aryCardIDs[j]);
if (i_code < j_code) {
var tmp = aryCardIDs[j];
aryCardIDs[j] = aryCardIDs[i];
aryCardIDs[i] = tmp;
}
}
}
}
return aryCardIDs;
},
//判断两个牌是否是连续的
is_continuous: function(bigcode, smallcode){
//是小王
var is_xiaowang = function(_code){
if (_code == 9553){
return true;
}
return false;
}
//是正7
var is_zheng7 = function(_code){
if (_code > 8000 && _code < 9000){
return true;
}
return false;
}
//是负7
var is_fu7 = function(_code){
if (_code > 7000 && _code < 8000){
return true;
}
return false;
}
//是正2
var is_zheng2 = function(_code){
if (_code > 3000 && _code < 4000){
return true;
}
return false;
}
//是负2
var is_fu2 = function(_code){
if (_code > 2000 && _code < 3000){
return true;
}
return false;
}
//是主A
var is_zhuA = function(_code){
if (_code > 1000 && _code < 2000 && _code % 100 == 14){
return true;
}
return false;
}
//是8
var is_8 = function(_code){
if (_code % 100 == 8){
return true;
}
return false;
}
//是6
var is_6 = function(_code){
if (_code % 100 == 6){
return true;
}
return false;
}
//普通的连牌
if (bigcode - smallcode == 1){
return true;
}
//小王、正7
if (is_xiaowang(bigcode) && is_zheng7(smallcode)){
return true;
}
//正7、负7
if (is_zheng7(bigcode) && is_fu7(smallcode)){
return true;
}
//负7、正2
if (is_fu7(bigcode) && is_zheng2(smallcode)){
return true;
}
//正2、负2
if (is_zheng2(bigcode) && is_fu2(smallcode)){
return true;
}
//负2、主A
if (is_fu2(bigcode) && is_zhuA(smallcode)){
return true;
}
//8、6
if (is_8(bigcode) && is_6(smallcode)){
return true;
}
return false;
},
//获取对子列表,cards为同一花色的排过序的牌列表
get_pairlist: function(mainflower, flowercards){
var pairlist = [];
var i = 0;
while (i < flowercards.length - 1){
//牌编码,两张牌两张牌一取
var code_i = cls_youle_erqiwang_arith.id_to_code(mainflower, flowercards[i]);
var code_j = cls_youle_erqiwang_arith.id_to_code(mainflower, flowercards[i + 1]);
if (code_i == code_j){
pairlist.push([flowercards[i], flowercards[i + 1]]);
i = i + 2;
} else {
i++;
}
}
return pairlist;
},
//获取拖拉机列表,pairlist为同一花色的排过序的对子列表,cardtype为拖拉机牌型
get_tuolaji_list: function(mainflower, pairlist, cardtype){
var tuolaji = [];
var count = cardtype - 300; //拖拉机的对子数量
var i = 0;
for (var i = 0; i < pairlist.length - count + 1; i++){
//每次按拖拉机的对子数量取N对
var tmplist = pairlist.slice(i, i + count);
//判断是否是cardtype类型的拖拉机
var is_tlj = true;
for (var j = 0; j < tmplist.length - 1; j++) {
var code_j1 = cls_youle_erqiwang_arith.id_to_code(mainflower, tmplist[j][0]);
var code_j2 = cls_youle_erqiwang_arith.id_to_code(mainflower, tmplist[j+1][0]);
if (!cls_youle_erqiwang_arith.is_continuous(code_j1, code_j2)){
is_tlj = false;
break;
}
}
if (is_tlj){
var tlj = [];
for (var k = 0; k < tmplist.length; k++){
tlj = tlj.concat(tmplist[k]);
}
tuolaji.push(tlj);
}
}
return tuolaji;
},
//主牌归一化大小(副7 同级取 7000、副2 同级取 2000,其余用牌编码),仅用于甩牌分量比大小
trump_rank: function(mainflower, cardid){
var code = cls_youle_erqiwang_arith.id_to_code(mainflower, cardid);
if (code > 7000 && code < 8000){ //副7
return 7000;
}
if (code > 2000 && code < 3000){ //副2
return 2000;
}
return code;
},
//把一组主牌分解为分量列表(design §5.4.3/§5.4.4)
//返回 [{type:'single'|'pair'|'tractor', len, value, cards:[]}...],value 为该分量最大牌的归一化 rank
decompose_trump: function(mainflower, cards){
var ordered = cls_youle_erqiwang_arith.order_cards(mainflower, cards.concat());
//先分出对子与单张
var pairs = []; //{code, cards:[id,id]}
var singles = []; //{card}
var i = 0;
while (i < ordered.length){
var code_i = cls_youle_erqiwang_arith.id_to_code(mainflower, ordered[i]);
if (i + 1 < ordered.length){
var code_n = cls_youle_erqiwang_arith.id_to_code(mainflower, ordered[i + 1]);
if (code_i == code_n){
pairs.push({code: code_i, cards: [ordered[i], ordered[i + 1]]});
i = i + 2;
continue;
}
}
singles.push(ordered[i]);
i++;
}
//连续对子组成拖拉机,其余为单独对子
var comps = [];
var j = 0;
while (j < pairs.length){
var run = [pairs[j]];
var k = j;
while (k + 1 < pairs.length && cls_youle_erqiwang_arith.is_continuous(pairs[k].code, pairs[k + 1].code)){
run.push(pairs[k + 1]);
k++;
}
if (run.length >= 2){
var tcards = [];
for (var r = 0; r < run.length; r++){
tcards = tcards.concat(run[r].cards);
}
comps.push({type: "tractor", len: run.length, value: cls_youle_erqiwang_arith.trump_rank(mainflower, run[0].cards[0]), cards: tcards});
} else {
comps.push({type: "pair", len: 1, value: cls_youle_erqiwang_arith.trump_rank(mainflower, run[0].cards[0]), cards: run[0].cards});
}
j = k + 1;
}
for (var s = 0; s < singles.length; s++){
comps.push({type: "single", len: 1, value: cls_youle_erqiwang_arith.trump_rank(mainflower, singles[s]), cards: [singles[s]]});
}
return comps;
},
//甩牌最大性判定(design §5.4.2):两名对手是否持有能压过甩牌任一分量的主牌/主对/主拖拉机
//comps:甩牌分量;opp_zhulist:各对手未出的主牌id列表数组。返回 true 表示有人能压(甩错)
opp_can_beat_flush: function(mainflower, comps, opp_zhulist){
for (var o = 0; o < opp_zhulist.length; o++){
var zhu = cls_youle_erqiwang_arith.order_cards(mainflower, opp_zhulist[o].concat());
var pairs = cls_youle_erqiwang_arith.get_pairlist(mainflower, zhu);
for (var c = 0; c < comps.length; c++){
var comp = comps[c];
if (comp.type == "single"){
//对手持有任一 rank 更大的主牌单张即可压
for (var a = 0; a < zhu.length; a++){
if (cls_youle_erqiwang_arith.trump_rank(mainflower, zhu[a]) > comp.value){
return true;
}
}
} else if (comp.type == "pair"){
//对手持有 rank 更大的主对即可压
for (var p = 0; p < pairs.length; p++){
if (cls_youle_erqiwang_arith.trump_rank(mainflower, pairs[p][0]) > comp.value){
return true;
}
}
} else {
//拖拉机:对手持有同长度、顶牌 rank 更大的主拖拉机即可压
var tlist = cls_youle_erqiwang_arith.get_tuolaji_list(mainflower, pairs, 300 + comp.len);
for (var t = 0; t < tlist.length; t++){
if (cls_youle_erqiwang_arith.trump_rank(mainflower, tlist[t][0]) > comp.value){
return true;
}
}
}
}
}
return false;
},
//从对子列表中移除指定拖拉机(flat牌id数组)用到的对子
remove_pairs: function(pairlist, tractorcards){
var result = [];
for (var i = 0; i < pairlist.length; i++){
var inT = false;
for (var j = 0; j < tractorcards.length; j++){
if (tractorcards[j] == pairlist[i][0]){
inT = true;
break;
}
}
if (!inT){
result.push(pairlist[i]);
}
}
return result;
},
//跟甩牌的分量拆解校验(design §5.4.4)
//inhandcards: 跟牌方出牌前的全部手牌;followcards: 提交的跟牌;demand: {tractors:[连对数...], pairs, singles}
//在"同花色凑张数、主牌最大化"之上,追加"拖拉机→对子→单张"的强制分量优先级:
//能凑同长主拖就必须凑、能凑主对就必须凑,不得拆散去垫。返回 true 表示合规。
flush_follow_ok: function(mainflower, inhandcards, followcards, demand){
var isTrump = function(cid){
return cls_youle_erqiwang_arith.id_to_code(mainflower, cid) >= 1000;
}
//手牌与跟牌里的主牌
var heldTrump = [];
var playedTrump = [];
for (var i = 0; i < inhandcards.length; i++){
if (isTrump(inhandcards[i])){ heldTrump.push(inhandcards[i]); }
}
for (var i = 0; i < followcards.length; i++){
if (isTrump(followcards[i])){ playedTrump.push(followcards[i]); }
}
heldTrump = cls_youle_erqiwang_arith.order_cards(mainflower, heldTrump);
playedTrump = cls_youle_erqiwang_arith.order_cards(mainflower, playedTrump);
//甩牌总张数
var total = demand.singles + demand.pairs * 2;
for (var t = 0; t < demand.tractors.length; t++){
total = total + demand.tractors[t] * 2;
}
//主牌最大化:手上主牌够则必须全打主牌,不够则全部主牌都要打出
var needTrump = (heldTrump.length >= total) ? total : heldTrump.length;
if (playedTrump.length != needTrump){
return false;
}
//按 拖拉机(长→短) → 对子 的优先级,算出跟牌方"必须打出"的拖拉机与对子数
var heldPairs = cls_youle_erqiwang_arith.get_pairlist(mainflower, heldTrump);
var _tractors = demand.tractors.concat();
_tractors.sort(function(a, b){ return b - a; }); //降序
var reqTractors = [];
var workPairs = heldPairs.concat();
var unmatchedTractorPairs = 0;
for (var t = 0; t < _tractors.length; t++){
var N = _tractors[t];
var tl = cls_youle_erqiwang_arith.get_tuolaji_list(mainflower, workPairs, 300 + N);
if (tl.length > 0){
reqTractors.push(N);
workPairs = cls_youle_erqiwang_arith.remove_pairs(workPairs, tl[0]);
} else {
//凑不成同长拖拉机,该拖拉机分量退化为对子需求
unmatchedTractorPairs = unmatchedTractorPairs + N;
}
}
//对子需求 = 对子分量 + 退化的拖拉机分量;受限于剩余可凑对子数
var wantedPairs = demand.pairs + unmatchedTractorPairs;
var reqPairs = (workPairs.length < wantedPairs) ? workPairs.length : wantedPairs;
//校验跟牌里的主牌是否包含这些必须的拖拉机与对子
var playedPairs = cls_youle_erqiwang_arith.get_pairlist(mainflower, playedTrump);
for (var r = 0; r < reqTractors.length; r++){
var ptl = cls_youle_erqiwang_arith.get_tuolaji_list(mainflower, playedPairs, 300 + reqTractors[r]);
if (ptl.length == 0){
return false; //必须打出的同长主拖拉机没打
}
playedPairs = cls_youle_erqiwang_arith.remove_pairs(playedPairs, ptl[0]);
}
if (playedPairs.length < reqPairs){
return false; //必须打出的主对子没凑够
}
return true;
},
//第一个位置是否可出牌
can_playcard: function(mainflower, cards, selfseat, seatlist, opp_zhulist){
/*
参数:
mainflower: 主牌花色
cards : 要出的牌id列表
selfseat : 出牌者的位置
seatlist : 座位列表,四维数组,
[ //第一层表示玩家,与位置序号对应
[ //第二层表示花色,与花色-1对应
[
0, //无该花色的牌标志
0 //该花色无对标志
], [0, 0], [0, 0], [0, 0]
],
[[0, 0], [0, 0], [0, 0], [0, 0]],
[[0, 0], [0, 0], [0, 0], [0, 0]]
]
返回结果json:
result : true可出,false不可出
flower : 花色,result=false时无此属性,下同
cardtype : 牌型
cardvalue: 牌型值,取满足牌型的最小牌的牌编码
*/
//返回的结果
var can = {};
can.result = true;
can.flower = null;
can.cardtype = null;
can.cardvalue = null;
//返回不能出牌的结果
var do_returnfalse = function(){
can.result = false;
delete can.flower;
delete can.cardtype;
delete can.cardvalue;
}
//规则外的约定,一次最多只能出14张牌
if (cards.length > 14){
do_returnfalse();
return can;
}
/////////// 计算牌型 ///////////
//将牌从大到小排序
cards = cls_youle_erqiwang_arith.order_cards(mainflower, cards);
var i = 0;
while (i < cards.length){
//牌编码
var code = cls_youle_erqiwang_arith.id_to_code(mainflower, cards[i]);
//牌的花色
var flower = parseInt(code / 100);
if (code > 1000){ //大于1000是主牌
flower = mainflower;
}
//是第一张牌
if (i == 0){
can.flower = flower;
can.cardtype = 101;
can.cardvalue = code;
i++;
continue;
}
//不是第一张牌
//花色必须一致
if (flower != can.flower){
do_returnfalse();
return can;
}
//之前是单张牌型
if (can.cardtype > 100 && can.cardtype < 200){
if (can.cardtype == 101 && code == can.cardvalue){
can.cardtype = 201; //是一对
} else {
can.cardtype = can.cardtype + 1; //单张甩牌
can.cardvalue = code;
}
i++;
continue;
}
//之前是对子或拖拉机牌型,则两张两张牌一取
//如果是最后一张牌,取不到后面一张牌了
if (i == cards.length - 1){
can.cardtype = 100 + i + 1; //单张甩牌
can.cardvalue = code;
i++;
continue;
}
//后面一张牌的编码
var code_2 = cls_youle_erqiwang_arith.id_to_code(mainflower, cards[i + 1]);
//后面一张牌的花色
var flower_2 = parseInt(code_2 / 100);
if (code_2 > 1000){ //大于1000是主牌
flower_2 = mainflower;
}
//后面一张牌的花色必须一致
if (flower_2 != flower){
do_returnfalse();
return can;
}
//当前两张牌不是对子
if (code_2 != code){
can.cardtype = 100 + i + 2; //单张甩牌
can.cardvalue = code_2;
i = i + 2;
continue;
}
//当前两张牌是对子
//之前是对子牌型
if (can.cardtype > 200 && can.cardtype < 300){
if (can.cardtype == 201 && cls_youle_erqiwang_arith.is_continuous(can.cardvalue, code)){
//之前是一对,且现在是连牌,则是拖拉机
can.cardtype = 302;
can.cardvalue = code;
} else {
//是多对
can.cardtype = can.cardtype + 1; //多对甩牌
can.cardvalue = code;
}
i = i + 2;
continue;
}
//之前是拖拉机牌型
if (can.cardtype > 300 && can.cardtype < 400){
if (cls_youle_erqiwang_arith.is_continuous(can.cardvalue, code)){
//现在是连牌,则还是拖拉机
can.cardtype = can.cardtype + 1;
can.cardvalue = code;
} else {
//是多对
can.cardtype = 200 + can.cardtype % 100 + 1; //多对甩牌
can.cardvalue = code;
}
i = i + 2;
continue;
}
}
/////////// 甩牌处理(design §5.4)///////////
//把要出的牌分解为分量;>1 个分量即为甩牌
var comps = cls_youle_erqiwang_arith.decompose_trump(mainflower, cards);
if (comps.length > 1){
//副牌绝对禁甩(§5.4.1):甩牌必须全是主牌
var allTrump = true;
for (var ci = 0; ci < cards.length; ci++){
if (cls_youle_erqiwang_arith.id_to_code(mainflower, cards[ci]) < 1000){
allTrump = false;
break;
}
}
if (!allTrump){
//副牌不能甩,属非法出牌(无甩错惩罚,直接拒绝)
do_returnfalse();
return can;
}
//最大性判定(§5.4.2):按全部手牌,两对手都不持有能压过任一分量的主牌
if (cls_youle_erqiwang_arith.opp_can_beat_flush(mainflower, comps, opp_zhulist)){
//甩错(§5.4.5):do_playcard 据此收回甩牌、只强制打出最小一张
do_returnfalse();
can.shuaicuo = true;
can.smallest = cards[cards.length - 1]; //cards 已从大到小排序,末位最小
return can;
}
//合法甩牌:记录分量需求,供跟牌逐分量强制匹配(§5.4.4)
var _dTractors = [];
var _dPairs = 0;
var _dSingles = 0;
for (var ci2 = 0; ci2 < comps.length; ci2++){
if (comps[ci2].type == "tractor"){
_dTractors.push(comps[ci2].len);
} else if (comps[ci2].type == "pair"){
_dPairs = _dPairs + 1;
} else {
_dSingles = _dSingles + 1;
}
}
can.shuai_demand = { tractors: _dTractors, pairs: _dPairs, singles: _dSingles };
}
return can;
},
//一组同花色牌里,成对部分按相邻拆成的连对 run 长度列表(单张忽略),降序
//(run 长度=该段连续对子的对数:孤立对子=1、两连对=2、三连对=3……)
tractor_run_lengths: function(mainflower, cards){
var comps = cls_youle_erqiwang_arith.decompose_trump(mainflower, cards);
var runs = [];
for (var i = 0; i < comps.length; i++){
if (comps[i].type == "tractor"){
runs.push(comps[i].len);
} else if (comps[i].type == "pair"){
runs.push(1);
}
//single 忽略:跟拖拉机时零散单张不参与对子覆盖度
}
runs.sort(function(a, b){ return b - a; });
return runs;
},
//给定可用连对 run 长度表与要取的对数 k,贪心"最长优先"得到字典序最大的规格档案
//(每次从最长 run 取 min(run, 剩余对数),即"能凑多长的拖拉机就先凑多长")
max_tractor_profile: function(runlens, k){
var avail = runlens.concat();
avail.sort(function(a, b){ return b - a; });
var profile = [];
var budget = k;
var idx = 0;
while (budget > 0 && idx < avail.length){
var take = avail[idx];
if (take > budget){ take = budget; }
profile.push(take);
budget = budget - take;
idx++;
}
return profile; //取用顺序已从大到小
},
//字典序比较:档案 a 是否严格劣于(小于)档案 b
profile_less: function(a, b){
var n = a.length < b.length ? a.length : b.length;
for (var i = 0; i < n; i++){
if (a[i] < b[i]){ return true; }
if (a[i] > b[i]){ return false; }
}
//前缀相同,较短者小
return a.length < b.length;
},
//§5.2 拖拉机跟牌"最大拖拉机覆盖度"校验(修复 get_followcard 降级递归 else 分支的欠约束):
//首家出拖拉机、且跟牌方该花色牌足够(跟牌须整手同花色对子)时,跟出的对子按相邻拆成拖拉机后的
//"规格档案"必须达到该手同花色对子在相同对数下可达的最优(最长拖拉机优先,逐档字典序比较),
//否则等于"放着更长的拖拉机不出、只出零散对子",不合法。
//返回 true 表示覆盖度达标或本情形不适用(放行,交由其他校验);false 表示不达标(拒绝)。
follow_tractor_cover_ok: function(mainflower, inhandcards, followcards, startcount, startflower){
//跟牌必须全是 startflower 花色(主牌算主花色;副7/副2 归主,不算副花色)
var _follow = cls_youle_erqiwang_arith.order_cards(mainflower, followcards.concat());
for (var i = 0; i < _follow.length; i++){
var codef = cls_youle_erqiwang_arith.id_to_code(mainflower, _follow[i]);
var flf = (codef > 1000) ? mainflower : parseInt(codef / 100);
if (flf != startflower){ return true; } //含非同花色(垫/混合),不适用
}
//跟牌必须整手成对
var followpairs = cls_youle_erqiwang_arith.get_pairlist(mainflower, _follow);
if (followpairs.length * 2 != _follow.length){ return true; } //非全对子(对子不够+补单张等),不适用
//跟牌方手中该花色的牌
var suit = [];
for (var h = 0; h < inhandcards.length; h++){
var codeh = cls_youle_erqiwang_arith.id_to_code(mainflower, inhandcards[h]);
var flh = (codeh > 1000) ? mainflower : parseInt(codeh / 100);
if (flh == startflower){ suit.push(inhandcards[h]); }
}
if (suit.length < startcount){ return true; } //该花色不够,允许混合/垫,本校验不适用
//该花色可用对子的连对 run,与跟牌自身的 run
var availruns = cls_youle_erqiwang_arith.tractor_run_lengths(mainflower, suit);
var best = cls_youle_erqiwang_arith.max_tractor_profile(availruns, startcount / 2);
var got = cls_youle_erqiwang_arith.tractor_run_lengths(mainflower, _follow);
//跟牌自身档案不得字典序劣于最优档案
return !cls_youle_erqiwang_arith.profile_less(got, best);
},
//根据第一个玩家的出牌获取后面玩家必出的和可出的牌
get_followcard: function(mainflower, inhandcards, startcount, startflower, startcardtype){
/*
参数:
mainflower : 主牌花色
inhandcards : 手上的牌id列表
startcount : 第一个玩家出牌的数量
startflower : 第一个玩家出牌的牌花色
startcardtype: 第一个玩家出牌的牌型
返回结果json:
mustcard: 必出的牌列表
cancard : 可出的牌列表
cantype : 可出的牌中需要的牌型
*/
//返回的结果
var re = {};
re.mustcard = [];
re.cancard = [];
re.cantype = null;
//拷贝数组
var _inhandcards = inhandcards.concat();
//将牌从大到小排序
_inhandcards = cls_youle_erqiwang_arith.order_cards(mainflower, _inhandcards);
//如果手上的牌数量与要求的出牌数量一样则全是必出的牌
if (_inhandcards.length == startcount){
re.mustcard = _inhandcards;
re.cancard = [];
re.cantype = null;
return re;
}
//获取相同花色的牌与其他花色的牌
var _startflowercards = []; //相同花色的牌
var _otherflowercards = []; //其他花色的牌
for (var i = 0; i < _inhandcards.length; i++){
//牌编码
var _code = cls_youle_erqiwang_arith.id_to_code(mainflower, _inhandcards[i]);
//牌的花色
var _flower = parseInt(_code / 100);
if (_code > 1000){ //大于1000是主牌
_flower = mainflower;
}
if (_flower == startflower){
_startflowercards.push(_inhandcards[i]);
} else {
_otherflowercards.push(_inhandcards[i]);
}
}
//如果没有相同花色的牌,则全部为可出的牌
if (_startflowercards.length == 0){
re.mustcard = [];
re.cancard = _inhandcards;
re.cantype = 100 + startcount;
return re;
}
//如果相同花色的牌数量不够,则相同花色的牌为必出的牌,其他花色为可出的牌
if (_startflowercards.length < startcount){
re.mustcard = _startflowercards;
re.cancard = _otherflowercards;
re.cantype = 100 + startcount - re.mustcard.length;
return re;
}
//如果相同花色的牌数量与要求的出牌数量一样则全是必出的牌,没有可出的牌
if (_startflowercards.length == startcount){
re.mustcard = _startflowercards;
re.cancard = [];
re.cantype = null;
return re;
}
//如果相同花色的牌数量大于要求的出牌数量
if (startcardtype == 101){
//一张单张
re.mustcard = [];
re.cancard = _startflowercards;
re.cantype = 100 + startcount;
return re;
} else if (startcardtype > 101 && startcardtype < 200){
//多张单张
re.mustcard = [];
re.cancard = _startflowercards;
re.cantype = 100 + startcount;
return re;
} else if (startcardtype == 201){
//一对
//获取对子列表
var pairlist = cls_youle_erqiwang_arith.get_pairlist(mainflower, _startflowercards);
//没有对子
if (pairlist.length == 0){
re.mustcard = [];
re.cancard = _startflowercards;
re.cantype = 100 + startcount;
return re;
}
//只有一对
if (pairlist.length == 1){
re.mustcard = pairlist[0];
re.cancard = [];
re.cantype = null;
return re;
}
//有多对
re.mustcard = [];
for (var i = 0; i < pairlist.length; i++){
re.cancard = re.cancard.concat(pairlist[i]);
}
re.cantype = 201;
return re;
} else if (startcardtype > 201 && startcardtype < 300){
//多对
re.mustcard = [];
re.cancard = _startflowercards;
re.cantype = 100 + startcount;
return re;
} else if (startcardtype > 300 && startcardtype < 400){
//拖拉机
//获取对子列表
var pairlist = cls_youle_erqiwang_arith.get_pairlist(mainflower, _startflowercards);
//没有对子
if (pairlist.length == 0){
re.mustcard = [];
re.cancard = _startflowercards;
re.cantype = 100 + startcount;
return re;
}
//对子数量不够
if (pairlist.length * 2 < startcount){
for (var i = 0; i < pairlist.length; i++){
re.mustcard = re.mustcard.concat(pairlist[i]);
}
re.cancard = min_ary_deduct(_startflowercards, re.mustcard);
re.cantype = 100 + startcount - re.mustcard.length;
return re;
}
//对子数量刚好够
if (pairlist.length * 2 == startcount){
for (var i = 0; i < pairlist.length; i++){
re.mustcard = re.mustcard.concat(pairlist[i]);
}
re.cancard = [];
re.cantype = null;
return re;
}
//对子有多
//拖拉机列表
var tuolaji_list = cls_youle_erqiwang_arith.get_tuolaji_list(mainflower, pairlist, startcardtype);
//有多个符合条件的拖拉机
if (tuolaji_list.length > 1){
re.mustcard = [];
re.cancard = [];
//这里要去重复项
for (var i = 0; i < tuolaji_list.length; i++){
for (var j = 0; j < tuolaji_list[i].length; j++) {
var found = false;
for (var k = 0; k < re.cancard.length; k++){
if (re.cancard[k] == tuolaji_list[i][j]){
found = true;
break;
}
}
if (!found){
re.cancard.push(tuolaji_list[i][j]);
}
}
}
re.cantype = startcardtype;
return re;
}
//只有一个符合条件的拖拉机
if (tuolaji_list.length == 1){
re.mustcard = tuolaji_list[0];
re.cancard = [];
re.cantype = null;
return re;
}
//没有符合条件的拖拉机
if (tuolaji_list.length == 0){
re.mustcard = [];
for (var i = 0; i < pairlist.length; i++){
re.cancard = re.cancard.concat(pairlist[i]);
}
re.cantype = startcardtype - 100;
//如果是连两对拖拉机
if (startcardtype == 302){
return re;
}
//如果是3连对及以上的拖拉机则降低拖拉机的规格再继续找
var tlj_type = startcardtype; //需要的牌型
var tlj_find = startcardtype - 1; //当前查找的牌型
//再继续找
while (tlj_type >= 302 && tlj_find >= 302){
var tlj_list = cls_youle_erqiwang_arith.get_tuolaji_list(mainflower, pairlist, tlj_find);
if (tlj_list.length == 1){
//找到一个
re.mustcard = re.mustcard.concat(tlj_list[0]);
re.cantype = re.cantype - (tlj_find - 100) + 200;
if (re.cantype < 201){
re.cancard = [];
re.cantype = null;
return re;
}
re.cancard = min_ary_deduct(re.cancard, tlj_list[0]);
//删除对子列表
i = 0;
while (i < pairlist.length){
if (min_ary_include(tlj_list[0], pairlist[i])){
pairlist.splice(i, 1);
} else {
i++;
}
}
tlj_type = (tlj_type - tlj_find) + 300;
continue;
}
if (tlj_list.length > 1){
//找到多个
if ((tlj_find - 300) * tlj_list.length <= (tlj_type - 300)){
re.mustcard = re.mustcard.concat(tlj_list[0]);
re.cantype = re.cantype - (tlj_find - 100) + 200;
if (re.cantype < 201){
re.cancard = [];
re.cantype = null;
return re;
}
re.cancard = min_ary_deduct(re.cancard, tlj_list[0]);
//删除对子列表
i = 0;
while (i < pairlist.length){
if (min_ary_include(tlj_list[0], pairlist[i])){
pairlist.splice(i, 1);
} else {
i++;
}
}
tlj_type = (tlj_type - tlj_find) + 300;
continue;
} else {
//候选等长拖拉机比剩余需求多(玩家可自选出哪一组):
//此处 cantype 无法表达"任选其一成拖拉机、其余自由",故返回宽松的"任意 N 对",
//由 can_followcard 的 follow_tractor_cover_ok 覆盖度校验兜底拒绝零散对子。
return re;
}
}
tlj_find = tlj_find - 1;
}
return re;
}
}
},
//第二、三个位置是否可跟牌
can_followcard: function(mainflower, inhandcards, followcards, startcount, startflower, startcardtype){
/*
参数:
mainflower : 主牌花色
inhandcards : 手上的牌id列表
followcards : 要跟的牌id列表
startcount : 第一个玩家出牌的数量
startflower : 第一个玩家出牌的牌花色
startcardtype: 第一个玩家出牌的牌型
返回结果json:
result : true可跟,false不可跟
cardvalue: 满足第一个玩家出牌的牌型值
noflower : 出牌后没有了第一个玩家出牌花色的牌标志
nopair : 出牌后没有了第一个玩家出牌花色的对子标志
*/
//返回的结果
var can = {};
can.result = true;
can.cardvalue = null;
can.noflower = false;
can.nopair = false;
//返回不能出牌的结果
var do_returnfalse = function(){
can.result = false;
delete can.cardvalue;
delete can.noflower;
delete can.nopair;
}
//牌的数量必须相等
if (followcards.length != startcount){
do_returnfalse();
return can;
}
//拷贝数组
var _inhandcards = inhandcards.concat();
var _followcards = followcards.concat();
//将牌从大到小排序
_inhandcards = cls_youle_erqiwang_arith.order_cards(mainflower, _inhandcards);
_followcards = cls_youle_erqiwang_arith.order_cards(mainflower, _followcards);
//根据第一个玩家的出牌获取后面玩家必出的和可出的牌
var get = cls_youle_erqiwang_arith.get_followcard(mainflower, _inhandcards, startcount, startflower, startcardtype);
////////// 判断是否可跟牌 ///////////
//如果有必出的牌则必须包含必出的牌
if (get.mustcard.length > 0){
if (!min_ary_include(_followcards, get.mustcard)){
do_returnfalse();
return can;
}
//除去必出的牌还要出的牌
_followcards = min_ary_deduct(_followcards, get.mustcard);
}
if (_followcards.length > 0){
//必须在可出的牌里面
if (!min_ary_include(get.cancard, _followcards)){
do_returnfalse();
return can;
}
//必须满足需要的牌型
if (get.cantype > 100 && get.cantype < 200){
//单张牌型
//牌数量要正确
if (_followcards.length != get.cantype - 100){
do_returnfalse();
return can;
}
} else if (get.cantype > 200 && get.cantype < 300){
//对子牌型
//牌数量要正确
if (_followcards.length != (get.cantype - 200) * 2){
do_returnfalse();
return can;
}
//对子数量要正确
var pairlist = cls_youle_erqiwang_arith.get_pairlist(mainflower, _followcards);
if (pairlist.length != get.cantype - 200){
do_returnfalse();
return can;
}
} else if (get.cantype > 300 && get.cantype < 400){
//拖拉机牌型
//牌数量要正确
if (_followcards.length != (get.cantype - 300) * 2){
do_returnfalse();
return can;
}
//要是正确的拖拉机牌型
var _fpairlist = cls_youle_erqiwang_arith.get_pairlist(mainflower, _followcards);
var _ftljlist = cls_youle_erqiwang_arith.get_tuolaji_list(mainflower, _fpairlist, get.cantype);
if (_ftljlist.length == 0){
do_returnfalse();
return can;
}
}
}
//§5.2 拖拉机跟牌最大覆盖度校验:修复 get_followcard 降级递归 else 分支的欠约束
//(同花色对子足够、无同长拖拉机、又有多组等长短拖拉机时,必须尽量长地出拖拉机,不得只出零散对子)
if (startcardtype > 300 && startcardtype < 400){
if (!cls_youle_erqiwang_arith.follow_tractor_cover_ok(mainflower, inhandcards, followcards, startcount, startflower)){
do_returnfalse();
return can;
}
}
/////////// 可以跟牌 ///////////
//根据跟牌情况获取是否有相同花色的牌,是否有对子
//如果第一张牌的花色与第一家的出牌花色不一样则认为没有了相同花色的牌
var _code = cls_youle_erqiwang_arith.id_to_code(mainflower, followcards[0]);
var _flower = parseInt(_code / 100);
if (_code > 1000){
_flower = mainflower;
}
if (_flower != startflower){
can.noflower = true;
can.nopair = true;
}
//如果最后一张牌的花色与第一家的出牌花色不一样则认为没有了相同花色的牌
var _code = cls_youle_erqiwang_arith.id_to_code(mainflower, followcards[followcards.length - 1]);
var _flower = parseInt(_code / 100);
if (_code > 1000){
_flower = mainflower;
}
if (_flower != startflower){
can.noflower = true;
can.nopair = true;
}
//如果第一家出的是对子或拖拉机牌型,而跟牌没有出对子则认为没有相同花色的对子
if (startcardtype > 200){
var pairlist = cls_youle_erqiwang_arith.get_pairlist(mainflower, followcards);
if (pairlist.length != startcardtype % 100){
can.nopair = true;
}
}
//根据第一家的出牌计算牌值大小
//一张单张
if (startcardtype == 101){
var _code = cls_youle_erqiwang_arith.id_to_code(mainflower, followcards[0]);
var _flower = parseInt(_code / 100);
if (_code > 1000){ //大于1000是主牌
_flower = mainflower;
}
if (_flower == startflower || _flower == mainflower){
if (_code > 7000 && _code < 8000){ //副7
can.cardvalue = 7000;
} else if (_code > 2000 && _code < 3000){ //副2
can.cardvalue = 2000;
} else {
can.cardvalue = _code;
}
} else {
can.cardvalue = 0;
}
return can;
}
//甩多张单张
if (startcardtype > 101 && startcardtype < 200){
//第一个玩家是甩牌,则跟牌一定没有第一家玩家的牌大
can.cardvalue = 0;
return can;
}
//一对
if (startcardtype == 201){
var pairlist = cls_youle_erqiwang_arith.get_pairlist(mainflower, followcards);
if (pairlist.length != 1){
can.cardvalue = 0;
return can;
}
var _code = cls_youle_erqiwang_arith.id_to_code(mainflower, followcards[0]);
var _flower = parseInt(_code / 100);
if (_code > 1000){
_flower = mainflower;
}
if (_flower == startflower || _flower == mainflower){
if (_code > 7000 && _code < 8000){ //副7
can.cardvalue = 7000;
} else if (_code > 2000 && _code < 3000){ //副2
can.cardvalue = 2000;
} else {
can.cardvalue = _code;
}
} else {
can.cardvalue = 0;
}
return can;
}
//甩多对
if (startcardtype > 201 && startcardtype < 300){
//第一个玩家是甩牌,则跟牌一定没有第一家玩家的牌大
can.cardvalue = 0;
return can;
}
//拖拉机
if (startcardtype > 300 && startcardtype < 400){
var pairlist = cls_youle_erqiwang_arith.get_pairlist(mainflower, followcards);
var tuolaji_list = cls_youle_erqiwang_arith.get_tuolaji_list(mainflower, pairlist, startcardtype);
if (tuolaji_list.length != 1){
can.cardvalue = 0;
return can;
}
var _code = cls_youle_erqiwang_arith.id_to_code(mainflower, followcards[0]);
var _flower = parseInt(_code / 100);
if (_code > 1000){
_flower = mainflower;
}
if (_flower == startflower || _flower == mainflower){
can.cardvalue = _code;
} else {
can.cardvalue = 0;
}
return can;
}
},
//闲家抠底倍数(design §6.3:单张主 ×1、主对子 ×2、N 连对拖拉机 ×2N)
get_bottom_multiple: function(mainflower, cards){
//拷贝数组
var _cards = cards.concat();
//将牌从大到小排序
_cards = cls_youle_erqiwang_arith.order_cards(mainflower, _cards);
//扣底要求赢下最后一轮的牌全是主牌(首末张都 >=1000 即全为主牌)
var _code = cls_youle_erqiwang_arith.id_to_code(mainflower, _cards[0]);
if (_code < 1000){
return 0;
}
_code = cls_youle_erqiwang_arith.id_to_code(mainflower, _cards[_cards.length - 1]);
if (_code < 1000){
return 0;
}
//单张主牌不翻倍
var _pairlist = cls_youle_erqiwang_arith.get_pairlist(mainflower, _cards);
if (_pairlist.length == 0){
return 1;
}
//取牌中最长的连对数 N,倍数为 2N(主对子按 N=1,即 2 倍)
var _n = 1;
var _len = 2;
while (true){
var _tljlist = cls_youle_erqiwang_arith.get_tuolaji_list(mainflower, _pairlist, 300 + _len);
if (_tljlist.length == 0){
break;
}
_n = _len;
_len = _len + 1;
}
return 2 * _n;
},
//冲关计算关数
get_chongguan: function(mainflower, cards){
var re = {};
re.count = 0; //关数
re.wang = 0; //王牌数量
re.cards = []; //王牌和冲关的牌
if (cards.length < 28){
return re;
}
//拷贝数组
var _cards = cards.concat();
//将牌从大到小排序
_cards = cls_youle_erqiwang_arith.order_cards(mainflower, _cards);
var _wlist = []; //王
var _2list = []; //2
var _7list = []; //7
var _incards = []; //是否添加得到了re.cards标志
for (var i = 0; i < _cards.length; i++) {
var _number = cls_youle_erqiwang_arith.id_to_number(_cards[i]);
if (_number >= 53){
_wlist.push(i);
re.wang = re.wang + 1;
re.cards.push(_cards[i]);
} else if (_number == 2){
_2list.push(i);
} else if (_number == 7){
_7list.push(i);
}
_incards.push(0);
}
//添加到re.cards
var do_addlisttocards = function(_list){
for (var i = 0; i < _list.length; i++){
if (!_incards[_list[i]]){
re.cards.push(_cards[_list[i]]);
_incards[_list[i]] = 1;
}
}
}
//三王四王冲关判断
if (_wlist.length >= 3){
re.count = 1;
var _code = 9553; //当前的牌编码
var i = 3; //后面查找的起始位置
if (_wlist.length == 4){
re.count = 3;
i = 4;
}
//往后查找,看是否还有冲关
while (i < _cards.length - 1){
//牌编码,两张牌两张牌一取
var code_i = cls_youle_erqiwang_arith.id_to_code(mainflower, _cards[i]);
var code_j = cls_youle_erqiwang_arith.id_to_code(mainflower, _cards[i + 1]);
if (code_i == code_j){
if (cls_youle_erqiwang_arith.is_continuous(_code, code_i)){
re.count = re.count + 1;
re.cards.push(_cards[i]);
re.cards.push(_cards[i + 1]);
_incards[i] = 1;
_incards[i + 1] = 1;
_code = code_i;
}
i = i + 2;
} else {
i++;
}
}
}
//六2六7判断(design §8.1:6/7/8 个 7 → 1/2/3 奖,6/7/8 个 2 同理)
if (_7list.length >= 6){
re.count = re.count + _7list.length - 5;
do_addlisttocards(_7list);
}
if (_2list.length >= 6){
re.count = re.count + _2list.length - 5;
do_addlisttocards(_2list);
}
//注:固定主牌 ≥10 张只触发亮牌(§8.2)、不算奖,故此处不再加奖
re.cards = cls_youle_erqiwang_arith.order_cards(mainflower, re.cards);
return re;
},
//计算判定的子数倍率(带符号):大光 +3 / 小光 +2 / 过庄 +1 / 升N级 -N(design §7.2.0)
//q: 小光/过庄分界与升级级距(见 get_qvalue:常规40,爬坡分段)
//先判是否达标(升级),再判大光/小光/过庄,顺序不可颠倒
get_upgrade: function(call, grade, q){
if (grade >= call){
//闲家捡分达到叫分:闲赢升级,级数 = 1 + floor((grade-call)/q)
return -1 * (parseInt((grade - call) / q) + 1);
}
if (grade == 0){
return 3; //大光
}
if (grade < q){
return 2; //小光
}
return 1; //过庄
}
}
//Node 导出;友乐/浏览器无 module 时跳过(dev-guide 01 §1)。min_ary_include/min_ary_deduct 为平台全局,两运行时按全局名引用
if (typeof module !== "undefined"){
module.exports = cls_youle_erqiwang_arith;
}