二七王:前端 CardMark,手牌标记推导与花色统计
三种标记互斥、优先级 tractor > zheng > zhu,据主花色本地推导。 拖拉机识别与服务端 decompose_trump 同思路:分对子后扫极大连续段, 主牌一组、副牌按花色分组。反面用例覆盖不连续、单对、跨花色三种不成拖的情形。 countByFlower 供选主面板的张数与对数,协议明确服务端不下发。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user