二七王:前端 CardMark,手牌标记推导与花色统计

三种标记互斥、优先级 tractor > zheng > zhu,据主花色本地推导。
拖拉机识别与服务端 decompose_trump 同思路:分对子后扫极大连续段,
主牌一组、副牌按花色分组。反面用例覆盖不连续、单对、跨花色三种不成拖的情形。
countByFlower 供选主面板的张数与对数,协议明确服务端不下发。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-26 20:02:44 +08:00
co-authored by Claude Opus 5
parent 39a920bed5
commit 3a5da5b975
2 changed files with 207 additions and 0 deletions
+117
View File
@@ -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;
}
};