回应"甩牌与跟甩牌测试是否完整"的核对,补齐缺口: - 甩牌首出(§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>
128 lines
8.9 KiB
JavaScript
128 lines
8.9 KiB
JavaScript
// paiju 单测:亮牌(§8.2) + 结算 get_paiju_account(§7/§8) 集成
|
||
require('./_shim');
|
||
const P = require('../class.paiju.js');
|
||
const t = require('./_assert')();
|
||
|
||
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;
|
||
|
||
// ============ §8.2 亮牌 get_liangpai ============
|
||
// 构造庄家(seat0)手牌 o_paiju;副花色中性单张填充到28
|
||
function fill(req, n) {
|
||
const u = new Set(req), o = req.slice();
|
||
for (const f of [2, 3, 4]) for (const x of [1, 5, 6, 8, 9, 10, 11, 12, 13]) {
|
||
if (o.length >= n) break;
|
||
const c = id(1, f, x);
|
||
if (!u.has(c)) { u.add(c); o.push(c); }
|
||
}
|
||
return o;
|
||
}
|
||
const mkHand = ids => ({ flower: 1, banker: 0, cards: ids.map(x => ({ id: x, playround: -1, dealowner: 1 })) });
|
||
const lp = ids => P.get_liangpai(mkHand(fill(ids, 28)));
|
||
t.eq('亮牌 3王', lp([big(1), small(1), small(2)]), { wang: 3 });
|
||
t.eq('亮牌 4王+6个7(固定主10)', lp([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)]), { zhu: 10, zhupair: 5, zhutuo: 1, wang: 4, qi: 6 });
|
||
t.eq('亮牌 6个2', lp([id(1, 1, 2), id(2, 1, 2), id(1, 2, 2), id(2, 2, 2), id(1, 3, 2), id(2, 3, 2)]), { er: 6 });
|
||
t.eq('亮牌 2王2个7(不达标)', lp([big(1), small(1), id(1, 1, 7), id(2, 1, 7)]), null);
|
||
t.eq('亮牌 6个7(仅qi)', lp([id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 3, 7), id(2, 3, 7)]), { qi: 6 });
|
||
t.eq('亮牌 固定主9不达标→null', lp([big(1), small(1), 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)]), null);
|
||
t.eq('亮牌 固定主10边界(仅zhu)', lp([big(1), small(1), 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)]), { zhu: 10, zhupair: 4, zhutuo: 1 });
|
||
|
||
// ============ §7/§8 结算 get_paiju_account 集成 ============
|
||
// 受控 o_paiju/o_desk/o_room:控制庄家算奖手牌(dealowner=1)与闲1捡分(playowner=1,score)
|
||
const mkDesk = roomtype => ({ seatlist: [[0, []], [0, []], [0, []]], o_room: { roomtype, asetcount: 6 }, method: { get_desk_account: m => m } });
|
||
const paiju = (call, flower, cards, roomtype) => ({ call, banker: 0, flower, cards, idx: 2, o_desk: mkDesk(roomtype), endtime: null, result: null });
|
||
// bankerIds→庄家算奖手牌(dealowner=1);xian1grade→闲1捡分(每张≤10分)
|
||
function mkCards(bankerIds, xian1grade) {
|
||
const cs = bankerIds.map(x => ({ id: x, dealowner: 1, playround: -1, playowner: -1, score: 0 }));
|
||
let g = xian1grade, i = 0;
|
||
while (g > 0) { const s = g >= 10 ? 10 : g; cs.push({ id: 200 + i, dealowner: 2, playround: 1, playowner: 1, score: s }); g -= s; i++; }
|
||
return cs;
|
||
}
|
||
const settle = (call, flower, cards, roomtype, type = 0) => P.get_paiju_account(paiju(call, flower, cards, roomtype), type, {});
|
||
const grades = msg => msg.data.aset.seatlist.map(p => p.grade);
|
||
const B0 = [id(1, 1, 1)]; // 庄家仅1张手牌(不足28→无算奖),用于纯算子用例
|
||
|
||
// 常规算子:庄赢/闲赢各档(无算奖)
|
||
t.eq('结算 65小光(grade20) X=4', grades(settle(65, 1, mkCards(B0, 20), "00000")), [8, -4, -4]);
|
||
t.eq('结算 65 upgrade/multiple', [settle(65, 1, mkCards(B0, 20), "00000").data.aset.upgrade, settle(65, 1, mkCards(B0, 20), "00000").data.aset.multiple], [2, 2]);
|
||
t.eq('结算 65大光(grade0)', grades(settle(65, 1, mkCards(B0, 0), "00000")), [12, -6, -6]);
|
||
t.eq('结算 65过庄(grade50)', grades(settle(65, 1, mkCards(B0, 50), "00000")), [4, -2, -2]);
|
||
t.eq('结算 65升2级(grade110)', grades(settle(65, 1, mkCards(B0, 110), "00000")), [-8, 4, 4]);
|
||
// 爬坡局(位3=1):40分大光 base8 → X=24
|
||
t.eq('结算 爬坡40大光', grades(settle(40, 1, mkCards(B0, 0), "00010")), [48, -24, -24]);
|
||
// 投降(type1):基础1、X=1 → 庄-2、闲各+1
|
||
t.eq('结算 投降 grade', grades(settle(70, -1, mkCards(B0, 0), "00000", 1)), [-2, 1, 1]);
|
||
t.eq('结算 投降 upgrade标识', settle(70, -1, mkCards(B0, 0), "00000", 1).data.aset.upgrade, -99);
|
||
// 傍王(位2=1) + 庄家3王:N_庄=1(常规算奖)+3(傍王)=4;65小光 X=4
|
||
// jf 庄+8/闲-4,aw 庄+32/闲-16 → 庄40、闲各-20(零和)
|
||
const B3wang = fill([big(1), small(1), small(2)], 28);
|
||
t.eq('结算 傍王65小光+庄3王', grades(settle(65, 1, mkCards(B3wang, 20), "00100")), [40, -20, -20]);
|
||
// 对照:同局不勾傍王 → N_庄=1(仅常规算奖):aw 庄+8/闲-4,jf 庄+8/闲-4 → 庄16、闲各-8
|
||
t.eq('结算 不傍王65小光+庄3王(仅常规算奖)', grades(settle(65, 1, mkCards(B3wang, 20), "00000")), [16, -8, -8]);
|
||
|
||
// ============ §6.3 扣底触发 get_bottom_account ============
|
||
// 仅当闲家(maxseat!=banker)用主牌赢末轮才扣底/翻倍
|
||
function mkBottomPaiju(maxseat, winCards, bottomScoreCards, flower) {
|
||
const cards = [];
|
||
for (let i = 0; i < 108; i++) cards[i] = { id: i, playround: -1, score: 0, dealowner: 1, playowner: -1 };
|
||
for (const [cid, sc] of bottomScoreCards) { cards[cid].playround = 0; cards[cid].score = sc; }
|
||
const pc = [[], [], []]; pc[maxseat] = winCards;
|
||
const o = { banker: 0, flower, cards, playproc: { maxseat, cards: pc } };
|
||
o.method = { get_burycard: () => P.get_burycard(o), get_grade_incard: c => P.get_grade_incard(o, c) };
|
||
return o;
|
||
}
|
||
// 闲1用主对(方块Q对)赢末轮,底2张K=20分 → 主对×2、grade2=40
|
||
const bo = P.get_bottom_account(mkBottomPaiju(1, [id(1, 1, 12), id(2, 1, 12)], [[id(1, 1, 13), 10], [id(2, 1, 13), 10]], 1), { data: {} });
|
||
t.eq('扣底 闲家主对赢 multiple', bo.data.bottom.multiple, 2);
|
||
t.eq('扣底 grade1/grade2', [bo.data.bottom.grade1, bo.data.bottom.grade2], [20, 40]);
|
||
// 闲1用副牌(副K)赢末轮 → 非主牌不扣底
|
||
const bo2 = P.get_bottom_account(mkBottomPaiju(1, [id(1, 2, 13)], [[id(1, 1, 13), 10]], 1), { data: {} });
|
||
t.eq('扣底 副牌赢不扣底', bo2.data.bottom.multiple, undefined);
|
||
// 庄家(maxseat==banker)赢末轮 → 不扣底
|
||
const bo3 = P.get_bottom_account(mkBottomPaiju(0, [id(1, 1, 12), id(2, 1, 12)], [[id(1, 1, 13), 10]], 1), { data: {} });
|
||
t.eq('扣底 庄家赢不扣底', bo3.data.bottom.multiple, undefined);
|
||
// 闲1用两连对拖拉机赢 → ×4
|
||
const bo4 = P.get_bottom_account(mkBottomPaiju(1, [id(1, 1, 13), id(2, 1, 13), id(1, 1, 12), id(2, 1, 12)], [[id(1, 4, 13), 10]], 1), { data: {} });
|
||
t.eq('扣底 两连对拖赢 ×4', bo4.data.bottom.multiple, 4);
|
||
|
||
// ============ §5.4.5 甩错惩罚执行 do_playcard ============
|
||
const A = require('../class.arith.js');
|
||
// 造首出场景:庄家(0)手牌与两闲家主牌,step5 本轮首出
|
||
function mkFirstPlay(flower, dealMap) {
|
||
const cards = [];
|
||
for (let i = 0; i < 108; i++) cards[i] = { id: i, dealowner: 99, playround: -1, playowner: -1, score: 0, flower: A.id_to_flower(i), number: A.id_to_number(i) };
|
||
for (const [cid, owner] of dealMap) cards[cid].dealowner = owner;
|
||
const sl = () => [[0, 0], [0, 0], [0, 0], [0, 0], [-1, -1]];
|
||
const o = { step: 5, banker: 0, flower, cards, seatlist: [sl(), sl(), sl()], playproc: {} };
|
||
o.method = { have_baofu: () => o.seatlist.some(s => s[4][0] == 0) };
|
||
P.new_playround(o, 1, 0);
|
||
return o;
|
||
}
|
||
// 庄家甩 大王+主K对;seat1 持主A对(能压主K对) → 甩错:收回甩牌、只强制打出最小一张(主K)、其余留手
|
||
const fp = mkFirstPlay(1, [[big(1), 1], [id(1, 1, 13), 1], [id(2, 1, 13), 1], [id(1, 1, 1), 2], [id(2, 1, 1), 2]]);
|
||
const re = P.do_playcard(fp, [big(1), id(1, 1, 13), id(2, 1, 13)]);
|
||
t.eq('甩错执行 shuaicuo=true', re.shuaicuo, true);
|
||
t.eq('甩错执行 只打1张且为主K', re.cards.length === 1 && A.id_to_number(re.cards[0]) === 13, true);
|
||
t.eq('甩错执行 大王未出(留手)', fp.cards[big(1)].playround, -1);
|
||
t.eq('甩错执行 打出的主K已出', fp.cards[re.cards[0]].playround > 0, true);
|
||
// 对照:合法甩牌 do_playcard 正常打出整套、不甩错
|
||
const fp2 = mkFirstPlay(1, [[big(1), 1], [big(2), 1], [id(1, 1, 13), 1], [id(1, 1, 5), 2], [id(1, 1, 6), 3]]); // 大王对+主K单;对手仅小主牌
|
||
const re2 = P.do_playcard(fp2, [big(1), big(2), id(1, 1, 13)]);
|
||
t.eq('合法甩牌 do_playcard 非甩错', !!re2.shuaicuo, false);
|
||
t.eq('合法甩牌 整套3张打出', re2.cards.length, 3);
|
||
|
||
// §5.4.4 集成:首出合法甩牌 → 跟牌方违反分量拆解 → do_playcard 拒;正确对位 → 接受
|
||
const fpf = mkFirstPlay(1, [
|
||
[big(1), 1], [id(1, 1, 12), 1], [id(2, 1, 12), 1], // 庄家 大王+主Q对(合法甩牌)
|
||
[id(1, 1, 11), 2], [id(2, 1, 11), 2], [id(1, 1, 9), 2], [id(1, 1, 8), 2] // seat1 主J对+主9+主8
|
||
]);
|
||
const lead = P.do_playcard(fpf, [big(1), id(1, 1, 12), id(2, 1, 12)]);
|
||
t.eq('甩牌集成 首出合法(记录shuai_demand)', lead.result && !lead.shuaicuo, true);
|
||
const badF = P.do_playcard(fpf, [id(1, 1, 11), id(1, 1, 9), id(1, 1, 8)]); // 拆散主J对
|
||
t.eq('甩牌集成 跟牌拆散主对→拒', badF.result, false);
|
||
const goodF = P.do_playcard(fpf, [id(1, 1, 11), id(2, 1, 11), id(1, 1, 9)]); // 用主J对对位
|
||
t.eq('甩牌集成 跟牌用主对→接受', goodF.result, true);
|
||
|
||
process.exit(t.done('paiju') ? 0 : 1);
|