// 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 }); // design §8.2:亮牌依据是【庄家埋牌后】的静态手牌快照,全程固定、不随出牌缩水 // (闲家断线重连时 get_deskinfo 会再取一次;若按当前剩余手牌重算,亮牌会缩水甚至变 null) const LIANG28 = fill([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)], 28); const EXPECT_LIANG = { zhu: 10, zhupair: 5, zhutuo: 1, wang: 4, qi: 6 }; t.eq('亮牌 埋牌后快照(出牌开始)', P.get_liangpai(mkHand(LIANG28)), EXPECT_LIANG); const played = mkHand(LIANG28); // 同一手牌,已打出2大王+1对♦7 for (const c of played.cards){ if (c.id === big(1) || c.id === big(2) || c.id === id(1, 1, 7) || c.id === id(2, 1, 7)){ c.playround = 1; } } t.eq('亮牌 打出4张后重连仍按快照', P.get_liangpai(played), EXPECT_LIANG); // 埋下的牌(playround==0)不算在快照里 const buried = mkHand(LIANG28); for (const c of buried.cards){ if (c.id === big(1) || c.id === big(2)){ c.playround = 0; } } t.eq('亮牌 已埋的牌不计入快照', P.get_liangpai(buried), { qi: 6 }); // ============ §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);