From 3a5da5b97579f3d72e8e7ce43781a67f8f9dbf86 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Wed, 26 Aug 2026 20:02:44 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BA=8C=E4=B8=83=E7=8E=8B=EF=BC=9A=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=20CardMark=EF=BC=8C=E6=89=8B=E7=89=8C=E6=A0=87?= =?UTF-8?q?=E8=AE=B0=E6=8E=A8=E5=AF=BC=E4=B8=8E=E8=8A=B1=E8=89=B2=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 三种标记互斥、优先级 tractor > zheng > zhu,据主花色本地推导。 拖拉机识别与服务端 decompose_trump 同思路:分对子后扫极大连续段, 主牌一组、副牌按花色分组。反面用例覆盖不连续、单对、跨花色三种不成拖的情形。 countByFlower 供选主面板的张数与对数,协议明确服务端不下发。 Co-Authored-By: Claude Opus 5 (1M context) --- client/js/01_SubGame/codes/core/CardMark.js | 117 ++++++++++++++++++++ client/tests/test_cardmark.js | 90 +++++++++++++++ 2 files changed, 207 insertions(+) create mode 100644 client/js/01_SubGame/codes/core/CardMark.js create mode 100644 client/tests/test_cardmark.js diff --git a/client/js/01_SubGame/codes/core/CardMark.js b/client/js/01_SubGame/codes/core/CardMark.js new file mode 100644 index 0000000..2d4e1f3 --- /dev/null +++ b/client/js/01_SubGame/codes/core/CardMark.js @@ -0,0 +1,117 @@ +/////////////////////////////////////////////////////////////// +////////// EQW_CardMark: 手牌牌面标记与花色统计(纯逻辑)/////// +/////////////////////////////////////////////////////////////// +// 清单 §1.6 / T-8:牌面三种标记,前端据主花色本地推导,服务端不下发。 +// 'tractor' 红色圆标「拖」= 该牌属于一组拖拉机 +// 'zheng' 橙色五角星 = 正2 / 正7(选定花色的 2 和 7) +// '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 +// +// 拖拉机只在【同一牌组】内成立:全部主牌算一组(副2/副7/王与主花色牌可相连), +// 副牌按各自花色分组。识别方式与服务端 decompose_trump 同思路: +// 先分出对子,再用 is_continuous 扫极大连续段,长度 ≥2 的段内所有牌标 tractor。 +var EQW_CardMark = EQW_CardMark || { + + MARK_TRACTOR: 'tractor', + MARK_ZHENG: 'zheng', + MARK_ZHU: 'zhu', + + //整手牌的标记表:{ 牌id: 标记 };没有标记的牌不出现在结果里 + marksOfHand: function (cards, mainflower) { + var marks = {}; + if (!cards || cards.length === 0) { return marks; } + + var mf = mainflower || 0; + var S = youle_erqiwang_shared_cards; + var i, code; + + //1) 分组:主牌一组(键 'Z'),副牌按花色分组(键为花色号) + 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])); + if (!groups[key]) { groups[key] = []; } + groups[key].push(cards[i]); + } + + //2) 每组内找拖拉机 + for (var g in groups) { + 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++; + } + 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; + } + } + + //3) 其余牌按 zheng / zhu 标注(拖拉机优先,已标的不覆盖) + 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'; //其余主牌 + } + } + + return marks; + }, + + //单张牌的标记;cards 是它所在的整手牌(拖拉机需要上下文才能判定) + markOf: function (cardId, cards, mainflower) { + var marks = this.marksOfHand(cards, mainflower); + return marks[cardId] || null; + }, + + //选主面板用:各花色的张数与对子数(清单 §1.5,服务端不下发、前端自算) + //按【牌面花色】统计,与主牌无关;王不属于任何花色,不计入 + countByFlower: function (cards) { + var result = { 1: { count: 0, pairs: 0 }, 2: { count: 0, pairs: 0 }, + 3: { count: 0, pairs: 0 }, 4: { count: 0, pairs: 0 } }; + if (!cards || cards.length === 0) { return result; } + + var S = youle_erqiwang_shared_cards; + var byFlowerNumber = {}; //{花色: {点数: 张数}} + var i, f, n; + + for (i = 0; i < cards.length; i++) { + f = S.id_to_flower(cards[i]); + if (f === 5) { continue; } //王 + n = S.id_to_number(cards[i]); + if (!byFlowerNumber[f]) { byFlowerNumber[f] = {}; } + byFlowerNumber[f][n] = (byFlowerNumber[f][n] || 0) + 1; + result[f].count++; + } + + for (f = 1; f <= 4; f++) { + var nums = byFlowerNumber[f]; + if (!nums) { continue; } + for (n in nums) { + if (!nums.hasOwnProperty(n)) { continue; } + result[f].pairs += Math.floor(nums[n] / 2); + } + } + + return result; + } +}; diff --git a/client/tests/test_cardmark.js b/client/tests/test_cardmark.js new file mode 100644 index 0000000..6b0ef66 --- /dev/null +++ b/client/tests/test_cardmark.js @@ -0,0 +1,90 @@ +// 手牌三种标记推导 + 选主面板的花色统计(清单 §1.6 / §1.5) +const { load } = require('./_load'); +const t = require('./_assert')(); + +load('client/js/01_SubGame/codes/shared/cards.js'); +load('client/js/01_SubGame/codes/core/CardOrder.js'); +load('client/js/01_SubGame/codes/core/CardMark.js'); + +const id = (d, f, n) => (d - 1) * 54 + (f - 1) * 13 + (n - 1); +const big = d => (d - 1) * 54 + 53; +const small = d => (d - 1) * 54 + 52; +const MF = 1; // 主花色 = 方块 + +// ---- 正2 / 正7(主花色的 2 和 7)标 zheng ---- +t.eq('正2 标 zheng', EQW_CardMark.markOf(id(1, MF, 2), [id(1, MF, 2)], MF), 'zheng'); +t.eq('正7 标 zheng', EQW_CardMark.markOf(id(1, MF, 7), [id(1, MF, 7)], MF), 'zheng'); + +// ---- 其他主牌标 zhu:双王、副2、副7、主花色普通牌 ---- +t.eq('大王 标 zhu', EQW_CardMark.markOf(big(1), [big(1)], MF), 'zhu'); +t.eq('小王 标 zhu', EQW_CardMark.markOf(small(1), [small(1)], MF), 'zhu'); +t.eq('副2 标 zhu', EQW_CardMark.markOf(id(1, 2, 2), [id(1, 2, 2)], MF), 'zhu'); +t.eq('副7 标 zhu', EQW_CardMark.markOf(id(1, 3, 7), [id(1, 3, 7)], MF), 'zhu'); +t.eq('主花色A 标 zhu', EQW_CardMark.markOf(id(1, MF, 1), [id(1, MF, 1)], MF), 'zhu'); + +// ---- 副牌无标记 ---- +t.eq('副牌普通牌无标记', EQW_CardMark.markOf(id(1, 2, 5), [id(1, 2, 5)], MF), null); + +// ---- 拖拉机:同花色两个连续对子 → 四张全标 tractor ---- +// 副牌花色2 的 9对 + 10对(编码相差1,连续) +const tractorHand = [id(1, 2, 9), id(2, 2, 9), id(1, 2, 10), id(2, 2, 10)]; +const m1 = EQW_CardMark.marksOfHand(tractorHand, MF); +t.eq('连续两对全标 tractor', tractorHand.map(c => m1[c]), ['tractor', 'tractor', 'tractor', 'tractor']); + +// ---- 反面:不连续的两个对子不成拖拉机(副牌无标记)---- +const notTractor = [id(1, 2, 9), id(2, 2, 9), id(1, 2, 12), id(2, 2, 12)]; +const m2 = EQW_CardMark.marksOfHand(notTractor, MF); +t.eq('不连续两对不成拖', notTractor.map(c => m2[c] || null), [null, null, null, null]); + +// ---- 反面:单个对子不是拖拉机 ---- +const onePair = [id(1, 2, 9), id(2, 2, 9)]; +const m3 = EQW_CardMark.marksOfHand(onePair, MF); +t.eq('单对不成拖', onePair.map(c => m3[c] || null), [null, null]); + +// ---- 反面:跨花色的对子不成拖(花色2的9对 + 花色3的10对)---- +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]); + +// ---- 优先级:属于拖拉机的正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)]; +const m5 = EQW_CardMark.marksOfHand(zhengInTractor, MF); +t.eq('拖拉机优先于 zheng', m5[id(1, MF, 2)], 'tractor'); +t.eq('拖拉机优先于 zhu', m5[id(1, 2, 2)], 'tractor'); + +// ---- 互斥:任何一张牌至多一个标记(marksOfHand 的值域)---- +const mixed = [big(1), small(1), id(1, MF, 2), id(1, MF, 7), id(1, 2, 5), id(1, 2, 9), id(2, 2, 9), id(1, 2, 10), id(2, 2, 10)]; +const m6 = EQW_CardMark.marksOfHand(mixed, MF); +const validMarks = Object.keys(m6).every(k => ['tractor', 'zheng', 'zhu'].indexOf(m6[k]) >= 0); +t.eq('标记值域合法', validMarks, true); + +// ---- markOf 与 marksOfHand 结果一致 ---- +const consistent = mixed.every(c => (m6[c] || null) === EQW_CardMark.markOf(c, mixed, MF)); +t.eq('markOf 与 marksOfHand 一致', consistent, true); + +// ---- 选主前(mainflower=0):只有固定主牌成主,主花色普通牌无标记 ---- +t.eq('选主前 方块A 无标记', EQW_CardMark.markOf(id(1, 1, 1), [id(1, 1, 1)], 0), null); +t.eq('选主前 2 仍是主', EQW_CardMark.markOf(id(1, 1, 2), [id(1, 1, 2)], 0), 'zhu'); +t.eq('选主前 王 仍是主', EQW_CardMark.markOf(big(1), [big(1)], 0), 'zhu'); + +// ---- countByFlower:选主面板的张数与对数 ---- +const forCount = [ + id(1, 1, 5), id(2, 1, 5), // 方块5 一对 + id(1, 1, 9), // 方块9 单张 + id(1, 2, 3), id(1, 2, 6), id(2, 2, 6), // 梅花:一张3 + 一对6 + big(1), small(1) // 王不计入任何花色 +]; +const cnt = EQW_CardMark.countByFlower(forCount); +t.eq('方块张数', cnt[1].count, 3); +t.eq('方块对数', cnt[1].pairs, 1); +t.eq('梅花张数', cnt[2].count, 3); +t.eq('梅花对数', cnt[2].pairs, 1); +t.eq('红心张数', cnt[3].count, 0); +t.eq('黑桃对数', cnt[4].pairs, 0); + +// ---- 边界 ---- +t.eq('空手牌 marks', EQW_CardMark.marksOfHand([], MF), {}); +t.eq('空手牌 count', EQW_CardMark.countByFlower([]), { 1: { count: 0, pairs: 0 }, 2: { count: 0, pairs: 0 }, 3: { count: 0, pairs: 0 }, 4: { count: 0, pairs: 0 } }); + +process.exit(t.done('cardmark') ? 0 : 1);