Files
erqiwang_youle/server/games/erqiwang/test/test_arith.js
T
joywayerandClaude Opus 5 5832b781c8 二七王:is_continuous 的 8→6 相邻补花色校验
design §5.3 的「6 与 8 可组成拖拉机」是同花色内的相邻关系,但
is_8/is_6 只判 code % 100,不校验花色,导致
is_continuous(♥8=308, ♣6=206) 与 is_continuous(主8=1408, ♣6=206)
都返回 true。

影响面:get_chongguan 是唯一会拿整手牌(含各花色副牌)去扫描连对
链的调用点,连对链走到主 8 之后,手里任意花色的一对 6 都会被误判
为连续、多算 1 奖,并把链锚点带进副牌继续错误延伸(design §8.1
明确 A 到 5 这一段必须是选定花色的主牌)。其余调用点入参都是同花
色牌表或纯主牌表,不受影响。

改为要求牌编码相差 2(同花色的 8 与 6 恒差 2,跨花色差 100 的倍
数),并保留 is_8/is_6 守卫以免把「主A-主Q」这类差 2 组合放行。

test_arith 新增 5 项:同花色 8-6 连、三组跨花色 8-6 不连、差 2 但
非 8/6 的组合不连。

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

177 lines
13 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.
// 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);
// —— 甩牌首出补全:各分量被压导致甩错 + 各牌型组合合法(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);
// ============ §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);
// ============ §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);