Files
erqiwang_youle/client/tests/test_cardmark.js
T
joywayerandClaude Opus 5 b81be36f6e 二七王:拖拉机极大连续段扫描与牌编码区间常量收敛进 shared
前后端此前各写了一遍「分对子 → is_continuous 扫极大连续段 → ≥2 对算拖拉机」:
服务端 class.arith.js decompose_trump 与前端 core/CardMark.js。规则内容(什么算连续、
几对起算拖拉机)散在两处,正是 shared/ 要消除的重复。

- shared/cards.js 新增 group_tractor_runs(mainflower, pairlist):返回极大连续段数组,
  孤立对子作为长度 1 的段返回,拼接后恰好还原入参;阈值导出为 TRACTOR_MIN_PAIRS。
- shared/cards.js 导出牌编码区间边界具名常量(CODE_ZHU_MIN / CODE_ZHENG2_* /
  CODE_FU2_* / CODE_ZHENG7_* / CODE_FU7_* / CODE_XIAOWANG / CODE_DAWANG /
  CODE_A_IN_ZHU),并在 id_to_code / is_continuous / trump_rank 内部改用它们。
- decompose_trump 改调共享扫描,对外行为完全不变(分量类型、顺序、cards 归属不变)。
- CardMark 删掉本地扫描改调共享版本,区间判定与标记值改引用常量,去掉 1000/3000/
  4000/8000/9000 裸字面量。
- 前端副本经 sync_shared.cmd 同步;服务端单测 624→645 checks 全绿,前端 8 个测试全绿。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 22:17:09 +08:00

120 lines
6.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 手牌三种标记推导 + 选主面板的花色统计(清单 §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);