// arith 纯算法单测:扣底(§6.3) / 算子(§7) / 算奖(§8.1) / 甩牌(§5.4) / 跟甩牌(§5.4.4) require('./_shim'); const A = require('../class.arith.js'); const t = require('./_assert')(); // 牌id构造:deck d(1-2), flower f(1-4), number n(1-13) 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; // 主花色=方块(1) // ============ §6.3 扣底倍数 get_bottom_multiple ============ const mult = cards => A.get_bottom_multiple(MF, cards); t.eq('扣底 单张主A', mult([id(1, 1, 1)]), 1); t.eq('扣底 单张正7', mult([id(1, 1, 7)]), 1); t.eq('扣底 主对K', mult([id(1, 1, 13), id(2, 1, 13)]), 2); t.eq('扣底 两连对KQ', mult([id(1, 1, 13), id(2, 1, 13), id(1, 1, 12), id(2, 1, 12)]), 4); t.eq('扣底 三连对KQJ', mult([id(1, 1, 13), id(2, 1, 13), id(1, 1, 12), id(2, 1, 12), id(1, 1, 11), id(2, 1, 11)]), 6); t.eq('扣底 单张副A(非主)', mult([id(1, 2, 1)]), 0); t.eq('扣底 小王对', mult([small(1), small(2)]), 2); t.eq('扣底 8-6连对', mult([id(1, 1, 8), id(2, 1, 8), id(1, 1, 6), id(2, 1, 6)]), 4); t.eq('扣底 两连对+单(取最高规格)', mult([id(1, 1, 13), id(2, 1, 13), id(1, 1, 12), id(2, 1, 12), id(1, 1, 9)]), 4); // ============ §7 算子:判定标签 + 每对子子数 X ============ function judge(call, grade, climb) { const base = A.get_base_bycall(call, climb); const q = A.get_qvalue(call, climb); const upg = A.get_upgrade(call, grade, q); const X = Math.abs(upg) * base; let label = upg === 3 ? '大光' : upg === 2 ? '小光' : upg === 1 ? '过庄' : upg < 0 ? '升' + (-upg) + '级' : '?'; return label + '/' + X; } // 常规算子 (design §7.2.1) t.eq('常规65 大光', judge(65, 0, false), '大光/6'); t.eq('常规65 小光', judge(65, 20, false), '小光/4'); t.eq('常规65 过庄', judge(65, 50, false), '过庄/2'); t.eq('常规65 升1', judge(65, 65, false), '升1级/2'); t.eq('常规65 升2', judge(65, 105, false), '升2级/4'); t.eq('常规60 大光', judge(60, 0, false), '大光/9'); t.eq('常规55 大光', judge(55, 0, false), '大光/12'); t.eq('常规50 大光', judge(50, 0, false), '大光/18'); t.eq('常规45 过庄', judge(45, 44, false), '过庄/6'); t.eq('常规40 grade20 小光(无过庄)', judge(40, 20, false), '小光/12'); t.eq('常规40 升1', judge(40, 40, false), '升1级/6'); t.eq('常规10 grade5 小光', judge(10, 5, false), '小光/12'); t.eq('常规10 升1', judge(10, 10, false), '升1级/6'); t.eq('常规5 大光', judge(5, 0, false), '大光/18'); t.eq('常规5 升1(无小光)', judge(5, 5, false), '升1级/6'); t.eq('常规70打牌 大光', judge(70, 0, false), '大光/6'); t.eq('常规70打牌 过庄', judge(70, 69, false), '过庄/2'); // 爬坡 (design §7.3.3) t.eq('爬坡40 大光', judge(40, 0, true), '大光/24'); t.eq('爬坡40 小光', judge(40, 19, true), '小光/16'); t.eq('爬坡40 过庄', judge(40, 20, true), '过庄/8'); t.eq('爬坡40 升1', judge(40, 40, true), '升1级/8'); t.eq('爬坡40 升2', judge(40, 60, true), '升2级/16'); t.eq('爬坡30 过庄', judge(30, 15, true), '过庄/10'); t.eq('爬坡20 升1', judge(20, 20, true), '升1级/12'); t.eq('爬坡10 过庄(无小光)', judge(10, 5, true), '过庄/14'); t.eq('爬坡10 升1', judge(10, 10, true), '升1级/14'); t.eq('爬坡5 大光', judge(5, 0, true), '大光/45'); t.eq('爬坡5 升1(无小光过庄)', judge(5, 5, true), '升1级/15'); t.eq('爬坡45 过庄', judge(45, 44, true), '过庄/7'); // ============ §8.1 算奖 get_chongguan ============ function fill28(req) { const used = new Set(req), out = req.slice(), NN = [1, 5, 6, 8, 9, 10, 11, 12, 13]; for (const f of [1, 2, 3, 4]) for (const n of NN) { if (out.length >= 28) break; const c = id(1, f, n); if (!used.has(c)) { used.add(c); out.push(c); } } return out; } const cg = (req, key) => A.get_chongguan(MF, fill28(req))[key]; t.eq('算奖 3王 count', cg([big(1), small(1), small(2)], 'count'), 1); t.eq('算奖 3王 wang', cg([big(1), small(1), small(2)], 'wang'), 3); t.eq('算奖 4王 count', cg([big(1), big(2), small(1), small(2)], 'count'), 3); t.eq('算奖 6个7(无王)', cg([id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 3, 7), id(2, 3, 7)], 'count'), 1); t.eq('算奖 8个2(无王)', cg([id(1, 1, 2), id(2, 1, 2), id(1, 2, 2), id(2, 2, 2), id(1, 3, 2), id(2, 3, 2), id(1, 4, 2), id(2, 4, 2)], 'count'), 3); t.eq('算奖 3王+正7对+副7对(连对链)', cg([big(1), small(1), small(2), id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7)], 'count'), 3); // ≥10固定主但互不成对 => 只3王(旧代码会误+1) t.eq('算奖 10固定主无对无链', cg([big(1), small(1), small(2), id(1, 1, 7), id(1, 2, 7), id(1, 3, 7), id(1, 4, 7), id(1, 1, 2), id(1, 2, 2), id(1, 3, 2)], 'count'), 1); // 连对链延伸到主A / 副花色A不算链 t.eq('算奖 连对链到主A对', cg([big(1), small(1), small(2), id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 1, 2), id(2, 1, 2), id(1, 2, 2), id(2, 2, 2), id(1, 1, 1), id(2, 1, 1)], 'count'), 6); t.eq('算奖 副花色A对不续链', cg([big(1), small(1), small(2), id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 1, 2), id(2, 1, 2), id(1, 2, 2), id(2, 2, 2), id(1, 2, 1), id(2, 2, 1)], 'count'), 5); // ============ §5.4 甩牌 decompose / 最大性 / can_playcard ============ const types = cards => A.decompose_trump(MF, cards).map(c => c.type + (c.len > 1 ? c.len : '')).sort().join(','); t.eq('分解 大王对+主K单', types([big(1), big(2), id(1, 1, 13)]), 'pair,single'); t.eq('分解 主K主Q两连对', types([id(1, 1, 13), id(2, 1, 13), id(1, 1, 12), id(2, 1, 12)]), 'tractor2'); t.eq('分解 主K对+主9对(不连)', types([id(1, 1, 13), id(2, 1, 13), id(1, 1, 9), id(2, 1, 9)]), 'pair,pair'); const play = (cards, opps) => A.can_playcard(MF, cards, 0, null, opps); const K1 = id(1, 1, 13), K2 = id(2, 1, 13), A1 = id(1, 1, 1), P5 = id(1, 1, 5), P6 = id(1, 1, 6), fK1 = id(1, 2, 13), f9 = id(1, 2, 9), fK2 = id(2, 2, 13), Q1 = id(1, 1, 12), Q2 = id(2, 1, 12); t.eq('甩牌 合法(大王对+主K单)', play([big(1), big(2), K1], [[P5, P6], []]).result, true); let r = play([big(1), big(2), K1], [[A1, P5], []]); t.eq('甩牌 甩错(对手有主A>主K) shuaicuo', r.shuaicuo, true); t.eq('甩牌 甩错 smallest=主K', r.smallest, K1); r = play([fK1, f9], [[], []]); t.eq('甩牌 副牌禁甩 result', r.result, false); t.eq('甩牌 副牌禁甩 无shuaicuo', r.shuaicuo, undefined); t.eq('甩牌 主单张(非甩)可出', play([big(1)], [[A1], [K1]]).result, true); t.eq('甩牌 副对子(非甩)可出', play([fK1, fK2], [[], []]).result, true); t.eq('甩牌 主拖拉机(单分量不受最大性约束)', play([K1, K2, Q1, Q2], [[big(1), big(2)], []]).result, true); // ============ §5.4.4 跟甩牌强制分量拆解 flush_follow_ok ============ const J1 = id(1, 1, 11), N9a = id(1, 1, 9), N9b = id(2, 1, 9), P8 = id(1, 1, 8); const fA = id(1, 2, 1), fB = id(1, 2, 5), fC = id(1, 2, 6), fD = id(1, 2, 8); const ffo = (hand, follow, demand) => A.flush_follow_ok(MF, hand, follow, demand); t.eq('跟牌 含主对KK', ffo([K1, K2, J1, N9a], [K1, K2, J1], { tractors: [], pairs: 1, singles: 1 }), true); t.eq('跟牌 拆散主对(3散)→拒', ffo([K1, K2, J1, N9a], [J1, N9a, K1], { tractors: [], pairs: 1, singles: 1 }), false); t.eq('跟牌 无主对散张可跟', ffo([J1, N9a, P6], [J1, N9a, P6], { tractors: [], pairs: 1, singles: 1 }), true); const hand5 = [K1, K2, Q1, Q2, P6, J1, N9a]; t.eq('跟牌 打2连拖+单', ffo(hand5, [K1, K2, Q1, Q2, P6], { tractors: [2], pairs: 0, singles: 1 }), true); t.eq('跟牌 有2连拖却打5散→拒', ffo(hand5, [K1, Q1, P6, J1, N9a], { tractors: [2], pairs: 0, singles: 1 }), false); t.eq('跟牌 有2连拖却只打一对→拒', ffo(hand5, [K1, K2, P6, J1, N9a], { tractors: [2], pairs: 0, singles: 1 }), false); const hand6 = [K1, K2, N9a, N9b, P6, J1, P8]; t.eq('跟牌 无同长拖两对退化', ffo(hand6, [K1, K2, N9a, N9b, P6], { tractors: [2], pairs: 0, singles: 1 }), true); t.eq('跟牌 有两对却打5散→拒', ffo(hand6, [K1, N9a, P6, J1, P8], { tractors: [2], pairs: 0, singles: 1 }), false); t.eq('跟牌 主牌最大化(有主用尽)', ffo([K1, Q1, fA, fB], [K1, Q1, fA], { tractors: [], pairs: 1, singles: 1 }), true); t.eq('跟牌 留主牌垫副→拒', ffo([K1, Q1, fA, fB], [K1, fA, fB], { tractors: [], pairs: 1, singles: 1 }), false); t.eq('跟牌 无主全垫副', ffo([fA, fB, fC, fD], [fA, fB, fC], { tractors: [], pairs: 1, singles: 1 }), true); // ============ §3 主牌顺序 / 相邻链 ============ // order_cards 结果应为 design §3 从大到小:大王>小王>正7>副7>正2>副2>主A>主K…主5>副牌 const big1 = big(1), small1 = small(1), Z7 = id(1, 1, 7), F7 = id(1, 2, 7), Z2 = id(1, 1, 2), F2 = id(1, 2, 2), zA = id(1, 1, 1), zK = id(1, 1, 13), z5 = id(1, 1, 5); t.eq('排序 主牌降序=设计顺序', A.order_cards(MF, [z5, F2, Z2, big1, F7, Z7, small1, zA, zK]), [big1, small1, Z7, F7, Z2, F2, zA, zK, z5]); // is_continuous 相邻链逐段(design §3/§5.3) const cont = (a, b) => A.is_continuous(A.id_to_code(MF, a), A.id_to_code(MF, b)); t.eq('相邻 大王-小王', cont(big(1), small(1)), true); t.eq('相邻 小王-正7', cont(small(1), Z7), true); t.eq('相邻 正7-副7', cont(Z7, F7), true); t.eq('相邻 副7-正2', cont(F7, Z2), true); t.eq('相邻 正2-副2', cont(Z2, F2), true); t.eq('相邻 副2-主A', cont(F2, zA), true); t.eq('相邻 主A-主K', cont(zA, zK), true); t.eq('相邻 主8-主6(6/8可连)', cont(id(1, 1, 8), id(1, 1, 6)), true); t.eq('相邻 主6-主5', cont(id(1, 1, 6), z5), true); t.eq('相邻 正7-主6 不连(7不入普通序)', cont(Z7, id(1, 1, 6)), false); t.eq('相邻 主8-正7 不连', cont(id(1, 1, 8), Z7), false); // ============ §7 算子:每档小光/过庄分界 + 升3级(补强)============ t.eq('常规65 g39小光', judge(65, 39, false), '小光/4'); t.eq('常规65 g40过庄(分界)', judge(65, 40, false), '过庄/2'); t.eq('常规65 g64过庄', judge(65, 64, false), '过庄/2'); t.eq('常规65 g65升1', judge(65, 65, false), '升1级/2'); t.eq('常规65 g104升1', judge(65, 104, false), '升1级/2'); t.eq('常规65 g105升2', judge(65, 105, false), '升2级/4'); t.eq('常规65 g145升3', judge(65, 145, false), '升3级/6'); process.exit(t.done('arith') ? 0 : 1);