Files
erqiwang_youle/server/games/erqiwang/test/test_arith.js
T
joywayerandClaude Opus 4.8 c29aec140c 二七王测试:补全甩牌/跟甩牌全部情形
回应"甩牌与跟甩牌测试是否完整"的核对,补齐缺口:
- 甩牌首出(§5.4):甩错由主对分量/拖拉机分量/双对之一被压引发(原仅测单张分量);
  纯双单张、纯双对(不连)、单张+拖拉机、单+对+拖三分量混合的合法;副牌禁甩·对手非缺门
- 甩错惩罚实际执行(§5.4.5):do_playcard 收回甩牌、只强制打出最小一张、其余留手;合法甩牌整套打出
- 跟甩牌(§5.4.4):拖拉机分量退化到单张(对子也不够)、多组拖拉机分量、混合demand逐分量对位
- 集成:首出合法甩牌→跟牌方拆散主对(违反§5.4.4)→ do_playcard 拒;正确对位→接受

全部单测 226 项断言全绿(arith 101 / paiju 31 等)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 01:34:02 +08:00

170 lines
12 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);
// ============ §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);