前后端此前各写了一遍「分对子 → 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>
288 lines
22 KiB
JavaScript
288 lines
22 KiB
JavaScript
// 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);
|
||
|
||
// ============ shared/cards.js:牌编码区间常量 + 极大连续段扫描 ============
|
||
const S = require('../shared/cards.js');
|
||
|
||
// 常量必须与 id_to_code 的实际产物一致(常量是区间判定的 SSOT,写歪了这里就红)
|
||
const inOpen = (code, lo, hi) => code > lo && code < hi;
|
||
t.eq('常量 正7 区间', inOpen(S.id_to_code(MF, id(1, 1, 7)), S.CODE_ZHENG7_MIN, S.CODE_ZHENG7_MAX), true);
|
||
t.eq('常量 副7 区间', inOpen(S.id_to_code(MF, id(1, 2, 7)), S.CODE_FU7_MIN, S.CODE_FU7_MAX), true);
|
||
t.eq('常量 正2 区间', inOpen(S.id_to_code(MF, id(1, 1, 2)), S.CODE_ZHENG2_MIN, S.CODE_ZHENG2_MAX), true);
|
||
t.eq('常量 副2 区间', inOpen(S.id_to_code(MF, id(1, 2, 2)), S.CODE_FU2_MIN, S.CODE_FU2_MAX), true);
|
||
t.eq('常量 主花色普通牌区间', inOpen(S.id_to_code(MF, id(1, 1, 5)), S.CODE_ZHU_MIN, S.CODE_ZHU_MAX), true);
|
||
t.eq('常量 主牌判据 code>CODE_ZHU_MIN 对主花色牌成立', S.id_to_code(MF, id(1, 1, 5)) > S.CODE_ZHU_MIN, true);
|
||
t.eq('常量 主牌判据 对副牌不成立', S.id_to_code(MF, id(1, 2, 5)) > S.CODE_ZHU_MIN, false);
|
||
t.eq('常量 小王编码', S.id_to_code(MF, small(1)), S.CODE_XIAOWANG);
|
||
t.eq('常量 大王编码', S.id_to_code(MF, big(1)), S.CODE_DAWANG);
|
||
t.eq('常量 主A 的 code%100', S.id_to_code(MF, id(1, 1, 1)) % 100, S.CODE_A_IN_ZHU);
|
||
t.eq('常量 拖拉机起算对数', S.TRACTOR_MIN_PAIRS, 2);
|
||
|
||
// group_tractor_runs:极大连续段(服务端 decompose_trump 与前端手牌「拖」标记共用)
|
||
const pr = (n) => [id(1, 1, n), id(2, 1, n)]; // 主花色 n 的对子
|
||
const runLens = (pairs) => S.group_tractor_runs(MF, pairs).map(r => r.length);
|
||
t.eq('连续段 空表', S.group_tractor_runs(MF, []), []);
|
||
t.eq('连续段 单个对子是长度1的段', runLens([pr(13)]), [1]);
|
||
t.eq('连续段 KQ 连续合成一段', runLens([pr(13), pr(12)]), [2]);
|
||
t.eq('连续段 KQJ 三连对是一段(极大)', runLens([pr(13), pr(12), pr(11)]), [3]);
|
||
t.eq('连续段 K 与 9 不连 → 两段', runLens([pr(13), pr(9)]), [1, 1]);
|
||
t.eq('连续段 KQ + 9 → 2+1', runLens([pr(13), pr(12), pr(9)]), [2, 1]);
|
||
t.eq('连续段 KQ + 98 → 2+2', runLens([pr(13), pr(12), pr(9), pr(8)]), [2, 2]);
|
||
// 8-6 相连(design §5.3:7 不与 6/8 相连),且必须同花色
|
||
t.eq('连续段 主8-主6 相连', runLens([pr(8), pr(6)]), [2]);
|
||
t.eq('连续段 跨花色 主8+副6 不相连', runLens([pr(8), [id(1, 2, 6), id(2, 2, 6)]]), [1, 1]);
|
||
// 段的拼接必须恰好还原入参(不丢对子、不改顺序)
|
||
const runsIn = [pr(13), pr(12), pr(9)];
|
||
t.eq('连续段 拼接还原入参', [].concat.apply([], S.group_tractor_runs(MF, runsIn)), runsIn);
|
||
// arith 上挂的引用与 shared 同一实现
|
||
t.eq('arith 挂了 group_tractor_runs 引用', A.group_tractor_runs === S.group_tractor_runs, true);
|
||
|
||
// ============ §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');
|
||
// 拖拉机段与孤立对子、单张混排:分量顺序与 cards 归属都不变(改走 shared 扫描后的行为一致性)
|
||
const mixComp = A.decompose_trump(MF, [id(1, 1, 13), id(2, 1, 13), id(1, 1, 12), id(2, 1, 12), id(1, 1, 9), id(2, 1, 9), id(1, 1, 5)]);
|
||
t.eq('分解 KQ拖+9对+5单 的分量序', mixComp.map(c => c.type + '/' + c.len), ['tractor/2', 'pair/1', 'single/1']);
|
||
t.eq('分解 拖拉机分量含4张', mixComp[0].cards.length, 4);
|
||
t.eq('分解 对子分量含2张', mixComp[1].cards.length, 2);
|
||
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);
|
||
// —— 甩牌首出补全:各分量被压导致甩错 + 各牌型组合合法(design §5.4.2/§5.4.3)——
|
||
r = play([big(1), K1, K2], [[A1, id(2, 1, 1)], []]); // 大王单+主K对;对手主A对
|
||
t.eq('甩牌 甩错·主对被更大主对压', r.shuaicuo, true);
|
||
r = play([big(1), Q1, Q2, id(1, 1, 11), id(2, 1, 11)], [[A1, id(2, 1, 1), K1, K2], []]); // 大王+主QJ两连对;对手主AK两连对
|
||
t.eq('甩牌 甩错·拖拉机被同长更大主拖压', r.shuaicuo, true);
|
||
t.eq('甩牌 合法·纯双单张(大王+主K)', play([big(1), K1], [[P5], []]).result, true);
|
||
t.eq('甩牌 合法·纯双对不连(主K对+主9对)', play([K1, K2, id(1, 1, 9), id(2, 1, 9)], [[P5, P6], []]).result, true);
|
||
r = play([K1, K2, id(1, 1, 9), id(2, 1, 9)], [[id(1, 1, 11), id(2, 1, 11)], []]); // 主9对被主J对压
|
||
t.eq('甩牌 甩错·双对之一被压', r.shuaicuo, true);
|
||
t.eq('甩牌 合法·单张+拖拉机', play([big(1), K1, K2, Q1, Q2], [[P5, P6], []]).result, true);
|
||
t.eq('甩牌 合法·单+对+拖三混', play([big(1), id(1, 1, 10), id(2, 1, 10), K1, K2, Q1, Q2], [[P5, P6], []]).result, true);
|
||
t.eq('甩牌 副禁甩·对手非缺门仍禁', play([fK1, f9], [[fK2, id(1, 2, 10)], []]).result, false);
|
||
// design §5.4.3「无组合数量限制」:曾有「一次最多14张」的规则外硬上限,会把 15 张以上的
|
||
// 合法甩牌整个拒掉。16 张全固定主牌(4王 + 8个7 + 正2对 + 副2对)、两对手无主牌 → 必须可甩
|
||
const SHUAI16 = [big(1), big(2), small(1), small(2),
|
||
id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 3, 7), id(2, 3, 7), id(1, 4, 7), id(2, 4, 7),
|
||
id(1, 1, 2), id(2, 1, 2), id(1, 4, 2), id(2, 4, 2)];
|
||
t.eq('甩牌 16张(超14张上限)仍可甩', play(SHUAI16, [[], []]).result, true);
|
||
t.eq('甩牌 16张 有分量需求下发', !!play(SHUAI16, [[], []]).shuai_demand, 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);
|
||
// —— 跟甩牌补全:拖分量退化到单张 / 多组拖拉机 / 混合demand(design §5.4.4)——
|
||
t.eq('跟牌 拖退化·1对不够必打+其余单', ffo([K1, K2, N9a, P8, P6, id(1, 1, 5)], [K1, K2, N9a, P8, P6], { tractors: [2], pairs: 0, singles: 1 }), true);
|
||
t.eq('跟牌 拖退化·有1对却拆散→拒', ffo([K1, K2, N9a, P8, P6, id(1, 1, 5)], [K1, N9a, P8, P6, id(1, 1, 5)], { tractors: [2], pairs: 0, singles: 1 }), false);
|
||
t.eq('跟牌 双拖·两组2连拖必打', ffo([K1, K2, Q1, Q2, id(1, 1, 10), id(2, 1, 10), N9a, N9b], [K1, K2, Q1, Q2, id(1, 1, 10), id(2, 1, 10), N9a, N9b], { tractors: [2, 2], pairs: 0, singles: 0 }), true);
|
||
t.eq('跟牌 混合·拖+对+单各对位', ffo([K1, K2, Q1, Q2, N9a, N9b, P8, id(1, 1, 5)], [K1, K2, Q1, Q2, N9a, N9b, P8], { tractors: [2], pairs: 1, singles: 1 }), true);
|
||
t.eq('跟牌 混合·有对却拆散→拒', ffo([K1, K2, Q1, Q2, N9a, N9b, P8, id(1, 1, 5)], [K1, K2, Q1, Q2, N9a, P8, id(1, 1, 5)], { tractors: [2], pairs: 1, singles: 1 }), false);
|
||
|
||
// ============ §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);
|
||
// 8/6 相邻必须同花色:跨花色的 8 与 6 不成连对(否则 get_chongguan 连对链会被副牌 6 对错误接续)
|
||
t.eq('相邻 副♥8-副♥6 同花色 连', cont(id(1, 3, 8), id(1, 3, 6)), true);
|
||
t.eq('相邻 副♥8-副♣6 跨花色 不连', cont(id(1, 3, 8), id(1, 2, 6)), false);
|
||
t.eq('相邻 主8-副♣6 跨花色 不连', cont(id(1, 1, 8), id(1, 2, 6)), false);
|
||
t.eq('相邻 副♠8-副♦6 跨花色 不连', cont(id(1, 4, 8), id(1, 1, 6)), false);
|
||
// 顺带确认非 8/6 的等差2组合仍不连(防守卫写宽)
|
||
t.eq('相邻 主A-主Q 不连(差2但非8/6)', cont(zA, id(1, 1, 12)), false);
|
||
|
||
// ============ §3「关于对子的判定」:对子只认同一张具体牌的两副 ============
|
||
// design §3 注:正/副7、正/副2 虽各占一个大小等级,但**成对只认同花色同点数的两副**;
|
||
// 两张不同花色的副7(如♠7+♥7)不构成一对,副2 同理;大王与小王点数不同也不能混成一对。
|
||
// 拖拉机的连对判定以此为基础(每节连对里的每个对子都须是同花色同点数两副)。
|
||
const pl = cards => A.get_pairlist(MF, A.order_cards(MF, cards.slice())).length;
|
||
t.eq('对子 正 同花色两副♥7 成对', pl([id(1, 3, 7), id(2, 3, 7)]), 1);
|
||
t.eq('对子 反 ♥7+♣7 跨花色副7 不成对', pl([id(1, 3, 7), id(1, 2, 7)]), 0);
|
||
t.eq('对子 反 正7+副7 不成对', pl([id(1, 1, 7), id(1, 2, 7)]), 0);
|
||
t.eq('对子 正 同花色两副♥2 成对', pl([id(1, 3, 2), id(2, 3, 2)]), 1);
|
||
t.eq('对子 反 ♥2+♣2 跨花色副2 不成对', pl([id(1, 3, 2), id(1, 2, 2)]), 0);
|
||
t.eq('对子 反 正2+副2 不成对', pl([id(1, 1, 2), id(1, 2, 2)]), 0);
|
||
t.eq('对子 正 大王两副 成对', pl([big(1), big(2)]), 1);
|
||
t.eq('对子 反 大王+小王 不成对', pl([big(1), small(1)]), 0);
|
||
// 连对以「同花色同点数两副」为基础:四张跨花色的副7/副2 一对都凑不出,自然也没有拖拉机
|
||
t.eq('对子 边界 ♥7♣7♥2♣2 四张 → 0 对', pl([id(1, 3, 7), id(1, 2, 7), id(1, 3, 2), id(1, 2, 2)]), 0);
|
||
t.eq('连对 边界 上述四张组不出拖拉机',
|
||
A.get_tuolaji_list(MF, A.get_pairlist(MF, A.order_cards(MF, [id(1, 3, 7), id(1, 2, 7), id(1, 3, 2), id(1, 2, 2)])), 302).length, 0);
|
||
// 对照:正7对 + 副7对(各自同花色两副)才是合法两连对
|
||
t.eq('连对 正 正7对+副7对 构成两连对',
|
||
A.get_tuolaji_list(MF, A.get_pairlist(MF, A.order_cards(MF, [id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7)])), 302).length, 1);
|
||
|
||
// ============ §5.3 拖拉机:副牌须同花色 / 主牌按链相邻·不限花色 ============
|
||
// 这一层此前只测到 is_continuous(两张牌相不相邻),没测到「这几个对子能不能真的组成拖拉机」。
|
||
// design §5.3 改写后明确:副牌**必须同一花色**;主牌只要在主牌序列里**位置相邻**即可、**不限花色**;
|
||
// 同一档位的两个对子(如 ♥7对 + ♣7对)平级、不相邻。
|
||
const tljOf = cards => {
|
||
const s = A.order_cards(MF, cards.slice());
|
||
const ps = A.get_pairlist(MF, s);
|
||
return ps.length >= 2 && A.get_tuolaji_list(MF, ps, 300 + ps.length).length > 0;
|
||
};
|
||
// —— 主牌链:跨花色也成连对 ——
|
||
t.eq('§5.3 主链 正7对+副7对(跨花色) 成连对', tljOf([id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7)]), true);
|
||
t.eq('§5.3 主链 副7对+正2对(跨花色) 成连对', tljOf([id(1, 2, 7), id(2, 2, 7), id(1, 1, 2), id(2, 1, 2)]), true);
|
||
t.eq('§5.3 主链 正2对+副2对(跨花色) 成连对', tljOf([id(1, 1, 2), id(2, 1, 2), id(1, 3, 2), id(2, 3, 2)]), true);
|
||
t.eq('§5.3 主链 副2对+主A对(跨花色) 成连对', tljOf([id(1, 2, 2), id(2, 2, 2), id(1, 1, 1), id(2, 1, 1)]), true);
|
||
t.eq('§5.3 主链 大王对+小王对 成连对', tljOf([big(1), big(2), small(1), small(2)]), true);
|
||
t.eq('§5.3 主链 小王对+正7对 成连对', tljOf([small(1), small(2), id(1, 1, 7), id(2, 1, 7)]), true);
|
||
t.eq('§5.3 主链 三连(副7对+正2对+副2对) 成立', tljOf([id(1, 2, 7), id(2, 2, 7), id(1, 1, 2), id(2, 1, 2), id(1, 3, 2), id(2, 3, 2)]), true);
|
||
t.eq('§5.3 主链 主8对+主6对 成连对(7被抽走)', tljOf([id(1, 1, 8), id(2, 1, 8), id(1, 1, 6), id(2, 1, 6)]), true);
|
||
// —— 同一档位的两个对子平级、不相邻 ——
|
||
t.eq('§5.3 同档 ♥7对+♣7对 不成连对', tljOf([id(1, 3, 7), id(2, 3, 7), id(1, 2, 7), id(2, 2, 7)]), false);
|
||
t.eq('§5.3 同档 ♥2对+♣2对 不成连对', tljOf([id(1, 3, 2), id(2, 3, 2), id(1, 2, 2), id(2, 2, 2)]), false);
|
||
// —— 副牌:必须同花色 ——
|
||
t.eq('§5.3 副牌 ♥K对+♥Q对 同花色 成连对', tljOf([id(1, 3, 13), id(2, 3, 13), id(1, 3, 12), id(2, 3, 12)]), true);
|
||
t.eq('§5.3 副牌 ♥K对+♣Q对 跨花色 不成连对', tljOf([id(1, 3, 13), id(2, 3, 13), id(1, 2, 12), id(2, 2, 12)]), false);
|
||
t.eq('§5.3 副牌 ♥8对+♥6对 成连对(7是主牌被抽走)', tljOf([id(1, 3, 8), id(2, 3, 8), id(1, 3, 6), id(2, 3, 6)]), true);
|
||
t.eq('§5.3 副牌 ♥8对+♣6对 跨花色 不成连对', tljOf([id(1, 3, 8), id(2, 3, 8), id(1, 2, 6), id(2, 2, 6)]), false);
|
||
t.eq('§5.3 副牌 ♥6对+♥5对 成连对(5为最小档)', tljOf([id(1, 3, 6), id(2, 3, 6), id(1, 3, 5), id(2, 3, 5)]), true);
|
||
t.eq('§5.3 副牌 ♥9对+♥K对 不相邻 不成连对', tljOf([id(1, 3, 9), id(2, 3, 9), id(1, 3, 13), id(2, 3, 13)]), false);
|
||
// —— §2 牌堆里没有 3 和 4,所以副牌序列的最小一档就是 5 ——
|
||
// (规则复核时有人把副牌链写成 "…-6-5-3",那需要牌堆保留 3;但 92 = 28×3 + 8 只有在
|
||
// 3、4 都剔除时才成立。这里就地钉住"牌堆里 3 和 4 各 0 张",避免链的末端再被写错。)
|
||
const _P = require('../class.paiju.js');
|
||
const _deck = _P.new({ paiju_list: [] }, 0).cards.filter(c => c.dealowner !== -1);
|
||
t.eq('§2 参与发牌的牌里 3 和 4 各 0 张',
|
||
[3, 4].map(n => _deck.filter(c => c.number === n).length), [0, 0]);
|
||
t.eq('§2 参与发牌共 92 张 = 28×3 + 暗牌8', [_deck.length, 28 * 3 + 8], [92, 92]);
|
||
|
||
// ============ §6.3 扣底触发的反面:赢末轮的牌不全是主牌就不扣底 ============
|
||
t.eq('扣底 反 副牌两连对赢末轮 → 0', mult([id(1, 2, 13), id(2, 2, 13), id(1, 2, 12), id(2, 2, 12)]), 0);
|
||
t.eq('扣底 反 混合出牌(主K+副K)赢末轮 → 0', mult([id(1, 1, 13), id(1, 2, 13)]), 0);
|
||
t.eq('扣底 反 主牌在前副牌在后也判 0(与提交顺序无关)', mult([id(1, 2, 13), id(1, 1, 13)]), 0);
|
||
|
||
// ============ §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);
|