From b81be36f6e65a28b784d02c4779340783786035a Mon Sep 17 00:00:00 2001 From: Joywayer Date: Wed, 26 Aug 2026 22:17:09 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BA=8C=E4=B8=83=E7=8E=8B=EF=BC=9A=E6=8B=96?= =?UTF-8?q?=E6=8B=89=E6=9C=BA=E6=9E=81=E5=A4=A7=E8=BF=9E=E7=BB=AD=E6=AE=B5?= =?UTF-8?q?=E6=89=AB=E6=8F=8F=E4=B8=8E=E7=89=8C=E7=BC=96=E7=A0=81=E5=8C=BA?= =?UTF-8?q?=E9=97=B4=E5=B8=B8=E9=87=8F=E6=94=B6=E6=95=9B=E8=BF=9B=20shared?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 前后端此前各写了一遍「分对子 → is_continuous 扫极大连续段 → ≥2 对算拖拉机」: 服务端 class.arith.js decompose_trump 与前端 core/CardMark.js。规则内容(什么算连续、 几对起算拖拉机)散在两处,正是 shared/ 要消除的重复。 - shared/cards.js 新增 group_tractor_runs(mainflower, pairlist):返回极大连续段数组, 孤立对子作为长度 1 的段返回,拼接后恰好还原入参;阈值导出为 TRACTOR_MIN_PAIRS。 - shared/cards.js 导出牌编码区间边界具名常量(CODE_ZHU_MIN / CODE_ZHENG2_* / CODE_FU2_* / CODE_ZHENG7_* / CODE_FU7_* / CODE_XIAOWANG / CODE_DAWANG / CODE_A_IN_ZHU),并在 id_to_code / is_continuous / trump_rank 内部改用它们。 - decompose_trump 改调共享扫描,对外行为完全不变(分量类型、顺序、cards 归属不变)。 - CardMark 删掉本地扫描改调共享版本,区间判定与标记值改引用常量,去掉 1000/3000/ 4000/8000/9000 裸字面量。 - 前端副本经 sync_shared.cmd 同步;服务端单测 624→645 checks 全绿,前端 8 个测试全绿。 Co-Authored-By: Claude Opus 5 (1M context) --- client/js/01_SubGame/codes/core/CardMark.js | 45 +++++------ client/js/01_SubGame/codes/shared/cards.js | 86 +++++++++++++++++---- client/tests/test_cardmark.js | 29 +++++++ server/games/erqiwang/class.arith.js | 54 ++++++------- server/games/erqiwang/shared/cards.js | 86 +++++++++++++++++---- server/games/erqiwang/test/test_arith.js | 41 ++++++++++ 6 files changed, 254 insertions(+), 87 deletions(-) diff --git a/client/js/01_SubGame/codes/core/CardMark.js b/client/js/01_SubGame/codes/core/CardMark.js index 2d4e1f3..d226f64 100644 --- a/client/js/01_SubGame/codes/core/CardMark.js +++ b/client/js/01_SubGame/codes/core/CardMark.js @@ -7,14 +7,14 @@ // 'zhu' 蓝色五角星 = 其余主牌(双王、副2、副7、主花色普通牌) // 三者【互斥】、每张牌至多一个,优先级 tractor > zheng > zhu。 // -// 牌编码区间(shared/cards.js 的 id_to_code 产物): -// 9553/9554 王 / 8000-9000 正7 / 7000-8000 副7 / 3000-4000 正2 -// 2000-3000 副2 / 1000-2000 主花色普通牌 / 100-500 副牌普通牌 -// ⇒ 主牌 ⟺ code > 1000 +// 牌编码区间边界一律取自 shared/cards.js 导出的具名常量(CODE_ZHU_MIN / CODE_ZHENG2_* / +// CODE_ZHENG7_* …)——那里是 id_to_code 的权威说明处,本文件不重抄 1000/3000/8000 之类裸值。 +// ⇒ 主牌 ⟺ code > CODE_ZHU_MIN // // 拖拉机只在【同一牌组】内成立:全部主牌算一组(副2/副7/王与主花色牌可相连), -// 副牌按各自花色分组。识别方式与服务端 decompose_trump 同思路: -// 先分出对子,再用 is_continuous 扫极大连续段,长度 ≥2 的段内所有牌标 tractor。 +// 副牌按各自花色分组。极大连续段的扫描与「几对起算拖拉机」是前后端共享算法 +// (shared/cards.js 的 group_tractor_runs / TRACTOR_MIN_PAIRS,服务端 decompose_trump 同源), +// 本文件只负责按段落标记,不再自己实现一遍扫描。 var EQW_CardMark = EQW_CardMark || { MARK_TRACTOR: 'tractor', @@ -34,7 +34,7 @@ var EQW_CardMark = EQW_CardMark || { var groups = {}; for (i = 0; i < cards.length; i++) { code = S.id_to_code(mf, cards[i]); - var key = (code > 1000) ? 'Z' : String(S.id_to_flower(cards[i])); + var key = (code > S.CODE_ZHU_MIN) ? 'Z' : String(S.id_to_flower(cards[i])); if (!groups[key]) { groups[key] = []; } groups[key].push(cards[i]); } @@ -44,22 +44,14 @@ var EQW_CardMark = EQW_CardMark || { if (!groups.hasOwnProperty(g)) { continue; } var ordered = S.order_cards(mf, groups[g].concat()); var pairs = S.get_pairlist(mf, ordered); - var j = 0; - while (j < pairs.length) { - var run = [pairs[j]]; - var k = j; - while (k + 1 < pairs.length && - S.is_continuous(S.id_to_code(mf, pairs[k][0]), S.id_to_code(mf, pairs[k + 1][0]))) { - run.push(pairs[k + 1]); - k++; + var runs = S.group_tractor_runs(mf, pairs); + for (var j = 0; j < runs.length; j++) { + var run = runs[j]; + if (run.length < S.TRACTOR_MIN_PAIRS) { continue; } //孤立对子(长度1的段)不是拖拉机 + for (var r = 0; r < run.length; r++) { + marks[run[r][0]] = this.MARK_TRACTOR; + marks[run[r][1]] = this.MARK_TRACTOR; } - if (run.length >= 2) { //≥2 个连续对子才是拖拉机 - for (var r = 0; r < run.length; r++) { - marks[run[r][0]] = 'tractor'; - marks[run[r][1]] = 'tractor'; - } - } - j = k + 1; } } @@ -67,10 +59,11 @@ var EQW_CardMark = EQW_CardMark || { for (i = 0; i < cards.length; i++) { if (marks[cards[i]]) { continue; } code = S.id_to_code(mf, cards[i]); - if ((code > 3000 && code < 4000) || (code > 8000 && code < 9000)) { - marks[cards[i]] = 'zheng'; //正2 / 正7 - } else if (code > 1000) { - marks[cards[i]] = 'zhu'; //其余主牌 + if ((code > S.CODE_ZHENG2_MIN && code < S.CODE_ZHENG2_MAX) || + (code > S.CODE_ZHENG7_MIN && code < S.CODE_ZHENG7_MAX)) { + marks[cards[i]] = this.MARK_ZHENG; //正2 / 正7 + } else if (code > S.CODE_ZHU_MIN) { + marks[cards[i]] = this.MARK_ZHU; //其余主牌 } } diff --git a/client/js/01_SubGame/codes/shared/cards.js b/client/js/01_SubGame/codes/shared/cards.js index a9d2c65..89bd286 100644 --- a/client/js/01_SubGame/codes/shared/cards.js +++ b/client/js/01_SubGame/codes/shared/cards.js @@ -13,6 +13,28 @@ // ⇒ 主牌 ⟺ code > 1000 var youle_erqiwang_shared_cards = youle_erqiwang_shared_cards || { + //——— 牌编码区间边界:上面那张区间表的机器可读形式,是【全部区间判定的唯一权威来源】——— + //一律是【开区间】:CODE_ZHENG7_MIN < code < CODE_ZHENG7_MAX ⟺ 正7,其余同理。 + //本文件内部(is_continuous / trump_rank / id_to_code)与前端 core/CardMark.js 都引用这些常量, + //不再各处重抄 3000/4000/8000/9000 之类裸字面量(工程总则 §1 SSOT / §6 配置优先于硬编码)。 + CODE_XIAOWANG: 9553, //小王(单值,非区间) + CODE_DAWANG: 9554, //大王(单值,非区间) + CODE_ZHENG7_MIN: 8000, //正7:8000 < code < 9000 + CODE_ZHENG7_MAX: 9000, + CODE_FU7_MIN: 7000, //副7:7000 < code < 8000 + CODE_FU7_MAX: 8000, + CODE_ZHENG2_MIN: 3000, //正2:3000 < code < 4000 + CODE_ZHENG2_MAX: 4000, + CODE_FU2_MIN: 2000, //副2:2000 < code < 3000 + CODE_FU2_MAX: 3000, + CODE_ZHU_MIN: 1000, //主牌 ⟺ code > CODE_ZHU_MIN;主花色普通牌:1000 < code < 2000 + CODE_ZHU_MAX: 2000, + CODE_A_IN_ZHU: 14, //主A 的 code % 100(A 在 id_to_code 里 +13 提到 14) + + //几对连续对子起算拖拉机(design §5.3)。group_tractor_runs 返回极大连续段, + //段长 >= 本值即拖拉机——服务端分解甩牌与前端手牌标记共用这一个阈值 + TRACTOR_MIN_PAIRS: 2, + //牌id转花色 id_to_flower: function(cardid){ if (cardid == 52 || cardid == 106){ @@ -43,11 +65,11 @@ var youle_erqiwang_shared_cards = youle_erqiwang_shared_cards || { id_to_code: function(mainflower, cardid){ if (cardid == 52 || cardid == 106){ //小王 - return 9553; + return youle_erqiwang_shared_cards.CODE_XIAOWANG; } if (cardid == 53 || cardid == 107){ //大王 - return 9554; + return youle_erqiwang_shared_cards.CODE_DAWANG; } var flower = youle_erqiwang_shared_cards.id_to_flower(cardid); var number = youle_erqiwang_shared_cards.id_to_number(cardid); @@ -56,18 +78,21 @@ var youle_erqiwang_shared_cards = youle_erqiwang_shared_cards || { switch (number) { case 1: + //A 提到 K 之上(14) code = code + 13; break; case 2: - code = code + 2000; + //2 提到副2 段 + code = code + youle_erqiwang_shared_cards.CODE_FU2_MIN; break; case 7: - code = code + 7000; + //7 提到副7 段 + code = code + youle_erqiwang_shared_cards.CODE_FU7_MIN; break; } - //主牌花色处理 + //主牌花色处理:整体再上移一个主牌段(副2→正2、副7→正7、普通牌→主花色普通牌) if (flower == mainflower){ - code = code + 1000; + code = code + youle_erqiwang_shared_cards.CODE_ZHU_MIN; } return code; }, @@ -92,44 +117,45 @@ var youle_erqiwang_shared_cards = youle_erqiwang_shared_cards || { //判断两个牌是否是连续的 is_continuous: function(bigcode, smallcode){ + var S = youle_erqiwang_shared_cards; //是小王 var is_xiaowang = function(_code){ - if (_code == 9553){ + if (_code == S.CODE_XIAOWANG){ return true; } return false; } //是正7 var is_zheng7 = function(_code){ - if (_code > 8000 && _code < 9000){ + if (_code > S.CODE_ZHENG7_MIN && _code < S.CODE_ZHENG7_MAX){ return true; } return false; } //是负7 var is_fu7 = function(_code){ - if (_code > 7000 && _code < 8000){ + if (_code > S.CODE_FU7_MIN && _code < S.CODE_FU7_MAX){ return true; } return false; } //是正2 var is_zheng2 = function(_code){ - if (_code > 3000 && _code < 4000){ + if (_code > S.CODE_ZHENG2_MIN && _code < S.CODE_ZHENG2_MAX){ return true; } return false; } //是负2 var is_fu2 = function(_code){ - if (_code > 2000 && _code < 3000){ + if (_code > S.CODE_FU2_MIN && _code < S.CODE_FU2_MAX){ return true; } return false; } //是主A var is_zhuA = function(_code){ - if (_code > 1000 && _code < 2000 && _code % 100 == 14){ + if (_code > S.CODE_ZHU_MIN && _code < S.CODE_ZHU_MAX && _code % 100 == S.CODE_A_IN_ZHU){ return true; } return false; @@ -201,6 +227,34 @@ var youle_erqiwang_shared_cards = youle_erqiwang_shared_cards || { return pairlist; }, + //把对子列表扫成【极大连续段】。pairlist 为同一牌组内、已按牌编码从大到小排好的对子列表 + //(形如 get_pairlist 的返回值:[[cardid, cardid], ...])。 + //返回 [[pair, ...], ...]:每一段是若干【相邻且 is_continuous】的对子,段与段之间必不连续; + //孤立对子也作为长度 1 的段返回,故 concat 所有段 == 原 pairlist(顺序不变)。 + //段长 >= TRACTOR_MIN_PAIRS 即拖拉机。 + //【唯一实现】服务端 decompose_trump(甩牌分解)与前端 core/CardMark.js(手牌「拖」标记) + //此前各写过一遍同样的扫描,规则内容(什么算连续、几对起算拖拉机)散在两处——现收敛到这里。 + group_tractor_runs: function(mainflower, pairlist){ + var runs = []; + var i = 0; + while (i < pairlist.length){ + var run = [pairlist[i]]; + var k = i; + while (k + 1 < pairlist.length){ + var code_k = youle_erqiwang_shared_cards.id_to_code(mainflower, pairlist[k][0]); + var code_k1 = youle_erqiwang_shared_cards.id_to_code(mainflower, pairlist[k + 1][0]); + if (!youle_erqiwang_shared_cards.is_continuous(code_k, code_k1)){ + break; + } + run.push(pairlist[k + 1]); + k++; + } + runs.push(run); + i = k + 1; + } + return runs; + }, + //获取拖拉机列表,pairlist为同一花色的排过序的对子列表,cardtype为拖拉机牌型 get_tuolaji_list: function(mainflower, pairlist, cardtype){ var tuolaji = []; @@ -233,11 +287,11 @@ var youle_erqiwang_shared_cards = youle_erqiwang_shared_cards || { //主牌归一化大小(副7 同级取 7000、副2 同级取 2000,其余用牌编码),仅用于甩牌分量比大小 trump_rank: function(mainflower, cardid){ var code = youle_erqiwang_shared_cards.id_to_code(mainflower, cardid); - if (code > 7000 && code < 8000){ //副7 - return 7000; + if (code > youle_erqiwang_shared_cards.CODE_FU7_MIN && code < youle_erqiwang_shared_cards.CODE_FU7_MAX){ //副7 + return youle_erqiwang_shared_cards.CODE_FU7_MIN; } - if (code > 2000 && code < 3000){ //副2 - return 2000; + if (code > youle_erqiwang_shared_cards.CODE_FU2_MIN && code < youle_erqiwang_shared_cards.CODE_FU2_MAX){ //副2 + return youle_erqiwang_shared_cards.CODE_FU2_MIN; } return code; }, diff --git a/client/tests/test_cardmark.js b/client/tests/test_cardmark.js index 6b0ef66..a6a67af 100644 --- a/client/tests/test_cardmark.js +++ b/client/tests/test_cardmark.js @@ -46,6 +46,35 @@ const crossFlower = [id(1, 2, 9), id(2, 2, 9), id(1, 3, 10), id(2, 3, 10)]; const m4 = EQW_CardMark.marksOfHand(crossFlower, MF); t.eq('跨花色不成拖', crossFlower.map(c => m4[c] || null), [null, null, null, null]); +// ---- 三连对:整段 6 张全标 tractor ---- +const tractor3 = [id(1, 2, 9), id(2, 2, 9), id(1, 2, 10), id(2, 2, 10), id(1, 2, 11), id(2, 2, 11)]; +const m3a = EQW_CardMark.marksOfHand(tractor3, MF); +t.eq('三连对全标 tractor', tractor3.map(c => m3a[c]), ['tractor', 'tractor', 'tractor', 'tractor', 'tractor', 'tractor']); + +// ---- 极大连续段:连续的 10/J 成拖,同花色不相连的 8 对不成拖(段与段互不粘连)---- +const runAndLone = [id(1, 2, 10), id(2, 2, 10), id(1, 2, 11), id(2, 2, 11), id(1, 2, 8), id(2, 2, 8)]; +const m3b = EQW_CardMark.marksOfHand(runAndLone, MF); +t.eq('连续段内标 tractor', [m3b[id(1, 2, 10)], m3b[id(1, 2, 11)]], ['tractor', 'tractor']); +t.eq('段外孤立对子不标', m3b[id(1, 2, 8)] || null, null); + +// ---- 与服务端同源:标记扫描走 shared 的 group_tractor_runs(前后端唯一实现)---- +t.eq('shared 导出极大连续段扫描', typeof youle_erqiwang_shared_cards.group_tractor_runs, 'function'); +t.eq('shared 导出拖拉机起算对数', youle_erqiwang_shared_cards.TRACTOR_MIN_PAIRS, 2); + +// ---- 区间边界取自 shared 具名常量(CardMark 内不再有裸字面量)---- +t.eq('shared 导出主牌下界', youle_erqiwang_shared_cards.CODE_ZHU_MIN, 1000); +t.eq('正2 落在 CODE_ZHENG2 区间', + youle_erqiwang_shared_cards.id_to_code(MF, id(1, MF, 2)) > youle_erqiwang_shared_cards.CODE_ZHENG2_MIN && + youle_erqiwang_shared_cards.id_to_code(MF, id(1, MF, 2)) < youle_erqiwang_shared_cards.CODE_ZHENG2_MAX, true); +t.eq('正7 落在 CODE_ZHENG7 区间', + youle_erqiwang_shared_cards.id_to_code(MF, id(1, MF, 7)) > youle_erqiwang_shared_cards.CODE_ZHENG7_MIN && + youle_erqiwang_shared_cards.id_to_code(MF, id(1, MF, 7)) < youle_erqiwang_shared_cards.CODE_ZHENG7_MAX, true); + +// ---- 标记值取自 MARK_* 常量,不在别处重写字面量 ---- +t.eq('MARK_TRACTOR 常量', EQW_CardMark.MARK_TRACTOR, 'tractor'); +t.eq('MARK_ZHENG 常量', EQW_CardMark.MARK_ZHENG, 'zheng'); +t.eq('MARK_ZHU 常量', EQW_CardMark.MARK_ZHU, 'zhu'); + // ---- 优先级:属于拖拉机的正2,标 tractor 而非 zheng ---- // 主牌序里 正2 与 副2 相连(is_continuous: 正2、负2) const zhengInTractor = [id(1, MF, 2), id(2, MF, 2), id(1, 2, 2), id(2, 2, 2)]; diff --git a/server/games/erqiwang/class.arith.js b/server/games/erqiwang/class.arith.js index f5c27eb..1e7e71f 100644 --- a/server/games/erqiwang/class.arith.js +++ b/server/games/erqiwang/class.arith.js @@ -64,31 +64,32 @@ var cls_youle_erqiwang_arith = cls_youle_erqiwang_arith || { return 40; }, - //以下 8 项为前后端共享算法,权威实现在 shared/cards.js(经 sync_shared.cmd 同步到前端)。 + //以下 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, - get_tuolaji_list: youle_erqiwang_shared_cards.get_tuolaji_list, - trump_rank: youle_erqiwang_shared_cards.trump_rank, + 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()); - //先分出对子与单张 - var pairs = []; //{code, cards:[id,id]} - var singles = []; //{card} + //先分出对子与单张(对子形状与 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({code: code_i, cards: [ordered[i], ordered[i + 1]]}); + pairs.push([ordered[i], ordered[i + 1]]); i = i + 2; continue; } @@ -96,26 +97,21 @@ var cls_youle_erqiwang_arith = cls_youle_erqiwang_arith || { singles.push(ordered[i]); i++; } - //连续对子组成拖拉机,其余为单独对子 + //连续对子组成拖拉机,其余为单独对子。极大连续段的扫描是前后端共享算法(shared/cards.js), + //此处只负责把段翻译成分量,不再自己实现「什么算连续、几对起算拖拉机」 + var runs = youle_erqiwang_shared_cards.group_tractor_runs(mainflower, pairs); 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++; + 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 >= 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}); + 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].cards[0]), cards: run[0].cards}); + comps.push({type: "pair", len: 1, value: cls_youle_erqiwang_arith.trump_rank(mainflower, run[0][0]), cards: tcards}); } - 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]]}); diff --git a/server/games/erqiwang/shared/cards.js b/server/games/erqiwang/shared/cards.js index a9d2c65..89bd286 100644 --- a/server/games/erqiwang/shared/cards.js +++ b/server/games/erqiwang/shared/cards.js @@ -13,6 +13,28 @@ // ⇒ 主牌 ⟺ code > 1000 var youle_erqiwang_shared_cards = youle_erqiwang_shared_cards || { + //——— 牌编码区间边界:上面那张区间表的机器可读形式,是【全部区间判定的唯一权威来源】——— + //一律是【开区间】:CODE_ZHENG7_MIN < code < CODE_ZHENG7_MAX ⟺ 正7,其余同理。 + //本文件内部(is_continuous / trump_rank / id_to_code)与前端 core/CardMark.js 都引用这些常量, + //不再各处重抄 3000/4000/8000/9000 之类裸字面量(工程总则 §1 SSOT / §6 配置优先于硬编码)。 + CODE_XIAOWANG: 9553, //小王(单值,非区间) + CODE_DAWANG: 9554, //大王(单值,非区间) + CODE_ZHENG7_MIN: 8000, //正7:8000 < code < 9000 + CODE_ZHENG7_MAX: 9000, + CODE_FU7_MIN: 7000, //副7:7000 < code < 8000 + CODE_FU7_MAX: 8000, + CODE_ZHENG2_MIN: 3000, //正2:3000 < code < 4000 + CODE_ZHENG2_MAX: 4000, + CODE_FU2_MIN: 2000, //副2:2000 < code < 3000 + CODE_FU2_MAX: 3000, + CODE_ZHU_MIN: 1000, //主牌 ⟺ code > CODE_ZHU_MIN;主花色普通牌:1000 < code < 2000 + CODE_ZHU_MAX: 2000, + CODE_A_IN_ZHU: 14, //主A 的 code % 100(A 在 id_to_code 里 +13 提到 14) + + //几对连续对子起算拖拉机(design §5.3)。group_tractor_runs 返回极大连续段, + //段长 >= 本值即拖拉机——服务端分解甩牌与前端手牌标记共用这一个阈值 + TRACTOR_MIN_PAIRS: 2, + //牌id转花色 id_to_flower: function(cardid){ if (cardid == 52 || cardid == 106){ @@ -43,11 +65,11 @@ var youle_erqiwang_shared_cards = youle_erqiwang_shared_cards || { id_to_code: function(mainflower, cardid){ if (cardid == 52 || cardid == 106){ //小王 - return 9553; + return youle_erqiwang_shared_cards.CODE_XIAOWANG; } if (cardid == 53 || cardid == 107){ //大王 - return 9554; + return youle_erqiwang_shared_cards.CODE_DAWANG; } var flower = youle_erqiwang_shared_cards.id_to_flower(cardid); var number = youle_erqiwang_shared_cards.id_to_number(cardid); @@ -56,18 +78,21 @@ var youle_erqiwang_shared_cards = youle_erqiwang_shared_cards || { switch (number) { case 1: + //A 提到 K 之上(14) code = code + 13; break; case 2: - code = code + 2000; + //2 提到副2 段 + code = code + youle_erqiwang_shared_cards.CODE_FU2_MIN; break; case 7: - code = code + 7000; + //7 提到副7 段 + code = code + youle_erqiwang_shared_cards.CODE_FU7_MIN; break; } - //主牌花色处理 + //主牌花色处理:整体再上移一个主牌段(副2→正2、副7→正7、普通牌→主花色普通牌) if (flower == mainflower){ - code = code + 1000; + code = code + youle_erqiwang_shared_cards.CODE_ZHU_MIN; } return code; }, @@ -92,44 +117,45 @@ var youle_erqiwang_shared_cards = youle_erqiwang_shared_cards || { //判断两个牌是否是连续的 is_continuous: function(bigcode, smallcode){ + var S = youle_erqiwang_shared_cards; //是小王 var is_xiaowang = function(_code){ - if (_code == 9553){ + if (_code == S.CODE_XIAOWANG){ return true; } return false; } //是正7 var is_zheng7 = function(_code){ - if (_code > 8000 && _code < 9000){ + if (_code > S.CODE_ZHENG7_MIN && _code < S.CODE_ZHENG7_MAX){ return true; } return false; } //是负7 var is_fu7 = function(_code){ - if (_code > 7000 && _code < 8000){ + if (_code > S.CODE_FU7_MIN && _code < S.CODE_FU7_MAX){ return true; } return false; } //是正2 var is_zheng2 = function(_code){ - if (_code > 3000 && _code < 4000){ + if (_code > S.CODE_ZHENG2_MIN && _code < S.CODE_ZHENG2_MAX){ return true; } return false; } //是负2 var is_fu2 = function(_code){ - if (_code > 2000 && _code < 3000){ + if (_code > S.CODE_FU2_MIN && _code < S.CODE_FU2_MAX){ return true; } return false; } //是主A var is_zhuA = function(_code){ - if (_code > 1000 && _code < 2000 && _code % 100 == 14){ + if (_code > S.CODE_ZHU_MIN && _code < S.CODE_ZHU_MAX && _code % 100 == S.CODE_A_IN_ZHU){ return true; } return false; @@ -201,6 +227,34 @@ var youle_erqiwang_shared_cards = youle_erqiwang_shared_cards || { return pairlist; }, + //把对子列表扫成【极大连续段】。pairlist 为同一牌组内、已按牌编码从大到小排好的对子列表 + //(形如 get_pairlist 的返回值:[[cardid, cardid], ...])。 + //返回 [[pair, ...], ...]:每一段是若干【相邻且 is_continuous】的对子,段与段之间必不连续; + //孤立对子也作为长度 1 的段返回,故 concat 所有段 == 原 pairlist(顺序不变)。 + //段长 >= TRACTOR_MIN_PAIRS 即拖拉机。 + //【唯一实现】服务端 decompose_trump(甩牌分解)与前端 core/CardMark.js(手牌「拖」标记) + //此前各写过一遍同样的扫描,规则内容(什么算连续、几对起算拖拉机)散在两处——现收敛到这里。 + group_tractor_runs: function(mainflower, pairlist){ + var runs = []; + var i = 0; + while (i < pairlist.length){ + var run = [pairlist[i]]; + var k = i; + while (k + 1 < pairlist.length){ + var code_k = youle_erqiwang_shared_cards.id_to_code(mainflower, pairlist[k][0]); + var code_k1 = youle_erqiwang_shared_cards.id_to_code(mainflower, pairlist[k + 1][0]); + if (!youle_erqiwang_shared_cards.is_continuous(code_k, code_k1)){ + break; + } + run.push(pairlist[k + 1]); + k++; + } + runs.push(run); + i = k + 1; + } + return runs; + }, + //获取拖拉机列表,pairlist为同一花色的排过序的对子列表,cardtype为拖拉机牌型 get_tuolaji_list: function(mainflower, pairlist, cardtype){ var tuolaji = []; @@ -233,11 +287,11 @@ var youle_erqiwang_shared_cards = youle_erqiwang_shared_cards || { //主牌归一化大小(副7 同级取 7000、副2 同级取 2000,其余用牌编码),仅用于甩牌分量比大小 trump_rank: function(mainflower, cardid){ var code = youle_erqiwang_shared_cards.id_to_code(mainflower, cardid); - if (code > 7000 && code < 8000){ //副7 - return 7000; + if (code > youle_erqiwang_shared_cards.CODE_FU7_MIN && code < youle_erqiwang_shared_cards.CODE_FU7_MAX){ //副7 + return youle_erqiwang_shared_cards.CODE_FU7_MIN; } - if (code > 2000 && code < 3000){ //副2 - return 2000; + if (code > youle_erqiwang_shared_cards.CODE_FU2_MIN && code < youle_erqiwang_shared_cards.CODE_FU2_MAX){ //副2 + return youle_erqiwang_shared_cards.CODE_FU2_MIN; } return code; }, diff --git a/server/games/erqiwang/test/test_arith.js b/server/games/erqiwang/test/test_arith.js index c0cbd44..dea46b5 100644 --- a/server/games/erqiwang/test/test_arith.js +++ b/server/games/erqiwang/test/test_arith.js @@ -85,11 +85,52 @@ t.eq('算奖 10固定主无对无链', cg([big(1), small(1), small(2), id(1, 1, t.eq('算奖 连对链到主A对', cg([big(1), small(1), small(2), id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 1, 2), id(2, 1, 2), id(1, 2, 2), id(2, 2, 2), id(1, 1, 1), id(2, 1, 1)], 'count'), 6); t.eq('算奖 副花色A对不续链', cg([big(1), small(1), small(2), id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 1, 2), id(2, 1, 2), id(1, 2, 2), id(2, 2, 2), id(1, 2, 1), id(2, 2, 1)], 'count'), 5); +// ============ shared/cards.js:牌编码区间常量 + 极大连续段扫描 ============ +const S = require('../shared/cards.js'); + +// 常量必须与 id_to_code 的实际产物一致(常量是区间判定的 SSOT,写歪了这里就红) +const inOpen = (code, lo, hi) => code > lo && code < hi; +t.eq('常量 正7 区间', inOpen(S.id_to_code(MF, id(1, 1, 7)), S.CODE_ZHENG7_MIN, S.CODE_ZHENG7_MAX), true); +t.eq('常量 副7 区间', inOpen(S.id_to_code(MF, id(1, 2, 7)), S.CODE_FU7_MIN, S.CODE_FU7_MAX), true); +t.eq('常量 正2 区间', inOpen(S.id_to_code(MF, id(1, 1, 2)), S.CODE_ZHENG2_MIN, S.CODE_ZHENG2_MAX), true); +t.eq('常量 副2 区间', inOpen(S.id_to_code(MF, id(1, 2, 2)), S.CODE_FU2_MIN, S.CODE_FU2_MAX), true); +t.eq('常量 主花色普通牌区间', inOpen(S.id_to_code(MF, id(1, 1, 5)), S.CODE_ZHU_MIN, S.CODE_ZHU_MAX), true); +t.eq('常量 主牌判据 code>CODE_ZHU_MIN 对主花色牌成立', S.id_to_code(MF, id(1, 1, 5)) > S.CODE_ZHU_MIN, true); +t.eq('常量 主牌判据 对副牌不成立', S.id_to_code(MF, id(1, 2, 5)) > S.CODE_ZHU_MIN, false); +t.eq('常量 小王编码', S.id_to_code(MF, small(1)), S.CODE_XIAOWANG); +t.eq('常量 大王编码', S.id_to_code(MF, big(1)), S.CODE_DAWANG); +t.eq('常量 主A 的 code%100', S.id_to_code(MF, id(1, 1, 1)) % 100, S.CODE_A_IN_ZHU); +t.eq('常量 拖拉机起算对数', S.TRACTOR_MIN_PAIRS, 2); + +// group_tractor_runs:极大连续段(服务端 decompose_trump 与前端手牌「拖」标记共用) +const pr = (n) => [id(1, 1, n), id(2, 1, n)]; // 主花色 n 的对子 +const runLens = (pairs) => S.group_tractor_runs(MF, pairs).map(r => r.length); +t.eq('连续段 空表', S.group_tractor_runs(MF, []), []); +t.eq('连续段 单个对子是长度1的段', runLens([pr(13)]), [1]); +t.eq('连续段 KQ 连续合成一段', runLens([pr(13), pr(12)]), [2]); +t.eq('连续段 KQJ 三连对是一段(极大)', runLens([pr(13), pr(12), pr(11)]), [3]); +t.eq('连续段 K 与 9 不连 → 两段', runLens([pr(13), pr(9)]), [1, 1]); +t.eq('连续段 KQ + 9 → 2+1', runLens([pr(13), pr(12), pr(9)]), [2, 1]); +t.eq('连续段 KQ + 98 → 2+2', runLens([pr(13), pr(12), pr(9), pr(8)]), [2, 2]); +// 8-6 相连(design §5.3:7 不与 6/8 相连),且必须同花色 +t.eq('连续段 主8-主6 相连', runLens([pr(8), pr(6)]), [2]); +t.eq('连续段 跨花色 主8+副6 不相连', runLens([pr(8), [id(1, 2, 6), id(2, 2, 6)]]), [1, 1]); +// 段的拼接必须恰好还原入参(不丢对子、不改顺序) +const runsIn = [pr(13), pr(12), pr(9)]; +t.eq('连续段 拼接还原入参', [].concat.apply([], S.group_tractor_runs(MF, runsIn)), runsIn); +// arith 上挂的引用与 shared 同一实现 +t.eq('arith 挂了 group_tractor_runs 引用', A.group_tractor_runs === S.group_tractor_runs, true); + // ============ §5.4 甩牌 decompose / 最大性 / can_playcard ============ const types = cards => A.decompose_trump(MF, cards).map(c => c.type + (c.len > 1 ? c.len : '')).sort().join(','); t.eq('分解 大王对+主K单', types([big(1), big(2), id(1, 1, 13)]), 'pair,single'); t.eq('分解 主K主Q两连对', types([id(1, 1, 13), id(2, 1, 13), id(1, 1, 12), id(2, 1, 12)]), 'tractor2'); t.eq('分解 主K对+主9对(不连)', types([id(1, 1, 13), id(2, 1, 13), id(1, 1, 9), id(2, 1, 9)]), 'pair,pair'); +// 拖拉机段与孤立对子、单张混排:分量顺序与 cards 归属都不变(改走 shared 扫描后的行为一致性) +const mixComp = A.decompose_trump(MF, [id(1, 1, 13), id(2, 1, 13), id(1, 1, 12), id(2, 1, 12), id(1, 1, 9), id(2, 1, 9), id(1, 1, 5)]); +t.eq('分解 KQ拖+9对+5单 的分量序', mixComp.map(c => c.type + '/' + c.len), ['tractor/2', 'pair/1', 'single/1']); +t.eq('分解 拖拉机分量含4张', mixComp[0].cards.length, 4); +t.eq('分解 对子分量含2张', mixComp[1].cards.length, 2); const play = (cards, opps) => A.can_playcard(MF, cards, 0, null, opps); const K1 = id(1, 1, 13), K2 = id(2, 1, 13), A1 = id(1, 1, 1), P5 = id(1, 1, 5), P6 = id(1, 1, 6), fK1 = id(1, 2, 13), f9 = id(1, 2, 9), fK2 = id(2, 2, 13), Q1 = id(1, 1, 12), Q2 = id(2, 1, 12); t.eq('甩牌 合法(大王对+主K单)', play([big(1), big(2), K1], [[P5, P6], []]).result, true);