/////////////////////////////////////////////////// ////////// cls_youle_erqiwang_arith: 算法 ///////// /////////////////////////////////////////////////// //跨模块依赖:Node 走 require 守卫;友乐/浏览器无 require,由 min_loadJsFile 加载为同名全局(dev-guide 01 §1) if (typeof require !== "undefined"){ var youle_erqiwang_shared_cards = require("./shared/cards.js"); } 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; }, //以下 9 项为前后端共享算法,权威实现在 shared/cards.js(经 sync_shared.cmd 同步到前端)。 //此处只挂引用:对外方法名与签名不变,全部既有调用点无需改动。 id_to_flower: youle_erqiwang_shared_cards.id_to_flower, id_to_number: youle_erqiwang_shared_cards.id_to_number, id_to_code: youle_erqiwang_shared_cards.id_to_code, order_cards: youle_erqiwang_shared_cards.order_cards, is_continuous: youle_erqiwang_shared_cards.is_continuous, get_pairlist: youle_erqiwang_shared_cards.get_pairlist, group_tractor_runs: youle_erqiwang_shared_cards.group_tractor_runs, get_tuolaji_list: youle_erqiwang_shared_cards.get_tuolaji_list, trump_rank: youle_erqiwang_shared_cards.trump_rank, //把一组主牌分解为分量列表(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()); //先分出对子与单张(对子形状与 shared get_pairlist 一致:[id, id]) var pairs = []; //[[id,id], ...] var singles = []; //[id, ...] 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([ordered[i], ordered[i + 1]]); i = i + 2; continue; } } singles.push(ordered[i]); i++; } //连续对子组成拖拉机,其余为单独对子。极大连续段的扫描是前后端共享算法(shared/cards.js), //此处只负责把段翻译成分量,不再自己实现「什么算连续、几对起算拖拉机」 var runs = youle_erqiwang_shared_cards.group_tractor_runs(mainflower, pairs); var comps = []; for (var j = 0; j < runs.length; j++){ var run = runs[j]; var tcards = []; for (var r = 0; r < run.length; r++){ tcards = tcards.concat(run[r]); } if (run.length >= youle_erqiwang_shared_cards.TRACTOR_MIN_PAIRS){ comps.push({type: "tractor", len: run.length, value: cls_youle_erqiwang_arith.trump_rank(mainflower, run[0][0]), cards: tcards}); } else { comps.push({type: "pair", len: 1, value: cls_youle_erqiwang_arith.trump_rank(mainflower, run[0][0]), cards: tcards}); } } 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; } //张数上界:design §5.4.3 明确甩牌「无组合数量限制」,故不设规则性上限; //这里只挡结构性越界——出牌阶段一名玩家最多持 28 张(埋牌后), //超过即不可能是合法出牌(调用方 mod.chupai 已先做 check_cards_inhand 持牌校验)。 //注:牌型编码在 28 张内不会溢出各自区间(单张≤128、多对≤214、拖拉机≤314) if (cards.length > 28){ 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); //已排序跟牌的完整快照:下面的必出/可出校验会用 min_ary_deduct 削减 _followcards, //而后半段的牌面值(cardvalue)与缺门/无对(noflower/nopair)判定必须基于「完整且已排序」的跟牌。 //这里绝不能改用入参 followcards——那是客户端提交的原始顺序,而 get_pairlist / get_tuolaji_list //都按降序相邻取对:乱序提交会漏判对子/拖拉机,把本该压过的主拖拉机算成牌面 0(本轮胜者、 //捡分归属、扣底倍数全被客户端的数组顺序左右),也能抹掉缺门标志污染 §9 牌况表。 //(dev-guide server 04 §8:前端不是数据源,服务端不得依赖客户端可控的入参顺序) var _sortfollow = _followcards.concat(); //根据第一个玩家的出牌获取后面玩家必出的和可出的牌 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, _sortfollow[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, _sortfollow[_sortfollow.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, _sortfollow); if (pairlist.length != startcardtype % 100){ can.nopair = true; } } //根据第一家的出牌计算牌值大小 //一张单张 if (startcardtype == 101){ var _code = cls_youle_erqiwang_arith.id_to_code(mainflower, _sortfollow[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, _sortfollow); if (pairlist.length != 1){ can.cardvalue = 0; return can; } var _code = cls_youle_erqiwang_arith.id_to_code(mainflower, _sortfollow[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, _sortfollow); 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, _sortfollow[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; }