// 手牌三种标记推导 + 选主面板的花色统计(清单 §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]); // ---- 三连对:整段 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)]; 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);