二七王:前端 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
+90
View File
@@ -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);